React의 렌더링 과정과 동작 원리

xxziiko·4일 전
0

[React]

목록 보기
1/5
post-thumbnail

React는 컴포넌트 기반의 선언적 UI 라이브러리로, 복잡한 사용자 인터페이스를 효율적으로 구축하도록 최적화된 도구입니다. 개인적으로 React는 직관적인 사용성과 명확한 데이터 흐름 덕분에 처음 배우기 어렵지 않다고 생각합니다. 하지만 이는 React의 첫인상일 뿐, React를 제대로 효율적으로 사용하기 위해서는 렌더링 과정과 동작 원리에 대한 깊은 이해가 필요합니다. 특히, 렌더링 과정의 정확한 이해는 성능 최적화와 불필요한 렌더링 방지에 매우 중요합니다. 이에 이번 글에서는 React의 렌더링 과정과 동작 원리를 자세히 알아보고자 합니다.



1. React의 렌더링 개념


React의 철학

  1. 컴포넌트 기반 아키텍처(Component-Based Architecture)

    React는 UI를 작은 컴포넌트 단위로 쪼개어 각각의 컴포넌트가 독립적으로 상태와 렌더링을 관리할 수 있도록 설계되었습니다. 컴포넌트는 재사용이 가능하여 큰 애플리케이션에서도 UI를 효율적으로 구성하고 관리할 수 있습니다. 공식문서에는 “컴포넌트는 독립적이며 재사용 가능한 작은 코드 조각” 이라고 설명하며, UI와 로직을 캡슐화하여 관리할 수 있는 방식을 강조합니다.

    예를 들어, 버튼, 카드, 모달과 같은 UI 요소들을 각각 컴포넌트로 만들어 여러 페이지나 기능에서 손쉽게 재사용 합니다.

  2. 선언적 UI(Declarative UI)

    React는 선언적 프로그래밍 방식을 사용하여 상태 변화에 따라 UI가 자동으로 갱신되도록 합니다. 선언적이란 “어떤 모습이어야 하는가”를 설명하는 방식입니다. 개발자는 특정 상태에서 UI가 어떤 모습이 되어야 하는지 정의하면 React가 그 상태에 맞게 UI를 렌더링합니다.

    <button disabled={!isEnabled}>Click Me</button>

    예를 들어, 버튼이 비활성화 되었을 때의 스타일을 상태에 따라 “어떻게” 표시할 것인지 선언적으로 정의합니다.

  3. 단방향 데이터 흐름(One-Way Data Flow)

    React는 단방향 데이터 흐름을 사용하여 데이터의 방향성을 명확히 하고, 컴포넌트 간 데이터 전달을 쉽게 추적할 수 있도록 설계되어있습니다. 이는 부모 컴포넌트가 자식 컴포넌트에게 props를 전달하는 방식으로 이루어집니다. 단방향 데이터 흐름은 데이터의 변경이 어디서 발생했는지 쉽게 파악할 수 있도록 하여 디버깅을 용이하게 하고, 상태를 더 예측 가능하게 만듭니다.

    즉, 이 구조는 복잡한 애플리케이션에서 데이터의 흐름을 관리하는데 유리하고, 부모-자식 관계를 통한 상태 변경의 영향을 쉽게 추적할 수 있습니다.


Virtual Dom과 Fiber

Virtual Dom

React의 렌더링 최적화를 위한 중요한 개념입니다.

실제 DOM의 가상 복사본으로, 상태나 props가 변경될 때마다 Virtual DOM을 새롭게 만들어 변경된 부분만 실제 DOM에 반영합니다.

Fiber

React의 재조정(Reconciliation) 알고리즘을 개선하기 위해 도입된 실행 모델이며, React의 렌더링 트리를 구성하고 관리하는 구조체

  1. Fiber의 역할

    React는 기본적으로 상태나 props가 변경되면 렌더링 트리를 통해 변경 사항을 계산하고 DOM을 업데이트합니다. 이때 Fiber는 렌더링 과정에서 효율적으로 작업을 분배하고, 트리를 단계별로 업데이트할 수 있게 해줍니다. Fiber 구조는 특히 시간 분할(Time-Slicing) 방식으로 작업을 수행하여, UI가 복잡하거나 업데이트가 빈번한 경우에도 사용자 인터페이스가 막히지 않도록 돕습니다.

  2. Fiber와 렌더링 트리

    Fiber는 React 컴포넌트의 각 요소를 Fiber 노드로 표현하여 트리 구조로 관리합니다. 이 Fiber 노드는 컴포넌트의 상태와 props 등 렌더링에 필요한 정보뿐만 아니라 업데이트 우선순위와 같은 메타 정보를 포함하고 있습니다. Fiber 구조 덕분에 React는 각 작업의 우선순위를 기반으로 렌더링을 최적화하며, 긴 작업을 여러 프레임에 걸쳐 나누어 실행할 수 있습니다.


렌더링 트리(Fiber)

React는 컴포넌트 트리를 기반으로 렌더링을 수행합니다. 컴포넌트 트리는 루트 컴포넌트부터 하위 컴포넌트까지의 계층 구조로 각 컴포넌트가 어떤 데이터를 받고 어떤 상태를 관리하는지 나타냅니다.

React는 이 컴포넌트 트리를 Fiber 구조로 관리하며, Fiber 트리를 통해 컴포넌트의 렌더링과 업데이트를 효율적으로 처리할 수 있습니다.



2. React의 렌더링 과정

React 렌더링 과정은 크게 초기 렌더링, 재조정(Reconciliation), 업데이트 및 커밋 단계로 나뉩니다.


1단계: 초기 렌더링(Initial Rendering)

import React from 'react';
import ReactDOM from 'react-dom';

const App = () => <h1>Hello, React!</h1>;

ReactDOM.render(<App />, document.getElementById('root'));
  1. 컴포넌트 트리 생성

    React 애플리케이션이 실행되면 최상위 컴포넌트에서 시작해 모든 하위 컴포넌트들이 재귀적으로 호출되어 컴포넌트 트리가 형성됩니다. 각 컴포넌트는 JSX를 기반으로 Virtual DOM을 생성합니다. Virtual DOM은 실제 DOM을 추상화한 구조체로, React는 이를 사용해 빠르게 변경 사항을 관리하고 계산할 수 있습니다.

  2. Virtual DOM 트리 생성

    모든 컴포넌트가 처음 렌더링될 때 각 컴포넌트의 JSX가 Virtual DOM으로 변환되며 트리를 형성합니다. 이 Virtual DOM 트리는 React의 렌더링 과정의 중심으로, 상태나 props가 변할 때마다 React가 Virtual DOM을 업데이트하고 비교하는 데 사용됩니다.

  3. 실제 DOM에 반영

    React는 생성한 Virtual DOM을 기반으로 Real DOM(실제 DOM)을 처음으로 렌더링합니다. 이때 모든 요소가 실제 DOM에 추가되어 사용자에게 보입니다.


2단계: 재조정(Reconciliation)

재조정은 상태나 props가 변경되어 렌더링이 다시 발생해야 할 때 진행됩니다. 여기서는 Virtual DOM이 중요한 역할을 합니다.

  1. 상태와 props의 변화 감지

    React의 렌더링은 상태나 props가 변경될 때만 발생합니다. 상태나 props가 변경되면 해당 컴포넌트와 그 하위 컴포넌트가 다시 렌더링되어 새로운 Virtual DOM이 생성됩니다.

  2. Virtual DOM 비교(Diffing 알고리즘)

    상태나 props가 변경되면 새로운 Virtual DOM이 생성되는데, 이때 기존 Virtual DOM과의 비교 작업이 수행됩니다. React의 Diffing 알고리즘은 트리를 순회하며 두 Virtual DOM을 비교하고, 변경된 부분만을 추적합니다.

    • 동일한 노드 재사용: React는 같은 유형의 컴포넌트가 계속해서 사용될 경우 노드를 재사용하여 최적화합니다.
    • 최소 변경 적용: 변경이 확인된 부분만을 실제 DOM에 반영해 불필요한 렌더링을 방지합니다.
  3. 컴포넌트 트리 업데이트

    Virtual DOM 비교 과정에서 파악된 변경 사항이 컴포넌트 트리에 반영되며, 변경된 컴포넌트만 다시 렌더링됩니다. 이 과정은 Virtual DOM에서만 일어나기 때문에 실제 DOM 업데이트보다 훨씬 빠릅니다.


3단계: 업데이트 및 커밋

Virtual DOM에서 변경이 완료되면 React는 실제 DOM에 적용해야 할 작업들을 준비하여 일괄적으로 업데이트합니다.

  1. Render Phase (렌더 단계)

    렌더 단계에서 React는 Virtual DOM을 기준으로 변경할 내용을 계산하고, 이 변경 내역을 내부에 저장합니다. 렌더 단계에서는 DOM에 직접적인 수정이 일어나지 않고, 변경할 요소를 React의 메모리에 저장하는 단계입니다.

  2. Commit Phase (커밋 단계)

    커밋 단계에서는 앞서 준비한 변경 사항이 실제 DOM에 반영됩니다. 이때 React는 필요한 요소만 실제 DOM에 추가, 수정 또는 제거하여 성능을 최적화합니다.

  3. 배치 업데이트와 우선순위 설정

    React는 성능을 최적화하기 위해 배치 업데이트와 우선순위 설정을 사용합니다. 이로 인해 변경사항이 한꺼번에 커밋되고 높은 우선순위의 업데이트는 빠르게 적용됩니다. 우선순위가 낮은 업데이트는 조금 더 효율적으로 처리됩니다.

    • 배치 업데이트: 여러 업데이트를 묶어 한꺼번에 처리함으로써 불필요한 재렌더링을 줄입니다.
    • 우선순위 설정: React 18에서는 Concurrency 모드를 통해 우선순위가 높은 업데이트를 먼저 처리하고, 우선순위가 낮은 작업은 나중에 실행하도록 최적화할 수 있습니다.


3. Concurrency 모드

React 18부터 도입된 기능으로, UI의 응답성을 높이고 렌더링을 효율적으로 처리하기 위한 새로운 렌더링 전략입니다. 사용자 입력과 같은 우선순위 작업에 신속하게 반응할 수 있도록 최적화되어 페이지가 느려지지 않고 매끄럽게 동작할 수 있게 합니다.

  1. 시간 분할(Time-Slicing)

    Concurrency 모드는 작업을 여러 프레임에 나누어 수행하는 시간 분할 방식을 사용합니다. 이는 단일 프레임에서 모든 작업을 처리하는 것이 아니라, 중간에 중요한 작업이 있으면 해당 작업을 우선 처리하고 나머지 작업은 뒤로 미루는 방식입니다. 긴 목록을 렌더링하면서 스크롤을 발생시킨다면 Concurrency 모드는 스크롤 작업을 우선적으로 처리하고 렌더링 작업을 잠시 중단했다가 재개할 수 있습니다.

  2. 자동 중단 및 재개

    React가 작업 중에 사용자 인터랙션과 같은 중요한 이벤트를 감지한다면 현재 작업을 중단하고, 우선순위가 높은 작업을 먼저 수행할 수 있도록 합니다.

    사용자가 버튼을 클릭하거나 스크롤하는 동안 React가 무거운 작업을 수행한다면Concurrency 모드는 무거운 작업을 일시 중단하고 사용자 입력을 먼저 처리하고, 이전 작업은 자동으로 재개됩니다.

  3. 우선순위 기반 렌더링

    각 작업의 우선순위를 지정하여 사용자 입력과 같은 우선순위가 높은 작업에 먼저 응답하고 우선순위가 낮은 작업은 나중에 처리합니다. 이를 통해 중요도가 낮은 작업은 지연 처리되어 사용자 경험이 매끄러워 상태 변경이 빈번한 대규모 애플리케이션에서도 높은 응답성을 유지할 수 있습니다.

  4. 새로운 Hook과 기능

    useTransitionstartTransition 같은 새로운 Hook이 추가되어 상태 업데이트의 우선순위를 명시적으로 설정할 수 있습니다. 이를 통해 개발자는 애니메이션, 필터링 등과 같이 중요도가 낮은 업데이트를 별도의 낮은 우선순위로 처리하여 더 중요한 작업에 리소스를 집중할 수 있습니다.

    import { useState, useTransition } from 'react';
    
    function MyComponent() {
      const [isPending, startTransition] = useTransition();
      const [data, setData] = useState(null);
    
      const handleClick = () => {
        startTransition(() => {
          // 낮은 우선순위로 처리
          setData(fetchData());
        });
      };
    
      return (
        <div>
          <button onClick={handleClick}>Load Data</button>
          {isPending && <p>Loading...</p>}
          {data && <p>{data}</p>}
        </div>
      );
    }
    
  5. Suspense와의 통합

    Concurrency 모드는 Suspense와 통합되어 비동기 데이터 로딩이 필요한 컴포넌트를 렌더링할 때 비동기 작업이 완료될 때까지 기다리며 앱의 다른 부분을 동시에 렌더링을 가능하게 하여 비동기 작업으로 인한 지연을 줄일 수 있습니다.



4. React의 렌더링 최적화 기법

React의 렌더링 과정에서 최적화할 수 있는 주요 기법은 다음과 같습니다.


React.memoPureComponent

React.memo는 함수형 컴포넌트를 메모이제이션하여 props가 변경되지 않으면 재렌더링을 방지하는 방법입니다. PureComponent는 클래스형 컴포넌트에서 propsstate의 얕은 비교를 통해 불필요한 렌더링을 막습니다.

interface MyComponentProps {
  value: string; 
}

const MyComponent: React.FC<MyComponentProps> = React.memo(({ value }) => {
  console.log("Rendering MyComponent");
  return <div>{value}</div>;
});

useCallback과 useMemo

useCallbackuseMemo는 함수와 값을 메모이제이션하여 재생성되는 것을 방지합니다. 자주 호출되는 함수나 값이 불필요하게 재계산되지 않도록 방지해 성능을 향상시킵니다.

const computeExpensiveValue = (a: number, b: number): number => {
  return a + b; 
};

const handleEvent = (a: number, b: number): void => {
  console.log("Event handled with", a, b);
};

const a:number = 1;
const b:number = 2;

const memoizedValue = useMemo<number>(() => computeExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(() => handleEvent(a, b), [a, b]);


결론

React는 컴포넌트 기반의 선언적 UI 라이브러리로, 직관적이고 강력한 데이터 흐름을 제공하지만 성능 최적화와 효율적인 사용을 위해서는 렌더링 과정과 동작 원리에 대한 깊은 이해가 필수적입니다.

특히 Virtual DOM과 Fiber 구조는 React의 효율적인 렌더링을 가능하게 하고, Concurrency 모드는 더 부드럽고 최적화된 사용자 경험을 제공합니다.

React의 렌더링 과정을 이해하면 컴포넌트 변화가 실제 DOM에 반영되는 방식과 최적화 포인트를 파악할 수 있어 불필요한 렌더링을 줄일 수 있습니다. 또한 React.memo, useCallback, useMemo와 같은 최적화 도구는 렌더링을 효율적으로 관리하며, Concurrency 모드는 중요한 작업을 우선 처리해 사용자 응답성을 높이는 데 기여합니다.

결국 React를 사용하는 이유는 복잡한 UI 상태를 예측 가능하게 관리하고 성능을 최적화할 수 있는 구조 때문입니다. 이러한 핵심 원리를 깊이 이해하고 활용하는 것은 고성능 React 애플리케이션 개발에 있어 필수적이라고 생각합니다.



ref.

https://ko.react.dev/learn

profile
코딩하는 감자 🥔

0개의 댓글