React19 Internals 0: 어떻게 동작할까?

Ted LeeTed Lee
6 min read

많은 개발자분들이 React를 사용하여 우아하고 동적인 사용자 인터페이스(UI)를 구현하지만, 정작 React가 내부적으로 어떻게 작동하는지에 대해서는 모르는 경우가 있습니다. React의 내부 동작 원리를 이해하면, 동작하는 코드를 작성하는 것을 넘어 더 효율적이고 최적화된 애플리케이션을 만들 수 있는 무기를 얻을 수 있습니다.

이 글에서는 React의 복잡한 내부 세계를 서버 컴포넌트클라이언트 컴포넌트의 상호작용을 중심으로 살펴보고, 특히 클라이언트에서의 동적 업데이트가 일어나는 과정을 트리거(Trigger), 스케줄(Schedule), 렌더(Render), 커밋(Commit)이라는 네 가지 핵심 단계로 나누어 설명합니다. 복잡도를 조금이나마 낮춰보기 위해 다양한 다이어그램을 활용해서 이해를 도왔습니다.

React 내부, 어떻게 학습해야 할까?

React 내부 세계에 대해 설명하기 전에, React를 학습하는 방식에 대해 간단하게 정리해보고자 합니다. React의 코드베이스는 방대하고 복잡해서 어디서부터 시작해야 할지 막막할 수 있습니다. 바로 코드베이스를 보며 학습하기 보다 다음과 같은 효과적인 방식을 시도해보길 권장합니다.

  1. 공식 문서 깊이 파기: React.dev 공식 문서(+블로그 포함)와 React Working Group 토론에는 React 핵심 팀의 철학과 의사결정 과정이 담겨있습니다. API 사용법을 넘어 그들의 생각을 읽는 것이 중요합니다.

  2. React 팀 팔로우하기: React 팀원들의 소셜 미디어나 블로그를 팔로우하면 코드에는 드러나지 않는 귀중한 정보와 토론의 흐름을 파악할 수 있습니다. (트위터가 없어지며 팀원들이 다들 흩어져서 이 방식은 조금 어려워지긴 했습니다)

  3. React 레포지토리 탐색하기: React GitHub 레포지토리의 코드뿐만 아니라, Pull Request(PR)와 코드 리뷰를 살펴보세요. 코드 주석(리액트 코드에는 설명용 주석이 많습니다)보다 더 상세한 구현 배경과 논의를 엿볼 수 있습니다.

  4. 단순 블로그 글이 아닌 코드를 신뢰하기: 인터넷의 수많은 글은 개념적인 설명에 그치는 경우가 많습니다.(이 글 포함) 실제 소스 코드를 직접 디버깅하고 분석하는 것이 가장 정확하게 이해하는 방법입니다.

  5. 핵심 경로(Critical Path)부터 찾기: 학습에 있어서 큰 덩어리로 시작하는건 효율적이지 않습니다. useState가 호출될 때부터 화면에 그려지기까지의 핵심적인 흐름을 먼저 파악한 뒤, 점진적으로 분할 정복을 하며 지식을 확장해 나가는 것이 효과적입니다.

React 패키지 구조 엿보기

React 패키지 내부를 들여다보면, 최신 React가 어떻게 구성되어 있는지 명확하게 알 수 있습니다.

  • 서버와 클라이언트의 분리: react.react-server.js와 같은 파일들은 서버 환경 전용 빌드입니다. 반면, index.js 등은 클라이언트(브라우저) 환경을 위한 것입니다. 이는 React가 서버와 클라이언트에서 각기 다른 역할을 수행하도록 설계되었음을 보여줍니다.

  • 클라이언트 렌더링 엔진: useState, useEffect와 같은 훅(Hook)과 4단계 업데이트 로직은 클라이언트용 빌드에 포함된 Reconciler와 Scheduler가 담당합니다.

  • React 컴파일러 지원: compiler-runtime.js 파일은 React 컴파일러(코드명: Forget)를 위한 런타임 코드로, 빌드 시점 최적화를 지원합니다.

이처럼 실제 패키지 구조는 우리가 논의할 서버/클라이언트 모델과 최적화 전략이 실제 코드 레벨에서 구현되어 있음을 증명합니다.

React 업데이트는 누가, 어떻게 시작할까요?

React 업데이트는 개발자, 서버, 그리고 사용자라는 세 주체에 의해 시작됩니다. 각 주체는 애플리케이션 생명주기의 다른 시점에서 업데이트를 유발합니다.

  1. 개발자 (Developer): 모든 것의 시작점입니다. 개발자는 컴포넌트 구조와 렌더링 로직을 코드로 작성합니다. 특히 전통적인 클라이언트 사이드 렌더링(CSR)에서는 개발자가 작성한 ReactDOM.createRoot().render() 코드가 직접적으로 애플리케이션의 첫 렌더링을 시작시킵니다.

  2. 서버 (Server): React 서버 컴포넌트(RSC) 환경에서는, 사용자의 페이지 요청을 받은 서버가 개발자가 작성한 코드를 실행하여 초기 UI를 렌더링합니다. 이는 서버에 의해 시작되는 업데이트입니다.

  3. 사용자 (User): 애플리케이션이 로드된 후, 사용자는 버튼 클릭이나 입력 같은 상호작용을 통해 클라이언트 컴포넌트의 상태를 변경합니다. 이는 후속 리렌더링을 유발(trigger)합니다.

이 관계는 아래 유즈케이스 다이어그램으로 명확히 표현할 수 있습니다.

flowchart TD
    subgraph "외부 액터(Actor)"
        DEV["👨‍💻 개발자"]
        SERVER["🌐 서버"]
        USER["👤 사용자"]
    end

    subgraph "React App 유즈케이스"
        UC1["초기 렌더링 시작<br/>(CSR: render() 호출)<br />(RSC: 페이지 요청)"]
        UC2["상태 변경에 의한<br/>리렌더링 (클라이언트)"]
        UC3["컴포넌트 트리<br/>렌더링/업데이트"]
        UC4["DOM 업데이트"]
        UC5["Effect 실행<br/>(useEffect, useLayoutEffect)"]
    end

    DEV -- "코드 작성 및 render() 호출" --> UC1
    SERVER -- "RSC 요청 처리" --> UC1
    USER -- "UI 상호작용" --> UC2

    UC1 -.->|include| UC3
    UC2 -.->|include| UC3
    UC3 -.->|include| UC4
    UC4 -.->|include| UC5

    style DEV fill:#e1f5fe
    style SERVER fill:#d1c4e9
    style USER fill:#f3e5f5
    style UC1 fill:#fff3e0
    style UC2 fill:#fff3e0
    style UC3 fill:#e8f5e8
    style UC4 fill:#e8f5e8
    style UC5 fill:#e8f5e8

클라이언트 업데이트의 4단계 동작 원리

'use client'로 명시된 클라이언트 컴포넌트에서 발생하는 모든 업데이트는 아래 플로우차트에 나타난 트리거 → 스케줄 → 렌더 → 커밋의 4단계 과정을 거칩니다. 이 흐름은 React가 UI를 업데이트하는 핵심 로직입니다.

graph TD
    subgraph "Trigger"
        A["시작: 초기 렌더링 또는<br/>상태 변경(setState) 호출"] --> B["업데이트 작업 생성<br/>(scheduleUpdateOnFiber)"];
    end

    B --> C["스케줄러에 작업 전달<br/>(scheduleCallback)"];

    subgraph "Schedule"
        C --> D["작업 우선순위 지정 및<br/>대기열 추가"];
        D --> E["스케줄러가 작업 실행<br/>(workLoop)"];
    end

    subgraph "Render"
        E --> F["렌더 단계 시작<br/>(performConcurrentWorkOnRoot)"];
        F --> G["컴포넌트 함수 호출<br/>새 파이버 트리 생성"];
        G --> H["상태 값 비교 (Bailout)<br/>& 기존 트리와 비교 (Diffing)"];
        H --> I{"변경 사항 있음?"};
    end

    I -- "No" --> J["작업 종료<br/>(렌더링 생략)"];

    subgraph "Commit"
        I -- "Yes" --> K["커밋 단계 시작<br/>(commitRoot)"];
        K --> L["DOM 변경 사항 적용<br/>(commitMutationEffects)"];
        L --> M["useLayoutEffect 실행"];
        M --> N["브라우저 렌더링"];
        N --> O["useEffect 실행<br/>(flushPassiveEffects)"];
    end

    O --> P["작업 종료"];

    style Trigger fill:#f9f,stroke:#333,stroke-width:2px
    style Schedule fill:#ccf,stroke:#333,stroke-width:2px
    style Render fill:#cfc,stroke:#333,stroke-width:2px
    style Commit fill:#fca,stroke:#333,stroke-width:2px
    style J fill:#eee,stroke:#333,stroke-width:2px,stroke-dasharray: 5 5
    style P fill:#eee,stroke:#333,stroke-width:2px

1. Trigger (트리거)

render()setState() 같은 이벤트가 발생하면, React는 어떤 컴포넌트를 다시 렌더링해야 할지 결정하고 업데이트 작업을 생성합니다.

2. Schedule (스케줄)

생성된 업데이트 작업의 우선순위를 정하고, 내부 스케줄러를 통해 언제 실행할지 결정합니다. 긴급한 업데이트는 먼저 처리(우선순위는 lane에 정의 되있습니다)하여 사용자 경험을 최적화합니다.

3. Render (렌더)

스케줄링된 컴포넌트를 실제로 렌더링하여 새로운 파이버 트리(Fiber Tree)를 구성하고, 기존 트리와 비교하여 변경 사항(diff)을 계산합니다. 이 단계는 실제 DOM을 건드리지 않으며, 더 급한 작업이 들어오면 중단될 수 있습니다.

4. Commit (커밋)

렌더 단계에서 계산된 변경 사항을 실제 DOM에 적용하는 마지막 단계입니다. 이 단계는 중단되지 않으며, DOM 업데이트 후 useLayoutEffect, 그리고 브라우저 페인팅 이후 useEffect를 순차적으로 실행합니다.

모듈 간의 상호작용

React 컴포넌트의 상호작용은 그것이 실행되는 환경(서버 또는 클라이언트)에 따라 크게 달라집니다.

1) 클라이언트 컴포넌트의 업데이트 상호작용

클라이언트 컴포넌트는 사용자의 상호작용(예: 버튼 클릭)에 의해 업데이트됩니다. 이 과정은 이전에 설명한 4단계(트리거 → 스케줄 → 렌더 → 커밋) 모델을 따르며, 최종적으로 실제 DOM을 변경하여 사용자에게 변경된 UI를 보여줍니다.

sequenceDiagram
    actor User
    participant Component
    participant ReactHooks as React Hooks<br/>(useState)
    participant Scheduler
    participant Reconciler as Reconciler<br/>(Render/Commit)
    participant DOM

    User->>+Component: 1. 버튼 클릭
    Component->>+ReactHooks: 2. setState() 호출
    ReactHooks->>+Reconciler: 3. 업데이트 예약<br/>(scheduleUpdateOnFiber)
    Reconciler->>+Scheduler: 4. 작업 스케줄링 요청<br/>(scheduleCallback)
    Scheduler-->>-Reconciler: 5. 렌더링 작업 시작<br/>(performConcurrentWorkOnRoot)
    loop 렌더(Render) 단계
        Reconciler->>Component: 6. 컴포넌트 함수 재호출
        Component-->>Reconciler: 7. 새로운 JSX 반환
    end
    Reconciler->>Reconciler: 8. 변경사항 계산 (Diffing)
    Reconciler->>+DOM: 9. DOM 업데이트 (Commit)
    Reconciler->>Component: 10. useEffect 실행
    DOM-->>-User: 11. 변경된 UI 표시

2) 서버 컴포넌트의 초기 렌더링 상호작용

서버 컴포넌트는 사용자의 최초 페이지 요청이나 내비게이션 요청에 의해 렌더링됩니다. 주된 목적은 데이터를 가져와 UI 구조를 생성하고, 이를 클라이언트로 스트리밍할 수 있는 특별한 형식(RSC Payload)으로 직렬화하는 것입니다.

sequenceDiagram
    participant Browser
    participant FrameworkServer as Framework Server <br />(e.g., Next.js)
    participant ReactServer
    participant Reconciler

    Browser->>+FrameworkServer: 1. 페이지 요청 (GET /)
    FrameworkServer->>+ReactServer: 2. 렌더링 요청
    ReactServer->>+Reconciler: 3. 서버 렌더링 시작
    loop Render on Server
        Reconciler->>Reconciler: 4. 서버 컴포넌트 실행 (DB/API 접근)
        Reconciler->>Reconciler: 5. 클라이언트 컴포넌트는 자리표시자로 처리
    end
    Reconciler-->>-ReactServer: 6. RSC 페이로드 생성
    ReactServer-->>-FrameworkServer: 7. 페이로드 스트림 반환
    FrameworkServer-->>-Browser: 8. UI 스트리밍 응답

이 과정은 사용자와의 상호작용이 없으며, 최종 결과물은 DOM 변경이 아닌 직렬화된 데이터입니다.

React를 구성하는 핵심 모듈들

이 모든 마법은 React 내부의 핵심 모듈들이 유기적으로 협력하기에 가능합니다. 각 모듈의 역할과 관계를 모듈 클래스 다이어그램으로 확인 해보겠습니다.

classDiagram
    direction LR
    class ReactDOMClient {
        +createRoot(container)
        +render(component)
    }
    class ReactCore {
        <<Hooks>>
        +useState()
        +useEffect()
        +useContext()
    }
    class Scheduler {
        -PriorityQueue tasks
        +scheduleCallback(callback, priority)
        -workLoop()
    }
    class Reconciler {
        <<Fiber>>
        +scheduleUpdateOnFiber()
        -performConcurrentWorkOnRoot()
        -commitRoot()
    }
    class ReactServer {
         - Renders Server Components
    }
    class DOM {
        <<Host Environment>>
        +createElement()
        +appendChild()
    }

    ReactDOMClient ..> Reconciler : 렌더링 시작 (클라이언트)
    ReactCore ..> Reconciler : 업데이트 요청 (클라이언트)
    Reconciler ..> Scheduler : 작업 스케줄링 (클라이언트)
    Reconciler ..> DOM : DOM 조작 (클라이언트)
    ReactServer ..> Reconciler : 초기 렌더링 (서버)
  • ReactDOMClient: render()를 통해 React 세계를 시작하는 진입점입니다.

  • ReactCore: useState, useEffect 등 우리가 사용하는 훅을 제공합니다.

  • Scheduler: 클라이언트 측 업데이트 작업의 우선순위를 관리하고 실행 시점을 조율합니다.

  • Reconciler: 파이버(Fiber) 아키텍처를 기반으로 작동하는 React의 핵심 엔진입니다. 서버에서는 초기 렌더링을, 클라이언트에서는 변경 사항을 계산(Render)하고 DOM에 적용(Commit)하는 역할을 모두 수행합니다.

  • ReactServer: 서버 환경에서 서버 컴포넌트를 렌더링하는 추상적인 주체입니다. 내부적으로 Reconciler를 사용하지만, Scheduler나 DOM 조작 로직은 사용하지 않습니다.

정리하며

최신 React는 개발자가 작성한 코드를 기반으로, 서버클라이언트가 각자의 역할을 수행하는 하이브리드 모델을 구현합니다.

서버는 react.react-server.js 빌드를 사용해 초기 로딩과 데이터 페칭을 최적화하고, 클라이언트는 index.js 빌드를 통해 트리거 → 스케줄 → 렌더 → 커밋 과정을 거쳐 사용자와의 동적 상호작용을 처리합니다. 여기에 React 컴파일러가 더해져 렌더링 과정을 더욱 최적화합니다.

이 통합적인 동작 원리를 실제 패키지 구조와 연결하여 이해하면, 복잡한 애플리케이션의 성능을 어떻게 최적화할 수 있는지에 대한 깊이 있는 통찰력을 얻을 수 있습니다.

0
Subscribe to my newsletter

Read articles from Ted Lee directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ted Lee
Ted Lee

Software engineer for web tech. Interested in sustainable growth as software engineer.