[React] 렌더 단계와 커밋 단계란?
리액트가 화면을 갱신하는 트리거 → 렌더 → 커밋 세 단계와, 렌더링이 순수해야 하는 이유를 정리한다.
📖 들어가며
리액트에서 "렌더링"이라는 말은 생각보다 자주 오해된다. 화면에 무언가 그려지는 순간을 떠올리기 쉽지만, 리액트가 말하는 렌더링은 조금 다르다.
리액트 앱에서 화면이 바뀌는 과정은 항상 세 단계로 설명할 수 있다. 공식 문서는 이를 레스토랑에 비유한다. 손님의 주문을 주방에 전달하고(트리거), 요리를 준비하고(렌더), 테이블에 내놓는(커밋) 순서다. 오늘은 각 단계에서 리액트가 무엇을 하는지 정리해보겠다!
1단계: 트리거(Trigger)
렌더링이 시작되는 경우는 두 가지다. 앱이 처음 뜰 때 createRoot(domNode).render(<App />)이 호출되는 초기 렌더링, 그리고 useState의 set 함수가 호출돼서 해당 컴포넌트가 렌더링 대기열에 들어가는 리렌더링이다. Next.js 같은 프레임워크를 쓰면 첫 번째 코드는 직접 볼 일이 거의 없다.
여러 set 함수가 연달아 호출되면 리액트는 이를 모아 한 번의 렌더로 처리한다. 이것을 **배칭(Batching)**이라고 하며, 렌더가 시작되기 전인 이 단계에서 일어난다. React 18에서 배칭이 어떻게 달라졌는지는 따로 정리할 예정!
2단계: 렌더(Render)
리액트에서 렌더링은 화면을 그리는 것이 아니라 컴포넌트 함수를 호출하는 것이다.
- 초기 렌더링에서는 루트 컴포넌트를 호출한다.
- 리렌더링에서는 state가 바뀐 컴포넌트를 호출한다.
- 호출 결과에 다른 컴포넌트가 포함되어 있으면 그 컴포넌트도 호출한다. 이 과정은 트리 끝까지 재귀적으로 이어진다.
호출 결과로 새로운 리액트 엘리먼트 트리(가상 돔)가 만들어진다. 리렌더링이라면 리액트는 새 트리와 이전 트리를 비교해 무엇이 달라졌는지 계산한다. 이 비교가 재조정(Reconciliation)이다.
중요한 점은 렌더 단계에서는 DOM을 건드리지 않는다는 것이다. 무엇을 바꿔야 하는지 계산만 하고, 실제 변경은 다음 단계에서 일어난다.
렌더링은 순수해야 한다
렌더링이 계산 단계이기 때문에 컴포넌트 함수는 **순수(pure)**해야 한다.
- 같은 입력(props, state, context)에는 항상 같은 JSX를 반환해야 한다.
- 렌더링 전에 존재하던 객체나 변수를 렌더링 중에 바꾸면 안 된다.
이 규칙을 지키지 않으면 렌더가 몇 번 일어나는지에 따라 결과가 달라지는 버그가 생긴다. 그래서 Strict Mode는 개발 환경에서 각 컴포넌트를 두 번 호출해 이런 문제를 미리 드러낸다.
부모가 렌더되면 자식도 렌더된다
리렌더링에서 리액트는 state가 바뀐 컴포넌트와 그 아래에 있는 모든 컴포넌트를 호출한다. 자식의 props가 바뀌지 않았어도 기본적으로 호출된다. 헤더에 있는 카운터의 state가 바뀌면 헤더 아래의 메뉴, 로고 컴포넌트까지 다시 호출되는 식이다.
"변경된 부분만 렌더된다"는 말은 커밋 단계 얘기지 렌더 단계 얘기가 아니다. 렌더 단계의 비용을 줄이는 건 재조정이 아니라 memo, useMemo, state 위치 조정 같은 별도의 최적화다.
3단계: 커밋(Commit)
렌더 단계에서 계산한 결과를 실제 DOM에 적용하는 단계다. 초기 렌더링이라면 appendChild()로 모든 DOM 노드를 만들어 붙이고, 리렌더링이라면 계산된 최소한의 변경만 DOM에 적용한다.
리액트는 렌더링 결과가 이전과 다른 경우에만 DOM 노드를 변경한다. 매초 시간을 갱신하는 컴포넌트를 보자.
function Clock({ time }) {
return (
<>
<h1>{time}</h1>
<input />
</>
);
}매초 Clock이 다시 호출되지만, 커밋 단계에서 실제로 바뀌는 것은 <h1>의 텍스트뿐이다. <input>은 이전과 같은 위치에 같은 타입으로 있으므로 리액트가 건드리지 않고, 사용자가 입력 중이던 텍스트도 사라지지 않는다.
커밋 단계는 동기적으로 한 번에 끝난다. 렌더 결과를 조금씩 나눠 반영하지 않기 때문에 사용자는 중간 상태를 보지 않는다.
에필로그: 브라우저 페인트
커밋으로 DOM이 바뀌면 브라우저가 화면을 다시 그린다. 이건 리액트의 단계가 아니라 브라우저의 작업이다. 공식 문서는 "렌더링"이라는 말과 섞이지 않도록 이 단계를 **페인팅(Painting)**이라고 부른다. 브라우저가 실제로 어떤 순서로 그리는지는 브라우저 렌더링 글에서 따로 정리할 예정!
정리
| 단계 | 하는 일 | DOM을 건드리나? |
|---|---|---|
| 트리거 | 렌더링 대기열에 등록하고, 여러 업데이트를 묶는다 | 아니오 |
| 렌더 | 컴포넌트를 호출해 새 엘리먼트 트리를 만들고, 이전 트리와 비교한다 | 아니오 |
| 커밋 | 계산된 차이를 DOM에 적용한다 | 예 |
| (페인트) | 브라우저가 화면을 다시 그린다 | 리액트 밖의 일 |
관련 글
- 가상 돔(Virtual DOM)이란?
- 재조정(Reconciliation)
- 브라우저 렌더링 파이프라인 (작성 예정)
- React 18의 Automatic Batching (작성 예정)