React19 Internals 2: 렌더링


React 19가 등장하며 많은 개발자들이 'React Compiler'라는 혁신적인 기능에 주목하고 있습니다. 하지만 컴파일러가 정확히 무엇을, 어떻게 최적화하는지 이해하려면 먼저 React 19의 표준 렌더링 파이프라인을 알아야 합니다.
이 글에서는 React 19의 렌더링 과정을 두 단계로 나누어 설명합니다.
1부에서는 모든 React 19 애플리케이션의 기본 동작인 표준 렌더링 파이프라인을 살펴봅니다.
2부에서는 React Compiler가 이 표준 파이프라인 위에서 어떻게 자동으로 작동하여, 우리가 수동으로 하던 최적화 작업을 자동화하는지 보겠습니다.
1부: React 19의 표준 렌더링 파이프라인
React 19의 기본적인 렌더링 과정은 이전 버전의 핵심 원칙을 계승합니다. 렌더링 과정은 크게 트리거(Trigger), 렌더(Render), 커밋(Commit) 세 단계의 파이프라인으로 이루어집니다.
React 4가지 핵심 원칙 간단 정리
상태(state)가 UI를 결정한다: 개발자가 상태(state = data)를 바꾸면 UI는 알아서 바뀐다.
선언적 프로그래밍: UI가 어떤(what) 상태여야 하는지 선언한다.
재조정(Reconciliation): 가상의 UI 구조(virtual DOM)에서 변경 전후 차이점 비교로 재조정한다.
컴포넌트 기반 아키텍쳐: UI를 독립적이며 재사용 가능한 컴포넌트 단위로 조립한다.
1. 트리거(Trigger) 단계: 변경의 시작
"업데이트가 필요하다"는 사실을 인지하고 작업을 예약하는 단계입니다.
시작점: 사용자의 클릭 등으로
useState
의 세터 함수가 호출되면서 시작됩니다.작업 예약: React는 이 업데이트가 얼마나 시급한지 판단하여 우선순위(
lanes
)를 매깁니다. 예를 들어, 버튼 클릭은 입력에 대한 즉각적인 피드백이 중요하므로 높은 우선순위를 갖습니다.경로 표시: 업데이트가 필요한 컴포넌트부터 최상위 루트(root)까지의 경로에 있는 모든 Fiber 노드(컴포넌트에 대한 정보 객체)에
lanes
와childLanes
라는 플래그를 남겨, "이쪽 라인을 점검해야 해!"라고 표시합니다.
2. 렌더(Render) 단계: 변경점 계산
"실제 DOM을 건드리지 않고, 가상으로 무엇이 변했는지 계산하는 두뇌 역할" 을 하는 복잡하고 중요한 단계입니다.
재조정(Reconciliation): React는 트리거 단계에서 표시된 경로를 따라 Fiber 트리를 순회하며 컴포넌트를 다시 실행합니다. 그리고 이전에 렌더링된 결과와 새로 생성된 결과를 비교합니다.
최적화 (Bailout): 재조정 과정에서, 만약 어떤 컴포넌트의 props가 이전과 동일하고 상태 변경도 없다면, React는 "이 컴포넌트와 그 아래 자식들은 모두 변경점이 없겠구나"라고 판단합니다. 그리고 해당 컴포넌트와 그 하위 트리 전체의 렌더링 과정을 통째로 "건너 뛰어(Bailout)" 버립니다. 이것이 바로 React.memo가 수동으로 이끌어내는 핵심 최적화이며, 불필요한 계산을 막아 성능을 향상시킵니다.
변경 계획 수립 (플래그 표시): 비교 결과 변경이 감지된 부분은 실제 DOM을 바로 바꾸는 대신, "이 노드는 DOM에 새로 추가해야 함(
Placement
)", "이 노드는 속성을 수정해야 함(Update
)", "이 노드는 제거해야 함(Deletion
)" 과 같이 해야 할 일들을 Fiber 노드에 플래그로 기록합니다. 이 플래그들이 모여 최종 **'변경 계획서'**가 됩니다. 이 때 변경 계획서는 Fiber 노드들의 집합인 Fiber 트리에 기록 되있습니다.
3. 커밋(Commit) 단계: 실제 화면에 반영
렌더 단계에서 수립된 '변경 계획서'를 실제 DOM에 적용하여 사용자가 변경을 볼 수 있게 하는 마지막 단계입니다.
계획 실행: React는 렌더 단계에서 플래그가 표시된 모든 Fiber 노드를 찾아냅니다.
DOM 업데이트: 이 플래그에 따라 DOM 노드를 추가, 수정, 삭제하는 실제 작업을 실행합니다. 이 과정은 화면 깜빡임 같은 현상을 막기 위해 동기적으로, 한 번에 일괄 처리됩니다.
작업 완료: 모든 DOM 조작이 끝나면 렌더링 과정이 최종적으로 완료됩니다.
이것이 React 19의 표준 렌더링 파이프라인입니다.
React 19 표준 렌더링 파이프라인 시각화
상태 변경부터 실제 UI 업데이트까지 이어지는 Trigger, Render, Commit 단계의 전체 흐름을 시각화합니다.
flowchart TD
subgraph "[1] 트리거 단계"
A["상태 변경<br />(예: setState)"]
B["업데이트 스케줄링"]
C["Fiber 경로에 lanes 표시<br />리렌더링 위치 지정"]
end
subgraph "[2] 렌더 단계"
D["루트에서 렌더링 시작"]
E["Fiber 트리 순회"]
F["업데이트 필요한가?<br />(Bailout 로직)"]
G["컴포넌트 건너뛰기"]
H["자식 컴포넌트 재조정"]
I["DOM 변경 플래그 표시<br/>(추가, 업데이트, 삭제)"]
J["다음 Fiber로 이동"]
K["순회 완료?"]
L["작업 완료된 Fiber 트리 생성"]
end
subgraph "[3] 커밋 단계"
M["커밋 단계 시작"]
N["실제 DOM에 변경사항 적용<br/>(삭제 → 업데이트 → 추가)"]
O["UI 업데이트 완료"]
end
A --> B
B --> C
C --> D
D --> E
E --> F
F -- "아니요" --> G
F -- "예" --> H
H --> I
G --> J
I --> J
J --> K
K -- "아니요" --> E
K -- "예" --> L
L --> M
M --> N
N --> O
2부: React Compiler, 똑똑한 최적화의 등장
그렇다면 React Compiler는 이 표준 파이프라인에서 어떤 역할을 할까요? 컴파일러는 "렌더 단계를 더 똑똑하게 만드는 조력자" 입니다. 먼저 컴파일러가 어떤 철학으로 설계되었는지 살펴보는 것이 중요합니다.
React Compiler 설계 원칙 (Goals & Non-Goals)
React Compiler의 핵심 설계 목표와, 의도적으로 제외된 비-목표를 시각화하여 프로젝트의 명확한 범위와 방향을 제시합니다.
mindmap
root((React Compiler<br/>설계 원칙))
subgraph Goals (핵심 목표)
id1["자동 및 제한적 리렌더링<br/>(성능 예측성 확보)"]
id2["시작 시간 성능 유지<br/>(코드 크기 및 오버헤드 최소화)"]
id3["익숙한 React 모델 유지<br/>(useMemo, useCallback 등 제거)"]
id4["관용적(Idiomatic) 코드 지원<br/>(React 규칙 준수 코드)"]
id5["명시적 타입/주석 불필요"]
subgraph Non-Goals (비-목표)
id6["완벽하게 최적화된 렌더링<br/>(런타임 오버헤드 방지)"]
id7["React 규칙을 위반하는 코드 지원"]
id8["레거시 기능 지원<br/>(클래스 컴포넌트 등)"]
id9["JavaScript 언어 100% 지원"]
차트에서 볼 수 있듯이, 컴파일러의 핵심 목표는 개발자가 useMemo
, useCallback
같은 API를 직접 사용하지 않아도, 자동으로 코드를 분석하여 불필요한 리렌더링을 방지하는 것입니다.
어떻게 가능할까요?
컴파일러는 빌드 시점에 코드를 미리 분석하여, 어떤 값이나 함수가 리렌더링 사이에 변하지 않을지 예측합니다.
반응형 스코프(Reactive Scopes) 분석: 컴파일러는 어떤 값들이 서로 의존하는지, 어떤 상태(state)가 변할 때 어떤 부분만 다시 계산하면 되는지를 파악하여 '반응형 스코프'라는 그룹으로 묶습니다.
자동 메모이제이션: 분석이 끝나면, 컴파일러는 마치 개발자가
useMemo
나useCallback
을 직접 추가한 것처럼 코드를 변환합니다. 이 변환된 코드는 렌더 단계에서 React의 Bailout 최적화를 극대화하여, 불필요한 리렌더링을 원천적으로 차단합니다.
개발자의 최적화 작업 변화 시각화
React Compiler가 도입되면서, 개발자가 수동으로 하던 최적화 작업을 컴파일러가 대신해주는 과정을 보여줍니다.
graph TD
subgraph "과거 (Compiler 이전)"
A[개발자] --> B("불필요한 리렌더링 확인");
B --> C{"최적화 필요한가?"};
C -- Yes --> D["useMemo, useCallback 등<br>API 직접 적용"];
C -- No --> E[코드 완료];
D --> E;
end
subgraph "현재 (React 19 + Compiler)"
F[개발자] --> G("React 규칙에 맞춰<br>컴포넌트/로직 작성");
G --> H("React Compiler가<br>자동으로 메모이제이션");
H --> I[코드 완료];
end
결론: 개발자는 로직에, 최적화는 React에 맡기자
정리하자면, React 19의 렌더링 방식은 다음과 같습니다.
기본적으로는 Trigger → Render → Commit 파이프라인을 따릅니다.
React Compiler는 이 파이프라인의 '렌더' 단계를 자동 최적화하여, 개발자가 수동으로 개입할 필요 없이도 최고의 성능을 내도록 돕습니다.
이는 React의 핵심 철학인 'UI를 선언적으로 만드는 것'에 더 깊이 다가가는 중요한 변화입니다. React Compiler는 단순히 편의성을 높이는 것을 넘어, React 생태계 전체의 성능 기준을 한 단계 끌어올리는 혁신을 목표로 하고 있습니다. 만약 이 새로운 기능이 제대로 자리 잡게 된다면, 이제 React를 사용하는 개발자들은 최적화에 대한 고민을 React에 맡기고, 다른 영역에 좀 더 집중할 수 있는 여유(slack)을 가질 수 있게 될 것입니다.
Subscribe to my newsletter
Read articles from Ted Lee directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ted Lee
Ted Lee
Software engineer for web tech. Interested in sustainable growth as software engineer.