React 함수형 생명주기
React 18 공식 문서를 바탕으로 함수형 컴포넌트 생명주기와 훅 타이밍을 정리합니다.
1. 함수형 생명주기: Render → Commit → Effect
React Docs – Render and Commit에서 설명하듯 React 18은 아래 세 단계로 동작합니다.

- Render Phase: 컴포넌트 함수가 호출되어 화면에 표시할 내용을 계산합니다.
순수 계산이어야 하며 DOM 변경이나 네트워크 요청은 허용되지 않습니다. - Commit Phase: React가 실제 DOM 노드를 변경합니다.
초기 렌더링은 모든 노드를 추가(appendChild)하고, 재렌더링 시에는 차이가 있는 부분만 최소한으로 수정합니다. - Effect Phase: Commit 이후
useInsertionEffect → useLayoutEffect → paint → useEffect순으로 cleanup과 실행이 이어집니다.
2. 훅으로 매핑하며 순수성 지키기
interface Profile {
id: string;
name: string;
}
function ProfileCard({ userId }: { userId: string }) {
const [profile, setProfile] = useState<Profile | null>(null);
// CSS-in-JS 라이브러리에서만 사용 (일반적으로는 필요 없음)
useInsertionEffect(() => {
// DOM 변형 직전에 <style> 태그 주입
}, []);
useLayoutEffect(() => {
measureCard(); // DOM 패치 직후이므로 레이아웃 읽기 OK
}, []);
useEffect(() => {
const controller = new AbortController();
fetchProfile(userId, controller.signal).then(setProfile);
return () => controller.abort();
}, [userId]);
return <Card>{profile ? profile.name : "Loading..."}</Card>;
}useInsertionEffect는 DOM 변형 직전에 동기 실행되며, CSS-in-JS 라이브러리가 스타일을 주입할 때 사용합니다. 일반 애플리케이션 코드에서는 거의 사용하지 않습니다.useLayoutEffect는 Commit 직후 동기 실행되므로 레이아웃 측정/DOM 스냅샷에 사용합니다.useEffect는 paint 이후 비동기 실행됩니다. 네트워크, 이벤트 구독, 타이머 등 사용자에게 노출된 뒤 실행해도 되는 작업을 맡깁니다.cleanup은 이전 렌더의 클로저를 기준으로 실행됩니다. 상태 1 → 2 변화 시 cleanup은 1을 들고 실행된 뒤 새 effect가 2를 참조합니다.- 의존성 배열은
Object.is비교를 사용하므로, 객체를 deps로 넘길 때는 memoization을 통해 참조를 안정화해야 합니다.
3. React 18의 동시성과 생명주기
React 18의 동시성 렌더링은 생명주기 관점에서 중요한 변화를 가져옵니다.
- 렌더링이 여러 번 시작되지만, 실제로 커밋되는 것은 한 번입니다.
useTransition과useDeferredValue는 커밋 우선순위를 조절합니다.- Effect는 커밋 이후에만 실행되므로, 중단된 렌더에서는 effect가 실행되지 않습니다.
function Counter() {
const [count, setCount] = useState(0);
// ❌ 렌더링 중 로깅: startTransition 사용 시 여러 번 출력됨
console.log('렌더됨:', count);
// ✅ Effect 로깅: 실제 커밋된 시점만 출력됨
useEffect(() => {
console.log('커밋됨:', count);
}, [count]);
return <div>{count}</div>;
}4. 실헝용 컴포넌트를 이용하여 타이밍 시각화
아래 실험용 컴포넌트는 useLayoutEffect, useEffect, cleanup, startTransition 로그를 모두 남깁니다.
Strict Mode 개발 서버를 켜면 공식 문서에서 설명한 패턴을 그대로 재현할 수 있습니다.
count: 0
커밋 이후 훅 동작
Child count: 0
로그
버튼을 눌러 commit/cleanup 타이밍을 관찰하세요.
관찰 포인트
- 동기 업데이트 시 React는 트리를 깊이 우선 탐색하므로 Child가 Parent보다 먼저 실행됩니다.
Render intent (렌더링 시작)
↓
Child useLayoutEffect cleanup
↓
Child useLayoutEffect
↓
Parent useLayoutEffect
↓
(paint - 화면에 그림)
↓
Child useEffect cleanup
↓
Child useEffect
↓
Parent useEffect- Transition 업데이트:
startTransition버튼을 누르면 count 옆에 (전환 중) 텍스트가 나타납니다. 이것이isPending플래그가true인 상태이며, 커밋이 백그라운드에서 준비되고 있음을 의미합니다. - 자식 토글:
showChild를 끄면 cleanup 로그가 바로 찍힌 뒤, 다시 켤 때 layout/effect가 새로 실행됩니다. - 로그 초기화: Strict Mode 이중 실행으로 헷갈릴 땐 로그를 지우고 다시 실험하면 됩니다.
5. Strict Mode와 생명주기
개발 환경에서 Strict Mode는 생명주기를 의도적으로 두 번 실행합니다.
function Timer() {
useEffect(() => {
console.log('Effect 실행');
const id = setInterval(() => console.log('tick'), 1000);
return () => {
console.log('Cleanup 실행');
clearInterval(id);
};
}, []);
}
// 개발 환경 콘솔 출력:
// Effect 실행
// Cleanup 실행 ← Strict Mode가 추가
// Effect 실행 ← Strict Mode가 다시 실행
// 프로덕션 환경 콘솔 출력:
// Effect 실행 ← 한 번만 실행왜 이렇게 하는가?
- Cleanup 로직이 누락되었는지 검증하기 위함입니다.
- 위 예시에서 cleanup이 없으면 타이머가 2개 생성되어 문제를 발견할 수 있습니다.
- Effect가 여러 번 실행되어도 문제없도록 코드를 작성하도록 유도합니다.
생명주기 관점:
1. 마운트 → Effect 실행
2. (Strict Mode) 언마운트 시뮬레이션 → Cleanup 실행
3. (Strict Mode) 재마운트 시뮬레이션 → Effect 재실행이는 React가 향후 컴포넌트를 보존했다가 복원하는 기능을 위한 준비입니다.
6. 핵심 원칙
생명주기의 3단계:
- Render: 순수 계산, DOM 접근 금지
- Commit: DOM 변경,
useLayoutEffect실행 - Effect: 화면 표시 후
useEffect실행
훅 선택 기준:
useLayoutEffect: DOM 측정, 깜빡임 방지 (동기, paint 차단)useEffect: 네트워크, 구독, 타이머 등 대부분의 작업 (비동기)
Cleanup 원칙:
- Effect에서 시작한 것은 cleanup에서 멈춰야 합니다.
- Cleanup은 이전 렌더의 클로저를 참조합니다.
- Strict Mode는 cleanup 누락을 검증합니다.
디버깅:
- 컴포넌트 함수 내 로그 = 렌더 횟수 (중단된 렌더 포함)
- Effect 내 로그 = 커밋 횟수 (실제 화면 변화)