React 개발자로서 컴포넌트의 생명주기와 부수 효과 관리는 항상 중요한 주제입니다. 그 중에서도 useEffect
는 함수형 컴포넌트에서 부수 효과를 다루는 핵심적인 Hook입니다. 이번 글에서는 useEffect
의 실제 동작 방식을 React 소스 코드를 통해 깊이 있게 살펴보고, 최적화 전략까지 알아보겠습니다.
useEffect
는 함수형 컴포넌트에서 부수 효과(side effects)를 수행하기 위한 Hook입니다. 부수 효과란 데이터 가져오기, 구독 설정, DOM 수동 조작 등 컴포넌트의 주 렌더링 프로세스 외의 작업들을 말합니다.
React의 소스 코드는 여러 패키지로 구성되어 있습니다. useEffect
와 관련된 주요 파일들은 다음과 같습니다:
packages/react/src/ReactHooks.js
: Hooks의 공개 API를 정의합니다.packages/react-reconciler/src/ReactFiberHooks.js
: Hooks의 실제 구현을 담당합니다.이러한 구조는 React의 모듈화된 설계를 보여주며, 공개 API와 내부 구현을 분리하여 유지보수성과 확장성을 높입니다.
packages/react/src/ReactHooks.js
파일에서 useEffect
의 공개 API를 찾을 수 있습니다:
export function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
이 코드는 다음과 같은 중요한 점들을 보여줍니다:
useEffect
는 create
함수와 deps
배열을 인자로 받습니다.dispatcher.useEffect
로 위임됩니다.resolveDispatcher
함수는 현재 React의 렌더링 단계에 따라 적절한 dispatcher를 반환합니다.useEffect
의 실제 구현은 packages/react-reconciler/src/ReactFiberHooks.js
파일에서 찾을 수 있습니다:
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps,
);
}
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps,
);
}
여기서 우리는 두 가지 주요 함수를 볼 수 있습니다:
mountEffect
: 컴포넌트가 처음 마운트될 때 호출됩니다.updateEffect
: 이후 업데이트 시 호출됩니다.두 함수 모두 create
함수(효과를 수행하는 함수)와 deps
(의존성 배열)를 인자로 받습니다.
의존성 배열은 useEffect
의 두 번째 인자로 전달되며, 이 배열의 값들이 변경될 때만 효과를 재실행합니다. React는 이전 렌더링의 의존성 값들과 현재 렌더링의 값들을 비교합니다:
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
이 함수는 Object.is
를 사용하여 각 의존성을 비교합니다. 모든 의존성이 동일하면 true
를 반환하여 효과를 재실행하지 않습니다.
Object.is()의 중요성:
- 참조 타입 비교: 객체나 배열같은 참조 타입의 경우, 내용이 같더라도 참조가 다르면 다른 것으로 간주됩니다.
- 원시 타입 비교: 숫자, 문자열 등의 원시 타입은 값 자체를 비교합니다.
- 특수한 경우 처리: NaN, +0, -0 등의 특수한 경우도 정확히 처리합니다.
클린업 함수는 컴포넌트가 언마운트되거나 다음 효과가 실행되기 직전에 호출됩니다:
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
이 함수는 모든 효과의 클린업 함수를 순회하며 실행합니다.
useEffect
는 렌더링이 완료된 후 비동기적으로 실행됩니다. 이는 브라우저가 화면을 그리는 것을 차단하지 않아 성능상 이점이 있습니다.
useEffect
를 사용하는 것은 성능을 저하시킬 수 있습니다.의존성 배열 최적화: 필요한 의존성만 포함시켜 불필요한 재실행을 방지합니다.
useEffect(() => {
// 효과 코드
}, [dependency1, dependency2]);
cleanup 함수 활용: 구독 해제 등의 정리 작업을 수행하여 메모리 누수를 방지합니다.
useEffect(() => {
const subscription = someAPI.subscribe();
return () => {
subscription.unsubscribe();
};
}, []);
useCallback과 함께 사용: 효과 내에서 사용하는 함수를 메모이제이션하여 불필요한 재실행을 방지합니다.
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
useEffect(() => {
memoizedCallback();
}, [memoizedCallback]);
useEffect
는 React 함수형 컴포넌트에서 부수 효과를 관리하는 강력한 도구입니다. 그 내부 동작을 이해하고 적절히 사용하는 것이 중요합니다. 이 글에서 살펴본 것처럼, React의 내부 구현은 복잡하지만 효율적으로 설계되어 있습니다.
개발자로서 우리는 이러한 도구의 장단점을 이해하고, 애플리케이션의 특성에 맞게 적절히 활용해야 합니다. useEffect
를 통한 부수 효과 관리는 React 애플리케이션의 성능과 유지보수성을 크게 향상시킬 수 있지만, 항상 그 사용을 신중히 고려해야 합니다.
React의 지속적인 발전과 함께, 우리도 이러한 Hook들의 사용법과 최적화 기법들을 계속해서 학습하고 적용해 나가야 할 것입니다.
이야... 대단하시군요 react도 분석하시고