← 개인 공부 · Frontend
STUDY NOTE · 개념 정리

React 이펙트 정리: useEffect vs useLayoutEffect vs useInsertionEffect와 생명주기

Frontend
이펙트는 "렌더링 결과를 React 바깥 세계(DOM, 네트워크, 타이머)와 맞추는 코드"다. 종류마다 다른 건 실행 시점 하나이고, 그 시점이 화면 깜빡임과 성능을 결정한다.

1. 렌더링과 부수 효과(side effect)

React 컴포넌트는 props와 state를 받아 JSX를 돌려주는 순수 함수여야 한다. 같은 입력이면 같은 출력, 그리고 실행 중에 바깥을 바꾸지 않는다. 하지만 실제 앱은 서버에서 데이터를 받고, 타이머를 돌리고, DOM 크기를 재야 한다. 이런 부수 효과를 렌더링과 분리해서 "렌더링이 끝난 뒤" 실행하도록 맡기는 장치가 이펙트다.

2. 컴포넌트 생명주기

단계의미함수형에서
마운트 (Mount)컴포넌트가 처음 화면에 나타남의존성 배열 [] 인 이펙트가 실행
업데이트 (Update)props / state가 바뀌어 다시 렌더의존성 값이 바뀐 이펙트가 cleanup 후 재실행
언마운트 (Unmount)화면에서 사라짐모든 이펙트의 cleanup 실행

클래스 컴포넌트의 메서드와 대응시키면 다음과 같다.

클래스 컴포넌트함수형 컴포넌트
constructoruseState 초기값
render함수 본문
componentDidMountuseEffect(fn, [])
componentDidUpdateuseEffect(fn, [deps])
componentWillUnmount이펙트가 반환하는 cleanup 함수
shouldComponentUpdateReact.memo
getSnapshotBeforeUpdate대응 훅 없음 (useLayoutEffect + ref로 흉내)

3. 한 번의 업데이트가 화면에 그려지기까지

  1. Render 단계: 컴포넌트 함수를 호출해 새 가상 DOM을 만들고 이전과 비교한다. 화면은 아직 그대로다.
  2. Commit 단계: 바뀐 부분만 실제 DOM에 반영한다.
  3. Paint: 브라우저가 화면을 그린다.
  4. 그 이후: useEffect 실행.

이펙트 세 가지는 이 타임라인에서 실행되는 위치가 다르다.

4. 세 가지 이펙트 비교

구분useInsertionEffectuseLayoutEffectuseEffect
실행 시점DOM 변경 전DOM 변경 후, 페인트 전페인트 후
동기/비동기동기동기 (페인트를 막음)비동기 (페인트를 막지 않음)
ref 사용아직 연결 안 됨가능가능
주 용도CSS-in-JS 라이브러리의 style 삽입DOM 측정 후 위치·크기 보정데이터 요청, 구독, 타이머, 로깅
서버(SSR)에서실행 안 됨실행 안 됨실행 안 됨
사용 빈도앱 코드에선 거의 없음필요할 때만기본값

5. useEffect

useEffect(() => {
// 1) 이펙트: 렌더 결과가 화면에 반영된 뒤 실행
const id = setInterval(tick, 1000);
// 2) cleanup: 다음 이펙트 실행 직전 / 언마운트 시 실행
return () => clearInterval(id);
}, [/* 3) 의존성 배열 */]);
의존성 배열실행 시점
생략매 렌더마다
[]마운트 시 한 번 (cleanup은 언마운트 시)
[a, b]마운트 시 + a 또는 b가 바뀔 때마다

의존성은 Object.is로 비교한다. 객체·배열·함수는 렌더마다 새로 만들어지면 "바뀐 것"으로 판단된다.

6. useLayoutEffect

DOM이 바뀐 직후, 브라우저가 그리기 전에 동기로 실행된다. 그래서 여기서 state를 바꾸면 사용자는 중간 상태를 보지 못하고 최종 결과만 본다.

  1. 같은 작업을 useEffect로 하면: 잘못된 위치로 한 번 그려짐 → 이펙트에서 보정 → 다시 그려짐 (깜빡임)
  2. useLayoutEffect로 하면: 보정까지 끝난 뒤 한 번만 그려짐

대가는 성능이다. 이 안의 작업이 끝날 때까지 화면 표시가 지연된다. 깜빡임이 실제로 보일 때만 쓴다.

7. SSR에서 안전한 레이아웃 이펙트

서버에는 DOM도 페인트도 없어서 레이아웃 이펙트가 실행되지 않는다. React 18까지는 서버 렌더 중 useLayoutEffect를 만나면 경고를 출력했기 때문에, 라이브러리들은 환경에 따라 훅을 바꿔 끼우는 패턴을 쓴다. 이름은 useIsomorphicLayoutEffect, useSafeLayoutEffect 등으로 다양하다.

export const useSafeLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;

이 패턴은 경고를 없앨 뿐, 서버 HTML을 측정된 상태로 만들어주지는 않는다. 서버 HTML은 측정 전 상태로 나가고 하이드레이션 후에 보정된다.

8. 함께 알아둘 것: useEffectEvent

이펙트 안에서 읽기만 하고 반응하고 싶지는 않은 값이 있을 때 쓴다. React 19.2에서 정식 기능이 됐다.

구분의존성 배열에 넣은 값useEffectEvent 안에서 읽은 값
값이 바뀌면이펙트가 재실행됨재실행 안 됨
읽는 값이펙트가 실행된 시점의 값항상 최신 값

9. 실제로 어떻게 적용되나

  1. 데이터 요청 (useEffect): 페이지 번호가 바뀔 때마다 요청하고, cleanup에서 이전 요청을 취소한다. 취소하지 않으면 늦게 도착한 이전 응답이 새 결과를 덮어쓰는 경쟁 상태가 생긴다. "의존성 변경 → cleanup → 재실행" 순서를 이용한 것이다.
  2. 툴팁 위치 (useLayoutEffect): 툴팁 높이를 재서 아래 공간이 부족하면 위로 띄운다. 측정과 보정이 페인트 전에 끝나므로 툴팁이 튀지 않는다.
  3. 이벤트 리스너 (useEffect + cleanup): resize 리스너를 등록하고 cleanup에서 해제한다. 해제하지 않으면 페이지를 떠나도 리스너가 남아 메모리 누수가 된다.
  4. 채팅방 연결 (useEffectEvent): roomId가 바뀔 때만 재연결하고, 알림에 쓰는 테마 값은 최신 값을 읽기만 한다.

10. 자주 헷갈리는 포인트

  1. StrictMode에서 이펙트가 두 번 실행된다: 개발 모드에서만 마운트 → cleanup → 마운트를 일부러 한 번 더 한다. cleanup이 제대로 되어 있는지 검사하는 것이다. 두 번 실행돼서 깨진다면 cleanup이 빠진 것이다.
  2. 이펙트가 필요 없는 경우: props나 state로 계산할 수 있는 값을 state에 복사하려고 이펙트를 쓰지 않는다. 렌더 중에 계산하면 된다. 사용자 클릭에 대한 반응은 이벤트 핸들러에서 처리한다.
  3. cleanup은 언마운트에만 실행되는 게 아니다: 의존성이 바뀌어 재실행되기 직전에도 실행된다.

11. 면접 질문으로 정리

  1. Q. useEffect와 useLayoutEffect의 차이는? useEffect는 브라우저가 화면을 그린 뒤 비동기로 실행되고, useLayoutEffect는 DOM 변경 직후 그리기 전에 동기로 실행된다. DOM을 측정해서 바로 보정해야 깜빡임이 없는 경우에 useLayoutEffect를 쓰고, 나머지는 useEffect를 쓴다.
  2. Q. cleanup 함수는 언제 실행되나? 언마운트될 때, 그리고 의존성이 바뀌어 이펙트가 다시 실행되기 직전에 실행된다.
  3. Q. 의존성 배열을 비우면? 마운트 시 한 번만 실행되고 cleanup은 언마운트 시 실행된다. 생략하면 매 렌더마다 실행된다.
  4. Q. 개발 모드에서 이펙트가 두 번 실행되는 이유는? StrictMode가 cleanup 누락을 찾기 위해 마운트-언마운트-마운트를 한 번 더 수행하기 때문이며, 프로덕션에서는 한 번만 실행된다.
Gunmo Lee
React 이펙트 정리: useEffect vs useLayoutEffect vs useInsertionEffect와 생명주기 | Gunmo's Dev Life