IT의 IT 블로그
리액트 렌더링 과정 정리 본문
이전 글에서 브라우저의 렌더링 과정을 정리해보았습니다.
이번 글에서는 그 흐름 위에서 React가 어떤 방식으로 렌더링을 수행하는지,
추가로, React의 렌더링 과정이 브라우저 렌더링 파이프라인과 어떻게 맞물려 동작하는지를 살펴보겠습니다.
본격적으로 React의 렌더링 과정을 살펴보기 전에,
먼저 기존 브라우저의 업데이트 렌더링 흐름을 간단히 확인해보겠습니다.
1. 렌더링 비용의 핵심 구간: Layout과 Paint
아래 그림에서 볼 수 있듯이, Critical Rendering Path(CRP) 중에서도
UI 업데이트 시 가장 많은 비용이 발생하는 단계는 Layout과 Painting입니다.
이 두 단계는 실제 픽셀 배치와 그리기를 수행하며,
계산량이 많고 메모리 접근이 일어나기 때문에 성능에 큰 영향을 미칩니다.

2. 렌더링 비용을 줄이는 방법
앞에서 살펴본 것처럼, 브라우저 렌더링 과정에서 Layout과 Paint는 가장 비용이 큰 단계입니다.
따라서 UI 성능을 개선하려면 이 단계가 불필요하게 여러 번 발생하지 않도록 DOM 업데이트 횟수를 최소화하는 것이 중요합니다.
아래는 같은 기능을 구현한 두 가지 코드 예시입니다.
차이는 오직 하나, DOM을 몇 번 수정하느냐입니다.
코드 예시 1) 안좋은 예제 : 반복문마다 DOM 갱신
<button onclick="onClick()">리스트 추가하기</button>
<ul id="ul"></ul>
<script>
function onClick() {
const $ul = document.getElementById("ul");
for (let i = 0; i < 3000; i++) {
$ul.innerHTML += `<li>${i}</li>`; // DOM write 3000번
}
}
</script>
문제점
innerHTML +=는 매 반복마다 다음 작업을 수행합니다.
- 기존 HTML 문자열을 다시 읽고
- 새 문자열과 결합한 뒤
- HTML을 재파싱하고
- DOM을 다시 갱신
이 과정에서 브라우저는 매번 Render Tree 갱신, Layout(Reflow), Paint(Repaint)가 발생할 수 있습니다.
결과적으로 이는 브라우저에게 “렌더링 업데이트를 3000번 수행하라” 에 가까운 요청이 됩니다.
코드 예시 2) 좋은 예제 : 메모리에서 구성 후 DOM은 한 번만 갱신
<button onclick="onClick()">리스트 추가하기</button>
<ul id="ul"></ul>
<script>
function onClick() {
const $ul = document.getElementById("ul");
let list = "";
for (let i = 0; i < 3000; i++) {
list += `<li>${i}</li>`; // 메모리에서만 누적
}
$ul.innerHTML = list; // DOM write 1번
}
</script>
개선점
- 반복문 동안에는 DOM을 전혀 건드리지 않고
- 메모리 상에서 문자열만 누적한 뒤
- 마지막에 한 번만 실제 DOM에 반영합니다.
이렇게 하면 DOM 변경 횟수가 3000번 → 1번으로 줄어들고,
Layout과 Paint가 재실행되는 횟수 역시 크게 감소합니다.
정리
같은 결과를 만들어내더라도,
- DOM을 자주 수정하는 방식은
→ Render Tree 재계산, Layout, Paint를 반복 유발 - DOM을 한 번에 갱신하는 방식은
→ 렌더링 파이프라인 실행 횟수를 최소화
결국 성능 최적화의 핵심 중 하나는 다음 한 줄로 요약할 수 있습니다.
DOM write를 최소화하고, 변경 사항을 한 번에 배치(batch)해서 반영하라.
3. React 의 VitualDom의 사용 이유
앞선 2번 예제처럼 DOM write를 최소화하며 성능을 최적화하는 방식이 가장 이상적이지만, 실제로는 모든 상황에서 이를 직접 구현하기가 쉽지 않습니다.
단순하고 규모가 작은 서비스라면 이러한 최적화 전략을 비교적 수월하게 적용할 수 있습니다.
그러나 애플리케이션의 규모가 커질수록, 자바스크립트로 제어해야 할 DOM의 수는 급격히 증가하고, 구조 또한 복잡해집니다.
이로 인해 DOM 변경을 일일이 추적하고, 효율적으로 배치하여 업데이트하는 로직을 직접 관리하는 것은 점점 더 어려워집니다.


앞서, 언급했듯이 렌더링에서 가장 비싼 작업은 메모리에서 변경하는 작업 이걸 줄이기 위해 React 렌더링 과정이 필요하고 Vitual Dom 을 활용해서 최소한으로 메모리를 사용합니다
4. React 렌더링 프로세스 전체 구조
앞서 살펴본 것처럼, 브라우저 렌더링에서 가장 비용이 큰 구간은 Layout과 Paint이며,
성능 최적화의 핵심은 DOM 변경 횟수를 최소화하고, 변경을 한 번에 묶어서 반영하는 것입니다.
React는 이 문제를 해결하기 위해
DOM을 직접 조작하는 대신 값 기반 UI 트리(Virtual DOM) 를 만들고,
변경 사항을 계산한 뒤 한 번에 실제 DOM에 반영하는 렌더링 파이프라인을 구성합니다.
React의 렌더링 흐름은 다음 단계로 정리할 수 있습니다.
4-1. Trigger – 렌더링이 시작되는 계기
렌더링은 항상 어떤 “변경 이벤트”에 의해 시작됩니다.
대표적인 Trigger는 다음과 같습니다.
- state 변경 (setState, useState setter)
- props 변경 (부모 컴포넌트 리렌더)
- context 값 변경
- forceUpdate
- 최초 마운트
Trigger는
렌더링이 시작되는 조건을 의미할 뿐,
렌더링 파이프라인 자체의 단계는 아닙니다.

4-2. Render Phase – 무엇이 바뀌어야 하는지 계산하는 단계
Render Phase는 실제 DOM을 건드리지 않고,
새로운 UI 설계도(Value UI Tree)를 만들고 이전 설계도와 비교하는 계산 단계입니다.
4-2-1. 컴포넌트 실행
state나 props가 변경되면 React는 컴포넌트 함수를 다시 실행합니다.
function App() {
return (
<div id="main">
<p>Hello</p>
</div>
);
}
이때 React는 DOM을 만들지 않고,
JSX를 React Element 객체로 변환합니다.
4-2-2. JSX → React Element → Virtual DOM(Value UI Tree)
JSX는 내부적으로 React.createElement 호출로 변환되며,
그 결과로 다음과 같은 React Element 객체 트리가 생성됩니다.
{
$$typeof: Symbol(react.element), // 이 객체가 React Element임을 식별하는 내부 태그
type: "div", // DOM 태그 이름 또는 컴포넌트 함수
key: null, // 리스트 렌더링 시 reconciliation을 위한 식별자
ref: null, // 실제 DOM이나 컴포넌트 인스턴스를 가리키는 참조
props: { // JSX에 전달된 속성들
id: "main",
children: {
$$typeof: Symbol(react.element),
type: "p",
key: null,
ref: null,
props: {
children: "Hello"
},
_owner: null,
_store: {}
}
},
_owner: null, // 이 Element를 생성한 컴포넌트 (디버깅 및 경고용)
_store: {} // 개발 모드에서의 검증 메타데이터
}
이러한 React Element 객체들이 트리 구조로 연결된 것이, 흔히 말하는 Virtual DOM입니다.
다만 개념적으로 더 정확히 표현하면, 이것은 실제 DOM의 복사본이 아니라
현재 UI 상태를 표현한 순수한 값 객체 트리(Value UI Tree) 입니다.
React는 실제 DOM 구조를 그대로 미러링하려는 것이 아니라,
라는 데이터 흐름을 만들고,
이 값의 차이만을 계산한 뒤 한 번에 실제 DOM에 반영합니다.
따라서 “Virtual DOM”이라는 이름은
실제로는 UI를 값으로 표현한 추상적인 트리 구조를 직관적으로 이해시키기 위해 붙인 용어에 가깝습니다.
4-2-3. Reconciliation – Fiber 위에서 수행되는 비교 과정
React는
- 이전 React Element 트리
- 새로 생성된 React Element 트리
를 비교하여 값이 달라진 노드만 찾아냅니다.
이 비교 알고리즘이 Reconciliation(Diffing) 입니다.
중요한 점은 이 과정이
Fiber 아키텍처 위에서 수행된다는 것입니다.
즉,
- JSX → React Element 트리 생성
- 이전 트리와 새 트리 비교
- 변경된 노드만 추출
- 이 모든 작업을 Fiber 노드 단위로 쪼개서 스케줄링
하는 구조로 동작합니다.
결국 React 렌더링의 본질은
“UI의 값 트리를 다시 만들고, 이전 값과 다른 부분만 계산하는 과정”
입니다.
4-2-4. Render Phase의 성질: Pure & Side-Effect Free
Render Phase는 반드시 다음 특성을 가집니다.
1) Pure (순수 계산)
- 같은 state, 같은 props → 항상 같은 결과
- 입력 → 출력만 존재
- 외부 상태 변경 없음
2) Side-Effect Free (부작용 없음)
이 단계에서는 다음을 하면 안 됩니다.
- DOM 직접 조작
- 네트워크 요청
- 타이머, 이벤트 리스너 등록
- 전역 상태 변경
이유는 Render Phase가
- 여러 번 실행될 수 있고
- 중단·재개될 수 있고 (Fiber)
- 계산 결과가 버려질 수도 있기 때문입니다.
그래서 이 단계는 반드시
“몇 번 실행되어도 안전한 순수 계산 단계”
여야 하며,
모든 부작용은 Commit Phase 이후(useEffect)로 미뤄집니다.
4-3. Commit Phase – 계산 결과를 실제 DOM에 반영하는 단계
Commit Phase는 Render Phase에서 계산한 변경 목록을 바탕으로
실제 DOM을 한 번에 수정하는 단계입니다.
이 단계에서 수행되는 작업:
- DOM 노드 생성 / 수정 / 삭제
- ref 연결
- useLayoutEffect, useEffect 실행
- 브라우저 렌더링 파이프라인 트리거
Commit Phase는 중단되지 않으며,
한 번 시작되면 원자적으로 끝까지 수행됩니다.
4-4. Browser – 실제 픽셀을 그리는 단계
DOM이 변경되면 이후는 브라우저의 책임입니다.
- Render Tree 갱신
- Layout (Reflow)
- Paint
- Composite
React는 Commit Phase까지만 관여하고,
실제 화면을 그리는 작업은 브라우저가 담당합니다.
4-5. Fiber 아키텍처 관점 요약
Fiber는 이 렌더링 구조를 다음처럼 재설계했습니다.
Render Phase (계산 단계)
- 작업을 작은 단위(Unit of Work)로 분해
- 중단 / 재개 가능
- 우선순위 기반 스케줄링
Commit Phase (반영 단계)
- 실제 DOM 반영은 한 번에 수행
- 중단 불가
- UI 일관성 보장
한 줄로 요약하면:
Render Phase는 쪼개서 계산하고,
Commit Phase는 한 번에 적용한다.
4-6. React / ReactDOM import의 의미
import React from 'react';
import ReactDOM from 'react-dom';
이 구조가 필요한 이유는 역할 분리가 명확하기 때문입니다.
- React
- JSX → React Element 생성
- Fiber 트리 구성
- Reconciliation 수행
- ReactDOM
- Commit Phase에서 실제 DOM에 반영
- 브라우저와의 연결 계층
즉,
React는 설계도 엔진,
ReactDOM은 실제 시공 엔진
에 해당합니다.
4-7. 전체 흐름 예시 (버튼 클릭 예제)
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
count: {count}
</button>
);
}
- 클릭 → state 변경 (Trigger)
- Render Phase
- 컴포넌트 재실행
- JSX → 값 트리 생성
- Fiber 위에서 Reconciliation
- Commit Phase
- 실제 DOM 텍스트 노드 수정
- effect 예약
- Browser
- Layout → Paint → Composite
5. React 렌더링과 브라우저 CRP의 연결 지점
앞선 내용에서 React의 Render Phase와 Commit Phase를 살펴보았다면,
자연스럽게 다음과 같은 질문이 생깁니다.
“그렇다면 React의 렌더링 과정은 브라우저의 Critical Rendering Path(CRP) 중 어디에 개입하는가?”
아래 그림은 브라우저의 CRP 흐름 위에
React의 렌더링 파이프라인이 어느 지점에서 시작되고,
어디까지 관여하는지를 함께 나타낸 것입니다.
전체 과정은 크게 세 구간으로 나눌 수 있습니다.

5-1. HTML 파싱 단계 – 아직 React는 개입하지 않는다
가장 위쪽 영역은 브라우저의 순수 CRP 시작 지점입니다.
브라우저는 HTML을 파싱하다가 다음 태그를 만나게 됩니다.
<script src="main.js"></script>
이 시점에서 수행되는 작업은 다음과 같습니다.
- main.js 파일을 네트워크로 다운로드
- 다운로드한 JS 파일을 메모리에 로드
- (defer/async 여부에 따라) 아직 실행하지 않을 수도 있음
중요한 점은, 이 단계에서는 React도 Virtual DOM도 아직 “실행 중”이 아니라
단지 파일 형태로 메모리에 존재할 뿐이라는 것입니다.
따라서 이 시점의 화면 렌더링은 전적으로 브라우저의 CRP에 의해 이루어집니다.
DOM + CSSOM → Render Tree → Layout → Paint
화면에는 보통 <div id="root"></div> 와 같은 빈 컨테이너만 그려진 상태입니다.
그림 상단의 흐름이 바로 이 구간을 나타냅니다.
5-2. JavaScript 실행 단계 – 여기서 React가 개입한다
중간 영역은 JavaScript 엔진(V8)이 main.js를 실제로 실행하는 구간입니다.
import React from "react";
import { createRoot } from "react-dom/client";
createRoot(document.getElementById("root")).render(<App />);
이 코드가 실행되면서 다음 순서가 진행됩니다.
- React 라이브러리 로드 및 런타임 초기화
- createRoot().render() 호출
- <App /> 컴포넌트 함수 실행
- JSX를 기반으로 Virtual DOM 트리 최초 생성
- 이전 VDOM이 없으므로 전체 트리를 실제 DOM으로 생성
- 생성된 DOM을 #root에 삽입(Commit)
즉, React의 렌더링은
HTML 파싱 시점이 아니라 JavaScript가 실행되고 render()가 호출되는 순간에 시작됩니다.
그림에서 createRoot().render() 아래에
Virtual DOM 생성 → 실제 DOM 변경으로 이어지는 블록이 바로 이 단계를 표현합니다.
여기서 핵심은 다음입니다.
React는 Render Tree를 직접 만들지 않는다.
React는 “DOM을 어떻게 바꿀지”를 계산하고,
실제 Render Tree 재구성과 Layout·Paint는 다시 브라우저에게 맡긴다.
5-3. 다시 CRP로 합류 – Reflow / Repaint 구간
React가 실제 DOM을 변경하면, 제어는 다시 브라우저 렌더링 엔진으로 넘어갑니다.
그림 하단의 CRP 흐름은 다음을 의미합니다.
DOM 변경 → Render Tree 재계산 → Layout (Reflow) → Paint (Repaint) → Composite
즉,
- React는 DOM 구조를 변경하고
- 브라우저는 그 변경된 DOM을 기반으로
- Render Tree를 다시 만들고
- Layout과 Paint를 수행하여
- 최종 픽셀을 그립니다.
글을 마치며
이번 글에서는 브라우저의 Critical Rendering Path 위에서
React의 Render Phase와 Commit Phase가
어느 지점에 위치하고, 어떤 역할을 수행하는지를 살펴보았습니다.
정리하면,
- React는 DOM을 직접 그리지 않고
- Virtual DOM과 Fiber를 통해 변경 사항을 계산한 뒤
- 실제 DOM 반영은 한 번에 수행하고
- 이후의 Layout과 Paint는 다시 브라우저 렌더링 파이프라인에 맡깁니다.
다음 글에서는 이 렌더링 흐름 위에서
React 컴포넌트의 생명주기(Lifecycle)가 어떤 타이밍에 호출되는지를 중심으로
각 단계가 Render Phase와 Commit Phase 중 어디에 속하는지를 이어서 정리해보겠습니다.
'React • TypeScript 학습 기록' 카테고리의 다른 글
| React 상태 끌어올리기(state lifting)와 props 흐름 정리 (0) | 2025.12.18 |
|---|