ReactHard#91
React의 재조정(Reconciliation) 알고리즘이란 무엇인지 설명해주세요.
#React#재조정#가상DOM#Fiber#key
힌트
가상 DOM 비교 시 key 속성의 역할을 생각해보세요.
정답 및 해설
React의 재조정(Reconciliation) 알고리즘이란 무엇인지 설명해주세요.
재조정(Reconciliation)은 React가 컴포넌트의 상태나 props가 변경될 때, 이전 가상 DOM(Virtual DOM)과 새로운 가상 DOM을 비교하여 실제 DOM을 최소한으로 업데이트하는 과정입니다. 실제 DOM 조작은 비용이 매우 크기 때문에, React는 가상 DOM에서 차이점만 계산하여 꼭 필요한 부분만 실제 DOM에 반영합니다.
Virtual DOM이란?
// 실제 DOM 구조를 JavaScript 객체로 표현한 것
const virtualDOM = {
type: 'div',
props: { className: 'container' },
children: [
{
type: 'h1',
props: {},
children: ['안녕하세요']
},
{
type: 'p',
props: { id: 'desc' },
children: ['React 재조정 설명입니다']
}
]
};
// JSX는 위와 같은 React.createElement() 호출로 변환됨
const element = (
<div className="container">
<h1>안녕하세요</h1>
<p id="desc">React 재조정 설명입니다</p>
</div>
);
재조정의 필요성
상태 변경 발생
↓
새 Virtual DOM 생성
↓
이전 Virtual DOM과 비교 (Diffing)
↓
변경된 부분만 계산
↓
실제 DOM에 최소한의 업데이트 적용
실제 DOM 조작은 리플로우(reflow), 리페인트(repaint)를 유발하므로 비용이 큽니다. Virtual DOM 비교는 메모리 내 JavaScript 객체 비교라 훨씬 빠릅니다.
Diffing 알고리즘 원칙
원칙 1: 다른 타입의 요소는 트리 전체를 재구성
// 이전 렌더링
<div>
<Counter />
</div>
// 새 렌더링 - 타입이 div → span으로 변경
<span>
<Counter />
</span>
루트 요소의 타입이 달라지면(div → span), React는 하위 트리 전체를 파괴하고 새로 생성합니다. Counter 컴포넌트도 언마운트 후 재마운트됩니다.
// 같은 타입이면 속성(props)만 업데이트
// 이전
<div className="before" title="old" />
// 새
<div className="after" title="old" />
// → className만 업데이트, title은 유지
원칙 2: key prop으로 리스트 항목 식별
// ❌ key 없이 리스트 렌더링
function ListWithoutKey({ items }) {
return (
<ul>
{items.map(item => (
<li>{item.name}</li> // key 없음 - 경고 발생
))}
</ul>
);
}
key 없이 맨 앞에 아이템을 추가하면:
이전: [B, C, D]
새: [A, B, C, D]
React의 판단 (key 없음):
- B → A 변경 (불필요한 업데이트)
- C → B 변경 (불필요한 업데이트)
- D → C 변경 (불필요한 업데이트)
- D 추가
→ 4번의 DOM 조작
// ✅ key prop으로 올바른 매핑
function ListWithKey({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li> // 안정적인 고유 ID 사용
))}
</ul>
);
}
key로 식별하면:
이전: [key=2:B, key=3:C, key=4:D]
새: [key=1:A, key=2:B, key=3:C, key=4:D]
React의 판단 (key 있음):
- key=1인 A 추가
- key=2,3,4 항목은 그대로 유지
→ 1번의 DOM 조작
key 사용 주의사항
// ❌ 잘못된 key 사용: 인덱스 사용 (항목 순서 변경 시 문제)
items.map((item, index) => <li key={index}>{item}</li>)
// ❌ 잘못된 key 사용: 매 렌더링마다 새로운 값 생성
items.map(item => <li key={Math.random()}>{item.name}</li>)
// ✅ 올바른 key 사용: 안정적인 고유 식별자
items.map(item => <li key={item.id}>{item.name}</li>)
// key를 컴포넌트 강제 리마운트에 활용 (언마운트 → 재마운트 트리거)
<UserProfile key={userId} userId={userId} />
// userId가 바뀌면 컴포넌트 전체가 새로 마운트됨
React Fiber (React 16+)
Fiber란?
React 16부터 도입된 새로운 재조정 엔진으로, 렌더링을 **작은 단위(fiber)**로 쪼개어 처리합니다.
기존 Stack Reconciler (React 15 이하):
- 동기적으로 전체 트리를 한 번에 처리
- 중간에 멈출 수 없음
- 큰 트리에서 메인 스레드 블로킹 발생
- 애니메이션, 사용자 입력 반응 지연
Fiber Reconciler (React 16+):
- 작업을 작은 단위(fiber)로 분리
- 중단(interrupt), 일시정지, 재개(resume) 가능
- 우선순위 기반 스케줄링
- Concurrent Mode 지원
Fiber 노드 구조
// 각 React 요소마다 하나의 Fiber 노드가 생성됨
const fiber = {
// 요소 타입 정보
type: 'div', // 함수 컴포넌트, 클래스 컴포넌트, DOM 태그
key: null,
// 상태
stateNode: domNode, // 실제 DOM 노드 또는 컴포넌트 인스턴스
memoizedState: null, // hooks의 상태 연결 리스트
// 트리 구조 (단방향 연결 리스트)
child: childFiber, // 첫 번째 자식
sibling: siblingFiber, // 다음 형제
return: parentFiber, // 부모
// 작업 정보
pendingProps: {}, // 처리해야 할 새 props
memoizedProps: {}, // 마지막으로 렌더링된 props
effectTag: 'UPDATE', // 수행할 작업 (PLACEMENT, UPDATE, DELETION)
lanes: 0, // 우선순위 정보
};
Fiber의 두 단계
1. Render Phase (비동기, 중단 가능)
├── 가상 DOM 트리를 순회
├── 변경사항 계산 (diffing)
├── 사이드 이펙트 목록 생성
└── 중간에 더 높은 우선순위 작업 있으면 중단 가능
2. Commit Phase (동기, 중단 불가)
├── 실제 DOM 변경 적용
├── componentDidMount/Update 호출
└── 반드시 완료되어야 하므로 중단 불가
우선순위 기반 스케줄링
// React 18의 우선순위 레벨 (개념적)
const priorities = {
Immediate: 1, // 동기적으로 즉시 처리 (클릭, 입력)
UserBlocking: 2, // 사용자 인터랙션 (드래그)
Normal: 3, // 일반 업데이트 (데이터 페치 결과)
Low: 4, // 덜 중요한 업데이트
Idle: 5, // 유휴 시간에 처리 (prefetch)
};
// React 18 Transition API - 낮은 우선순위 표시
import { startTransition, useTransition } from 'react';
function SearchComponent() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function handleChange(e) {
// 높은 우선순위: 입력 상태 즉시 업데이트
setQuery(e.target.value);
// 낮은 우선순위: 검색 결과 업데이트 (사용자 입력이 더 중요)
startTransition(() => {
setResults(performExpensiveSearch(e.target.value));
});
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner /> : <ResultList results={results} />}
</div>
);
}
Concurrent Mode와 재조정
// React 18의 createRoot로 Concurrent Mode 활성화
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
// React 17 이하 (레거시 모드)
// ReactDOM.render(<App />, document.getElementById('root'));
Concurrent Mode에서는 렌더링 작업을 중단하고 더 긴급한 작업을 먼저 처리할 수 있습니다.
재조정 최적화 기법
// 1. React.memo - props가 같으면 리렌더링 건너뜀
const MemoizedComponent = React.memo(function MyComponent({ data }) {
return <div>{data.title}</div>;
});
// 커스텀 비교 함수
const MemoizedComponent = React.memo(
function MyComponent({ user }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => prevProps.user.id === nextProps.user.id
);
// 2. shouldComponentUpdate (클래스 컴포넌트)
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return nextProps.value !== this.props.value;
}
}
// 3. PureComponent - shallow comparison 자동 수행
class MyComponent extends React.PureComponent {
render() {
return <div>{this.props.value}</div>;
}
}
// 4. key를 활용한 의도적 리마운트
// 사용자가 변경되면 폼을 완전히 초기화
<ProfileForm key={currentUserId} userId={currentUserId} />
정리
| 개념 | 설명 | 비고 |
|---|---|---|
| Virtual DOM | 실제 DOM의 JavaScript 객체 표현 | 메모리에서 비교 작업 수행 |
| Diffing | 이전/새 Virtual DOM 비교 알고리즘 | O(n) 시간 복잡도 |
| key prop | 리스트 항목 고유 식별자 | 안정적인 ID 사용 필수 |
| Fiber | React 16+ 재조정 엔진 | 작업 단위 분리, 중단 가능 |
| Render Phase | 변경사항 계산 단계 | 비동기, 중단 가능 |
| Commit Phase | 실제 DOM 변경 단계 | 동기, 중단 불가 |
| Concurrent Mode | React 18의 동시성 렌더링 | 우선순위 기반 스케줄링 |
| startTransition | 낮은 우선순위 상태 업데이트 | UI 응답성 향상 |
핵심: React의 재조정은 "최소한의 DOM 변경"을 목표로 하며, Fiber를 통해 비동기 렌더링과 우선순위 처리가 가능해졌습니다. key prop은 재조정의 성능을 좌우하는 중요한 힌트입니다.