React의 내부 동작 원리를 체계적으로 분석합니다. JSX에서 JavaScript로의 변환 과정, Virtual DOM의 작동 원리, Reconciliation 알고리즘, 그리고 Fiber Architecture의 핵심 개념을 다룹니다.
이 글은 회사에서 진행한 전사 세미나 내용을 정리한 글입니다.
발표 자료는 여기에서 확인할 수 있습니다.
React를 "어떻게 사용하는가"가 아닌, React가 "내부적으로 어떻게 동작하는가"를 다룹니다.
useState, useEffect 같은 Hook의 사용법이나 컴포넌트 작성법은 다루지 않습니다.
실무에서 React를 사용한 지 3년이 넘었습니다.
그동안 useState, useEffect를 사용하면서 편리함을 느꼈지만, 정작 내부에서 어떻게 동작하는지는 깊게 생각해본 적이 없었습니다.
회사에서 React로 개발하면서 가끔 이런 순간들이 있었습니다.
그때마다 검색으로 해결책을 찾았지만, 근본적인 "왜?"에 대한 답은 찾지 못했습니다.
성능 이슈가 발생하면서 React의 내부 동작 원리를 제대로 이해해야겠다고 생각했고, 이번 기회에 React를 깊이 파보기로 했습니다.
function HelloWorld() {
return <h1>Hello, World!</h1>;
} 처음 이 코드를 봤을 때 "JavaScript에서 HTML을 사용한다"는 점이 신기했습니다.
하지만 JSX는 JavaScript가 아닙니다.
브라우저는 JSX를 이해하지 못합니다.
JSX는 JavaScript의 문법 확장(Syntax Extension)일 뿐이며, 실행 전에 반드시 순수 JavaScript로 변환되어야 합니다.
💡 Transpilation이란?
한 언어의 코드를 다른 언어로 변환하는 과정입니다.
여기서는 JSX를 JavaScript로 변환하는 것을 의미합니다.
Babel 같은 도구가 이 역할을 담당합니다.
Babel 같은 트랜스파일러가 JSX를 React.createElement 함수 호출로 변환합니다.
function HelloWorld() {
return <h1>Hello, World!</h1>;
} function HelloWorld() {
return React.createElement("h1", null, "Hello, World!");
} React.createElement(
type, // 'h1', 'div' 같은 태그명 또는 컴포넌트
props, // { className: 'title', onClick: ... } 같은 속성
...children // 자식 요소들
); 세 가지 인자를 받습니다
type: 생성할 요소의 타입props: 요소의 속성들children: 자식 요소들 (가변 인자)<div className="container">
<h1>제목</h1>
<p>내용</p>
</div> React.createElement(
"div",
{ className: "container" },
React.createElement("h1", null, "제목"),
React.createElement("p", null, "내용")
); 중첩된 JSX 구조는 중첩된 함수 호출로 변환됩니다.
자식 요소들이 부모의 children 인자로 전달되는 구조입니다.
React.createElement는 최종적으로 JavaScript 객체를 반환합니다.
{
type: 'div',
props: {
className: 'container',
children: [
{
type: 'h1',
props: { children: '제목' }
},
{
type: 'p',
props: { children: '내용' }
}
]
},
key: null,
ref: null,
// ... 기타 내부 속성들
} 이 JavaScript 객체가 바로 Virtual DOM 노드입니다.
💡 핵심 정리
- JSX는 문법 설탕(Syntax Sugar)
- Babel이
React.createElement호출로 변환- 최종적으로 JavaScript 객체(React Element) 생성
- 이 객체가 Virtual DOM을 구성하는 기본 단위
React 17부터는 새로운 JSX 변환 방식이 도입되었습니다.
변화 내용// React 17 이전
import React from "react";
function App() {
return <h1>Hello</h1>;
}
// Babel 변환 결과
React.createElement("h1", null, "Hello"); // React 17 이후
// import React 불필요!
function App() {
return <h1>Hello</h1>;
}
// Babel 변환 결과
import { jsx as _jsx } from "react/jsx-runtime";
_jsx("h1", { children: "Hello" }); 더 이상 파일마다 import React를 작성하지 않아도 되는 이유가 여기 있습니다.
새로운 JSX Transform이 필요한 함수를 자동으로 import하기 때문입니다.
초기 웹 개발에서는 jQuery를 사용해 DOM을 직접 조작했습니다.
💡 DOM(Document Object Model)이란?
웹 페이지의 구조를 표현하는 객체 모델입니다.
HTML 요소들을 JavaScript로 조작할 수 있게 해주는 인터페이스예요.
// 사용자 정보 업데이트
function updateUserProfile(user) {
$("#user-name").text(user.name);
$("#user-age").text(user.age);
$("#user-email").text(user.email);
$("#user-phone").text(user.phone);
// ... 수십 개의 필드
}
// 여러 곳에서 호출
updateUserProfile(newUser); 이 방식의 문제점
1. 성능 문제SPA(Single Page Application)가 등장하면서 이 문제는 더욱 심각해졌습니다.
페이지 전체를 JavaScript로 관리하다 보니 DOM 조작이 기하급수적으로 증가했습니다.
Virtual DOM은 실제 DOM의 추상화된 버전입니다.
메모리 상에만 존재하는 JavaScript 객체로, 실제 DOM보다 훨씬 가볍고 빠르게 조작할 수 있습니다.
동작 방식💡 Virtual DOM이란?
실제 DOM의 가벼운 복사본입니다.
JavaScript 객체로 표현된 UI 구조로, 메모리 상에만 존재합니다.
실제 DOM을 조작하기 전에 Virtual DOM에서 변경사항을 계산하고, 최소한의 실제 DOM 조작만 수행합니다.

Virtual DOM이 무조건 빠른 것은 아닙니다.
오히려 다음과 같은 오버헤드가 있습니다
// DOM 직접 조작 (빠르지만 복잡함)
document.getElementById("count").textContent = count;
document.getElementById("user").textContent = user.name;
if (user.isPremium) {
document.getElementById("badge").style.display = "block";
} else {
document.getElementById("badge").style.display = "none";
}
// ... 수십 개의 업데이트와 조건문
// Virtual DOM (약간 느릴 수 있지만 단순함)
return (
<div>
{count} {user.name}
{user.isPremium && <Badge />}
</div>
); 트레이드오프: 약간의 성능을 포기하고 개발 생산성을 얻음
✅ Virtual DOM의 진짜 목적은 개발 경험(DX) 개선React 팀의 Dan Abramov는 이렇게 말했습니다.

번역하지면...
"React가 DOM보다 빠르다"는 말은 사실이 아닙니다.
React는 유지보수 가능한 애플리케이션을 만드는 데 도움을 주는 도구이며,
대부분의 사용 사례에서 성능도 충분합니다.
즉, Virtual DOM은 성능을 높이기 위한 기술이 아닙니다.
복잡한 DOM 조작을 대신해, 개발자가 "무엇을 그리고 싶은지"에만 집중할 수 있도록 돕는 추상화 계층입니다.
덕분에 UI 코드는 단순해지고, 유지보수성이 크게 향상됩니다.
// "어떻게" 업데이트할지 일일이 지시
function updateUserProfile(user) {
const container = document.getElementById("profile");
const nameElement = container.querySelector(".name");
const emailElement = container.querySelector(".email");
// DOM 요소를 찾아서
nameElement.textContent = user.name;
emailElement.textContent = user.email;
// 조건부 렌더링도 직접 처리
if (user.isPremium) {
container.classList.add("premium");
} else {
container.classList.remove("premium");
}
} // "무엇을" 렌더링할지만 정의
function UserProfile({ user }) {
return (
<div className={user.isPremium ? "profile premium" : "profile"}>
<h1 className="name">{user.name}</h1>
<p className="email">{user.email}</p>
</div>
);
} Virtual DOM 덕분에
Virtual DOM도 완벽하지는 않습니다.
메모리 오버헤드이러한 한계로 인해 다른 접근 방식들이 등장했습니다
하지만 React의 선택은 여전히 유효합니다.
개발 경험과 충분한 성능 사이의 균형점을 잘 찾았습니다.
💡 Reconciliation(재조정)이란?
React가 Virtual DOM 트리를 비교하여 실제 DOM에 어떤 변경을 적용해야 하는지 결정하는 과정입니다.
쉽게 말해 "이전 화면"과 "새로운 화면"을 비교해서 "바뀐 부분만 찾아내는 알고리즘"이에요.
두 개의 트리를 비교하는 일반적인 알고리즘(트리 편집 거리 알고리즘)의 시간 복잡도는 O(n³)입니다.
왜 O(n³)일까?모든 노드 비교: O(n²)
최소 편집 거리 계산: O(n)
결과: O(n²) × O(n) = O(n³)
노드 1,000개 트리 비교
= 1,000,000,000번의 연산
= 16ms 내 처리 불가능 (60fps 유지 실패)
60fps를 유지하려면 각 프레임을 16ms 안에 처리해야 하는데, O(n³) 알고리즘으로는 불가능합니다.
이것이 React가 휴리스틱을 도입한 이유입니다.
두 개의 트리를 비교하는 문제는 컴퓨터 과학에서 오랫동안 연구된 난제입니다.
일반적인 알고리즘은 O(n³)의 시간 복잡도를 가지지만, React는 두 가지 과감한 가정을 통해 이를 O(n)으로 최적화했습니다.
React 공식 문서에서는 이를 "휴리스틱(Heuristic)에 의존한다"고 표현합니다.

React의 트레이드오프💡 휴리스틱(Heuristic)이란?
완벽하지는 않지만 대부분의 경우 잘 작동하는 경험 기반의 접근 방식입니다.
React는 "완벽한 최적화"보다 "충분히 빠른 실용적인 해결책"을 선택했어요.
대신 React는 "실제 애플리케이션에서 자주 발생하는 패턴"에 최적화되어 있습니다.
// 이전
<div>
<Counter />
</div>
// 이후
<span>
<Counter />
</span> div가 span으로 변경되면
Counter 컴포넌트도 언마운트 후 재마운트대부분의 경우 타입이 변경되면 완전히 다른 UI를 의미합니다.
세밀하게 비교하는 것보다 새로 만드는 것이 더 빠릅니다.
// ❌ 나쁜 예
{
items.map((item, index) => <li key={index}>{item.name}</li>);
}
// ✅ 좋은 예
{
items.map((item) => <li key={item.id}>{item.name}</li>);
} React에서는 배열을 렌더링할 때 key라는 속성을 반드시 지정해야 합니다.
💡 key란?
React가 리스트의 각 항목을 고유하게 식별하기 위해 사용하는 특별한 속성입니다.
key를 통해 React는 "어떤 항목이 추가/삭제/이동했는지" 정확히 파악할 수 있어요.
리스트 맨 앞에 새 아이템 추가했다고 가정해 보겠습니다.
// 이전 상태
[
<li key={0}>Apple</li>, // key 0
<li key={1}>Banana</li>, // key 1
<li key={2}>Cherry</li> // key 2
];
// Orange 추가 (index를 key로 사용)
[
<li key={0}>Orange</li>, // key 0 (이전: Apple)
<li key={1}>Apple</li>, // key 1 (이전: Banana)
<li key={2}>Banana</li>, // key 2 (이전: Cherry)
<li key={3}>Cherry</li> // key 3 (새로 추가)
];
// ❌ React는 모든 항목이 변경되었다고 판단
React는 key를 기준으로 요소를 식별합니다
// 고유 ID를 key로 사용
[
<li key="orange">Orange</li>, // 새로 추가
<li key="apple">Apple</li>, // 그대로 유지
<li key="banana">Banana</li>, // 그대로 유지
<li key="cherry">Cherry</li> // 그대로 유지
];
// ✅ React는 orange만 새로 추가되었다고 판단
React는 key를 통해
// 정렬, 필터링, 추가/삭제가 있는 리스트
{
items.map((item, index) => <Item key={index} />); // ❌
} {
items.map((item) => <Item key={Math.random()} />); // ❌ 매번 새로운 key
} {
items.map((item) => <Item key={item.name} />); // ❌ name이 중복될 수 있음
} {
items.map((item) => <Item key={item.id} />); // ✅ 데이터베이스 ID
} // 정적 리스트 (추가/삭제/정렬 없음)
const STATIC_MENU = ["Home", "About", "Contact"];
{
STATIC_MENU.map((item, index) => <MenuItem key={index} />); // ✅ 허용
} {
items.map((item) => <Item key={`${item.category}-${item.id}`} />); // ✅
} Diffing은 이전 Virtual DOM과 새로운 Virtual DOM을 비교하여 어떤 부분이 변경되었는지 찾는 과정입니다.
마치 "틀린 그림 찾기" 게임처럼, 두 트리의 차이점을 빠르게 찾아내는 알고리즘이에요.

<App>과 <App> 비교 (같음 → 유지)<A>와 <A> 비교 (같음 → 유지)<B>와 <C> 비교 (다름 → <B> 삭제, <C> 생성)<A>의 자식 <D> 유지<B>가 삭제되므로 하위 트리 전체 삭제핵심은!
서로 다른 레벨의 노드는 비교하지 않습니다.
이것이 전체 트리를 한 번만 순회(O(n))할 수 있는 이유입니다.
// 이전
<div className="before" />
// 이후
<div className="after" /> className 속성만 "before" → "after"로 변경// 이전
<Counter count={0} />
// 이후
<Counter count={1} /> 이것이 바로 컴포넌트가 리렌더링되어도 state가 사라지지 않는 이유입니다.
// 이전
<ul>
<li key="1">Item 1</li>
<li key="2">Item 2</li>
<li key="3">Item 3</li>
</ul>
// 순서 변경
<ul>
<li key="3">Item 3</li>
<li key="1">Item 1</li>
<li key="2">Item 2</li>
</ul> 이제 React의 메모이제이션, 최적화 도구들이 왜 필요한지 이해할 수 있습니다.
React.memo💡 메모이제이션(Memoization)이란?
이전에 계산한 결과를 저장해두고 재사용하는 최적화 기법입니다.
같은 입력에 대해서는 다시 계산하지 않고 저장된 결과를 반환해요.
const MemoizedComponent = React.memo(Component); const expensiveValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]); 모두 Reconciliation 과정을 최소화하기 위한 도구들입니다.
React는 상태나 props의 변경을 감지할 때 얕은 비교(Shallow Comparison)를 사용합니다.
💡 얕은 비교란?
객체의 참조(메모리 주소)만 비교하고, 객체 내부의 값은 비교하지 않는 방식입니다.
즉, 겉만 보고 판단하는 거예요.
빠르지만 내부 변화를 감지하지 못할 수 있어요.
// React의 비교 방식
function shallowEqual(obj1, obj2) {
// 1. 참조 비교
if (Object.is(obj1, obj2)) {
return true;
}
// 2. 타입 체크
if (typeof obj1 !== "object" || obj1 === null || typeof obj2 !== "object" || obj2 === null) {
return false;
}
// 3. 첫 번째 레벨의 속성만 비교
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
// obj2에 해당 key가 없는지 체크
if (!obj2.hasOwnProperty(key)) {
return false;
}
if (!Object.is(obj1[key], obj2[key])) {
return false;
}
}
return true;
} React는 === 대신 Object.is를 사용합니다.
// === 연산자의 한계
NaN === NaN; // false
+0 === -0; // true
// Object.is의 정확한 비교
Object.is(NaN, NaN); // true (수학적으로 정확)
Object.is(+0, -0); // false (부호 구분)
// useEffect, useMemo, useCallback의 의존성 배열 비교
useEffect(() => {
// effect
}, [value]); // value가 NaN일 수 있다면?
// Object.is 덕분에 NaN도 올바르게 비교됨
React는 모든 Hook의 의존성 배열을 Object.is로 비교합니다.
const complexState = {
user: {
name: "찬규",
profile: {
age: 28,
address: {
city: "서울",
district: "강남구"
}
},
friends: [
{ id: 1, name: "친구1", profile: { ... } },
{ id: 2, name: "친구2", profile: { ... } },
// ... 수백 명
]
},
settings: {
// ... 복잡한 설정
}
}; 이것을 깊은 비교한다면
매 렌더링마다 이런 비교를 한다면 성능에 큰 영향을 미칩니다.
❌ 잘못된 예: 직접 수정💡 불변성이란?
데이터를 직접 수정하지 않고, 변경이 필요하면 새로운 데이터를 만드는 원칙입니다.
원본은 절대 건드리지 않습니다
const [user, setUser] = useState({
name: "찬규",
age: 28
});
// ❌ 객체를 직접 수정
user.age = 26;
setUser(user); // 같은 참조 → React가 변경 감지 못함
user 객체의 참조가 그대로Object.is(prevUser, user) → true// 스프레드 연산자로 새 객체 생성
setUser({ ...user, age: 26 });
// 또는 함수형 업데이트
setUser((prev) => ({ ...prev, age: 26 })); 새로운 객체를 만들면
// ✅ 올바른 예시
const [items, setItems] = useState([1, 2, 3]);
// ❌ 나쁜 예
items.push(4);
setItems(items);
// ✅ 좋은 예 - 추가
setItems([...items, 4]); // 스프레드
setItems(items.concat(4)); // concat
setItems((prev) => [...prev, 4]); // 함수형 업데이트
// ✅ 좋은 예 - 삭제
setItems((prev) => prev.filter((item) => item !== 3)); // 값으로 삭제
setItems((prev) => prev.filter((_, index) => index !== 0)); // 인덱스로 삭제
// ✅ 좋은 예 - 수정
setItems((prev) => prev.map((item) => (item === 2 ? 20 : item))); // 값 변경
setItems((prev) => prev.map((item, index) => (index === 1 ? item * 10 : item))); // 인덱스로 변경
const [user, setUser] = useState({
name: "찬규",
profile: {
age: 28,
address: {
city: "서울"
}
}
});
// ❌ 나쁜 예
user.profile.address.city = "부산";
setUser(user);
// ✅ 좋은 예 (하지만 복잡함)
setUser({
...user,
profile: {
...user.profile,
address: {
...user.profile.address,
city: "부산"
}
}
}); 중첩이 깊을 때는 Immer 라이브러리를 사용하면 편리합니다.
import { produce } from "immer";
setUser(
produce((draft) => {
draft.profile.address.city = "부산";
})
); Immer는
const state = reactive({
count: 0
});
state.count++; // 자동으로 반응
this.count++; // Zone.js가 자동으로 감지
setCount(count + 1); // 명시적 업데이트
React는 "마법보다는 명시성"을 선택했습니다.
더 번거롭지만, 코드를 읽었을 때 정확히 무슨 일이 일어나는지 알 수 있습니다.
Fiber는 React 16에서 도입된 새로운 Reconciliation 엔진입니다.
작업을 작은 단위로 쪼개서, 중요한 것부터 처리하고, 필요하면 중간에 멈추고 다시 시작할 수 있습니다.
마치 멀티태스킹하는 운영체제처럼 동작합니다
React 15 이전에는 Stack Reconciler를 사용했습니다.
💡 Stack Reconciler란?
React 15 이전에 사용하던 Reconciliation 방식입니다.
재귀 호출 스택을 사용하여 동기적으로 처리했기 때문에 "Stack" Reconciler라고 불립니다.
// Stack Reconciler의 동작 방식 (단순화)
function reconcileChildren(parent) {
parent.children.forEach((child) => {
reconcileChildren(child); // 재귀 호출
updateDOM(child);
});
} // 10,000개의 아이템을 렌더링
function LargeList({ items }) {
return (
<div>
{items.map((item) => (
<ExpensiveComponent key={item.id} data={item} />
))}
</div>
);
} 사용자가 입력하는 동안
브라우저는 JavaScript 실행 중에는 렌더링을 할 수 없습니다.
60fps를 유지하려면 16ms마다 프레임을 그려야 하는데, 긴 작업이 이를 방해합니다.
| 구분 | Stack Reconciler (이전) | Fiber (현재) |
|---|---|---|
| 처리 방식 | 재귀적, 동기적 | 반복적, 비동기적 |
| 인터럽트 | 불가능 | 가능 |
| 우선순위 | 없음 | 존재 |
| 렌더링 방식 | All or Nothing | 중단/재개 가능 |
| 기반 기능 | 없음 | Suspense, useTransition 등 |
| 사용자 입력 반응 | 느릴 수 있음 | 항상 빠름 |

한 번 시작하면 끝까지 실행 → UI 먹통
Fiber (현재)
각 단위마다 중단 가능 → 사용자 입력 처리 가능
각 컴포넌트는 하나의 Fiber 노드에 대응됩니다.
💡 Fiber는 두 개의 트리를 유지합니다
- Current Tree: 화면에 표시 중인 UI
- Work-in-Progress Tree: 다음 렌더링을 준비 중인 UI
렌더링이 완료되면 두 트리를 교체(스왑)합니다.
이를 통해 작업 중단/재개가 안전하게 가능해집니다.
// Fiber 노드의 주요 속성 (단순화)
{
type: 'div', // 컴포넌트 타입
props: {...}, // props
stateNode: DOMNode, // 실제 DOM 노드
// 트리 구조
child: FiberNode, // 첫 번째 자식
sibling: FiberNode, // 다음 형제
return: FiberNode, // 부모
// 작업 관련
alternate: FiberNode, // 이전 Fiber (이중 버퍼링용)
effectTag: 'UPDATE', // 수행할 작업 (UPDATE, DELETE 등)
// 우선순위
lanes: number, // 작업 우선순위
} Fiber는 작업에 우선순위(Priority)를 부여합니다.
| Priority | 설명 | 예시 | 특징 |
|---|---|---|---|
| Immediate | 즉시 수행해야 하는 동기 업데이트 | 상태 초기화, 중요 렌더 | 절대 지연 불가 |
| UserBlocking | 사용자 입력에 즉각 반응해야 하는 작업 | 클릭, 입력, 드래그 | UX에 직접적 영향 |
| Normal | 일반 렌더링 | 데이터 로딩 후 화면 갱신 | 기본 렌더링 우선순위 |
| Low | 백그라운드에서 수행되는 비중요 작업 | 이미지 프리패칭, 비가시 콘텐츠 렌더 | 프레임 유지가 우선 |
function SearchBox() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
// 높은 우선순위: 즉시 입력 반영
setQuery(value);
// 낮은 우선순위: 검색 결과는 뒤로
startTransition(() => {
const filtered = items.filter((item) => item.name.includes(value));
setResults(filtered);
});
};
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultsList results={results} />
</>
);
} query 상태 즉시 업데이트Concurrent Mode는 React 18에서 도입된 새로운 렌더링 모드입니다.
여러 작업을 동시에(Concurrent) 준비하고, 우선순위에 따라 유연하게 처리할 수 있습니다.
Fiber 덕분에 가능해진 기능으로 사용자 경험을 최우선으로 하는 렌더링 방식입니다
<Suspense fallback={<Skeleton />}>
<UserProfile />
<UserPosts />
</Suspense> 데이터 로딩 중
<Skeleton /> 먼저 표시const [isPending, startTransition] = useTransition();
function updateTab(newTab) {
startTransition(() => {
setTab(newTab);
});
} isPending으로 로딩 상태 표시 가능function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => searchItems(deferredQuery), [deferredQuery]);
return <ResultsList results={results} />;
} query는 즉시 업데이트deferredQuery는 지연 업데이트// ❌ 절대 안 됨
function Component() {
console.log("렌더링"); // 여러 번 찍힐 수 있음
callAPI(); // 중복 호출 위험
document.title = "새 제목"; // 부작용
return <div>Hello</div>;
}
// ✅ 순수 함수로 작성
function Component() {
// 오직 렌더링 로직만
const value = computeSomething();
// ✅ 부작용은 useEffect에서
useEffect(() => {
callAPI();
document.title = "새 제목";
}, []);
return <div>{value}</div>;
} Fiber는 작업을 중단하고 다시 시작할 수 있습니다.
따라서 컴포넌트 함수가 여러 번 호출될 수 있습니다.
문제 상황💡 부작용(Side Effect)이란?
함수의 외부 세계에 영향을 주는 모든 행위를 말합니다.
- API 호출 (서버 상태 변경)
- DOM 직접 조작 (브라우저 상태 변경)
- 타이머 설정, 로컬 스토리지 접근 등
만약 컴포넌트에서 API를 직접 호출한다면
1차 렌더링 시도 → callAPI() 호출 ✅
우선순위 높은 작업 발생 → 중단
2차 렌더링 시도 → callAPI() 또 호출 ❌ (중복!)
3차 렌더링 시도 → callAPI() 또 호출 ❌❌ (더 중복!)
useEffect는 렌더링이 완료된 후 딱 1번만 실행됩니다.
useEffect(() => {
callAPI(); // ✅ 마운트 시 1번만 실행
}, []); // 빈 배열 = 컴포넌트 생명주기 동안 1번
// ❌ 외부 변수 수정
let count = 0;
function Component() {
count++; // 순수 함수 아님
return <div>{count}</div>;
}
// ✅ 순수 함수
function Component({ initialCount }) {
const [count, setCount] = useState(initialCount);
return <div>{count}</div>;
} Fiber 환경에서는 순수 함수로 작성하는 것이 필수입니다.
지금까지 React의 핵심 개념들을 살펴보았습니다.
React.createElement로 변환이 글을 준비하면서, 그리고 세미나를 진행하면서 얻은 것들입니다.
기술적 이해처음에는 "React 내부가 그렇게 복잡하겠어?"라고 가볍게 생각했습니다.
하지만 파고들수록 수많은 고민과 트레이드오프 속에서 설계된 라이브러리라는 걸 느꼈습니다.
"왜 이렇게 만들었을까?"
이 질문을 계속 던지다 보니, 단순히 코드를 작성하는 것과 원리를 이해하고 작성하는 것의 차이가 얼마나 큰지 깨달았습니다.
예전에는 검색으로 해결책만 찾았다면, 이제는 문제의 근본 원인이 보입니다.
// 왜 이렇게 하면 안 될까?
items.push(newItem);
setItems(items);
// 왜 key를 index로 쓰면 warning이 뜰까?
{
items.map((item, i) => <Item key={i} {...item} />);
}
// 왜 이 컴포넌트는 계속 리렌더링될까?
<Child onClick={() => console.log("click")} />; 이런 질문들에 이제는 내부 동작 원리를 바탕으로 답할 수 있습니다.
"이렇게 하면 된다"가 아니라 "왜 이렇게 해야 하는가"를 설명할 수 있게 되었습니다.
이 글이 React를 더 깊이 이해하고, 더 나은 개발자로 성장하는 데 도움이 되었으면 합니다.
P.S. 세미나 준비하면서 정리한 내용입니다.
발표 자료는 여기에서 확인하실 수 있습니다.
궁금한 점이나 더 알고 싶은 부분이 있으면 댓글로 남겨주세요.
끝까지 읽어주셔서 감사합니다.
| Idle |
| 시스템이 한가할 때만 수행 |
| 로깅, 통계, 사후 분석 |
| 가장 낮은 우선순위 |