훅의 생명주기와 부수 효과
훅의 실행 타이밍을 마운트, 리렌더, 언마운트 기준으로 정리합니다.
학습 목표
마운트,언마운트,리렌더가 무엇인지 정확히 구분할 수 있습니다.useEffect와useLayoutEffect의 동작 타이밍을 설명할 수 있습니다.- 글로벌 이벤트와 타이머를 안전하게 등록/해제할 수 있습니다.
- React 18에서 상태 업데이트가 배칭되는 이유를 설명할 수 있습니다.
1) 문맥: 렌더 파이프라인을 알아야 훅을 이해할 수 있다
React는 상태 변경이 발생하면 먼저 렌더에서 다음 상태 기반 UI 계산을 하고,
그 다음 커밋에서 DOM을 반영합니다.
렌더단계: 훅 실행 + 상태 계산(순수)커밋단계: DOM 변경, ref/이벤트 바인딩브라우저 페인트: 픽셀 그리기
부수 효과는 렌더 계산 바깥에서 다룹니다. 즉, 렌더는 “계산”, effect는 “동기화”입니다.
2) 마운트 / 언마운트란 무엇인가
마운트
컴포넌트가 트리에 처음 들어가고 React가 그 인스턴스를 관리하기 시작하는 시점입니다.
useEffect,useLayoutEffect의 setup이 실행될 준비가 생깁니다.
언마운트
컴포넌트가 트리에서 제거되어 React가 더 이상 관리하지 않는 시점입니다.
- 리스너/타이머/구독 같은 외부 리소스 정리가 반드시 필요합니다.
리렌더
상태나 props 변화로 같은 컴포넌트가 다시 렌더링되는 업데이트 경로입니다.
- 마운트/언마운트와 달리 인스턴스는 유지됩니다.
3) useEffect의 타이밍
- 렌더 계산이 끝나고 커밋 후에 실행 스케줄됨
- 마운트 시 초기 실행
- 의존성 변경 시
- 이전 effect cleanup
- 새 effect 실행
- 언마운트 시 마지막 cleanup 실행
cleanup은 부수효과의 “마무리 장치”입니다. 시작한 동작은 반드시 정리해야 합니다.
useLayoutEffect는 DOM 변경 직후, paint 이전에 실행되어
grid/position 측정처럼 DOM 의존 동작에 맞습니다.
일반적인 동기화는 useEffect를 기본으로 둡니다.
4) 실무 패턴: 이벤트와 타이머 정리
window 이벤트/타이머는 컴포넌트가 화면에서 사라져도 계속 살아남을 수 있어,
정리가 없으면 메모리 누수와 예기치 않은 setState 호출이 생깁니다.
function LayoutWatch() {
const [width, setWidth] = useState(0)
const [resizeCount, setResizeCount] = useState(0)
useEffect(() => {
const onResize = () => {
setWidth(window.innerWidth)
setResizeCount((count) => count + 1)
}
onResize()
const timerId = setInterval(() => {
setResizeCount((count) => count + 1)
}, 1000)
window.addEventListener('resize', onResize)
return () => {
window.removeEventListener('resize', onResize)
clearInterval(timerId)
}
}, [])
return (
<div>
width: {width}, resize events: {resizeCount}
</div>
)
}핵심은 addEventListener를 추가했다면 removeEventListener를, setInterval을 시작했다면 clearInterval을 꼭 맞춰주는 것입니다.
5) React 18 배칭: 여러 상태 업데이트를 한 번에 반영하기
batching은 여러 상태 업데이트를 합쳐서 렌더링 횟수를 줄여주는 동작입니다.
React 18부터는 이벤트 핸들러뿐 아니라 비동기 경로에서도 더 넓게 적용됩니다.
- 같은 함수/핸들러 안에서의 여러
setState는 한 번 렌더에 묶일 수 있습니다. setTimeout, Promise then, effect 내부의 연속 업데이트도 배칭 대상이 됩니다.
function Counter() {
const [a, setA] = useState(0)
const [b, setB] = useState(0)
const onClick = () => {
setA((prev) => prev + 1)
setB((prev) => prev + 1)
// 보통 한 번 렌더로 반영
}
useEffect(() => {
const timer = setTimeout(() => {
setA((prev) => prev + 1)
setB((prev) => prev + 1)
// 여기도 배칭되어 커밋 횟수를 줄임
}, 0)
return () => clearTimeout(timer)
}, [])
return <button onClick={onClick}>{a + b}</button>
}flushSync를 쓰면 배칭을 의도적으로 끊고 즉시 렌더링을 강제로 만들 수 있습니다.
필요할 때만 제한적으로 사용하세요.
6) 운영 체크리스트
useEffect의 setup/cleanup이 반드시 한 쌍인가?- 의존성 배열이 의도하지 않은 무한 루프를 만들지 않나?
- 마운트-언마운트 경계에서 이벤트/타이머/구독이 정리되는가?
- 언마운트 뒤에도 state 업데이트가 시도되지 않도록 되어 있는가?
- 같은 함수 안 여러 상태 업데이트가 언제 배칭되는지 설명 가능한가?
핵심 요약
- 훅의 정확한 사용은 렌더 파이프라인(렌더-커밋)을 이해할 때 시작됩니다.
- 마운트는 시작, 언마운트는 정리로 기억하면 정돈됩니다.
- 전역 이벤트/타이머는 반드시 cleanup을 작성해야 메모리 누수를 줄일 수 있습니다.
- React 18 배칭은 여러 상태 갱신을 묶어 렌더 성능을 개선합니다.