STUDY NOTE · 개념 정리
React 컴포넌트 생명주기 정리: 클래스 메서드와 함수형 컴포넌트의 대응
Frontend
생명주기는 컴포넌트가 생성(마운트) → 갱신(업데이트) → 제거(언마운트) 되는 과정이다. 클래스 컴포넌트는 단계마다 메서드를 두고, 함수형 컴포넌트는 Hook으로 같은 시점을 표현한다.
1. 세 단계
| 단계 | 언제 | 대표적으로 하는 일 |
| 마운트 (Mount) | 컴포넌트가 처음 DOM에 붙을 때 | 초기 데이터 요청, 구독 시작, 타이머 등록 |
| 업데이트 (Update) | props·state·context가 바뀌어 다시 렌더될 때 | 바뀐 값에 맞춰 외부와 다시 동기화 |
| 언마운트 (Unmount) | DOM에서 제거될 때 | 구독 해제, 타이머 정리, 요청 취소 |
에러가 났을 때를 위한 에러 처리 단계도 따로 있다(Error Boundary).
2. 업데이트가 일어나는 원인
| 원인 | 설명 |
| state 변경 | setState / useState의 setter 호출 |
| 부모 리렌더 | 부모가 렌더되면 자식도 렌더 (props가 같아도) |
| context 변경 | 구독 중인 Provider의 value가 바뀜 |
| forceUpdate | 클래스에서 강제 렌더 (거의 쓰지 않음) |
props가 바뀌어서 렌더되는 게 아니라, 부모가 렌더되면 자식이 렌더된다는 점이 중요하다. props 비교로 건너뛰려면 React.memo를 쓴다.
3. Render 단계와 Commit 단계
| 구분 | Render 단계 | Commit 단계 |
| 하는 일 | 컴포넌트 함수(render) 호출, 이전 결과와 비교 | 바뀐 부분을 실제 DOM에 반영 |
| 순수해야 하나 | 예 (부수효과 금지) | 부수효과 가능 |
| 중단·재시작 | 가능 (동시성 렌더링) | 불가 (한 번에 끝까지) |
| 여기서 실행되는 것 | constructor, render, getDerivedStateFromProps, shouldComponentUpdate | componentDidMount/Update, useLayoutEffect, (이후) useEffect |
Render 단계는 React가 여러 번 실행하거나 중간에 버릴 수 있다. 그래서 렌더 중에 API 호출이나 DOM 조작 같은 부수효과를 넣으면 안 된다.
4. 클래스 컴포넌트 생명주기 메서드
| 단계 | 메서드 (호출 순서) | 용도 |
| 마운트 | constructor | state 초기화, 메서드 바인딩 |
| 마운트 | static getDerivedStateFromProps | props로 state를 맞춰야 할 때 (드묾) |
| 마운트 | render | JSX 반환 (순수 함수) |
| 마운트 | componentDidMount | DOM 접근, 데이터 요청, 구독 |
| 업데이트 | static getDerivedStateFromProps | 위와 동일 |
| 업데이트 | shouldComponentUpdate | false면 렌더 생략 (최적화) |
| 업데이트 | render | 다시 렌더 |
| 업데이트 | getSnapshotBeforeUpdate | DOM 변경 직전 값 기록 (스크롤 위치 등) |
| 업데이트 | componentDidUpdate | 변경 후 처리, 이전 props와 비교해 재요청 |
| 언마운트 | componentWillUnmount | 정리 작업 |
| 에러 | static getDerivedStateFromError | 에러 시 대체 UI용 state 설정 |
| 에러 | componentDidCatch | 에러 로깅 |
componentWillMount, componentWillReceiveProps, componentWillUpdate는 동시성 렌더링에서 여러 번 호출될 수 있어 위험하다는 이유로 UNSAFE_ 접두사가 붙은 레거시가 됐다.
5. 함수형 컴포넌트에서의 대응
| 클래스 | 함수형 |
| constructor | useState 초기값 (무거우면 useState(() => 계산)) |
| render | 함수 본문 자체 |
| componentDidMount | useEffect(fn, []) |
| componentDidUpdate | useEffect(fn, [의존성]) |
| componentWillUnmount | useEffect가 반환하는 cleanup |
| shouldComponentUpdate | React.memo (+ useMemo, useCallback) |
| getSnapshotBeforeUpdate | 대응 Hook 없음 (useLayoutEffect + ref로 흉내) |
| getDerivedStateFromProps | 렌더 중 계산, 또는 key로 초기화 |
| Error Boundary | 대응 Hook 없음 (클래스로 작성해야 함) |
함수형에서는 "언제 실행할까"보다 "무엇과 동기화할까"로 생각한다. useEffect 하나가 마운트·업데이트·언마운트를 의존성 배열로 함께 표현한다.
6. 실행 순서 (부모-자식)
| 시점 | 순서 |
| 렌더 | 부모 render → 자식 render (위에서 아래로) |
| 마운트 완료 | 자식 componentDidMount / useEffect → 부모 (아래에서 위로) |
| 언마운트 | 부모 cleanup → 자식 cleanup (위에서 아래로) |
자식이 먼저 DOM에 붙어야 부모가 완성되므로 마운트 완료 콜백은 자식부터 실행된다.
7. 자주 헷갈리는 포인트
- key가 바뀌면 리렌더가 아니라 재마운트: 같은 위치라도 key가 다르면 React는 다른 컴포넌트로 보고 언마운트 후 새로 마운트한다. state가 초기화된다.
- StrictMode의 이중 실행: 개발 모드에서 마운트 → 언마운트 → 마운트를 한 번 더 해서 cleanup 누락을 드러낸다. 프로덕션에서는 한 번만 실행된다.
- Error Boundary가 잡지 못하는 것: 이벤트 핸들러 안의 에러, 비동기 코드(setTimeout, Promise)의 에러, 서버 렌더링 에러, Error Boundary 자신의 에러.
8. 실제로 어떻게 적용되나
- 목록 페이지 데이터 요청: 마운트 시 요청하고, 페이지 번호가 바뀌면(업데이트) 다시 요청하고, 언마운트 시 진행 중인 요청을 취소한다. 함수형에서는 의존성이 [page]인 useEffect 하나와 cleanup으로 세 단계를 모두 처리한다.
- 채팅 스크롤 유지: 새 메시지가 위에 추가될 때 스크롤 위치를 보존하려면 DOM이 바뀌기 직전 높이를 기록해야 한다. 클래스의 getSnapshotBeforeUpdate, 함수형에서는 useLayoutEffect + ref를 쓰는 대표 사례다.
- 선택한 사용자가 바뀌면 폼 초기화: props 변화를 감지해 state를 초기화하는 대신
<Form key={userId} />로 재마운트시킨다. - 화면 일부의 에러 격리: 위젯 하나에서 에러가 나도 페이지 전체가 하얗게 되지 않도록 그 영역을 Error Boundary로 감싼다.
9. 면접 질문으로 정리
- Q. React 생명주기를 설명해주세요. 마운트, 업데이트, 언마운트 세 단계가 있고, 클래스는 componentDidMount·componentDidUpdate·componentWillUnmount 같은 메서드로, 함수형은 useEffect와 cleanup으로 각 시점의 작업을 처리한다.
- Q. Render 단계와 Commit 단계의 차이는? Render 단계는 컴포넌트를 호출해 바뀔 내용을 계산하는 순수한 단계로 중단·재시작될 수 있고, Commit 단계는 실제 DOM에 반영하고 부수효과를 실행하는 단계다.
- Q. componentDidMount와 useEffect(fn, [])는 완전히 같나? 실행 시점이 다르다. componentDidMount는 DOM 반영 직후 동기로 실행되어 useLayoutEffect에 가깝고, useEffect는 보통 화면을 그린 뒤 실행된다.
- Q. 리렌더링과 재마운트의 차이는? 리렌더링은 같은 인스턴스가 다시 렌더되어 state가 유지되고, 재마운트는 기존 컴포넌트를 제거하고 새로 만들어 state가 초기화된다. key 변경이 대표적인 재마운트 원인이다.