- 본 게시글은 React 19.2.4 버전을 기준으로 작성되어있습니다.
- 틀린 부분이 있을 수 있습니다. 댓글로 알려주시면 감사하겠습니다 : )
- Github : https://github.com/facebook/react
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>
증가
</button>
</div>
);
}
export default Counter;
React의 useState는 컴포넌트의 상태를 저장하고, 상태가 바뀌면 다시 렌더링하도록 만드는 훅입니다. React를 사용하면 거의 필수적으로 사용되는 훅입니다.
1. useState는 어떻게 작동할까? - 1편
useState는 packages/react/src/ReactHooks.js에 아래처럼 정의 되어있다.
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
initialState로 제네릭으로 전달받은 S는 함수 타입과 값 자체로 설정할 수 있다. 우리가 useState(0), useState(() => 0) 처럼 state를 초기화할 수 있는 이유이다. 초기 계산 비용이 큰 데이터를 state로 지정할때 함수 형태로 지정할 수 있다는 것을 알 수 있다.
이후 resolveDispatcher를 통해 Hook Dispatcher를 설정한다. 해당 코드를 살펴보자.
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
if (__DEV__) {
if (dispatcher === null) {
console.error(
'... .. .',
);
}
}
// Will result in a null access error if accessed outside render phase. We
// intentionally don't throw our own error because this is in a hot path.
// Also helps ensure this is inlined.
return ((dispatcher: any): Dispatcher);
const dispatcher = ReactSharedInternals.H; 로 dispatcher를 정의한 후 return 하는 것을 확인할 수 있다.
그러면 ReactSharedInternals.H 는 무엇일까? packages/react/src/ReactHooks.js 으로 접근하여 확인해보자.
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
const ReactSharedInternals =
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
export default ReactSharedInternals;
??? 그냥 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE를 return 하고 있는 코드다. 저 구문의 의미를 packages/react/index.js에서 확인해보자.
export {
__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
__COMPILER_RUNTIME,
Children,
Component,
...,
..,
.
} from './src/ReactClient';
./src/ReactClient 에는 아래처럼 ReactSharedInternals를 저 긴 이름으로 지정하여 export 하고 있는 것을 확인할 수 있었다.
ReactSharedInternals as __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
그렇다면 ReactSharedInternals가 정의 되어있는 packages/react/src/ReactSharedInternalsClient.js를 분석해보자.
export type SharedStateClient = {
H: null | Dispatcher, // ReactCurrentDispatcher for Hooks
A: null | AsyncDispatcher, // ReactCurrentCache for Cache
T: null | Transition, // ReactCurrentBatchConfig for Transitions
S: null | onStartTransitionFinish,
G: null | onStartGestureTransitionFinish,
...
..
.
};
가장 먼저 이전에 ReactSharedInternals.H 는 Dispatcher를 저장하고 있는 것을 확인할 수 있다. 그렇다면 이 Dispatcher는 뭐하는 친구일까? react-reconciler/src/ReactInternalTypes에서 확인해보자.
Reconciler는 Virtual DOM을 활용해 UI 변경 사항을 비교하고, 실제 DOM에 최소한의 변경사항만 반영하여 렌더링 성능을 최적화하는 리액트의 핵심 패키지이다. 리액트의 핵심을 뜯어본다니.. 두근 두근 하지 않을 수가 없다.
type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;
export type Dispatcher = {
use: <T>(Usable<T>) => T,
readContext<T>(context: ReactContext<T>): T,
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>],
useContext<T>(context: ReactContext<T>): T,
...
..
.
};
React를 사용할때 흔히 보았던 다양한 내장 훅들이 정의 되어있는 것을 확인할 수 있다. 여기서 우리가 궁금한 useState는 익숙한 형태로 정의 되어있는 것을 확인할 수 있다. 초기에 확인했던 아래 packages/react/src/ReactHooks.js의 코드와 비슷하다는 것을 확인할 수 있다.
가장 먼저 BasicStateAction<S>를 살펴보자.
type BasicStateAction<S> = (S => S) | S;
(S => S) | S 를 살펴보면 우리가 익숙하게 useState를 사용했었던 코드들이 생각날 수 있다.
setCount(5);
setCount(prev => prev + 1);
위의 코드 처럼 값을 직접 전달하거나, 이전 상태를 기반으로 값을 업데이트 했었던 이유가 바로 저런 형태를 허용하기 때문이었다.
BasicStateAction<S>를 Dispatch<A>로 감싸주는데 이를 통해 상태를 업데이트하는 BasicStateAction<S>의 타입을 제네릭으로 전달해주는 것을 알 수 있다. 즉 setState함수의 타입을 지정해준다.
지금까지 코드를 분석해보면 useState는 우리가 잘 이해하고있는 값 또는 이전 상태 기반 함수를 받아 상태를 업데이트하는 함수를 반환하는 Hook이라는 것을 알 수 있다.
여기까지 코드는 우리가 잘 아는 useState의 모양과 동일했다. 하지만 실질적으로 리액트가 동작하는 mount, update 등 우리가 알지 못했던 내부적인 동작을 더 분석해야한다. 다음 포스팅에서는 해당 내용을 분석해볼 것이다.