다크 모드
Chapter 02. React 렌더링 엔진의 비밀: 가상 DOM(Fiber), 불변성, 그리고 React Compiler
"1장에서 완성한
BoardApp.tsx는 어떻게setPosts호출만으로 화면을 스스로 다시 그렸을까? 왜 React는 브라우저 DOM을 즉시 건드리지 않고 '가상 DOM(Fiber)'이라는 중간 설계도를 거치는가?"
📋 이 장에서 처음 만나는 개념 — 모르는 문법이 나오면 0장 준비운동에서 찾아볼 수 있습니다.
| 개념 | 한 줄 요약 | 설명 위치 |
|---|---|---|
| 명령형 상태 vs React의 스냅샷 모델 | 자바스크립트 변수는 왜 저절로 화면을 바꾸지 못하는가 | 2.1 |
| 가상 DOM (VNode) & Fiber 아키텍처 | 실제 DOM 조작 비용을 줄이기 위해 메모리에 올려둔 경량 객체 트리 | 2.2 |
| Render Phase vs Commit Phase | 컴포넌트 함수를 실행해 차이점(Diff)을 찾는 단계와 실제 DOM에 반영하는 단계 | 2.2 (이 장의 핵심) |
Object.is 얕은 비교와 불변성 | React가 상태 변경 여부를 0.001초 만에 판별하는 기준 | 2.3 |
| 자동 배치 (Automatic Batching) | 여러 번의 setState를 모아 단 1번만 리렌더링하는 스케줄링 기법 | 2.3 |
파생 상태, useMemo, useCallback, useEffect | 화면에 보여 줄 계산값 / 함수 참조 보존 / 상태 변경에 따른 부수 효과 | 2.4 |
| React 19 Compiler (Forget) | 수동 useMemo/useCallback 없이 빌드 타임에 자동으로 메모이제이션하는 컴파일러 | 2.4 |
TIP
🏁 이 장의 시작 파일
이 장은 BoardApp.tsx의 동작 원리를 깊이 있게 이해하는 아키텍처 장입니다. BoardApp.tsx는 1장(또는 1.5장)을 마친 상태 그대로 두고 읽으십시오. 이 장에서 손대는 파일: 없음 (50줄 코드로 React 렌더러와 useState를 직접 구현하는 실습은 이어지는 2.5장에서 진행합니다.)
NOTE
바쁜 분을 위한 3줄 요약
- React는
setState에 **이전과 다른 값/참조(!Object.is(prev, next))**가 들어오면 컴포넌트 함수를 다시 실행해 새 가상 DOM(Fiber)을 만들고, 이전 트리와 비교(Diffing)해 달라진 부분만 실제 DOM에 한 번에 반영(Commit)합니다. - 한 함수 안에서
setState를 여러 번 호출해도 React는 이를 모았다가 단 1번만 렌더링합니다(Automatic Batching). - 화면에 보여 줄 계산값은 파생 변수(또는
useMemo), 값이 바뀌면 무언가 외부 작업을 해야 할 때는useEffect를 쓰며, React 19 Compiler가 도입되면 수동 메모이제이션 부담이 크게 줄어듭니다.
2.1 자바스크립트는 원래 반응형(Reactive)이 아니다
1장에서 직접 코딩한 BoardApp.tsx의 삭제 로직을 다시 살펴봅시다:
tsx
// BoardApp.tsx의 삭제 로직
setPosts((prevPosts) => prevPosts.filter((p) => p.id !== id));만약 우리가 setPosts를 쓰지 않고 그냥 일반 자바스크립트 배열 변수를 고쳤다면 어떻게 되었을까요?
자바스크립트 언어 자체는 본래 반응형 언어가 아닙니다:
typescript
let price = 1000;
let quantity = 2;
let total = price * quantity; // 2000
price = 2000;
console.log(total); // 🚨 여전히 2000! (자동으로 4000이 되지 않는다)자바스크립트는 명령형(Imperative) 언어입니다. price = 2000으로 바꿨다고 해서 연관된 계산이나 화면 갱신이 자동으로 일어나는 기능은 브라우저 자바스크립트 엔진(V8)에 없습니다.
💡 비유: 엑셀(Excel) vs 자바스크립트
엑셀 스프레드시트를 떠올려 보십시오. A1 셀에 1,000원을 넣고 B1에 2개를 넣은 뒤 C1에=A1*B1을 써 두면, A1 값을 2,000원으로 바꾸는 순간 C1은 알아서 4,000원이 됩니다. 엑셀을 쓰는 신입사원도 당연하게 생각하는 이 기능이, 놀랍게도 자바스크립트에서는 기본 지원되지 않습니다. 자바스크립트는 누가 등을 떠밀어 함수를 다시 실행시켜 주지 않으면 스스로 다시 계산할 줄 모르는 고지식한 언어이기 때문입니다.
이 문제를 해결하기 위해 프레임워크들은 두 가지 길 중 하나를 선택했습니다:
- Vue 3의 길 (
Proxy가로채기): 객체를Proxy로 감싸 속성을 읽고 쓰는 순간을 가로챕니다. - React 19의 길 (명시적
setState와 함수 재실행 스냅샷): 객체를 마법처럼 개조하지 않고 순수 자바스크립트 값 그대로 둡니다. 대신 개발자가setState(새값)으로 변경 신호를 보내면, React가 컴포넌트 함수를 처음부터 다시 실행하여 새 화면 설계도를 얻어냅니다!
2.2 왜 가상 DOM(Virtual DOM)과 Fiber인가? (Render vs Commit)
"상태가 바뀔 때마다 BoardApp() 함수를 처음부터 다시 실행한다고요? 그러면 화면의 <table className="data-table"> 전체를 매번 다 부수고 새로 그리나요? 엄청 느리지 않나요?"
정확한 질문입니다! 브라우저의 실제 DOM(Document Object Model)을 수정하는 것은 컴퓨터 과학에서 가장 값비싼 연산 중 하나입니다. DOM 노드 하나를 바꾸면 브라우저는 화면 전체의 기하학적 위치와 크기를 다시 계산하는 **리플로우(Reflow / Layout)**와 픽셀을 다시 칠하는 리페인트(Repaint) 과정을 거치며 CPU를 혹사시킵니다.
jQuery가 대량 목록에서 느렸던 이유는 루프를 돌며 .append()할 때마다 실제 브라우저 DOM을 직접 찔러댔기 때문입니다:
javascript
// jQuery의 비극: 100번 돌 때마다 브라우저 레이아웃 엔진이 100번 신음함
for (let i = 0; i < 100; i++) {
$('#tbody').append('<tr><td>' + data[i].title + '</td></tr>');
}React는 이 문제를 해결하기 위해 가상 DOM(Virtual DOM)과 Fiber 아키텍처라는 영리한 중간 완충 계층을 두고, 렌더링 파이프라인을 철저하게 **2단계(Render 단계 → Commit 단계)**로 분리했습니다.
🏗️ 비유: 건축 설계도(가상 DOM Fiber) vs 실제 공사 현장(브라우저 DOM)
건물을 지을 때 벽돌 위치를 10cm 옮기고 싶다고 해서, 포클레인을 끌고 공사 현장으로 달려가 벽을 허물고 다시 쌓는 건축가는 없습니다.
사무실 책상에 앉아 **종이 설계도(메모리 속 가상 DOM 객체)**를 새로 그려 이전 설계도와 겹쳐 보며 어디가 달라졌는지 빨간 펜으로 동그라미(Diffing)를 친 뒤, 최종 수정 지시서 딱 한 장만 현장 인부들에게 넘겨 실제 공사(브라우저 DOM 반영)는 꼭 필요한 벽돌 한 장만 단 한 번에 시공합니다.
1단계: 렌더 단계 (Render Phase / Reconciliation)
setPosts가 호출되면 React는BoardApp()함수를 다시 호출합니다.- JSX는 실제 HTML 태그가 아니라
{ type: 'tr', key: 3, props: { ... } }같은 **가벼운 자바스크립트 객체(React Element)**를 반환합니다(0장 0.2 참고). 자바스크립트 메모리에 객체 수천 개를 만드는 일은 1밀리초도 걸리지 않습니다. - React는 이 설계도를 바탕으로 작업 단위인 **Fiber 노드 트리(Work-in-Progress Tree)**를 만들고, 직전 화면의 Fiber 트리(
current)와 비교(Reconciliation / Diffing)합니다. - 1장에서
<tr key={post.id}>를 붙인 이유가 바로 이 단계 때문입니다.key가 있어야 React가 "1번, 2번, 4번 행은 그대로이고 3번 행만 삭제되었다"는 사실을 정확히 알아냅니다.
2단계: 커밋 단계 (Commit Phase)
- 렌더 단계에서 찾아낸 **"진짜로 바뀐 차이점 목록"**만 들고 실제 브라우저 DOM으로 갑니다.
- 전체 테이블을 다시 그리는 것이 아니라, 삭제된 3번
<tr>딱 하나에 대해서만tbody.removeChild(tr3)과 배지 숫자의textContent변경만 수행합니다! - 실제 DOM 갱신이 끝나고 브라우저가 화면을 그리면, 마지막으로
useEffect에 등록된 함수들을 실행합니다.
🔬 심화(건너뛰어도 됨): 왜 이름을 그냥 가상 DOM이라 안 하고 'Fiber'라고 부르나요?
React 15까지는 한 번 트리 비교(Reconciliation)를 시작하면 끝날 때까지 중간에 멈출 수 없는 재귀 호출 구조(Stack Reconciler)였습니다. 그래서 컴포넌트가 10,000개면 비교하는 50ms 동안 브라우저 메인 스레드가 꽉 막혀 사용자가 타자를 쳐도 글자가 안 찍히는 렉(Jank)이 발생했습니다.
React 16에서 전면 재설계되어 React 19의 동시성(Concurrency) 기반이 된 Fiber 아키텍처는 트리 노드를 단방향 연결 리스트(Linked List) 구조로 바꿨습니다. 덕분에 무거운 렌더링 작업을 잘게 쪼개어 하다가도, 사용자의 클릭이나 키보드 입력 같은 급한 작업이 들어오면 하던 렌더링을 잠시 일시 정지(Yield)하고 사용자 입력을 먼저 처리할 수 있습니다. 10장과 13장에서 다룰 useTransition과 <Activity>가 바로 이 Fiber 엔진 위에서 돌아갑니다!
2.3 불변성(Object.is)과 자동 배치(Automatic Batching)
1) 왜 원본을 직접 수정하면 React가 모를까? (Object.is)
0장 0.3에서 봤듯, React는 setState(nextValue)가 호출되었을 때 이전 상태 prevValue와 새 상태 nextValue를 자바스크립트 표준 함수인 **Object.is(prevValue, nextValue)**로 비교합니다.
typescript
// 브라우저 콘솔(F12)에서 직접 확인해 보십시오:
Object.is(1, 1); // true (같음 -> 리렌더링 안 함)
Object.is(1, 2); // false (다름 -> 리렌더링 발동!)
const arr = [1, 2];
arr.push(3); // 원본 배열에 3을 추가했지만...
Object.is(arr, arr); // 🚨 true! (메모리 주소가 그대로이므로 React는 "안 바뀌었네!" 하고 무시함)
const nextArr = [...arr, 3]; // 새 배열 생성
Object.is(arr, nextArr); // ✅ false! (메모리 주소가 달라졌으므로 즉시 리렌더링 발동!)만약 React가 Object.is(얕은 참조 비교)를 쓰지 않고 객체 안의 모든 속성을 끝까지 뒤지는 깊은 비교(Deep Comparison)를 했다면, 게시글 1,000개가 담긴 배열의 상태를 바꿀 때마다 수만 개의 필드를 비교하느라 브라우저가 느려졌을 것입니다. **"불변성을 지켜 새 참조를 만든다"**는 규칙은 개발자가 약간의 수고(...spread, filter, map)를 들이는 대신, 비교 비용을 O(1), 즉 0.001초로 줄이는 최고의 성능 타협안입니다.
2) React 18 & 19의 자동 배치 (Automatic Batching)
1장의 게시글 등록 성공 코드를 다시 떠올려 봅시다:
tsx
// handleSubmit 내부: 상태 변경 함수를 연달아 3번 호출했다!
setPosts((prev) => [res.data, ...prev]);
setForm({ title: '', author: '', content: '' });
setIsSubmitting(false);상태 변경 함수가 3번 연속으로 호출되었습니다. 그렇다면 BoardApp() 컴포넌트 함수도 3번 다시 실행될까요?
아닙니다! React는 단 1번만 리렌더링합니다.
React는 이벤트 핸들러, async/await 비동기 함수, setTimeout 안 어디서든 동시에 일어난 여러 상태 변경을 대기열에 모아 두었다가, 현재 실행 중인 코드 블록이 끝나는 순간 단 한 번의 렌더링(Batching)으로 합쳐서 처리합니다. (1.5장 1.5.2에서 setPosts 바로 다음 줄에 아직 새 DOM이 없었던 이유도 바로 이 자동 배치 때문입니다.)
2.4 계산된 값과 부수 효과: 파생 상태, useMemo, useCallback, 그리고 React Compiler
상태가 준비되었을 때 우리는 두 가지 일을 합니다: ① 상태를 가공해 화면에 보여 줄 값을 계산하거나, ② 상태가 바뀌었을 때 **서버 통신·타이머 같은 외부 작업(부수 효과, Side Effect)**을 실행합니다. 이 둘은 목적이 정반대이므로 명확히 구분해야 합니다.
1) 화면에 보여 줄 계산값: 파생 상태(Derived State)와 useMemo
가장 흔한 초보자 실수는 "기존 상태로부터 계산할 수 있는 값"을 또 useState로 만드는 것입니다.
tsx
const [posts, setPosts] = useState<Post[]>([]);
// ❌ 나쁜 예: posts.length로 알 수 있는 값을 별도 useState로 중복 관리 (동기화 버그의 온상!)
// const [postCount, setPostCount] = useState(0);
// ✅ 좋은 예 1 (가벼운 계산): 그냥 일반 상수로 계산한다! 리렌더링될 때마다 0.0001초 만에 계산됨
const postCount = posts.length;
const hasPosts = postCount > 0;
// ✅ 좋은 예 2 (무거운 계산): 수천 건 배열 필터링·정렬은 useMemo로 캐싱한다!
const noticePosts = useMemo(() => {
return posts.filter((p) => p.category === 'NOTICE');
}, [posts]); // posts 참조가 바뀔 때만 다시 계산하고, 평소에는 캐시된 배열을 즉시 반환- 규칙:
useMemo에 넘기는 계산 함수는 **순수 함수(값을 계산해서 반환만 함)**여야 합니다. 그 안에서 API를 호출하거나 다른 상태를 바꾸면 안 됩니다.
2) 함수 참조 보존: useCallback
컴포넌트 함수가 리렌더링될 때마다 그 안에 선언된 일반 함수(handleDelete)도 매번 새로 만들어집니다. 평소에는 아무 문제가 없지만, 13장처럼 자식 컴포넌트를 React.memo로 최적화할 때는 함수의 메모리 주소(참조)가 유지되어야 합니다.
tsx
// 의존성 배열([])이 바뀌지 않는 한 동일한 함수 참조(메모리 주소)를 유지한다
const handleDelete = useCallback(async (id: number) => {
await axios.delete(`/api/posts/${id}`);
setPosts((prev) => prev.filter((p) => p.id !== id));
}, []);🆕 처음 보는 개념: React 19 Compiler (React Forget)와 자동 메모이제이션
React 18까지는 개발자가 어디에 useMemo와 useCallback을 감싸야 할지 직접 고민해야 했습니다.
React 19와 함께 정식 도입된 **React Compiler (babel-plugin-react-compiler)**를 프로젝트에 켜 두면, 빌드 시점에 컴파일러가 코드를 분석해 파생 계산값과 함수를 자동으로 메모이제이션해 줍니다!
tsx
// 개발자가 이렇게 평범하게 작성해도, React 19 Compiler가 빌드 시점에 알아서 캐싱 코드를 심어 준다!
const noticePosts = posts.filter((p) => p.category === 'NOTICE');다만 ① 컴파일러가 꺼진 프로젝트나 라이브러리 코드를 읽고 쓸 줄 알아야 하고, ② useEffect의 의존성 참조 안정성을 세밀하게 제어할 때는 여전히 useMemo/useCallback의 원리를 알아야 하므로 본 교재에서는 기본 원리와 컴파일러 방식을 함께 다룹니다(13장에서 성능 프로파일링으로 직접 비교합니다).
3) 상태가 바뀌었을 때 외부 작업 실행: useEffect
반면 검색어가 바뀌었을 때 백엔드 API를 호출하거나, 로컬 스토리지에 저장하거나, 타이머를 거는 작업은 화면 계산이 아니라 **부수 효과(Side Effect)**입니다. 이때는 useEffect를 씁니다.
tsx
const [keyword, setKeyword] = useState('');
// keyword 상태가 바뀔 때마다(그리고 첫 마운트 시) 실행되는 부수 효과
useEffect(() => {
console.log(`검색어 변경 감지: ${keyword} -> 서버 조회 실행`);
// 여기서 API 호출 수행 (3장에서 디바운스·요청 취소와 함께 실습)
}, [keyword]);| 비교 항목 | 일반 파생 변수 (const x = ...) | useMemo(() => ..., [deps]) | useEffect(() => ..., [deps]) |
|---|---|---|---|
| 목적 | 가벼운 화면 표시용 값 계산 | 무거운 화면 표시용 값 계산 캐싱 | API 호출, 타이머, DOM 조작 등 부수 효과 |
| 실행 시점 | 렌더링 도중 (동기) | 렌더링 도중 (deps 바뀔 때만 재계산) | 렌더링 & DOM 커밋이 끝난 직후 (비동기) |
| 반환값 | 계산된 값 | 캐싱된 계산값 | 클린업 함수 (return () => ...) |
| 게시판 예시 | postCount, hasPrevPage | 10,000건 필터링, totalPages | 마운트 시 loadPosts(), 검색어 디바운스 조회 |
TIP
판단 기준 한 줄 요약: "이 값을 화면에 보여주기 위해 계산하는가?" → 파생 변수 또는 useMemo. "이 값이 바뀌면 **외부 세계와 통신(API, 타이머, 스토리지)**해야 하는가?" → useEffect(또는 클릭 시점이라면 이벤트 핸들러).
💡 비유로 1초 만에 정리하는 useMemo vs useEffect
useMemo= 태양광 계산기: 화면에 숫자를 띄우기 위해 존재합니다. 입력값(posts)이 안 바뀌면 아까 계산해 둔 액정의 결과(캐시)를 그대로 재사용합니다. 화면에 보여줄 가공 데이터는 무조건 계산기입니다!useEffect= 현관문 동작 감지 센서: 사람이 지나갈 때마다(deps변경) 경보기를 울리거나 외부에 전화를 겁니다(부수 효과). 상태가 바뀌었을 때 "서버에 API 요청을 날리거나, 로컬 스토리지에 저장하거나, 타이머를 켜는 등 화면 밖의 작업(Side Effect)을 해야 할 때" 씁니다.
2.5 BoardApp.tsx의 상태 설계 3원칙
실무에서 어떤 값을 useState로 만들고, 어떤 값을 일반 변수나 useRef로 만들어야 할지 헷갈린다면 아래 기준표를 적용하십시오:
| 구분 | 질문 | 선택할 도구 | BoardApp.tsx 적용 예시 |
|---|---|---|---|
| 1. 반응형 화면 상태 | 값이 바뀌면 화면도 다시 그려져야 하고, 다른 상태로부터 계산할 수 없는 원본 데이터인가? | useState | posts, isLoading, form, selectedId |
| 2. 파생 상태 (계산값) | 이미 있는 useState로부터 계산해서 바로 구할 수 있는 값인가? | 일반 상수 / useMemo (절대 useState 금지!) | const postCount = posts.lengthconst totalPages = Math.ceil(...) |
| 3. 비렌더링 참조값 | 값이 바뀌어도 화면을 다시 그릴 필요가 없거나, 실제 DOM 요소를 가리키는가? | useRef | titleInputRef, tableBodyRef, 디바운스 타이머 ID |
WARNING
파생 상태를 useState에 중복 저장하지 마십시오!
예를 들어 posts 배열이 있는데 postCount를 별도 useState(0)로 만들면, 글을 추가·삭제할 때마다 setPosts와 setPostCount를 둘 다 챙겨야 합니다. 하나라도 빼먹으면 1장의 jQuery 레거시와 똑같은 상태 불일치 버그가 재발합니다. 계산할 수 있는 값은 무조건 렌더링 중에 계산하십시오!
2.6 [직접 해보기] 실습 미션 💪
미션: 잘못된 useEffect + useState 코드를 깔끔한 파생 상태(useMemo)로 리팩터링하기
신입 개발자가 BoardApp.tsx에서 "'개발(DEV)' 카테고리 글의 개수"를 화면에 표시하려고 아래처럼 코드를 짰습니다:
tsx
// ❌ 신입 개발자의 안티패턴 코드: 파생 값을 useState + useEffect로 관리함
const [devCount, setDevCount] = useState(0);
useEffect(() => {
const filtered = posts.filter((p) => p.category === 'DEV');
setDevCount(filtered.length);
}, [posts]);- 위 코드는 왜 불필요하게 렌더링을 2번 일으키는지 설명해 보십시오.
useState와useEffect를 모두 지우고, 단 한 줄(또는useMemo)로 리팩터링해 보십시오.
💡 미션 정답 코드 확인하기
- 왜 2번 렌더링되는가?:
posts가 바뀌어 1차 렌더링이 끝나고 화면이 그려진 뒤에야useEffect가 실행되고, 그 안에서setDevCount를 또 호출하므로 불필요한 2차 리렌더링이 연속으로 발생합니다. 코드도 길어지고 성능도 손해입니다. - 올바른 리팩터링 코드:
tsx
// ✅ 방법 A (권장 - 배열이 수백 건 이하일 때): 렌더링 중 즉시 계산 (리렌더링 1번으로 끝!)
const devCount = posts.filter((p) => p.category === 'DEV').length;
// ✅ 방법 B (배열이 수천 건 이상이거나 React Compiler 미사용 환경일 때): useMemo로 캐싱
const devCount = useMemo(
() => posts.filter((p) => p.category === 'DEV').length,
[posts]
);2.7 이번 장 핵심 요약 및 다음 단계 🚀
핵심 3줄 요약
- React는
setState호출 시Object.is로 참조 변화를 감지하여 컴포넌트 함수를 다시 실행하고, 이전 Fiber 트리와 새 트리를 비교(Render 단계)한 뒤 바뀐 부분만 실제 DOM에 반영(Commit 단계)합니다. - 한 이벤트 안에서
setState를 여러 번 호출해도 **자동 배치(Automatic Batching)**를 통해 단 1번만 리렌더링됩니다. - 기존 상태로부터 계산 가능한 값은 별도
useState를 만들지 않고 **파생 변수(또는useMemo)**로 계산하며, 상태 변경에 따른 외부 작업(API 호출, 타이머)만useEffect에서 수행합니다.
바뀐 파일
| 파일 | 무엇이 바뀌었나 |
|---|---|
| (없음) | 이 장은 원리를 이해하는 장입니다. 직접 엔진을 만들어 보려면 이어지는 2.5장의 practice/mini-react.ts 실습을 진행하십시오. |
자주 하는 실수 (Troubleshooting)
| 증상 | 원인 | 해결 |
|---|---|---|
posts.push(newPost); setPosts(posts);를 했는데 화면이 그대로임 | 같은 배열 참조를 넘겨 Object.is(prev, next)가 true가 됨 | setPosts(prev => [...prev, newPost])로 새 배열 생성 |
setCount(count + 1) 바로 다음 줄에서 console.log(count)를 찍었더니 이전 값이 나옴 | setCount는 다음 렌더링을 예약할 뿐 현재 함수 스코프의 상수 count를 바꾸지 않음 | 계산된 다음 값이 당장 필요하면 const next = count + 1; setCount(next);로 변수에 담아 사용 |
파생 값을 useEffect 안에서 setState로 갱신해 렌더링이 두 번씩 일어남 | 계산 가능한 값을 별도 상태로 중복 선언함 | useEffect와 별도 useState를 지우고 파생 상수/useMemo로 변경 |
셀프 체크 — 답을 먼저 떠올린 뒤 펼쳐 보십시오.
- 클릭 핸들러 하나에서
setTitle(''),setAuthor(''),setContent('')세 개를 연속으로 호출했습니다.BoardApp컴포넌트 함수는 몇 번 다시 실행될까요? - React가 객체 내부 속성을 일일이 깊은 비교(Deep Compare)하지 않고 메모리 주소만 얕게 비교(
Object.is)하는 이유는 무엇인가요? - "검색어가 바뀌면 서버에 다시 조회한다"와 "게시글이 0건이면 안내 문구를 보여 준다" 중
useEffect가 어울리는 것은 무엇이고, 다른 하나에는 무엇이 어울릴까요? - React 19 Compiler가 도입되면 개발자의 코드 작성 방식에서 무엇이 가장 크게 달라지나요?
셀프 체크 정답
- 1번입니다. 자동 배치(Automatic Batching) 덕분에 세 번의 상태 변경이 하나로 묶여 마지막에 한 번만 렌더링됩니다.
- 수천 개의 게시글이 담긴 배열이나 깊은 객체를 매번 끝까지 비교하면 비교 연산 자체에 CPU 시간을 너무 많이 쓰게 됩니다. 불변성 규칙(새 참조 생성)을 지키는 대신
Object.is로 메모리 주소만 비교하면 어떤 크기의 데이터든 0.001초(O(1)) 만에 변경 여부를 판별할 수 있습니다. - 검색어 재조회는 "상태가 바뀌면 외부 API를 호출한다(부수 효과)"이므로
useEffect, 0건 안내는 "화면에 보여 주기 위한 계산"이므로 JSX 안의 파생 조건식(posts.length === 0)이 어울립니다. - 빌드 타임에 컴파일러가 파생 계산값과 함수를 자동으로 메모이제이션해 주므로, 성능 최적화를 위해 코드 곳곳에 수동으로
useMemo와useCallback을 감싸던 보일러플레이트가 크게 줄어듭니다.
다음 챕터로 이어지는 질문
이제 우리는 React 렌더링 엔진이 가상 DOM과 불변성 위에서 어떻게 돌아가는지 큰 그림을 잡았습니다.
잠시 2.5장(선택·심화)에서 50줄 코드로 React의 useState와 렌더러를 직접 만들어 보며 "왜 훅을 조건문(if) 안에서 부르면 안 되는지"를 눈으로 확인하거나, 바로 Chapter 03으로 넘어가 실무 개발자들이 가장 많이 겪는 오래된 클로저(Stale Closure) 버그와 React 19.2+의 신무기 useEffectEvent, 실시간 검색 디바운스를 실습해 보십시오!