React - Trigger, Render and Commit

Take!·2025년 8월 7일
0

React

목록 보기
4/5

Render and Commit

React는 사용자에게 화면을 보여주기 전에 총 3개의 단계를 걸쳐 작업을 처리한다. 각각은 Triggering, Rendering, Committing phase이며 Commit phase가 끝나야 비로소 사용자는 모니터에서 화면을 볼 수 있게 된다.

Step1: Trigger Phase (트리거 단계)

업데이트의 시작점이다. 다음과 같은 상황에서 트리거가 발생한다.

<script>
//	초기 렌더링
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

// 상태 업데이트
const [count, setCount] = useState(0);
setCount(count + 1); // 🔥 트리거!

// Context 값 변경
const ThemeContext = React.createContext();
const [theme, setTheme] = useState('dark');
setTheme('light'); // 🔥 트리거!
</script>

트리거 조건 정리:

  • 초기 렌더링 (root.render() 호출)
  • 상태 업데이트 (setState, useState setter)
  • 부모 컴포넌트의 리렌더링
  • Context 값 변경
  • 강제 업데이트 (forceUpdate)

Step2: Render Phase (렌더 단계)

이 단계는 Virtual DOM 트리를 구성하고 재조정을 수행하는 단계이다.

주요 특징:

  • ✅ 순수함수적: Side effect가 없음
  • ✅ 중단 가능: 우선순위가 높은 작업이 있으면 중단 후 재시작
  • ✅ 비동기적: 메인 스레드를 블로킹하지 않음

Step3: Commit Phase (커밋 단계)

실제 DOM을 업데이트하고 side effect를 실행한다.

3개의 하위 단계가 있다:

<script>
function CommitPhaseExample() {
	const [count, setCount] = useState(0);
    
    //	Before Mutation
    //	DOM 변경 전 실행
    const prevCount = useRef();
    
    //	Mutation
    //	실제 DOM 변경이 일어나는 시점
    
    //	Layout
    //	DOM 변경 후 실행
    useLayoutEffect(() => {
    	prevCount.current = count;
    }, [count]);
    
    return <div>Count: {count}</div>
}
</script>

Fiber Node: React의 작업 단위

  • Fiber는 React 16에서 도입된 새로운 재조정 엔진의 핵심!

Fiber Node의 구조

<script>
// 실제 Fiber 노드의 주요 속성들
const fiberNode = {
	//	🏷️ 컴포넌트 정보
    type: 'div',			// HTML 태그 or 컴포넌트 함수
    key: 'unique-key',		//	React key
    elementType: 'div'		//	원본 타입
    
    // 🌳 트리 구조 (Linked List로 구현)
    child: null, 			//	첫 번째 자식
    sibling: null,			//	다음 형제
    return: null,			//	부모 (parent가 아닌 reture!)
    
    // 📦 데이터
    memoizedProps: {},     // 이전 props
    pendingProps: {},      // 새로운 props  
    memoizedState: {},     // 이전 state (Hook 체인)
  
    // 🔄 업데이트
    updateQueue: null,     // 상태 업데이트 큐

    // 🚩 작업 표시
    flags: 0,             // 이 노드의 side effect
    subtreeFlags: 0,      // 하위 트리의 side effect

    // 🔗 Fiber 아키텍처
    alternate: null,      // 다른 트리의 대응 노드
    lanes: 0,            // 우선순위 (Concurrent Features)
}
</script>

why Linked List?

<script>
// 기존 방식 (React 15): 재귀적 순회 - 중단 불가
function walkTreeRecursive(element) {
  doWork(element);
  element.children.forEach(child => {
    walkTreeRecursive(child); // 🚫 중단할 수 없음
  });
}

// Fiber 방식: 반복적 순회 - 중단 가능
function walkTreeIterative(fiber) {
  while (fiber) {
    doWork(fiber);
    
    // ⏸️ 여기서 중단 가능!
    if (shouldYield()) {
      return fiber; // 나중에 여기서 재시작
    }
    
    fiber = getNextFiber(fiber);
  }
}
</script>

Double Buffering: Current Tree vs WorkInProgress Tree

  • React는 더블 버퍼링 기법을 사용해 두 개의 Fiber 트리를 관리한다.

Current Tree(현재 트리)

<script>
// 지금 화면에 보이는 UI
const currentTree = {
  // ✅ 실제 DOM에 반영된 안정적인 상태
  // ✅ 사용자가 보고 있는 UI
  // ✅ 이전 렌더링의 결과물
};
</script>

WorkInProgress Tree(작업 중 트리)

<script>
// 다음에 보여질 UI (작업 중)
const workInProgressTree = {
  // 🚧 새로운 state와 props를 반영 중
  // 🚧 render phase에서 구성되는 트리
  // 🚧 commit 후 current tree가 될 예정
};
</script>

트리 전환 과정

<script>
function updateProcess() {
  // 1️⃣ 업데이트 시작: current 복제 → workInProgress 생성
  let workInProgress = createWorkInProgress(current);
  
  // 2️⃣ Render Phase: workInProgress 트리 구성
  while (workInProgress !== null) {
    workInProgress = performUnitOfWork(workInProgress);
  }
  
  // 3️⃣ Commit Phase: 트리 교체 (포인터 스왑)
  root.current = finishedWork; // 🔄 단순한 포인터 교체!
}
</script>

트리 교체의 장점:

  • ⚡ 원자적 업데이트: 한 번에 모든 변경사항 반영
  • 🛡️ 일관성 보장: 중간 상태가 사용자에게 노출되지 않음
  • 🔄 롤백 가능: 오류 발생 시 이전 상태로 복원 가능

⚖️ 재조정 알고리즘 (Reconciliation)

  • 재조정은 이전 트리새로운 트리를 비교하여 최소한의 DOM 변경을 찾는 과정이다!

React의 3가지 가정

1. 서로 다른 타입 --> 서로 다른 트리

<script>
// Before
<div>
  <Counter />
</div>

// After  
<span>  {/* 🔥 타입이 변경됨! */}
  <Counter />
</span>

// 결과: div와 Counter 모두 완전히 새로 생성 (언마운트 → 마운트)
</script>

2. Key를 통한 자식 식별

<script>
// ❌ key 없음: 비효율적
{items.map(item => 
  <TodoItem data={item} />  // 순서 변경시 모든 항목 재생성
)}

// ✅ key 있음: 효율적
{items.map(item => 
  <TodoItem key={item.id} data={item} />  // 재정렬만 발생
)}
</script>
  • Current Tree와 WorkInProgress Tree를 비교할 때 key를 중심으로 비교하기 때문!

3. 같은 위치의 같은 타입 --> 업데이트

<script>
// Before
<div className="old">Hello</div>

// After
<div className="new">Hi</div>

// 결과: className만 업데이트, DOM 노드 재사용
</script>

실제 재조정 코드 흐름

<script>
function reconcileChildren(current, workInProgress, nextChildren) {
  if (current === null) {
    // 🆕 초기 렌더링: 새로운 자식들 마운트
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren
    );
  } else {
    // 🔄 업데이트: 이전 자식들과 비교 후 재조정
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,     // 이전 자식
      nextChildren       // 새로운 자식
    );
  }
}

function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
  // 🎯 여기서 실제 diffing 알고리즘 실행
  
  if (typeof newChild === 'object' && newChild !== null) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(
          reconcileSingleElement(returnFiber, currentFirstChild, newChild)
        );
      // ... 다른 타입들
    }
  }
  
  if (Array.isArray(newChild)) {
    return reconcileChildrenArray(returnFiber, currentFirstChild, newChild);
  }
  
  // ... 더 많은 경우들
}
</script>

Side Effect 플래그 시스템

  • 변경사항은 비트 플래그로 효율적으로 관리됨!
<script>
// React의 실제 플래그들
const Flags = {
  NoFlags: 0b000000000000000000,
  PerformedWork: 0b000000000000000001,
  Placement: 0b000000000000000010,    // 삽입
  Update: 0b000000000000000100,       // 업데이트  
  Deletion: 0b000000000000001000,     // 삭제
  ChildDeletion: 0b000000000000010000,
  ContentReset: 0b000000000000100000,
  Callback: 0b000000000001000000,
  DidCapture: 0b000000000010000000,
  Ref: 0b000000001000000000,          // ref 변경
  Snapshot: 0b000000010000000000,     // getSnapshotBeforeUpdate
  Passive: 0b000000100000000000,      // useEffect
  // ... 더 많은 플래그들
};

// 플래그 사용 예시
function markUpdate(fiber) {
  fiber.flags |= Update;  // 비트 OR 연산으로 플래그 추가
}

function hasUpdate(fiber) {
  return (fiber.flags & Update) !== NoFlags;  // 비트 AND로 플래그 확인
}
</script>

🔗 전체 처리 흐름 한눈에 보기

<script>
// 전체 업데이트 흐름
function fullUpdateCycle() {
  
  // 1️⃣ TRIGGER PHASE
  console.log('🔥 Trigger: 상태 업데이트 발생');
  const update = createUpdate(newState); 	//	setState 호출
  enqueueUpdate(fiber, update);				//	queue에 등록
  scheduleUpdateOnFiber(fiber);				//	렌더링 스케쥴 등록
  
  // 2️⃣ RENDER PHASE  
  console.log('🎨 Render: Virtual DOM 구성 시작');
  
  function performWorkOnRoot(root) {
    // Current 트리를 복제하여 WorkInProgress 생성
    let workInProgress = createWorkInProgress(root.current);
    
    // 작업 단위별로 처리 (중단 가능)
    while (workInProgress !== null) {
      try {
        workInProgress = performUnitOfWork(workInProgress);
      } catch (thrownValue) {
        // 에러 처리
        handleError(root, thrownValue);
      }
    }
    
    return finishedWork;
  }
  
  function performUnitOfWork(unitOfWork) {
    const current = unitOfWork.alternate;
    
    // 🔄 Begin work: 컴포넌트 실행 & 재조정
    let next = beginWork(current, unitOfWork);
    
    if (next === null) {
      // 🏁 Complete work: side effect 수집
      next = completeUnitOfWork(unitOfWork);
    }
    
    return next;
  }
  
  // 3️⃣ COMMIT PHASE
  console.log('✅ Commit: DOM 업데이트 시작');
  
  function commitRoot(finishedWork) {
    // 3-1: Before Mutation
    console.log('  📸 Before Mutation: 스냅샷 찍기');
    commitBeforeMutationEffects(finishedWork);
    
    // 3-2: Mutation  
    console.log('  🔧 Mutation: DOM 실제 변경');
    commitMutationEffects(finishedWork);
    
    // 트리 교체 (핵심!)
    root.current = finishedWork;
    console.log('  🔄 트리 교체 완료');
    
    // 3-3: Layout
    console.log('  📐 Layout: 레이아웃 effect 실행');
    commitLayoutEffects(finishedWork);
    
    // 4: Passive (비동기)
    scheduleCallback(() => {
      console.log('  ⚡ Passive: useEffect 실행');
      commitPassiveEffects(finishedWork);
    });
  }
}
</script>

🎯 실전 예시로 이해하기

<script>
function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);
  
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>
        증가
      </button>
    </div>
  );
}

// 버튼 클릭시 발생하는 일들:

// 1️⃣ TRIGGER
// setCount(count + 1) 호출
// → 업데이트 큐에 새로운 상태 추가
// → 스케줄러에 작업 등록

// 2️⃣ RENDER  
// Current Fiber Tree (count: 0)
//   Counter Fiber
//   ├── div Fiber  
//   ├── h1 Fiber (textContent: "0")
//   └── button Fiber
//
// WorkInProgress Tree 구성 (count: 1) 
//   Counter Fiber (새로운 state: {count: 1})
//   ├── div Fiber (변경 없음)
//   ├── h1 Fiber (textContent: "1") ← 🔥 Update 플래그
//   └── button Fiber (변경 없음)

// 3️⃣ COMMIT
// Before Mutation: (없음)
// Mutation: h1의 textContent를 "1"로 변경  
// Layout: (없음)
// Passive: useEffect 실행 → document.title = "Count: 1"
</script>

정리

핵심 포인트

  • 3단계 처리: Trigger → Render → Commit
  • Fiber 아키텍처: 중단 가능한 작업 단위
  • 더블 버퍼링: Current ↔ WorkInProgress 트리 교체
  • 재조정 알고리즘: 최소 변경으로 DOM 업데이트
  • 플래그 시스템: 효율적인 side effect 관리
profile
확장성 있는 설계와 유지보수가 용이한 클린 코드 지향하는 개발자입니다.

0개의 댓글