시작하는 중
useState(함수)??? 본문
오픈 단톡방에서 신기한 것을 봐서 정리하려고 한다.
바로.. useState에 함수를 넣는 것
https://github.com/vinitus/useStateHook/blob/master/src/App.tsx
GitHub - vinitus/useStateHook: useState 훅 깊게 파보기
useState 훅 깊게 파보기. Contribute to vinitus/useStateHook development by creating an account on GitHub.
github.com
내가 만든 예시이다.
중요한 부분을 살펴보자면
// App.tsx
...
function one1(): number {
console.log('One1 function is called');
return 1;
}
function one2(): number {
console.log('One2 function is called');
return 1;
}
const [test, setTest] = useState(1);
const [test1, setTest1] = useState(one1);
const [test2, setTest2] = useState(one2());
const [test3, setTest3] = useState((): number => {
console.log('One3 function is called');
return 1;
});
...
여기서 useState에 넘어가는 인자들이다.
0. test의 경우에는 평소처럼 숫자 1을 넣는 것이다.
1. 콜백함수로 one1를 넘긴다
2. 함수의 실행한 결과를 인자로 넘긴다.
3. 익명 콜백 함수를 넘긴다.
문제는 이 모든 것들이 정상적으로 작동한다는 것이다.
하지만, 조금은 다른 점이 있다. reset 버튼을 누르게 된다면, one2함수는 버튼을 누를 때마다 계속 실행된다.
그렇기 때문에 첫 상태가 복잡한 로직의 결과라면, 함수를 만들고 이 함수를 useState의 콜백 함수로 넘기는 것이 가능하다.
이것이 어떻게 유용하냐면 ..
// 첫 번째 예제
...
let longWorkResult = 0;
for (let i = 0; i < 100; i += 1) {
longWorkResult += i;
}
const [result, setResult] = useState(longWorkResult);
...
// 두 번째 예제
...
function longWork(): number {
let result = 0;
for (let i = 0; i < 100; i += 1) {
result += i;
}
return result;
}
const [result, setResult] = useState(longWork);
...
지금은 0~99까지 단순한 더하기이지만, 더 복잡한 로직들이 있다면 우리는 매번 이 함수가 렌더링될 때마다 해야한다.
useState로 들어가는 인자는 그 복잡한 로직 연산 결과이다. 그러므로 연산이 반드시 필요하다.
때문에, 이벤트 루프의 메인 스레드를 반드시 막는다. 이것이 중요한 것
리렌더링시 사용되지 않음에도 불구하고, 계속 계산되어야한다. useCallback을 사용할 수도 있겠지만, useState에 콜백함수로 넘기는 지연 초기화를 사용한다면 쉽게 할 수 있다!
사실 여기까지만 알고, 지연 초기화란 이런 것이고 어떻게 사용할 수 있다 정도로 끝내도 상관없다.
나는 근데 이런거 못참음.
1. 왜 콜백함수로 넘기면 이 값이 계산되는 것인지도 신기했고 왜 한번만 실행되는가도 신기했다.
2. 왜 함수를 호출하여 넘기면 계속 실행되는 것인지가 너무 궁금했다. 그냥 애초에 useState에 인자로 넘기면, 다음 렌더링에서 인자로 넘긴 함수의 호출은 실행될 필요가 없는거 아닌가? 싶었다.
그래서 찾아보고 생각하기로 했다.
1. useState(one1)는 왜 유효한 것일까
이를 위해선 useState를 React.js에서 찾는 여행을 떠나야한다. 너무 좋은 블로그 글이 있어서 이를 참고하기로 했다.
useState는 initialState라는 인자를 받는다. dispatcher라는 변수에 resolveDispatcher의 리턴을 할당해준다.
우선, 이 resolveDispatcher는 또 ReactCurrentDispatcher의 current가 할당된 dispatcher를 반환할 뿐이다.
그럼 ReactCurrentDispatcher가 뭔지 알아야한다.
current라는 객체 하나가 끝이다.. 근데 https://github.com/facebook/react/blob/v16.12.0/packages/react/src/ReactCurrentDispatcher.js#L15
/**
* Keeps track of the current dispatcher.
*/
const ReactCurrentDispatcher = {
/**
* @internal
* @type {ReactComponent}
*/
current: (null: null | Dispatcher),
};
여기서 보면, crurrent는 Dispatcher가 될 수 있고 이 타입은 ReactFiberHooks에서 온다.
즉, 여기가 끝이 아니라는 것...
export const ContextOnlyDispatcher: Dispatcher = {
readContext,
...
useState: throwInvalidHookError,
///
};
const HooksDispatcherOnMount: Dispatcher = {
readContext,
...
useState: mountState,
...
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
...
useState: updateState,
...
};
여기서 보면 Dispatcher는 updateState, mountState, throwInvalidHookError가 올 수 있다.
우리는 초기 렌더링이니까, mountState를 살펴보면
이 부분이 핵심이다. 즉, 인자로 들어온 값이 "함수"라면 -> 실행하는 것
그렇기에, 콜백함수를 넘기면 실행되고 그 인자는 함수에서 함수가 실행된 값으로 업데이트되는 것이다.
2. useState(one2())는 왜 자꾸 실행될까
참 많은 고민을 했다. react.dev도 찾아보고, 거의 모든 블로그 글도 찾아봤다. 거진 2시간 찾아본 듯
이거 찾아보다가 React 깃허브도 읽어본 것이다.
하지만, JS 자체로 생각해보니까 답이 나왔다.
React는 useState의 인자가 쓸모 없다는 것을 알지만, JS 자체는 알지 못한다는 것
너무 리액트의 관점에서 생각했던 것 같다..
리렌더링시 JS는 해당 함수형 컴포넌트를 실행시키면서 코드 한줄한줄 실행한다. 그렇기에 JS는 useState(one2())를 만나면 useState를 실행하기 위해, one2()가 실행되고 결과를 도출해내서 어떤 값이 정해져야 실행된다.
하지만, React는 useState의 인자는 더 이상 쓸모 없기에 쓰이지 않는다는 것을 React는 안다. 때문에, 함수의 결과 도출되지만, 이를 신경쓰지 않고 상태만 업데이트될 뿐인 것이다.
당연한 것이었으니깐..
reference
'react' 카테고리의 다른 글
react router의 원리 (0) | 2023.05.04 |
---|---|
yarn dev를 하면 vite에서 일어나는 일 (0) | 2023.05.02 |
내 코드 리팩토링 하기 (0) | 2023.04.14 |
react router v6.4에 생긴 기능들 (0) | 2023.04.12 |
react-redux의 dispatch의 return은 무엇일까 (0) | 2023.03.27 |