React Fiber 아키텍처와 Reconciliation 심화 이해
들어가며
안녕하세요, 여러분. 오늘은 React의 핵심 엔진인 Fiber 아키텍처에 대해 깊이 있게 살펴보려고 합니다. 특히 Fiber가 어떻게 Reconciliation을 수행하는지, 그리고 이를 이해하는 것이 왜 성능 최적화에 중요한지 함께 알아보겠습니다.
Fiber 아키텍처의 핵심: Reconciliation Engine
Fiber는 React 16에서 도입된 새로운 reconciliation 엔진입니다. 쉽게 말해, 우리가 작성한 컴포넌트 트리를 실제 DOM에 효율적으로 반영하는 역할을 담당합니다.
🎯 Reconciliation이란?

Reconciliation은 React가 DOM을 업데이트하는 과정입니다. 우리가 선언적으로 “이렇게 보여줘”라고 작성하면, React가 “어떻게 만들지” 알아서 처리해주는 마법 같은 과정이죠.
컴포넌트 정체성(Identity)과 상태 유지
먼저 흥미로운 예제를 하나 보겠습니다:
const UserInfoForm = () => {
const [isEditing, setIsEditing] = useState(false);
return (
<div className="form-container">
<button onClick={() => setIsEditing(!isEditing)}>
{isEditing ? "취소" : "편집"}
</button>
{isEditing ? (
<input
type="text"
placeholder="이름을 입력하세요"
className="edit-input"
/>
) : (
<input
type="text"
placeholder="이름을 입력하세요"
disabled
className="view-input"
/>
)}
</div>
);
};💡 놀라운 발견
편집 모드에서 무언가를 입력하고 “취소” 버튼을 눌렀다가 다시 “편집”을 누르면… 입력했던 텍스트가 그대로 남아있습니다!
왜 그럴까요? 두 input 엘리먼트가 다른 props를 가지고 있는데도 말이죠.
핵심 원리: React는 같은 타입의 엘리먼트가 같은 위치에 있으면, DOM 엘리먼트를 재사용하고 props만 업데이트합니다.
하지만 만약 이렇게 바꾸면 어떻게 될까요?
{isEditing ? (
<input type="text" placeholder="이름을 입력하세요" />
) : (
<div className="view-only-display">여기에 이름이 표시됩니다</div>
)}이제는 토글할 때마다 입력값이 사라집니다. 엘리먼트 타입이 다르기 때문이죠.

Element Tree: Virtual DOM을 넘어서
많은 분들이 “React는 Virtual DOM을 사용한다”고 알고 계실 텐데, 더 정확히는 Element Tree라는 표현이 맞습니다.
🌳 JSX에서 Element Tree로

📌 중요한 차이점
- DOM 엘리먼트:
type이 문자열 ('div','input') - React 컴포넌트:
type이 함수 참조 (실제 컴포넌트 함수)
// 커스텀 컴포넌트의 경우
{
type: Input, // Input 함수 자체가 참조됨
props: {
id: "company-tax-id",
placeholder: "사업자 등록번호 입력"
}
}Fiber의 Reconciliation 작동 원리
Fiber가 UI를 업데이트할 때 거치는 단계:

- 새로운 Element Tree 생성 - 컴포넌트 함수들을 호출
- 이전 Tree와 비교 - 효율적인 diff 알고리즘 적용
- 필요한 DOM 작업 계산 - 최소한의 변경사항 도출
- 효율적으로 적용 - 배치 처리 및 우선순위 고려
1️⃣ 엘리먼트 타입이 정체성을 결정한다
// 첫 번째 렌더
<div>
<Counter />
</div>
// 두 번째 렌더
<span>
<Counter />
</span>div가 span으로 바뀌었으므로, React는 전체 서브트리를 다시 만듭니다. Counter의 상태도 사라집니다!
2️⃣ 트리에서의 위치가 중요하다

// showDetails가 true일 때
<>
{showDetails ? <UserProfile userId={123} /> : <LoginPrompt />}
</>
// showDetails가 false일 때
<>
{showDetails ? <UserProfile userId={123} /> : <LoginPrompt />}
</>Fragment의 첫 번째 자식 위치를 하나의 “슬롯”으로 봅니다. showDetails가 바뀌면 같은 위치에 다른 컴포넌트 타입이 오므로, 이전 컴포넌트는 언마운트되고 새 컴포넌트가 마운트됩니다.
하지만 이런 경우는 어떨까요?
// isPrimary에 관계없이 UserProfile은 유지됨
<>
{isPrimary ? (
<UserProfile userId={123} role="primary" />
) : (
<UserProfile userId={456} role="secondary" />
)}
</>같은 위치에 같은 타입(UserProfile)이므로 컴포넌트 인스턴스는 유지되고 props만 업데이트됩니다.
3️⃣ Key로 위치 기반 비교를 오버라이드
key 속성은 개발자가 컴포넌트 정체성을 명시적으로 제어할 수 있게 해줍니다:

const TabContent = ({ activeTab, tabs }) => {
return (
<div className="tab-container">
{tabs.map((tab) => (
<div key={tab.id} className="tab-content">
{activeTab === tab.id ? (
<UserProfile
key="active-profile" // 고정된 key 사용
userId={tab.userId}
role={tab.role}
/>
) : (
<div key="placeholder" className="placeholder">
{tab.userId}의 프로필을 보려면 탭을 선택하세요
</div>
)}
</div>
))}
</div>
);
};“active-profile”이라는 key를 사용함으로써, 다른 탭으로 전환해도 UserProfile 컴포넌트의 상태가 유지됩니다.
Key의 숨겨진 힘
🔑 리스트뿐만이 아니다
Key는 리스트에서만 사용하는 것이 아닙니다. 전략적인 DOM 제어에도 활용할 수 있습니다:
const UserForm = ({ userId }) => {
// Uncontrolled 컴포넌트 사용
return (
<form>
<input
key={userId} // userId가 바뀌면 DOM 엘리먼트 재생성
name="username"
defaultValue="" // value 대신 defaultValue 사용
/>
</form>
);
};userId가 바뀔 때마다 React는 완전히 새로운 DOM 엘리먼트를 생성합니다. Uncontrolled input의 상태는 DOM 자체에 있으므로, 이렇게 하면 사용자를 전환할 때 입력값이 자동으로 초기화됩니다.
🎯 동적 리스트와 정적 엘리먼트의 조합
<>
{items.map((item) => (
<ListItem key={item.id} />
))}
<StaticElement /> {/* items가 변해도 재마운트되지 않음 */}
</>React는 내부적으로 이렇게 표현합니다:
[
// 전체 동적 배열이 하나의 자식으로 취급됨
[
{ type: ListItem, key: "1" },
{ type: ListItem, key: "2" },
],
{ type: StaticElement }, // 항상 두 번째 위치 유지
]리스트가 변경되어도 StaticElement는 항상 위치 2를 유지하므로 재마운트되지 않습니다.
성능 패턴: State Colocation
State Colocation은 상태를 사용하는 곳과 최대한 가깝게 두는 패턴입니다.

❌ 나쁜 예: 전체 앱이 리렌더링
const App = () => {
const [filterText, setFilterText] = useState("");
const filteredUsers = users.filter(user =>
user.name.includes(filterText)
);
return (
<>
<SearchBox filterText={filterText} onChange={setFilterText} />
<UserList users={filteredUsers} />
<ExpensiveComponent /> {/* 필터와 무관한데 리렌더링됨 */}
</>
);
};✅ 좋은 예: 관련 컴포넌트만 리렌더링
const UserSection = () => {
const [filterText, setFilterText] = useState("");
const filteredUsers = users.filter(user =>
user.name.includes(filterText)
);
return (
<>
<SearchBox filterText={filterText} onChange={setFilterText} />
<UserList users={filteredUsers} />
</>
);
};
const App = () => {
return (
<>
<UserSection />
<ExpensiveComponent /> {/* 필터 변경 시 리렌더링 안 됨 */}
</>
);
};컴포넌트 설계: 변화에 최적화하기
🏗️ 단일 책임 원칙 적용

// ❌ 너무 많은 책임을 가진 컴포넌트
const ProductPage = ({ productId }) => {
const [selectedSize, setSelectedSize] = useState("medium");
const [quantity, setQuantity] = useState(1);
const [shipping, setShipping] = useState("express");
const [reviews, setReviews] = useState([]);
useEffect(() => {
fetchProductDetails(productId);
fetchReviews(productId).then(setReviews);
}, [productId]);
// 사이즈 변경 시 리뷰까지 리렌더링됨
return (
<div>
<ProductInfo /* ... */ />
<ShippingOptions /* ... */ />
<Reviews reviews={reviews} />
</div>
);
};✅ 관심사별로 분리된 설계
const ProductPage = ({ productId }) => {
return (
<div>
<ProductConfig productId={productId} />
<ReviewsSection productId={productId} />
</div>
);
};
const ProductConfig = ({ productId }) => {
const [selectedSize, setSelectedSize] = useState("medium");
const [quantity, setQuantity] = useState(1);
const [shipping, setShipping] = useState("express");
// 제품 설정 관련 로직만
return (/* ... */);
};
const ReviewsSection = ({ productId }) => {
const [reviews, setReviews] = useState([]);
useEffect(() => {
fetchReviews(productId).then(setReviews);
}, [productId]);
// 리뷰 관련 로직만
return <Reviews reviews={reviews} />;
};실무 가이드라인
📋 Fiber와 함께 작업하기 위한 체크리스트
- 컴포넌트 정의는 부모 컴포넌트 밖에 - 리마운트 방지
- 상태는 아래로 내리기 - 리렌더 경계 격리
- 같은 위치의 컴포넌트 타입 일관성 유지 - 불필요한 언마운트 방지
- Key 전략적 활용 - 리스트뿐만 아니라 정체성 제어에도
- 리렌더 이슈 디버깅 시 Element Tree로 생각하기
- React.memo는 도구일 뿐 - 근본적인 알고리즘은 바뀌지 않음
마무리
Fiber 아키텍처의 Reconciliation을 이해하면, React 성능 패턴의 “왜”를 알 수 있습니다. Composition이 왜 효과적인지, Key가 왜 필요한지, 컴포넌트 안에 컴포넌트를 정의하면 왜 문제가 되는지 모두 설명됩니다.
이 지식으로 우리는 과도한 메모이제이션으로 Reconciliation 알고리즘과 싸우는 대신, React가 컴포넌트를 식별하고 업데이트하는 방식에 맞춰 자연스럽게 성능이 좋은 애플리케이션을 만들 수 있습니다.
여러분은 어떤 패턴을 사용하고 계신가요? 함께 논의해보면 좋겠습니다. 감사합니다! 🚀