Overview

Hee Suh·2024년 6월 4일
1
post-thumbnail

JSer.devReact Internals Deep Dive를 번역하여 정리한 글입니다.

💡 React Internals Deep Dive는 React 소스 코드를 살펴보며 React 동작 원리에 딥다이브하는 시리즈입니다. 1번 에피소드인 Overview부터 에피소드를 하나씩 정리해서 포스팅하려고 합니다.

  • "💬"로 시작하는 문장은 제가 추가한 문장입니다.
  • "📌"로 시작하는 코드 주석은 JSer가 추가한 주석입니다.
  • ⚠️ React@18.2.0을 기반으로 작성되었으며, 최신 버전에서는 구현이 변경되었을 수 있습니다.

📝 How does React work under the hood ? The Overview of React internals

1. Tips on learning React internals

Official materials

React.dev는 React API뿐만 아니라, React 코어 팀의 생각도 배울 수 있는 곳이다. 예제, 주의 사항와 함께 선택의 근거를 자세히 설명하고 있다.

React Working Group은 React 18을 위해 만들어졌으며, 새로운 아이디어에 대한 토론을 확인할 수 있다.

React team

React core team members를 팔로우해서 React 팀이 무엇을 하고 있는지 계속 업데이트하자.

React repo

React repo@github에 있는 PR과 코드 리뷰에, 코드에 있는 주석보다 더 나은 설명이 포함되어있다.

React source code

인터넷에 있는 대부분의 글은 몇 가지 일반적인 아이디어만 설명하고 있기 때문에, 글을 읽은 후에도 여전히 React Quizzes를 풀 수 없다.

React 소스 코드를 직접 살펴보면 분명 도움이 될 것이다.

Cf. https://bigfrontend.dev/react-quiz

Find the critical path

한 번에 모든 걸 이해하지 않아도 된다. 전반적인 작동 방식을 파악하고 퍼즐을 하나씩 맞춰나가면서 이해의 폭을 넓혀보자!

2. Debugging the overview of React internals with breakpoints

Demo를 이용해서 같이 디버깅해보자!

2.1 Set up breakpoints

// 💬 디버깅할 코드
const { useState, useEffect } = React;

function App() {
  const [count, setCount] = useState(1);
  debugger;   // 📌 컴포넌트가 실행되는 시점을 알려주는 debugger
  useEffect(() => {
    debugger; // 📌 effect hooks가 실행되는 시점을 알려주는 debugger
    setCount((count) => count + 1);
  }, []);
  return <button>{count}</button>;
}

ReactDOM.createRoot(document.getElementById("container")).render(<App />);

DOM container에 breakpoint를 추가하여, DOM이 조작될 때 어떤 일이 일어나는지 call stack을 통해 파악

DOM container에 breakpoint를 추가하여, DOM이 조작될 때 어떤 일이 일어나는지 call stack을 통해 파악

2.2 First pause at rendering the component

컴포넌트가 mount 되면 debugger에 의해 정지

컴포넌트가 mount 되면 debugger에 의해 정지
  1. ReactDOMRoot.render() → 직접 작성한 사용자 측 코드로, 우선 createRoot()를 한 후 render()를 한다.

  2. scheduleUpdateOnFiber() → 앱의 어느 부분을 렌더링해야 하는지 React 런타임에 알려준다. Initial mount의 경우, 이전 버전이 없으므로 root에서 호출된다.

  3. ensureRootIsScheduled() → performConcurrentWorkOnRoot() 가 스케줄링되도록 “보장”하는 중요한 호출이다.

  4. scheduleCallback() → 스케줄링이 일어나는 곳으로, React Scheduler의 일부분이며, MessageChannelpostMessage()를 이용하여 비동기로 macrotask를 예약한다. Cf. v8 macrotask queue

  5. workLoop() → React Scheduler가 예약된 작업을 처리하는 곳이다.

  6. performConcurrentWorkOnRoot() → 예약된 작업이 실행되며, 컴포넌트가 실제로 렌더링된다.

2.3 Second pause at DOM manipulation

DOM이 조작될 때 debugger에 의해 정지

DOM이 조작될 때 debugger에 의해 정지

2.2의 “render” 단계 이후에 DOM을 업데이트하는 “commit” 단계다.

  1. commitRoot() → 이전 렌더링 단계에서 파생된 필수 DOM 업데이트를 커밋할뿐만 아니라, effect 처리와 같은 더 많은 작업도 수행한다.

  2. commitMutationEffects() → host DOM을 업데이트한다.

2.4 Third pause at execution of effects

useEffect() 호출에서 debugger에 의해 정지

useEffect() 호출에서 debugger에 의해 정지
  1. flushPassiveEffects() → useEffect()에서 생성된 모든 effects를 flush한다.

flushPassiveEffects()postMessage()에 의해 비동기화되기 때문에 즉시 실행되지 않고 스케줄링되며, commitRoot() 내부에 있다.

2.5 Pause again at rendering component

컴포넌트가 리렌더링 되면 debugger에 의해 정지

컴포넌트가 리렌더링 되면 debugger에 의해 정지

useEffect()에서 re-render를 trigger하기 위해 setState()를 호출했을 때, 콜 스택을 확인해보면 전체 re-render는 performConcurrentWorkOnRoot()에서 mountIndeterminateComponent()대신 updateFunctionComponent()를 호출하는 것을 제외하고 initial render와 유사하다.

React 소스 코드에서 mount는 initial render를 뜻하는데, initial render에는 비교(diff)해야 할 이전 버전이 없기 때문이다.

3. The overview of React internals

Cf. Render and Commit in react.dev
출처: https://react.dev/learn/render-and-commit Illustrated by Rachel Lee Nabors

출처: https://react.dev/learn/render-and-commit Illustrated by Rachel Lee Nabors
  • Step 1: Trigger a render
  • Step 2: React renders your components 
  • Step 3: React commits changes to the DOM

💬 React 공식 문서에서는 컴포넌트가 화면에 표시되는 과정을 세 단계인 Trigger, Render, Commit으로 설명하고 있다. JSer는 Trigger와 Render 사이에 있는 Schedule 단계를 명시하여 내부 동작 원리를 설명하고 있다.

출처: https://jser.dev/2023-07-11-overall-of-react-internals

출처: https://jser.dev/2023-07-11-overall-of-react-internals

3.1 Trigger

컴포넌트가 렌더링되는 이유는 두 가지가 있다.

  • initial render
  • state 업데이트로 인한 re-render

렌더링되는 이유와 상관 없이, “Trigger” 단계에서 모든 작업이 시작된다. “Trigger”는 앱의 어느 부분을(scheduleUpdateOnFiber()) 어떻게 렌더링 해야 하는지 React 런타임에 알려주는 단계다.

“작업 생성” 단계로 생각할 수 있으며, ensureRootIsScheduled()가 수행되면 작업 생성이 완료되고, 해당 작업은 scheduleCallback()에 의해 Scheduler로 전달된다.

3.2 Schedule

React Scheduler는 task를 우선순위에 따라 처리하는 우선순위 큐다.

scheduleCallback()은 렌더링이나 effect 실행 같은 task를 예약하기 위해 런타임 코드에서 호출된다.

Scheduler 내부의 workLoop()는 task들이 실제로 실행되는 곳이다.

Cf. How React Scheduler works?

3.3 Render

Render는 스케줄링(예약)된 작업이다.

새로운 Fiber Tree를 계산하고, host DOM에 적용하기 위해 어떤 업데이트가 필요한지 파악한다.

ℹ️ Fiber Tree
앱의 현재 상태를 나타내는 내부 트리와 같은 구조다.
Cf. 예전에는 Virtual DOM이라고 불렸으나, 이제는 DOM에만 해당되는 것이 아니라서 React team에서 더 이상 Virutal DOM이라고 부르지 않는다.

performConcurrentWorkOnRoot()는 Trigger 단계에서 생성되고, Scheduler에서 우선순위가 조정되며, Render에서 실제로 실행된다.

Concurrent feature가 사용되는 경우, “Render” 단계는 중단(interrupt)되었다가 다시 시작될 수 있으므로 단계가 상당히 복잡해진다.

3.4 Commit

새로운 Fiber Tree가 구성되고 최소한의 업데이트가 나왔다면, host DOM에 업데이트를 “commit”할 시간이다.

이 단계에서는 DOM 조작(commitMutationEffects())과 Effects(flushPassiveEffects()commitLayoutEffects()) 처리 등 여러 작업이 수행된다.

4. Summary

React Internals Deep Dive의 첫 번째 에피소드로, debugger를 이용하여 리액트 동작 원리를 파악하는 방법을 알아보았다.

References

profile
원리를 파헤치는 것을 좋아하는 프론트엔드 개발자입니다 🏃🏻‍♀️

0개의 댓글