useState
const [state, setState] = useState(initialState)
useState
는 배열을 반환하고 비구조화 할당을 통해 state
, setState
를 추출하여 사용한다. 그렇다면 useState
는 어디서 오는 것일까?
함수형 컴포넌트는 렌더링이 발생하면 함수 자체가 다시 호출된다. 그렇다면 어떻게 함수가 다시 호출되었을 때 이전의 상태값을 가져올 수 있는 것인가?
바로! useState
는 closure로 구현되어 있기 때문에 이전의 상태값을 유지할 수 있다!
그렇다면 closure란 무엇인가? 이에 대해 먼저 확인해 보자!
closure란?
mdn에 따르면 함수와 그 주변 상태(lexical environment, 함수가 정의될 때 주변의 스코프와 변수에 대한 정보를 포함하는 것)에 대한 참조가 묶인 것이라고 한다. 즉, closure는 내부함수에서 외부 함수의 범위에 대한 접근을 제공한다.
이에 대해 예제와 함께 자세히 알아보자!
function outerFunc() {
const name = "apple";
function innerFunc() {
console.log(name);
}
return innerFunc;
}
const myFunc = outerFunc();
myFunc(); // "apple"
이 예제는 innerFunc()
내부 함수가 실행되기 전에 외부 함수에서 반환된다.
다른 프로그래밍 언어에서는 함수 안의 지역 변수들은 그 함수들이 처리되는 동안에만 존재한다. outerFunc()
는 innerFunc()
을 반환하고 생을 마감해 name
변수에 더 이상 접근할 수 없을 것으로 예상하지만 위에 결과값은 "apple"가 출력되고 있다!
그 이유는 javascript의 특징 중 하나인 closure를 형성하기 때문이다. closure는 반환된 내부함수가 자신이 선언되었을 때의 환경(lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경 밖에서 호출되어도 그 환경에 접근할 수 있는 함수이다. 그래서 외부 함수가 이미 반환되었어도 내부함수가 외부함수의 변수를 참조하고 있으므로 외부함수의 변수는 계속 유지된다.
참고자료
useState 내부 동작 구현해 보기
useState 동작 원리는 이 closure의 특징을 이용하는데 closure를 활용해 useState의 내부를 간단하게 구현해 보자(실제로는 더 복잡하게 구현되어 있을 것이다)
const MyReact = (function() {
let _val
return {
render(Component) {
const Comp = Component()
Comp.render()
return Comp
},
useState(initialValue) {
_val = _val || initialValue
function setState(newVal) {
_val = newVal
}
return [_val, setState]
}
}
})()
위 코드는 React 모듈을 나타낸다. MyReact
는 두 개의 closure를 반환하고 있다. _val
에는 상태를 저장해 준다. useState
가 _val
를 참고하고 있기 때문에 _val
은 메모리에 존재하고 있다.(closure 원리에 따라 _val
의 값은 없어지지 않고 유지된다!)useState
에서 _val
은 처음에 undefined
일 경우 initialValue
를 할당한다. 이후에 useState
가 다시 호출될 때는 _val
에 할당된 값이 있으므로 기존의 값을 그대로 사용한다. useState
안에 setState
를 통해 _val
의 값을 업데이트할 수 있다.
function Counter() {
const [count, setCount] = MyReact.useState(0)
return {
click: () => setCount(count + 1),
render: () => console.log('render:', { count })
}
}
let App
App = MyReact.render(Counter) // render: { count: 0 }
App.click() // rerendering
App = MyReact.render(Counter) // render: { count: 1 }
Counter
를 하나의 컴포넌트로 생각해 보자.
처음에 MyReact.render(Counter)
를 실행하면 useState
에서 _val
에 초기값으로 0을 할당한다.
App.click()
으로 _val
이 1로 업데이트된다. 실제 React에서는 state의 값이 변경되었기 때문에 컴포넌트를 리렌더링 할 것이다. 우리도 그 과정을 추가하기 위해 App.click()
후 render를 실행해 줬다.
다시 렌더링 하면 _val
이 1로 유지되어 있는 것을 확인할 수 있다!
setState 후에 console.log에는 왜 새로운 값이 반영이 안 될까?
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
console.log(count); // 0
}, []);
위 예시처럼 console.log에는 1이 아닌 0이 찍히는데 그 이유에 대해서 알아보자!
react는 상태값이 변경되면 렌더링 함수를 호출해 가상 돔을 업데이트하고 가상 돔과 실제 돔을 비교해서 실제 돔을 업데이트하는 과정을 거친다. 이때 비교하여 실제 돔에 반영하는 과정을 reconciliation(조정)이라고 한다.
setCount(count+1)
를 하면 메모리에 있는 _val
(closure의 상태값)을 변경한 것이지 내가 사용하는 count
가 변경된 것은 아니다. 새로운 상태값을 확인하기 위해서는 리렌더링이 되어야 한다. Javascript는 싱글 스레드로 돌아가기 때문에 useEffect
에 실행이 끝난 이후에 리렌더링이 진행된다. console.log()를 실행하는 시점에는 리렌더링이 되기 전이므로 상태값은 0으로 나오는 것이다. (console.log() 시점에 count
는 새로 업데이트된 closure의 상태값이 아니라, 이전에 가져온 closure의 값을 바라보고 있다.)
그렇다면 상태값을 1,000,000번 변경하면 1,000,000번 리렌더링이 되는 것인가? 결론은 아니다! react는 퍼포먼스 향상을 위해 16ms동안 변경된 상태값들을 모아서 한 번에 리렌더링을 진행한다. 이것을 Batch Update라고 한다.
useCallback
useCallback에 넘겨준 함수는 리렌더링마다 무조건 재생성된다.
단지, dependency를 비교해서 이번에 새로 생성한 함수를 리턴하는지 이전에 생성한 저장해 둔 함수를 리턴하는지 차이가 있을뿐이다!
그럼 왜 어차피 재생성 되는데 왜 쓰는거죠...?
useCallback를 사용하면 dependency가 변경되지 않았다면 이전에 생성된 함수를 사용해 참조값을 동일하게 유지할 수 있다.
언제 사용하면 좋을까?
- useEffect안에서 dependency로 사용될 때 동일한 참조값을 유지해 useEffect의 불필요한 실행을 막을 수 있다.
- React.memo로 감싸진 자식 컴포넌트에 props로 함수를 전달할 경우 리렌더링을 방지할 수 있다.
- custom hooks가 외부에서 사용될 때 dependency에 사용될 수 있어 custom hooks의 함수의 경우 useCallback으로 감싸줘 동일한 참조값을 유지하는게 좋다.
+ useCallback의 내부구현 사항 참고자료
'IT > React' 카테고리의 다른 글
[Zustand] Toast 알림을 전역에서 관리 (0) | 2024.05.11 |
---|---|
recoil로 모달 전역 상태관리하기(+typescript) (1) | 2024.01.10 |
필터 구현 로직 custom hook으로 만들기(+뒤로가기 시 필터값 유지) (0) | 2023.12.05 |
Pagination custom hook (0) | 2023.11.06 |
[React] Button Component 잘 만드는 방법! (0) | 2023.11.05 |