React란 무엇인가요?
- 예상 시간
- 6분
React는 사용자 인터페이스를 component 단위로 선언적으로 만들 수 있게 해주는 JavaScript library입니다. state가 바뀌면 Virtual DOM으로 변경 전후를 비교하고, 실제 DOM에는 바뀐 부분만 최소한으로 반영합니다. view layer에 집중한 library라서 routing, 데이터 패칭, 상태 관리는 직접 선택해서 조합해야 합니다.
2011년 Facebook의 광고 시스템이 복잡해지면서 Jordan Walke가 React의 초기 형태를 만들었습니다. 당시 지배적이던 jQuery 기반 개발에서는 DOM을 직접 조작하다 보니 UI 상태와 DOM이 어긋나는 버그가 잦았습니다. Backbone, AngularJS 같은 MVC 프레임워크도 있었지만, 양방향 데이터 바인딩(Model ↔ View가 서로 변경)은 state가 어디서 바뀌는지 추적하기 어려웠습니다.
React는 MVC에서 V(View)에만 해당합니다. Model(데이터)이나 Controller(로직 흐름)를 직접 규정하지 않고, 주어진 state를 화면으로 변환하는 역할만 담당합니다. 그래서 routing, 데이터 패칭, 전역 상태 관리는 React에 포함되어 있지 않고 직접 선택해서 조합해야 합니다. Angular처럼 이 모든 것을 내장한 full framework와 대비되는 지점입니다.
React의 핵심 아이디어는 UI를 state => UI 형태의 순수 함수로 보는 것이었습니다. state가 바뀌면 매번 전체를 다시 그리되, 실제 DOM 조작은 최소화하기 위해 Virtual DOM을 중간에 두었습니다. 단방향 데이터 흐름을 강제해 state 변경의 출처를 항상 추적할 수 있게 했습니다.
2013년 오픈소스로 공개될 때 JSX가 HTML과 JavaScript를 섞는다는 이유로 거부감이 컸지만, "관심사 분리는 언어가 아닌 기능 단위로 해야 한다"는 React 팀의 주장이 점차 받아들여졌습니다. Angular처럼 routing, 폼, HTTP까지 모두 내장한 full framework와 달리, React는 view layer에만 집중해 다른 라이브러리와 자유롭게 조합할 수 있다는 점이 빠른 확산의 이유 중 하나였습니다.
꼬리질문
조금 더 깊게 물어본다면
답변 뒤에 이어질 수 있는 질문들을 하나씩 열어볼 수 있어요.
Virtual DOM이란?
실제 DOM의 가벼운 JavaScript 객체 복사본입니다. state가 변경될 때 새 Virtual DOM 트리를 만들고, 이전 것과 비교(diffing)해 실제로 바뀐 부분만 DOM에 반영합니다.
jQuery 시대에는 개발자가 DOM을 직접 조작했습니다. 버튼 클릭 → $('#count').text(newCount) 같은 방식입니다. 애플리케이션이 단순할 때는 괜찮지만, 수십 개 컴포넌트가 서로 연결된 복잡한 UI에서는 어떤 이벤트가 어떤 DOM을 바꿨는지 추적하기 어려워지고, DOM과 실제 데이터 상태가 어긋나는 버그가 생겼습니다.
React의 해결책은 "매번 전체를 다시 그린다"는 단순한 아이디어입니다. state가 바뀌면 컴포넌트 함수를 다시 호출해 새 Virtual DOM 트리를 만들고, 이전 트리와 비교해 실제로 달라진 부분만 DOM에 반영합니다. 개발자는 DOM 조작을 전혀 신경 쓰지 않고 "이 state에서 UI가 어떤 모습이어야 하는가"만 선언하면 됩니다.
Virtual DOM은 일반 JavaScript 객체입니다. { type: 'div', props: { className: 'btn' }, children: [...] } 형태로 실제 DOM 노드보다 훨씬 가볍습니다. 두 Virtual DOM 트리를 비교하는 비용은 실제 DOM 조작 비용보다 훨씬 작기 때문에, 전체를 다시 계산하고 최소한만 반영하는 전략이 성립합니다.
DOM 조작이 왜 비싼가요?
브라우저는 DOM이 바뀌면 렌더링 파이프라인(Style → Layout → Paint → Composite)을 다시 실행해야 합니다. 특히 Layout(reflow)이 일어나면 다른 요소들의 위치까지 재계산해야 해서 비용이 큽니다.
브라우저의 렌더링 파이프라인은 단계마다 비용이 다릅니다.
- Layout(reflow): 요소의 크기와 위치를 계산합니다. 한 요소의 크기가 바뀌면 주변 요소들의 위치도 연쇄적으로 재계산해야 해서 가장 비쌉니다.
width,height,margin,top,left같은 속성 변경이나.offsetWidth를 읽으면 발생합니다. - Paint: 요소를 픽셀로 그립니다. Layout이 바뀌지 않아도 배경색이나 텍스트 색 변경 시 발생합니다.
- Composite: GPU가 레이어를 합성합니다.
transform이나opacity만 바뀌면 Layout과 Paint를 건너뛰고 이 단계만 실행합니다. 세 단계 중 가장 저렴합니다.
"layout thrashing"이라는 문제도 있습니다. DOM 쓰기(스타일 변경) 직후 바로 DOM 읽기(.offsetWidth 등)를 하면 브라우저가 강제로 synchronous layout을 실행해야 합니다. 브라우저는 여러 DOM 변경을 한 번에 묶어 처리하려 하지만, 중간에 레이아웃 값을 읽으면 그 전에 강제로 계산해야 하기 때문입니다. 이 패턴이 루프 안에서 반복되면 성능이 크게 떨어집니다.
Virtual DOM은 변경 사항을 한 번에 묶어 DOM에 반영하므로, 이 파이프라인 실행 횟수를 최소화합니다.
diffing algorithm은 어떻게 동작하나요?
두 트리를 완전히 비교하면 O(n³)인데, React는 두 가지 가정으로 O(n)까지 줄입니다. 같은 위치에 타입이 바뀌면 기존 트리를 버리고 새로 만들고, 같은 타입이면 속성만 업데이트합니다. 리스트는 key로 어떤 아이템이 변경됐는지 판단합니다.
일반적인 트리 편집 거리(tree edit distance) 알고리즘은 O(n³)입니다. 두 트리 사이의 최소 편집 연산(삽입, 삭제, 이동)을 찾으려면 한 트리의 각 노드를 다른 트리의 모든 노드와 비교하고, 그 중 최선의 변환을 찾아야 합니다. 노드 1000개면 연산이 10억 번이 되어 실용적이지 않습니다.
React는 실제 UI 트리에 성립하는 두 가지 heuristic으로 이를 O(n)으로 줄입니다.
- 첫 번째 heuristic은 같은 위치에 다른 타입의 요소가 오면 하위 트리 전체를 버리고 새로 만듭니다. UI에서
<div>가<span>으로 바뀌는 경우는 실제로 드물기 때문에 이 가정이 현실적으로 유효합니다. - 두 번째 heuristic은 리스트 아이템을
key로 식별합니다. key가 없으면 순서로 비교하는데, 맨 앞에 새 아이템이 추가되면 모든 아이템이 바뀐 것처럼 처리됩니다. key가 있으면 아이템이 이동했는지, 추가됐는지, 삭제됐는지 정확히 파악합니다.
이 두 가정 덕분에 트리를 한 번만 순회(O(n))하면서 비교가 끝납니다.
key prop이 왜 필요한가요?
key가 없으면 React는 리스트를 순서로 비교합니다. 맨 앞에 아이템을 추가하면 모든 아이템이 변경된 것처럼 인식해 전체를 리렌더링합니다. key가 있으면 어떤 아이템이 이동하거나 추가됐는지 정확히 파악할 수 있습니다.
// key 없이 → 앞에 추가 시 전체 리렌더링
{items.map((item) => <li>{item.name}</li>)}
// key 있음 → 새 아이템만 DOM에 추가
{items.map((item) => <li key={item.id}>{item.name}</li>)}배열 인덱스를 key로 쓰면 안 됩니다. 맨 앞에 새 아이템이 추가되면 기존 아이템들의 인덱스가 전부 밀립니다. React는 key가 같으면 같은 아이템으로 인식하기 때문에, 인덱스를 key로 쓴 상태에서 앞에 삽입이 일어나면 컴포넌트 내부 state가 원래 자리에 남아 엉뚱한 아이템에 붙게 됩니다. 예를 들어 각 아이템이 input을 갖는 목록에서 맨 앞에 새 항목을 추가하면, 기존 input 값이 아래로 밀려 내려가지 않고 같은 인덱스 자리에 고정된 것처럼 보이는 버그가 생깁니다. DB의 id처럼 고유한 식별자를 써야 합니다.
React Fiber란?
React 16에서 도입된 reconciliation 엔진입니다. 이전 방식은 렌더링을 중간에 멈출 수 없어 큰 트리 처리 시 메인 스레드가 블로킹됐습니다. Fiber는 렌더링 작업을 작은 단위로 쪼개 우선순위에 따라 중단하고 재개할 수 있습니다.
브라우저는 60fps를 유지하려면 16ms마다 한 번씩 프레임을 그려야 합니다. React 15까지의 stack reconciler는 재귀 함수 호출로 트리를 순회했습니다. 재귀가 시작되면 call stack이 전부 소진될 때까지 제어권이 브라우저로 돌아가지 않습니다. 컴포넌트 트리가 크면 한 번의 렌더링이 100ms 이상 메인 스레드를 점유할 수 있고, 그 사이 애니메이션이 끊기거나 사용자 입력이 지연됩니다.
Fiber는 이 재귀 순회를 linked list + 반복 루프로 바꿉니다. 각 fiber node(컴포넌트 하나)를 처리한 뒤 스케줄러에 제어를 반환할 수 있습니다. 스케줄러는 현재 프레임에 남은 시간이 있는지 확인하고, 없으면 렌더링을 일시 중단하고 더 급한 작업을 먼저 처리합니다. OS의 cooperative scheduling과 유사합니다. 프로세스가 자발적으로 CPU를 양보하는 방식입니다.
작업을 쪼개는 기준은 세 가지입니다.
- 단위: fiber node 하나(컴포넌트 또는 DOM 요소 하나)가 작업의 최소 단위입니다. 컴포넌트 함수를 호출하고 결과를 처리하는 것이 한 단위입니다.
- 순회 순서: child → sibling → parent(return) 순서로 linked list를 따라 이동합니다. 재귀와 달리 다음 작업 단위가 포인터로 명확히 정해져 있어 어느 시점에서든 중단하고 재개할 수 있습니다.
- 중단 시점: 각 fiber node를 처리한 직후, 스케줄러가 현재 프레임에 남은 시간(
MessageChannel기반)을 확인합니다. 시간이 부족하면 지금까지의 진행 상황을 유지한 채 멈추고, 다음 프레임에 이어서 처리합니다.
이 기반 위에 React 18의 Concurrent Mode가 동작합니다. 렌더링 작업에 우선순위(lane)가 생겨, 사용자 입력 처리는 높은 우선순위로 즉시 처리하고 덜 급한 화면 갱신은 낮은 우선순위로 처리할 수 있습니다. startTransition이 이 낮은 우선순위를 명시적으로 지정하는 API입니다.
JSX란 무엇인가요?
JavaScript 안에 HTML과 유사한 문법을 쓸 수 있게 해주는 문법 확장입니다. 브라우저는 JSX를 직접 이해하지 못하므로, Babel이 JSX를 React.createElement() 호출로 변환합니다.
// 개발자가 쓰는 JSX
const element = <button className="btn">클릭</button>;
// Babel이 변환한 결과
const element = React.createElement("button", { className: "btn" }, "클릭");JSX가 처음 공개됐을 때 "HTML을 JavaScript에 섞는다"는 이유로 거부감이 컸습니다. 당시 업계 표준은 HTML, CSS, JavaScript를 분리하는 "관심사 분리"였습니다. React 팀은 이를 "관심사 분리가 파일 분리를 의미하지 않는다"고 반박했습니다. 버튼 하나의 마크업과 동작은 실제로 강하게 결합되어 있고, 파일만 분리하면 실제 의존성은 숨겨질 뿐이라는 주장이었습니다.
React.createElement는 Virtual DOM 노드를 나타내는 일반 JavaScript 객체를 반환합니다. JSX는 이 함수 호출을 사람이 읽기 쉬운 트리 형태로 추상화한 것입니다. React 16까지는 JSX 파일 상단에 import React from 'react'가 필요했습니다. Babel이 JSX를 React.createElement 호출로 변환했고, 이 함수를 쓰려면 React 객체가 반드시 현재 스코프에 있어야 했기 때문입니다.
React 17부터는 JSX를 React.createElement 대신 react/jsx-runtime의 _jsx 함수로 변환합니다. 그리고 Babel이 이 import를 파일마다 자동으로 추가해줍니다.
// React 17+ Babel이 자동으로 삽입
import { jsx as _jsx } from 'react/jsx-runtime';
// <button>클릭</button> 변환 결과
_jsx("button", { children: "클릭" });개발자가 직접 import React를 쓰지 않아도 되는 이유는 Babel이 필요한 import를 알아서 넣어주기 때문입니다. 변환 대상 함수가 바뀌었고, 그 함수의 import를 자동화한 것입니다.
JSX 없이 React를 사용할 수 있나요?
가능합니다. JSX는 문법 설탕(syntactic sugar)이고, React.createElement를 직접 호출하면 됩니다. 다만 컴포넌트가 복잡해질수록 읽기 어려워져 실무에서는 항상 JSX를 씁니다.
// JSX 없이 중첩 구조 표현
React.createElement(
'div',
{ className: 'card' },
React.createElement('h2', null, title),
React.createElement('p', null, description),
React.createElement('button', { onClick: handleClick }, '클릭')
)이렇게 쓸 수는 있습니다. 그런데 코드를 보면 알 수 있듯이 겨우 3단계 중첩인데도 벌써 읽기 불편합니다. 실제 컴포넌트에서 중첩이 더 깊어지면 어느 인자가 어느 노드에 해당하는지 파악하기 어렵고, UI 구조가 코드에서 전혀 보이지 않습니다. JSX는 이 함수 호출을 들여쓰기 트리로 바꿔 실제 DOM 구조 그대로 읽힙니다. 기능은 완전히 동일합니다.
선언적 UI란 무엇인가요?
어떤 DOM 조작을 순서대로 할지 대신, 특정 state에서 UI가 어떤 모습이어야 하는지를 코드로 표현하는 방식입니다. state만 바꾸면 React가 그에 맞는 UI가 되도록 DOM을 맞춰줍니다.
jQuery 시대의 개발 방식은 이벤트마다 DOM을 직접 조작하는 명령형이었습니다. "로그인되면 로그아웃 버튼을 보여라. 관리자면 관리자 메뉴도 보여라. 알림이 있으면 배지를 추가해라." 상태 조합이 늘어날수록 각 경우를 모두 직접 처리해야 했고, 빠뜨린 케이스에서 버그가 생겼습니다.
선언형은 이 모든 경우를 단 하나의 렌더 함수로 표현합니다. isLoggedIn, isAdmin, notificationCount라는 state가 있으면, 그 조합에서 UI가 어떤 모습이어야 하는지 한 번만 선언합니다. 이후 state가 바뀌면 React가 현재 UI와 새 UI의 차이를 계산해 DOM을 맞춥니다. 개발자가 놓친 케이스는 없습니다. state가 결정되면 UI는 자동으로 따라옵니다.
SQL과 유사한 추상화입니다. SELECT * FROM users WHERE age > 20처럼 어떤 데이터를 원하는지만 선언하면 DB 엔진이 어떻게 가져올지 결정합니다. 선언형은 "무엇"을 정의하고 "어떻게"는 런타임에 맡기는 방식입니다.
명령형 vs 선언형 패러다임 차이는?
명령형은 어떻게(how) 할지를 순서대로 기술하고, 선언형은 무엇(what)이어야 하는지를 기술합니다.
// 명령형: 어떻게 바꿀지를 직접 지시
const el = document.getElementById("count");
el.textContent = String(count + 1);
count += 1;// 선언형: 어떤 모습이어야 하는지만 기술
<span>{count}</span>명령형 코드는 중간 상태가 모두 노출됩니다. count를 먼저 업데이트할지, DOM을 먼저 바꿀지 순서가 중요하고, 그 사이 에러가 나면 부분적으로만 업데이트된 상태가 남습니다. 선언형은 state만 바꾸면 React가 최종 상태에서 DOM을 한 번에 맞춰주므로 중간 상태 문제가 생기지 않습니다.
컴포넌트가 순수 함수여야 하는 이유는?
같은 props가 주어지면 항상 같은 JSX를 반환해야 합니다. 순수하면 React가 렌더링 결과를 예측해 건너뛰거나 재시도할 수 있고, React.memo 같은 최적화가 가능합니다.
수학에서 순수 함수는 같은 입력이면 항상 같은 출력을 반환하고, 외부 상태를 읽거나 변경하지 않습니다. React 컴포넌트도 같은 props를 받으면 항상 같은 JSX를 반환해야 순수합니다.
React 18 Concurrent Mode에서는 렌더링 도중 작업을 중단하고 다시 시작할 수 있습니다. 컴포넌트 함수가 순수하지 않다면, 중단 후 재실행될 때 side effect가 두 번 발생할 수 있습니다. 렌더링 중 API 요청을 보내거나 전역 변수를 수정한다면, 재시도마다 의도치 않은 결과가 생깁니다.
React.StrictMode가 개발 환경에서 컴포넌트를 두 번 호출하는 이유가 이 순수성을 검사하기 위해서입니다. 두 번 호출해도 같은 결과가 나와야 순수한 컴포넌트입니다. 순수하면 React가 결과를 캐싱하거나(React.memo), 나중에 실행하거나, 아예 건너뛸 수 있어 최적화 여지가 생깁니다.
컴포넌트란 무엇인가요?
UI를 독립적으로 재사용 가능한 단위로 나눈 것입니다. React에서 컴포넌트는 props를 받아 JSX를 반환하는 함수입니다.
function Button({ label }) {
return <button>{label}</button>;
}각 컴포넌트는 자신의 state와 생명주기를 가질 수 있고, 조합해 복잡한 UI를 만듭니다. 컴포넌트를 나누는 기준은 재사용성보다 응집도입니다. 함께 바뀌는 로직과 UI는 같은 컴포넌트에 두고, 독립적으로 바뀌는 부분이 생기면 분리합니다.
Props와 state의 차이는?
Props는 부모가 자식에게 전달하는 값으로 자식에서 직접 변경할 수 없습니다. State는 컴포넌트가 직접 소유하는 값으로, setter를 통해서만 변경하고 변경 시 리렌더링이 발생합니다.
자식이 props를 직접 바꿀 수 없는 이유는 단방향 데이터 흐름을 유지하기 위해서입니다. state 변경의 출처가 항상 명확해져서 "이 값은 어디서 바뀌는가"를 추적하기 쉬워집니다. AngularJS의 양방향 바인딩은 자식도 부모 값을 바꿀 수 있어서 데이터 흐름이 복잡해지는 문제가 있었고, React는 이를 의도적으로 단방향으로 제한했습니다.
자식이 부모 state를 바꾸고 싶으면 부모가 setter 함수를 props로 내려주는 방식을 씁니다. 이 패턴을 "lifting state up"이라고 하며, 데이터 흐름이 항상 부모 → 자식 방향으로만 흐릅니다.
let 대신 state를 쓰는 이유는?
두 가지입니다. 렌더링마다 함수 전체가 다시 실행되어 let 변수는 매번 초기화됩니다. 또한 let 변수를 직접 바꿔도 React가 변경을 감지하지 못해 리렌더링이 일어나지 않습니다.
React 컴포넌트는 일반 함수입니다. 렌더링이 일어날 때마다 함수 전체가 다시 호출됩니다. 함수 안의 let count = 0은 호출마다 0으로 초기화됩니다. 화면에 보이는 값을 증가시켰어도, 다음 렌더링에서 함수가 다시 호출되면 0으로 돌아갑니다.
useState는 값을 React 내부의 fiber node에 저장합니다. 함수가 다시 호출돼도 그 값은 React가 관리하는 공간에 유지됩니다. setter를 호출하면 React가 새 값을 저장하고 다음 렌더링을 예약합니다. Vue가 Proxy를 사용해 변수 할당을 가로채는 것과 달리, React는 명시적인 setter 호출에 의존합니다. 마법 없이 "이 값이 바뀌었다"고 React에 명시적으로 알리는 방식입니다.
불변성이 왜 중요한가요?
React는 state 변경 여부를 Object.is로 비교합니다. 객체를 직접 수정하면 참조값이 바뀌지 않아 변경을 감지하지 못합니다. 새 객체를 만들어야 참조가 달라져 리렌더링이 트리거됩니다.
Object.is는 얕은 비교입니다. obj.name = 'foo'처럼 직접 수정해도 같은 객체를 가리키는 참조값은 변하지 않으므로 React는 변경을 감지하지 못합니다. { ...obj, name: 'foo' }는 새 객체를 만들어 참조가 달라지기 때문에 변경이 감지됩니다.
얕은 비교를 쓰는 이유는 성능 때문입니다. 깊은 비교(deep equal)는 중첩된 객체를 모두 순회해야 해서 비용이 크지만, 참조 비교는 O(1)입니다. 불변성을 지키면 개발자가 새 객체를 만드는 것으로 변경을 보장하므로, React는 참조 비교만으로 충분합니다.
불변성을 지키면 이전 state 스냅샷이 보존됩니다. Redux DevTools의 time-travel 디버깅, undo/redo 기능이 가능한 이유입니다. 객체를 직접 수정했다면 이전 상태를 복원할 방법이 없습니다.
렌더링은 어떻게 이루어지나요?
세 단계입니다. Trigger(state 변경 등으로 렌더링 예약) → Render(컴포넌트 함수 호출, 이전 결과와 비교) → Commit(바뀐 부분만 실제 DOM에 반영). 렌더링이 일어나도 결과가 같으면 DOM은 바뀌지 않습니다.
"렌더링"이라는 단어가 React에서 "컴포넌트 함수 호출"을 의미한다는 점이 혼란의 원인입니다. React에서 렌더링 ≠ DOM 변경입니다.
Render 단계는 순수 계산입니다. 컴포넌트 함수를 호출해 새 Virtual DOM을 만들고 이전과 비교합니다. DOM을 건드리지 않습니다. Fiber 덕분에 이 단계는 중단되거나 재시작될 수 있습니다.
Commit 단계는 실제 DOM을 변경합니다. Render 단계의 diff 결과를 실제 DOM에 반영합니다. 중간에 멈추면 UI가 부분적으로만 업데이트된 상태가 화면에 보일 수 있어서, 이 단계는 동기적으로 한 번에 완료됩니다. Commit 후에 useEffect가 실행됩니다.
React.memo는 Render 단계 자체를 건너뛰는 최적화입니다. props가 바뀌지 않으면 함수 호출 자체를 생략합니다. DOM 조작 없이 계산 비용도 아낍니다.
React가 state 변경 후 바로 DOM을 바꾸나요?
아닙니다. state 변경 후 render를 예약하고, 변경된 UI를 계산한 뒤 commit 단계에서 DOM에 반영합니다. 여러 setter가 연속으로 호출되면 batching이 적용돼 하나의 렌더링으로 묶입니다.
useState의 setter는 비동기입니다. 호출 즉시 state가 바뀌지 않습니다. setter 직후 같은 렌더링 내에서 state를 읽으면 여전히 이전 값이 나옵니다. React가 새 state를 저장하고 다음 렌더링을 예약하기 때문입니다.
React 18부터 automatic batching이 기본 동작입니다. 이전에는 React 이벤트 핸들러 안에서만 batching이 적용됐고, setTimeout이나 fetch 콜백 안에서는 setter마다 렌더링이 일어났습니다.
// React 18에서 이 두 setter는 setTimeout 안에서도 한 번의 렌더링으로 묶임
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
}, 1000);batching이 없다면 setCount와 setFlag를 호출할 때 렌더링이 두 번 일어나고, 첫 번째 렌더 결과가 화면에 잠깐 보일 수 있습니다. batching으로 묶으면 최종 상태 하나만 화면에 반영됩니다.
reconciliation이란?
Virtual DOM 비교를 통해 실제로 변경된 부분만 DOM에 반영하는 과정입니다. 이전 Virtual DOM 트리와 새 트리를 비교해 최소한의 DOM 조작을 결정합니다.
reconciliation은 두 단계로 나뉩니다. 어디가 달라졌는지 계산하는 Render 단계, 그 차이를 실제 DOM에 반영하는 Commit 단계입니다. Fiber는 이 중 Render 단계를 작업 단위로 쪼개 우선순위에 따라 처리합니다.
reconciliation의 결과물을 "effect list"라고 합니다. 추가, 업데이트, 삭제가 필요한 DOM 노드 목록입니다. Commit 단계에서 이 목록을 순서대로 실행합니다. Commit은 중간에 멈출 수 없습니다. UI가 부분적으로만 업데이트된 채 화면에 보이면 안 되기 때문입니다.
React 16부터 Fiber가 reconciliation을 담당합니다. 작업을 작은 단위로 쪼개 우선순위에 따라 처리하기 때문에, 렌더링 도중 사용자 입력 같은 급한 작업을 먼저 처리할 수 있습니다.
한 줄 정리
React는 UI를 component 단위로 선언적으로 만들기 위한 JavaScript library입니다.