전체 목록
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 사용 필수
FiberReact 16+ 재조정 엔진작업 단위 분리, 중단 가능
Render Phase변경사항 계산 단계비동기, 중단 가능
Commit Phase실제 DOM 변경 단계동기, 중단 불가
Concurrent ModeReact 18의 동시성 렌더링우선순위 기반 스케줄링
startTransition낮은 우선순위 상태 업데이트UI 응답성 향상

핵심: React의 재조정은 "최소한의 DOM 변경"을 목표로 하며, Fiber를 통해 비동기 렌더링과 우선순위 처리가 가능해졌습니다. key prop은 재조정의 성능을 좌우하는 중요한 힌트입니다.