Published on

React의 Render와 Commit 단계 이해하기

Authors
  • avatar
    Name
    GW

개요

React를 사용하다 보면 이런 말을 듣습니다.

Render 단계에서는 DOM을 변경하지 않습니다.

Commit 단계에서 실제 DOM이 변경됩니다.

그런데 Render와 Commit이 정확히 무엇을 의미하는지 헷갈리는 경우가 많습니다.

많은 개발자가 Render를 화면을 그리는 과정이라고 생각하지만, 실제로는 조금 다릅니다.

React에서 Render는 어떤 화면을 보여줄지 계산하는 과정이고,

Commit은 그 계산 결과를 실제 DOM에 반영하는 과정입니다.

이 둘을 구분해서 이해하면

  • React가 화면을 어떻게 업데이트하는지
  • 왜 useEffect는 Commit 이후에 실행되는지
  • 왜 Fiber가 Render를 중단할 수 있는지

까지 자연스럽게 이해할 수 있습니다.

이번 글에서는 React의 Render와 Commit 단계가 각각 어떤 역할을 하는지 알아보겠습니다.

state가 변경되면 무슨 일이 일어날까?

다음 코드를 살펴보겠습니다.

const Counter = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount((prev) => prev + 1);
  };

  return <button onClick={handleClick}>{count}</button>;
};

버튼을 클릭하면 count가 증가합니다.

겉으로 보기에는 버튼의 숫자만 바뀌는 것처럼 보이지만, React 내부에서는 여러 단계의 작업이 순서대로 진행됩니다.

조금 단순하게 표현하면 다음과 같습니다.

state 변경
↓
Render
↓
Commit
↓
브라우저 화면 업데이트

React 공식 문서에서는 이 과정을 요리사가 음식을 준비하고 손님에게 전달하는 과정에 비유합니다.

React의 Trigger, Render, Commit 단계
  • Trigger: 주방에 주문이 전달됨 → 화면 업데이트가 필요한 상황이 발생
  • Render: 주방에서 요리를 준비함 → 컴포넌트를 실행하여 새로운 화면을 계산
  • Commit: 완성된 요리를 테이블에 전달함 → 계산된 변경 사항을 실제 DOM에 반영

여기서 Trigger는 Render가 시작되는 계기를 의미합니다. 초기 렌더링이나 state 업데이트 등이 발생하면 Render가 Trigger됩니다.

React는 state가 변경되었다고 해서 곧바로 DOM을 수정하지 않습니다.

먼저 어떤 화면이 되어야 하는지 계산(Render) 하고, 그 결과를 실제 DOM에 반영(Commit) 합니다.

이처럼 React는 화면을 업데이트하는 과정을 Render와 Commit으로 나누어 관리합니다.

Render 단계란 무엇일까?

Render 단계는 새로운 화면을 계산하는 과정입니다.

여기서 중요한 점은

Render 단계에서는 실제 DOM을 변경하지 않습니다.

예를 들어 state가 변경되면 React는 컴포넌트 함수를 다시 실행합니다.

const Counter = () => {
  const [count, setCount] = useState(0);

  console.log('Render');

  return <div>{count}</div>;
};

컴포넌트 함수가 다시 실행되면 React는 새로운 Virtual DOM을 만들고, 이전 Virtual DOM과 비교하여 어떤 부분이 변경되었는지 계산합니다.

조금 단순하게 표현하면 다음과 같습니다.

state 변경
↓
컴포넌트 실행
↓
새로운 Virtual DOM 생성
↓
이전 Virtual DOM과 비교

여기까지가 Render 단계입니다.

아직 브라우저 화면은 아무것도 바뀌지 않았습니다.

즉, Render는 화면을 그리는 과정이 아니라,

어떤 화면을 보여줘야 하는지 계산하는 과정

입니다.

Commit 단계란 무엇일까?

Render 단계가 끝나면 React는 어떤 부분이 변경되어야 하는지 이미 계산을 마친 상태입니다.

이제 그 계산 결과를 실제 DOM에 반영하는 단계가 시작됩니다.

이 과정을 Commit 단계라고 합니다.

예를 들어 버튼의 숫자가 0에서 1로 변경되었다면,

Render 단계에서는 "버튼의 텍스트를 1로 변경해야 한다." 라는 계산만 수행합니다.

그리고 Commit 단계에서 실제 DOM을 수정합니다.

조금 단순하게 표현하면 다음과 같습니다.

Render (변경할 내용 계산 완료)
↓
Commit (실제 DOM 변경)

즉, Render는 계산이고, Commit은 실제 반영입니다.

Commit 이후에는 무슨 일이 일어날까?

Commit 단계에서 DOM이 변경되었다고 해서 모든 작업이 끝난 것은 아닙니다.

DOM이 변경되면 브라우저는 필요한 렌더링 작업을 수행하고, React는 이후 Effect와 같은 작업을 처리합니다.

일반적인 흐름을 단순화해서 표현하면 다음과 같습니다.

state 변경
↓
Render
↓
Commit
(실제 DOM 변경)
↓
브라우저 렌더링
(Style → Layout → Paint → Composite)
↓
화면에 표시
↓
useEffect

이러한 흐름을 이해하면 useEffect가 렌더링 및 DOM 변경과 어떤 관계를 가지는지 이해하는 데 도움이 됩니다.

일반적으로 useEffect는 브라우저가 변경된 화면을 표시한 이후 실행됩니다.

다만 항상 이 순서가 보장되는 것은 아니며, 클릭과 같은 사용자 상호작용으로 발생한 Effect는 화면이 표시되기 전에 실행될 수도 있습니다.

브라우저 렌더링

Commit 단계에서 DOM이 변경되면 브라우저는 변경된 내용을 화면에 반영하기 위한 렌더링 과정을 수행합니다.

일반적인 전체 렌더링 흐름을 단순화하면 Style 계산 → Layout → Paint → Composite 순으로 이해할 수 있습니다.

React
1. Render (새로운 화면 계산)
↓
2. Commit (실제 DOM 변경)
↓
Browser
3. Style
↓
4. Layout
↓
5. Paint
↓
6. Composite
↓
화면에 표시

다만 DOM이 변경될 때마다 위의 모든 단계가 항상 실행되는 것은 아닙니다.

변경된 속성이나 내용에 따라 Layout이나 Paint와 같은 일부 단계가 필요하지 않을 수도 있습니다.

참고로 위 다이어그램에는 HTML 파싱이나 CSS 파싱 과정이 포함되어 있지 않습니다.

HTML을 파싱하여 DOM을 생성하고 CSS를 파싱하여 CSSOM을 생성하는 과정은 초기 페이지 로드에서 이루어지는 중요한 렌더링 과정입니다.

React의 state 변경으로 리렌더링이 발생하는 일반적인 상황에서는 이미 만들어진 DOM을 Commit 단계에서 필요한 부분만 변경하므로, 초기 페이지 로드처럼 HTML 전체를 다시 파싱하여 DOM을 처음부터 생성하지 않습니다.

따라서 React의 리렌더링 이후 브라우저가 화면을 업데이트하는 과정을 이해할 때는 Style → Layout → Paint → Composite를 기본적인 흐름으로 이해하되, 변경 내용에 따라 일부 단계가 생략될 수 있습니다.

useEffect는 언제 실행될까?

많은 사람들이 useEffect가 Render 이후에 실행된다고 생각합니다.

조금 더 정확하게 말하면, 일반적으로 다음과 같은 시점에 실행됩니다.

Render와 Commit이 완료되고 브라우저가 변경된 화면을 표시한 이후에 실행됩니다.

useEffect(() => {
  console.log('Effect');
}, []);

예를 들어 위의 코드는 Render 도중 실행되는 것이 아니라, DOM이 변경되고 화면이 업데이트된 뒤 실행됩니다.

그래서 useEffect에서는 다음과 같은 부수 효과(Side Effect)를 안전하게 수행할 수 있습니다.

  • API 요청
  • 이벤트 등록
  • 타이머
  • 외부 라이브러리 초기화

Render와 Commit은 왜 분리되어 있을까?

여기서 한 가지 궁금증이 생길 수 있습니다.

왜 React는 Render와 Commit을 굳이 분리했을까?

만약 Render 단계에서 바로 DOM을 변경한다면, React는 화면을 계산하는 도중에 이미 실제 화면을 수정하게 됩니다.

컴포넌트 A 계산
↓
DOM 변경
↓
컴포넌트 B 계산
↓
오류 또는 작업 중단
↓
일부만 변경된 DOM이 남음

예를 들어 위와 같이 동작한다면, 계산 도중 오류가 발생하거나 작업이 중단될 경우 화면이 중간 상태로 남을 수도 있습니다.

그래서 React는 먼저

어떤 화면이 되어야 하는지 끝까지 계산하고

그 이후

한 번에 DOM을 변경

하는 방식을 선택했습니다.

이렇게 하면 화면이 항상 일관된 상태를 유지할 수 있습니다.

또한 Render 단계와 Commit 단계를 분리했기 때문에 React는 Render를 여러 번 수행하거나 중간에 취소하는 것도 가능해졌습니다.

이것이 이후 React Fiber와 동시성 렌더링(Concurrent Rendering)이 등장하는 기반이 됩니다.

Render는 중단될 수 있지만 Commit은 중단되지 않는다

Render와 Commit은 역할뿐 아니라 작업을 처리하는 방식도 다릅니다.

Render 단계는 실제 DOM을 변경하지 않고 새로운 화면을 계산하는 과정입니다.

아직 사용자에게 보이는 화면을 수정하지 않았기 때문에 React는 필요에 따라 이 작업을 잠시 멈추거나, 우선순위가 높은 작업을 먼저 처리하거나, 진행 중이던 결과를 버리고 처음부터 다시 계산할 수 있습니다.

Render 시작
↓
일부 컴포넌트 계산
↓
더 중요한 업데이트 발생
↓
현재 Render 중단
↓
중요한 업데이트 먼저 처리
↓
기존 Render 재개 또는 다시 시작

예를 들어 많은 컴포넌트를 렌더링하는 중에 사용자가 입력하면, React는 사용자 입력처럼 더 긴급한 작업을 먼저 처리할 수 있습니다.

반면 Commit 단계는 계산된 변경 사항을 실제 DOM에 반영합니다.

Commit 도중 작업을 멈추면 일부 DOM만 변경된 불완전한 화면이 사용자에게 노출될 수 있습니다.

Commit 시작
↓
일부 DOM 변경
↓
작업 중단
↓
화면이 중간 상태로 노출될 수 있음

따라서 React는 Commit을 시작하면 해당 변경 사항을 중간에 나누거나 중단하지 않고 동기적으로 완료합니다.

Render
→ 중단 가능
→ 다시 시작 가능
→ 결과를 버릴 수 있음

Commit
→ 중단하지 않음
→ 실제 DOM에 한 번에 반영

이 차이 때문에 Render 단계의 코드는 여러 번 실행되더라도 안전해야 합니다.

컴포넌트 본문의 코드는 Render가 일어날 때마다 다시 실행됩니다.

따라서 컴포넌트 본문에서 API 요청, 이벤트 등록, DOM 직접 변경 같은 사이드 이펙트를 실행하면, Render가 다시 실행될 때 같은 작업도 의도치 않게 반복될 수 있습니다.

React가 사이드 이펙트를 Render와 분리해 Effect에서 처리하도록 권장하는 이유도 여기에 있습니다.

왜 React는 이렇게 설계했을까?

React의 목표는 가능한 한 예측 가능한 UI를 만드는 것입니다.

이를 위해 React는 UI를 계산하는 과정과 실제 화면을 변경하는 과정을 명확하게 분리했습니다.

Render 단계에서는 "현재 state를 기준으로 화면이 어떻게 보여야 하는가?" 를 계산합니다.

이 과정은 여러 번 실행될 수 있기 때문에 사이드 이펙트 없이 순수하게 동작해야 합니다.

반면 Commit 단계에서는 Render 단계에서 계산한 결과를 실제 DOM에 반영합니다.

이렇게 역할을 분리하면 Render가 여러 번 실행되거나 중단되더라도 실제 DOM에 불완전한 결과를 반영하지 않고, 완료된 Render 결과만 Commit 단계에서 화면에 반영할 수 있습니다.

useLayoutEffect는 언제 실행될까?

지금까지의 흐름을 보면 useEffect는 일반적으로 브라우저가 변경된 화면을 표시한 이후 실행된다고 설명했습니다.

그렇다면 useLayoutEffect는 언제 실행될까요?

useLayoutEffect는 Commit 단계에서 DOM이 변경된 이후, 브라우저가 변경된 화면을 다시 그리기 전에 동기적으로 실행됩니다.

조금 단순하게 표현하면 다음과 같습니다.

state 변경
↓
Render
(새로운 화면 계산)
↓
Commit
(DOM 변경)
↓
useLayoutEffect
↓
브라우저가 변경된 화면을 표시
↓
useEffect
(일반적인 경우)

이러한 실행 순서 덕분에 useLayoutEffect에서는 사용자에게 변경된 화면이 보이기 전에 DOM의 크기나 위치를 측정하거나 필요한 작업을 수행할 수 있습니다.

예를 들어 다음과 같은 작업에 사용할 수 있습니다.

  • 요소의 크기나 위치를 측정하고 그 결과를 화면에 즉시 반영해야 하는 경우
  • 화면이 표시되기 전에 스크롤 위치를 조정해야 하는 경우
  • 화면이 깜빡이지 않도록 DOM의 시각적인 상태를 조정해야 하는 경우

useLayoutEffect가 실행되는 동안에는 브라우저의 화면 업데이트가 차단되므로, 작업이 오래 걸리면 성능에 영향을 줄 수 있습니다.

따라서 대부분의 Effect에는 useEffect를 사용하고, 화면이 표시되기 전에 DOM을 측정하거나 변경해야 하는 경우에만 useLayoutEffect를 사용하는 것이 좋습니다.

정리

React는 state가 변경되면 곧바로 DOM을 수정하지 않습니다.

먼저 새로운 화면을 계산한 뒤, 그 결과를 실제 DOM에 반영하는 두 단계로 나누어 처리합니다.

핵심 내용을 정리하면 다음과 같습니다.

  • Render는 새로운 화면을 계산하는 단계입니다.
  • Render 단계에서는 실제 DOM을 변경하지 않습니다.
  • Commit은 계산 결과를 실제 DOM에 반영하는 단계입니다.
  • 브라우저는 Commit 이후 변경된 DOM을 화면에 그립니다.
  • useLayoutEffect는 Commit 이후, 브라우저가 변경된 화면을 다시 그리기 전에 실행됩니다.
  • useEffect는 브라우저 렌더링이 완료된 후 실행됩니다.

React가 Render와 Commit을 분리한 이유는 UI를 더 예측 가능하고 안정적으로 업데이트하기 위해서입니다.

먼저 새로운 화면을 충분히 계산한 뒤, 실제 DOM은 Commit 단계에서 한 번에 반영하기 때문에 화면이 중간 상태로 변경되는 일을 방지할 수 있습니다.

또한 Render와 Commit을 분리했기 때문에 React는 Render를 중단하거나 다시 시작하는 등 더 유연한 렌더링 전략을 사용할 수 있게 되었습니다.

참고 자료