실제 프로젝트를 진행하면서 기능 구현 자체보다 렌더링 구조와 성능 최적화가 훨씬 중요하다는 점을 체감했습니다. 특히 React 렌더링 파이프라인을 이해하면서, 렌더링은 컴포넌트 호출을 통한 계산, 커밋은 실제 DOM 변경이라는 분리가 얼마나 중요한지 알게 되었습니다.
처음에는 “React가 알아서 최적화해주겠지”라는 믿음이 있었지만, 불필요한 렌더링이 누적되면서 계산 비용, 메모리 사용량, 페인트 비용으로 이어지는 것을 직접 경험했습니다. 렌더링과 커밋을 구분해 생각하니, 상태 관리와 리렌더링 전략을 더 체계적으로 설계할 수 있었고 성능 문제를 사전에 예측하고 개선할 수 있다는 점이 유익했습니다.
이번 경험을 통해, 단순히 동작하는 코드를 만드는 것을 넘어 내부 동작 원리를 이해하고 주도적으로 관리하는 개발이 얼마나 중요한지 깨달았습니다. 앞으로는 최적화와 구조 설계 측면에서도 더 깊이 고민하며 개발할 수 있을 것 같습니다.
프론트엔드 개발을 하다 보면 “리렌더링이 일어났다”, “DOM이 업데이트됐다” 같은 말을 자주 듣습니다. 하지만 렌더링(Rendering), 커밋(Commit), 브라우저 페인팅(Painting)의 차이를 정확히 이해하지 못하면 최적화나 버그 해결 과정에서 헤매기 쉽습니다.
React는 화면에 UI를 표시하기 전에 렌더링 파이프라인이라는 단계를 거칩니다. 이 글에서는 React가 화면을 갱신하는 과정 전체를 파이프라인으로 정리해보겠습니다.
React 렌더링 파이프라인이란?
React에서 UI 업데이트는 크게 세 단계로 구분됩니다.
- Triggering (렌더링 트리거)
→ 언제 렌더링을 시작할지 결정합니다. 초기 렌더링과 props 업데이트의 변경 시 호출됩니다. - Rendering (렌더링)
→ JSX를 계산하고 새로운 가상 DOM 트리를 생성합니다. 이 단계에서 실제 DOM은 변경되지 않습니다. - Committing (커밋)
→ 변경된 부분만 실제 DOM에 반영(diffing & reconciliation)하여 불필요한 DOM 조작을 최소화합니다. - Painting (브라우저 페인팅)
브라우저가 실제 화면을 다시 그리는 과정으로, 이 과정에서 레이아웃 계산, 스타일 적용, 픽셀 렌더링 등이 발생합니다.
React 렌더링 파이프라인은 UI가 사용자에게 보이기 전까지 거치는 내부 절차라고 할 수 있습니다.
단계별 특징
1-1) Trigger: 렌더링 트리거
Trigger 단계는 React가 “언제 컴포넌트를 렌더링할지 결정하는 시점”입니다.
렌더링이 무작정 일어나지 않고 특정 이벤트나 상태 변화가 있을 때만 시작됩니다. 주요 계기는 두 가지입니다.
- 초기 렌더링: 앱이 처음 실행될 때 root.render(<App />) 호출
- State 업데이트: setState 혹은 useState의 setter 함수 실행
→ 트리거가 발생하면 React는 해당 컴포넌트를 렌더링 큐에 추가합니다.
1. 초기 렌더링
- 앱이 처음 실행될 때, 예를 들어 root.render(<App />) 호출 시 발생합니다.
- 이때 React는 루트 컴포넌트를 렌더링 큐에 올리고, 가상 DOM을 계산하기 위한 Render 단계로 넘어갑니다.
- 초기 렌더링은 페이지가 켜질 때 한 번만 발생하지만, 이후 리렌더링 구조를 이해하는 기준점이 됩니다.
2. 상태(State) 또는 Props 업데이트
- useState나 useReducer의 setter 함수 호출, 부모 컴포넌트로부터 받은 props가 바뀔 때 발생합니다.
- React는 변경된 컴포넌트와 그 자식들을 렌더링 큐에 등록하고, Render 단계에서 새로운 가상 DOM을 계산합니다.
- 이 과정에서 중요한 점은 렌더링이 시작됐다고 해서 곧바로 DOM이 바뀌는 것은 아니다라는 점입니다. DOM 변경은 Commit 단계에서 일어나며, 이 분리를 이해해야 불필요한 리렌더링과 성능 이슈를 예측할 수 있습니다.

컴포넌트의 상태를 업데이트하면 자동으로 렌더링 대기열에 추가되는 것을, 리액트 공식문서에서는 '레스토랑의 손님이 첫 주문 이후에 디저트 등의 메뉴를 추가 주문하는 것으로 상상해 볼 수 있습니다' 라는 예시를 들어 설명하고 있습니다.
1-2) Render: 컴포넌트 렌더링
Render 단계는 “화면에 바로 그리는 단계가 아니라, 컴포넌트 함수를 호출해 JSX를 계산하는 단계”입니다.
Render 단계에서 React는 가상 DOM(Virtual DOM) 트리를 새로 생성하며, Commit 단계에서 실제 DOM과 비교하여 최소한의 업데이트를 수행합니다.
Render 단계 특징
1) 초기 렌더링
- 루트 컴포넌트부터 호출하여 재귀적으로 자식 컴포넌트까지 함수 호출이 일어납니다.
- 이 과정에서 JSX가 계산되어 새로운 가상 DOM 트리가 만들어집니다.
- 초기 렌더링은 페이지가 켜질 때 한 번 발생하지만, 이후 리렌더링 구조를 이해하는 기준점이 됩니다.
2) 리렌더링 (State / Props 업데이트)
- useState, useReducer, props 변경 등으로 트리거가 발생하면, React는 변경된 컴포넌트를 렌더링 큐에 등록합니다.
- 등록된 컴포넌트부터 시작해 재귀적으로 자식 컴포넌트들을 호출하며 가상 DOM을 다시 계산합니다.
- 중요한 점은 렌더링(Render) 단계에서 계산만 이루어지고, DOM은 아직 변경되지 않는다는 것입니다.
- 즉, Render 단계와 Commit 단계가 분리되어 있기 때문에, 불필요한 리렌더링이 쌓이면 계산 비용과 메모리 사용량으로 이어질 수 있습니다.
Render 단계를 레스토랑의 주방에 비유해보자면, 손님이 주문(트리거)을 하면 요리사가 요리를 준비(렌더링)하지만, 실제 식탁에 놓이는 것은 아직 아닙니다. 준비된 요리(가상 DOM)는 Commit 단계에서 식탁(DOM)에 올려진다고 이해할 수 있습니다.
(Render 단계 == 음식 준비)
3) Commit: DOM 업데이트
Commit 단계는 Render 단계에서 계산된 가상 DOM을 실제 DOM에 반영하는 단계입니다.
이 단계부터 화면에 변화가 실제로 나타나며, 브라우저가 사용자에게 UI를 보여주기 시작합니다.
Commit 단계 특징
1) 초기 렌더링
- 앱이 처음 실행될 때 Render 단계에서 계산된 가상 DOM을 바탕으로 실제 DOM 노드를 생성합니다.
- 예를 들어 appendChild 등을 통해 DOM 트리에 붙이는 작업이 이루어집니다.
- 이 단계가 끝나야 사용자가 화면을 볼 수 있습니다.
2) 리렌더링
- 상태(State)나 Props가 업데이트되어 Render 단계에서 새 가상 DOM이 만들어지면, Commit 단계에서는 변경된 부분만 실제 DOM에 반영합니다.
- React는 가상 DOM과 실제 DOM의 차이를 비교(diffing)하고, 최소한의 DOM 작업만 수행(reconciliation)합니다.
- 이렇게 최소 작업만 수행하기 때문에 불필요한 DOM 업데이트를 줄이고 성능을 최적화할 수 있습니다.
Render 단계가 요리를 준비하는 주방이라면, Commit 단계는 실제로 식탁에 음식을 내놓는 단계입니다. 계산된 요리를 그대로 식탁에 올리되, 이전과 달라진 요리만 바꾸어 놓는 방식으로 불필요한 낭비를 줄이는 과정입니다.
4) Paint: 브라우저 페인팅
React 단계는 아니지만, DOM 업데이트가 끝나면 브라우저가 다시 그리기(repaint, reflow)를 수행합니다.
React의 최적화 덕분에 브라우저는 불필요한 페인팅을 최소화할 수 있습니다.
1) 브라우저 작업
Commit 단계에서 변경된 DOM과 스타일이 브라우저 렌더 트리에 반영되면, 브라우저는 Reflow(레이아웃 계산)와 Repaint(화면 그리기)를 수행합니다.
- Reflow: 요소의 위치, 크기, 레이아웃이 바뀌었는지 계산
- Repaint: 색상, 그림자 등 시각적 속성을 실제로 화면에 그림
2) React 최적화와 연계
- React는 Commit 단계에서 가상 DOM을 이용해 최소한의 DOM 업데이트만 수행합니다.
- 덕분에 브라우저는 불필요한 Reflow/Repaint를 줄일 수 있어 성능 부담이 감소합니다.
Render와 Commit 단계가 요리를 준비하고 식탁에 올리는 과정이라면, Paint 단계는 실제로 손님이 음식의 색과 배치를 확인하며 맛보는 순간으로 볼 수 있습니다. 여기서 불필요한 Reflow, Repaint가 많으면 화면이 깜빡이거나 성능이 떨어지게 됩니다.
실사용 예시
1) 초기 렌더링
import { createRoot } from "react-dom/client";
import App from "./App.js";
const root = createRoot(document.getElementById("root"));
root.render(<App />);
- root.render(<App />) → Trigger
React가 “앱을 렌더링해야 한다”는 신호를 받습니다. - App() 호출 및 가상 DOM 생성 → Render
루트 컴포넌트와 자식 컴포넌트가 순차적으로 호출되며, 가상 DOM 트리가 만들어집니다. - DOM에 <div>, <h1> 등이 붙음 → Commit
계산된 가상 DOM과 실제 DOM을 비교한 뒤, 실제 DOM에 필요한 노드만 추가하거나 갱신합니다. - 브라우저가 화면에 그림 → Paint
최종적으로 사용자 화면에 UI가 표시됩니다.
초기 렌더링은 앱이 켜질 때 한 번만 발생하며, 이후 리렌더링 구조를 이해하는 기준점이 됩니다.
2) state 업데이트로 인한 리렌더링
function Counter() {
const [count, setCount] = useState(0);
return (
<>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
</>
);
}
- 버튼 클릭 시 setCount(count + 1) → Trigger
React가 “Counter 컴포넌트를 다시 렌더링해야 한다”는 신호를 받습니다. - React가 Counter 다시 호출, 새로운 JSX 반환 → Render
count 값이 업데이트되며, <h1>과 <button>을 포함한 가상 DOM이 재계산됩니다. - <h1>의 textContent만 갱신 → Commit
변경된 부분만 실제 DOM에 반영합니다. 기존 <button> DOM은 건드리지 않습니다. - 브라우저가 <h1>을 다시 그림 → Paint
화면에 변경된 내용만 표시되어 불필요한 리렌더링과 페인팅이 최소화됩니다.
React가 렌더링과 DOM 업데이트를 분리하고, 최소한의 작업만 수행함으로써 성능을 최적화하는 구조를 보여줍니다.
React 렌더링의 보조 개념
1) Batching (배칭)
React는 여러 state 업데이트를 한 번의 렌더링으로 묶어 처리합니다.
setCount(c => c + 1);
setCount(c => c + 1);
위 코드처럼 같은 이벤트 안에서 여러 번 상태를 업데이트해도, React는 실제로 한 번만 Render → Commit 과정을 수행합니다.
이 덕분에 불필요한 리렌더링을 줄이고, 성능을 최적화할 수 있습니다.
배칭은 특히 이벤트 핸들러, setTimeout, Promise 콜백 등 다양한 환경에서 발생하며, React 18 이후 concurrent 모드에서는 더 강력하게 동작합니다.
2) Strict Mode와 순수 함수 원칙
React는 컴포넌트를 “순수 함수”로 설계해야 합니다.
- 같은 입력(props, state) → 같은 출력(JSX)
- 부작용(side effect)은 렌더링 중 발생하지 않아야 함
개발 모드에서 Strict Mode를 켜면, React가 이런 규칙을 지키도록 검증하기 위해 일부 컴포넌트를 두 번 호출하기도 합니다. 이는 의도적으로 렌더링 동작을 테스트하여 부작용으로 인한 버그를 조기에 발견할 수 있게 해줍니다.
3) 렌더링 ≠ DOM 업데이트
많은 사람들이 혼동하는 부분인데, 렌더링(Render)은 DOM을 바꾸는 단계가 아닙니다.
DOM 변경은 오직 Commit 단계에서만 일어납니다.
정리
React 렌더링 파이프라인은 다음과 같이 요약할 수 있습니다.
- Trigger – 초기 렌더링 또는 state 업데이트로 렌더링 요청 발생
- Render – 컴포넌트 호출, 가상 DOM 계산
- Commit – 변경된 부분만 실제 DOM 반영
- Paint – 브라우저가 화면을 다시 그림
이 흐름을 이해하면 불필요한 리렌더링을 줄이고, 성능 최적화 지점을 찾고, 디버깅도 훨씬 수월해집니다.
'📚 CS > React' 카테고리의 다른 글
| [React] useRef 알아보기 (0) | 2025.09.16 |
|---|---|
| [React] useEffect, useLayoutEffect 비교하기 (0) | 2025.09.14 |
| [React] 클래스형 컴포넌트 vs 함수형 컴포넌트 (0) | 2025.09.12 |
| [React] React Hooks: 함수형 컴포넌트 알아보기 (0) | 2025.09.11 |
| [React] Virtual DOM & React Element (0) | 2025.09.10 |