리액트 렌더링을 공부하고 구현해서 간단한 투두를 만들어보는게 목표였다. 그런데 useState를 구현해두지 않아서 만들 수 있는게 제한적이었다.
useState
내부 동작도 확인하고 구현하기 위해 학습해보았다.
목차
useState란?
ㅤ사용법
ㅤ상태 업데이트 함수로 업데이트를 하지 않는다면?useState 실제 코드 확인하기
ㅤReactHooks
ㅤReactFiberHooks
ㅤmountState
ㅤupdateState
ㅤrerenderState
ㅤ정리하기useState 흐름과 동작 원리 정리하기
ㅤuseState의 전체 동작 흐름
ㅤ리액트의 훅
ㅤ클로저로 구현된 useState
useState
는 리액트에서 제공하는 훅으로, 상태 관리를 가능하게 한다.
useState와 같은 훅은 조건문, 반복문, 중첩된 함수에서 선언할 수 없다. 이 이유는 뒤에서 자세히 다룰 예정이다.
count
, setCount
2개의 값이 있다. count는 현재 상태 값이고, setCount는 상태를 변경해주는 함수다.useState
를 사용하면 상태와 상태 업데이트 함수를 반환해준다. 그렇다면 상태 업데이트는 상태 업데이트 함수를 통해 수행한다. 그런데 useState가 반환하는 상태 변경 함수를 사용하지 않고 그냥 상태를 조작한다면 어떻게 될까?
감지하지 못하는 이유는 다음과 같다.
그렇다면 상태 업데이트 함수를 사용해 상태를 업데이트하면 무엇이 다를까?
코드는 리액트 19.0.0 버전을 참고하였습니다.
리액트의 훅이 모여있는 파일이다. useState, useEffect, useCallback, useRef 등 자주 본 훅이 있을 것이다.
여기서는 내부 구현을 볼 수는 없어서 내부 구현은 밑에 작성해두었다.
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
return ((dispatcher: any): Dispatcher);
}
export function getCacheForType<T>(resourceType: () => T): T {
const dispatcher = ReactSharedInternals.A;
if (!dispatcher) {
// If there is no dispatcher, then we treat this as not being cached.
return resourceType();
}
return dispatcher.getCacheForType(resourceType);
}
export function useContext<T>(Context: ReactContext<T>): T {
const dispatcher = resolveDispatcher();
return dispatcher.useContext(Context);
}
export function unstable_useContextWithBailout<T>(
context: ReactContext<T>,
select: (T => Array<mixed>) | null,
): T {
if (!(enableLazyContextPropagation && enableContextProfiling)) {
throw new Error('Not implemented.');
}
const dispatcher = resolveDispatcher();
return dispatcher.unstable_useContextWithBailout(context, select);
}
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
export function useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialArg, init);
}
export function useRef<T>(initialValue: T): {current: T} {
const dispatcher = resolveDispatcher();
return dispatcher.useRef(initialValue);
}
export function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
export function useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useInsertionEffect(create, deps);
}
export function useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useLayoutEffect(create, deps);
}
export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useCallback(callback, deps);
}
export function useMemo<T>(
create: () => T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useMemo(create, deps);
}
(... 코드가 길어 일부 훅은 지웠습니다.)
export function useActionState<S, P>(
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [Awaited<S>, (P) => void, boolean] {
if (!enableAsyncActions) {
throw new Error('Not implemented.');
} else {
const dispatcher = resolveDispatcher();
// $FlowFixMe[not-a-function] This is unstable, thus optional
return dispatcher.useActionState(action, initialState, permalink);
}
}
이 파일에 useState 구현이 있었지만, 함수 호출하는 부분이 많아 실제 동작을 이해하기 어렵다.
먼저 ReactFiberHooks에 있는 useState 함수를 간단하게 이해해보고, 각각을 자세히 알아볼 생각이다.
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
warnInvalidHookAccess();
updateHookTypesDev();
const prevDispatcher = ReactSharedInternals.H;
ReactSharedInternals.H = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return rerenderState(initialState);
} finally {
ReactSharedInternals.H = prevDispatcher;
}
}
rerenderState(initialState)
에서 수행한다.그렇다면 rerenderState
함수를 찾아서 동작 과정을 찾아볼 것이다. 또한 디스패처 시스템이 어떻게 동작하는지도 봐야한다.
mountState
,updateState
,rerenderState
를 알아볼 것이다.
마찬가지로 이 함수들도 ReactFiberHooks 파일에 있다.
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
const hook = mountWorkInProgressHook(); // 훅 객체 생성
if (typeof initialState === 'function') { // initialState가 함수일 때
// useState(() => functionValue())와 같이 호출되는 경우
const initialStateInitializer = initialState;
initialState = initialStateInitializer(); // 함수를 호출해 초기 상태 계산
}
// 상태 및 큐 초기화
hook.memoizedState = hook.baseState = initialState; // 초기 상태 저장
const queue: UpdateQueue<S, BasicStateAction<S>> = { // 업데이트 큐 객체 생성
pending: null, // 처리 대기 중인 업데이트
lanes: NoLanes, // 업데이트 우선순위 정보
dispatch: null, // 나중에 설정될 setState 함수
lastRenderedReducer: basicStateReducer, // 상태 계산에 사용되는 리듀서
lastRenderedState: (initialState: any), // 마지막으로 렌더링된 상태
};
hook.queue = queue; // 생성된 큐를 훅에 연결
return hook;
}
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountStateImpl(initialState); // 훅 초기화
// 디스패치 함수 생성
const queue = hook.queue;
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind( // dispatchSetState 함수를 현재 렌더링 중인 Fiber와 상태 큐에 바인딩하여 디스패치 함수 생성
null,
currentlyRenderingFiber,
queue,
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch]; // 상태 값과 상태 업데이트 함수 반환
}
mountState
는 첫 렌더링 시 호출되는 useState 구현이다.
컴포넌트가 처음 마운트될 때 상태를 초기화하는 내부 구현이다.
mountWorkInProgressHook
: 새로운 훅 객체를 생성하고, 현재 컴포넌트의 훅 리스트에 연결한다.
훅 객체란?
const hook = {
memoizedState: null, // 마지막으로 렌더링된 상태
baseState: null, // 기본 상태 (업데이트 적용 전)
baseQueue: null, // 기본 업데이트 큐
queue: null, // 현재 업데이트 큐
next: null // 다음 훅 링크
}
리액트는 컴포넌트 인스턴스 별로 훅들을 연결 리스트
형태로 관리한다. 각 훅은 자신만의 상태와 큐를 가지고, next 속성을 통해 다음 훅과 연결된다. 이 방식을 통해 리액트는 훅의 호출 순서를 추적하고 각 렌더링 사이에 상태를 유지할 수 있다.
예를 들어 컴포넌트에서 useState를 2개 사용해주고 useEffect를 사용해준다면?
이 3개의 훅은 연결리스트로 나타내는 것이다. 이 순서를 유지해서 관리하기 때문에 순서가 바뀌게 되면 상태 업데이트가 제대로 되지 않는다. 그래서 이런 훅들을 조건문을 사용해 정의해주는 방식을 사용할 수 없는 것이다.
mountStateImpl를 호출해서 훅을 초기화해준다. 디스패치 함수를 생성하고, [hook.memoizedState, dispatch]를 반환한다. 이는 각각 상태와 상태 변경 함수이다.
const [count, setCount] = useState(0)
와 같이 호출하면 반환해주는 값인 것이다.
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind( // dispatchSetState 함수를 현재 렌더링 중인 Fiber와 상태 큐에 바인딩하여 디스패치 함수 생성
null,
currentlyRenderingFiber, // 훅을 사용하는 Fiber 객체 정보
queue, // 상태 업데이트 큐
): any);
dispatchSetState
함수에 바인딩해서 디스패치 함수를 생성한다. 밑에 해당 함수 내용을 더 자세히 적어두었다. 상태를 설정하는 함수인 setState가 호출될 때 실행되는 함수다. 리액트 상태 업데이트의 핵심 부분이다.
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
const lane = requestUpdateLane(fiber); // 업데이트 우선순위 계산
const didScheduleUpdate = dispatchSetStateInternal(fiber, queue, action, lane);
if (didScheduleUpdate) {
startUpdateTimerByLane(lane);
}
markUpdateInDevTools(fiber, lane, action);
}
dispathchSetState
는 상태 설정 함수(setState)의 상위 레벨 wrapper이다.
dispatchSetStateInternal
함수에 위임function dispatchSetStateInternal<S, A>(
fiber: Fiber, // 업데이트할 컴포넌트를 나타내는 Fiber 노드
queue: UpdateQueue<S, A>, // 해당 상태와 관련된 업데이트 큐
action: A, // 새 상태 값 또는 이전 상태를 받아 새 상태를 반환하는 함수
lane: Lane, // 해당 업데이트의 우선순위
): boolean {
const update: Update<S, A> = { // 업데이트 객체 생성
lane, // 업데이트 우선순위
revertLane: NoLane, // 취소 가능한 업데이트라면 사용
action, // 새 상태 값 or 상태 업데이트 함수
hasEagerState: false, // 상태가 미리 계산되었는지 여부
eagerState: null, // 미리 계산된 값
next: (null: any), // 연결 리스트의 다음 업데이트
};
if (isRenderPhaseUpdate(fiber)) {
// 렌더링 중에 상태 업데이트가 발생한 경우: 렌더링 도중 상태 업데이트가 발생한 것을 특별한 큐인 renderPhaseUpdates 큐에 추가
enqueueRenderPhaseUpdate(queue, update);
} else {
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes && // 현재 컴포넌트에 다른 업데이트가 없고
(alternate === null || alternate.lanes === NoLanes) // 작업 중인 버전인 alternate도 업데이트가 없다면
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher = null;
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) { // 새 상태와 현재 상태가 동일한지 확인
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update); // 동일하면 업데이트 큐에 추가하지만, 컴포넌트 리렌더링하지 않고 조기 종료
return false; // 상태가 실제로 변경되지 않으면 불필요한 리렌더링을 피함
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {}
}
}
// 일반적인 업데이트 처리
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); // 업데이트 큐에 추가하고, 루트 Fiber 노드를 반환
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane); // 루트에서 업데이트 스케줄링
entangleTransitionUpdate(root, queue, lane); // 트랜지션 관련 업데이트 처리
return true; // 업데이트가 스케줄링되었음을 나타냄
}
}
return false;
}
dispatchSetStateInternal
는 상태 업데이트의 실제 처리를 담당한다.
dispatchSetStateInternal는 리액트 상태 업데이트의 핵심 부분으로, 상태 업데이트 객체를 생성하고 렌더링 단계 업데이트를 특별히 처리해준다. 또한 가능한 경우 상태 변경을 미리 계산하여 불필요한 리렌더링을 방지하고, 업데이트를 큐에 추가해서 컴포넌트 리렌더링을 스케줄링한다.
업데이트 큐
에 추가한다사용자가 const [count, setCount] = useState(0);
을 호출할 때 흐름을 정리해보자.
mountState
가 실행된다. 그렇다면 상태 업데이트 함수인 setCount는 dispatchSetState.bind(null, fiber, queue)
로 생성된다.updateState
를 호출되고 큐에 있는 모든 업데이트를 적용한다.function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}
updateState
는 컴포넌트가 리렌더링(업데이트)될 때 호출되는 useState 구현이다.
컴포넌트가 리렌더링될 때 상태 업데이트를 처리한다.
updateState는 컴포넌트 리렌더링 중에 호출된다. useState 훅을 사용하는 컴포넌트가 다시 렌더링될 때 호출된다.
mountState
를 사용하여 훅을 초기화한다.updateState
를 사용하여 현재 상태를 검색하고 필요한 경우 업데이트한다.updateState는 간단하게 구현되어 있다.
반환하는 값은 updateReducer(basicStateReducer, initialState)
이다. updateReducer 함수를 봐야 이해가 될거 같아서 찾아보았다.
리듀서 함수를 상태 변환을 처리하는 함수이다. updateReducer는 리액트 훅 시스템에서 상태 업데이트를 처리하는 핵심 함수다. 이 함수는 리렌더링 중 useReducer/useState가 호출될 때 실행된다.
updateReducer의 목적
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}
function updateReducerImpl<S, A>(
hook: Hook, // 현재 작업 중인 훅
current: Hook, // 이전 렌더링 훅
reducer: (S, A) => S, // 상태 변환을 처리하는 리듀서 함수
): [S, Dispatch<A>] {
// 훅의 업데이트 큐가 있는지 확인 (큐가 없다면 조건부로 호출되었을 가능성이 높음)
const queue = hook.queue;
if (queue === null) {
throw new Error(
'Should have a queue. You are likely calling Hooks conditionally, ' +
'which is not allowed. (https://react.dev/link/invalid-hook-call)',
);
}
queue.lastRenderedReducer = reducer;
// 대기 중인 업데이트 처리 준비 (이전 렌더링에서 처리되지 않은 업데이트와 새로 추가된 업데이트 가져오기)
let baseQueue = hook.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
// 상태 계산 준비
const baseState = hook.baseState;
if (baseQueue === null) {
hook.memoizedState = baseState;
} else {
// 업데이트 적용
const first = baseQueue.next;
let newState = baseState; // 현재까지 계산된 새 상태
let newBaseState = null; // 다음 렌더링의 기본 상태가 될 값
// newBaseQueueFirst, newBaseQueueLast: 다음 렌더링으로 넘겨질 처리되지 않은 업데이트들의 연결 리스트
let newBaseQueueFirst = null;
let newBaseQueueLast: Update<S, A> | null = null;
let update = first;
let didReadFromEntangledAsyncAction = false;
do {
// 업데이트 처리 로직
const updateLane = removeLanes(update.lane, OffscreenLane); // 업데이트 우선순위
const isHiddenUpdate = updateLane !== update.lane;
// 현재 렌더링에서 업데이트를 건너뛸지 여부
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);
if (shouldSkipUpdate) { // 업데이트를 건너뛰는 경우
const clone: Update<S, A> = { // 업데이트 복제
lane: updateLane,
revertLane: update.revertLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane); // 다음 렌더링에서 이 업데이트 처리하도록 설정
} else { // 업데이트 적용하는 경우
const revertLane = update.revertLane;
if (!enableAsyncActions || revertLane === NoLane) {
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
lane: NoLane,
revertLane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
if (updateLane === peekEntangledActionLane()) {
didReadFromEntangledAsyncAction = true;
}
} else {
if (isSubsetOfLanes(renderLanes, revertLane)) {
update = update.next;
if (revertLane === peekEntangledActionLane()) {
didReadFromEntangledAsyncAction = true;
}
continue;
} else {
const clone: Update<S, A> = {
lane: NoLane,
revertLane: update.revertLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
revertLane,
);
markSkippedUpdateLanes(revertLane);
}
}
const action = update.action;
if (update.hasEagerState) {
newState = ((update.eagerState: any): S); // 미리 계산된 상태 사용
} else {
newState = reducer(newState, action); // 리듀서로 새 상태 계산
}
}
update = update.next;
} while (update !== null && update !== first);
// 최종 상태 설정
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
if (didReadFromEntangledAsyncAction) {
const entangledActionThenable = peekEntangledActionThenable();
if (entangledActionThenable !== null) {
throw entangledActionThenable;
}
}
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
if (baseQueue === null) {
queue.lanes = NoLanes;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch]; // 상태와 디스패치 함수 반환
}
updateReducer
는 훅의 업데이트 큐를 검증하고 설정한다. 그리고 대기 중 업데이트와 기존 업데이트를 병합해서 업데이트 큐를 순회하며 현재 렌더링에서 처리할 업데이트와 다음 렌더링으로 넘길 업데이트를 확인한다. 처리할 업데이트마다 리듀서를 적용해서 새 상태를 계산해주고, 최종 상태를 설정하고 다음 렌더링을 위한 정보를 저장해준다. 최종적으로 계산된 새 상태와 디스패치 함수를 반환해준다.
function rerenderState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return rerenderReducer(basicStateReducer, initialState);
}
rerenderState
함수는 컴포넌트가 강제로 리렌더링될 때 호출되는 useState 구현이다.
rerenderReducer 함수를 호출하여 상태를 다시 계산한다.
rerenderState가 호출되는 경우는 다음과 같다.
mountState
가 호출되어 초기 훅 설정이 이루어진다.dispatchSetState
가 호출된다.updateState
가 호출되어 큐에 있는 업데이트를 처리한다.rerenderState
가 호출되어 업데이트를 처리한다.function Counter() {
const [count, setCount] = useState(0);
}
컴포넌트에서 useState를 정의하고, 해당 컴포넌트가 처음으로 렌더링되면 리액트는 현재 상황에 맞는 디스패처를 설정한다.
이때 처음으로 렌더링되므로 mount되며, mountState
를 호출한다.
hook.memoizedState = hook.baseState = initialState
위에서 반환 받은 setState
를 호출하면, 상태 업데이트가 발생한다.
dispatchSetState
를 호출한다.dispatchSetStateInternal
을 호출해 실제 업데이트 로직을 처리한다.updateState
를 호출하여 업데이트 디스패처를 설정한다.updateReducer
를 호출한다.위에서 언급했는데 리액트는 훅을 호출할 때 순서를 지켜야한다. 그래서 모든 렌더링마다 동일한 순서로 호출해야하는 규칙이 있다.
모든 렌더링마다 동일한 순서로 호출해야하기 때문에 조건문에 정의하는 것이 안된다. 어떤 조건에서는 훅을 호출하고 어떤 조건에서는 호출하지 않는다면 렌더링마다 동일한 순서로 훅이 호출되지 않는다.
리액트 훅이 렌더링 사이에 상태를 유지하려면 이전 렌더링 상태와 현재 렌더링의 훅 호출을 매칭해야한다.
훅으르 추적하기 위해 이름이나 명시적 식별자를 사용하지 않는다. 대신 렌더링 중에 훅이 호출되는 순서를 사용한다. 훅 데이터의 연결 리스트를 생성하여 컴포넌트와 연결하는데 각 훅의 상태는 이 리스트의 특정 위치에 저장된다.
Fiber.memoizedState → Hook1 → Hook2 → Hook3 → null
처음 렌더링될 때 훅을 만날 때마다 연결 리스트를 생성하고 이후 렌더링될 때 훅 호출을 하며 병렬로 이 리스트를 순회한다. 순서를 지켜야하기 때문에 조건문 안에 훅을 호출해서 조건에 따라 호출될 때도 있고 안 될때도 있게 설정하면 순서가 깨지기 떄문에 이런 식으로 사용할 수 없는 것이다.
위에서 언급한대로 훅에 대한 식별자가 없기 때문에 순서를 통해 알 수 있다고 했다.
그렇다면 이전 렌더링 상태가 잘못된 훅에 할당되어 상태 손실이 발생한다.
간단한 예시를 보자.
// 1. 첫 번째 렌더링 (count = 0)
function Counter() {
const [count, setCount] = useState(0); // 훅 #1: state = 0
// count = 0 이므로 조건부 훅 건너뜀
const [name, setName] = useState("드뮴"); // 훅 #2: state = "드뮴"
return <div>...</div>;
}
// 버튼 클릭 후 setCount(1) 호출
// 2. 두 번째 렌더링 (count = 1)
function Counter() {
const [count, setCount] = useState(0); // 훅 #1: state = 1
if (count > 0) {
const [message, setMessage] = useState("채마야 공부해라"); // 훅 #2: "드뮴"을 받음
}
const [name, setName] = useState("드뮴"); // 훅 #3: 이전 상태가 존재하지 않음
// 리액트가 충돌하거나 기본값을 사용
return <div>...</div>;
}
0 - "드뮴"
으로 저장된다.const [message, setMessage] = useState("채마야 공부해라");
가 count 훅 뒤에 오게 되고, 그 다음으로 name이 호출되는 순서가 된다.const [name, setName] = useState("드뮴");
이었기에 두번째 렌더링에서 message의 useState는 이전 상태를 "드뮴"으로 받게된다.위와 같이 순서로 식별하기 때문에 순서를 유지해줘야했다. 그런데 훅마다 고유한 이름을 생성해 구별하면 더 편할거 같은데 왜 순서를 지키도록 해서 순서를 통해 상태 업데이트를 관리할까?
따라서 훅 규칙을 지켜서 사용만 하면 순서대로 식별하는 것은 간단한 방법이기 때문에, 이 방법을 채택한다.
리액트 훅 시스템은 훅이 렌더링마다 같은 순서로 호출된다는 가정하에 작동한다. 규칙을 어기면 상태가 엉망이 되고 잘못된 상태가 할당되며 애플리케이션 충돌로 이어질 수 있다.
클로저
는 함수가 자신이 선언된 렉시컬 환경을 기억하고, 그 함수가 원래의 스코프 밖에서 실행될 때도 그 환경에 접근할 수 있는 능력을 말한다.
자바스크립트에서 함수 내부에 변수를 선언하게 되면 그 변수는 함수 내에서만 접근이 가능하다. 이를 함수 스코프라고 하는데, 렉시컬 스코프는 내부 함수가 자신을 포함하는 외부 함수에 선언된 변수에 접근할 수 있다는 의미다.
그래서 클로저는 내부 함수가 외부 함수 실행이 완료되어도 자신의 렉시컬 스코프에 대한 참조를 유지할 때 형성된다.
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountStateImpl(initialState);
const queue = hook.queue;
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
컴포넌트 함수가 완료되어도 setState 함수는 자신이 속한 컴포넌트와 상태 큐를 기억하고 있기 때문에 컴포넌트 상태 업데이트가 가능하다.
클로저 개념을 통해 useState를 설명하면 다음과 같다.
클로저는 함수와 그 함수가 선언된 환경의 조합이다. 리액트에서 useState가 반환하는 상태 설정 함수인 setState는 어떤 컴포넌트에 속하는지, 어떤 상태 큐를 업데이트해야하는지에 대한 정보를 기억한다. 즉, 어떤 컴포넌트에 속하는지는 Fiber 객체를 기억하는 것이고 이를 기억하고 언제 어디서든 호출되어도 올바른 컴포넌트에서 상태 업데이트를 수행한다.
클로저를 통해 상태가 오직 setState를 통해 변경되도록 보장한다. 단방향 데이터 흐름의 원칙을 강화한다. 또한 상태 값을 리액트 내부에 저장하고 컴포넌트는 읽기 전용 복사본만 제공되기 때문에 상태를 보호할 수 있다.
자세한 서명 감사드립니다 👍