[React]렌더링 프로세스, 리액트 작동원리, Fiber

김건휘·2024년 11월 5일
0

React

목록 보기
11/19
post-thumbnail

📌렌더링이란?

리액트에서 렌더링(rendering)이란, 컴포넌트의 내용을 화면에 표시하거나 업데이트하는 과정을 의미한다.

  • 초기 렌더링: 구성 요소가 화면에 처음 나타날 때 발생
  • 리렌더링: 이미 화면에 있는 구성 요소의 두 번째 및 연속 렌더링

📌렌더링 프로세스

리액트에서는 크게 아래의 3단계를 거쳐 렌더링 과정이 이루어진다.
1. 렌더링 트리거
2. 컴포넌트 렌더링
3. DOM에 커밋

리액트 공식문서에서는 리액트를 웨이터에 비유한다. 1.렌더링 트리거 (setState와 같은 상태 변경)는 손님의 주문을 주방으로 전달. 2. 컴포넌트 렌더링 : 주방에서 요리를 준비. 3.DOM에 변경사항을 커밋 : 테이블에 전달.
=> 위의 과정이 이루어질 수 있도록 리액트는 중간에서 웨이터와 같이 요청, 준비, 전달의 역할을 한다고 할 수 있다.

📌1단계: 렌더링 트리거

컴포넌트 렌더링이 일어나는 데에는 두 가지 이유가 있다.

  • 컴포넌트의 초기 렌더링인 경우
  • 컴포넌트의 state가 업데이트된 경우

✅초기 렌더링

컴포넌트가 처음으로 화면에 나타날 때, 리액트는 컴포넌트 트리를 바탕으로 가상 DOM을 생성하고, 이를 실제 DOM에 반영하여 화면에 처음으로 렌더링하는 과정.

✅State 업데이트 시 리렌더링

컴포넌트의 state가 변경될 때마다, 리액트는 해당 컴포넌트를 다시 렌더링하여 변경된 내용을 가상 DOM에 반영하고, 변경된 부분만 실제 DOM에 업데이트하는 과정.

📌2단계: React 컴포넌트 렌더링

렌더링을 트리거한 후 React는 컴포넌트를 호출하여 화면에 표시할 내용을 파악한다. “렌더링”은 React에서 컴포넌트를 호출하는 것이다.

  • 초기 렌더링에서 React는 루트 컴포넌트를 호출합니다.
  • 이후 렌더링에서 React는 state 업데이트가 일어나 렌더링을 트리거한 컴포넌트를 호출한다.

React는 기본적으로 부모 컴포넌트가 렌더링되면, 그 안에 있는 모든 자식 컴포넌트를 재귀적으로 렌더링한다. => 일반적으로 컴포넌트가 렌더링되면 그 안에 있는 모든 컴포넌트 역시 렌더링 된다.

또 주목해야할 점은 일반적인 렌더링 과정에서, React는 "Props가 변경되었는지 여부"는 신경쓰지 않는다. 그저 부모 컴포넌트가 렌더링되었기 때문에 자식 컴포넌트도 무조건 렌더링하는 것이다.

✅예시코드

export default function Clock({ time }) {
  return (
    <>
      <h1>{time}</h1>
      <input />
    </>
  );
}

위의 코드와 같이 time이 +1초씩 증가하는 코드가 있다고 생각해보자. 우리들은 보통 props로 전달받은 time이 변경되기 때문에 자식 컴포넌트인 Clock 컴포넌트에서 렌더링 된다고 생각할 수 있지만, 정확하게 말하면 부모 컴포넌트에서 time 값이 변경되면서 부모가 다시 렌더링되고, 그로 인해 자식 컴포넌트인 Clock 컴포넌트도 다시 렌더링된다는 관점으로 해석해야 적절하다는 것이다.

🧐왜 그렇게 동작할까?

리액트는 컴포넌트 계층을 리렌더링하는 기본 동작을 통해 데이터 흐름을 유지하고 단방향 데이터 바인딩을 보장할 수 있다. 부모가 업데이트되면 자식들도 자동으로 갱신되므로, 데이터가 의도치 않게 오래된 상태로 남아 있는 것을 방지할 수 있다.

리액트는 컴포넌트를 언제 렌더링할지 매우 효율적으로 처리하지만, "부모 렌더링 → 자식 렌더링"의 단순 규칙을 따르는 것이 최적화 측면에서 더 낫다고 판단하였다.

🧐그럼 props 변경 여부와는 전혀 관련이 없는건가?

리액트의 기본 렌더링 방식에서는 자식 컴포넌트가 props가 변경되지 않아도 렌더링되는 것이 맞지만, 특정 경우에 props 변경 여부를 기반으로 최적화를 할 수 있다.

  • memo를 사용하여, 부모가 렌더링되더라도 props가 이전과 동일한 경우 자식 컴포넌트의 렌더링을 방지하는 방식으로 최적화 가능

📌3단계: React가 DOM에 변경사항을 커밋

컴포넌트를 렌더링(호출)한 후 React는 DOM을 수정한다.

  • 초기 렌더링의 경우 React는 appendChild() DOM API를 사용하여 생성한 모든 DOM 노드를 화면에 표시한다.
  • 리렌더링의 경우 React는 필요한 최소한의 작업(렌더링하는 동안 계산된 것)을 적용하여 DOM이 최신 렌더링 출력과 일치하도록 한다.

React는 렌더링 간에 차이가 있는 경우에만 DOM 노드를 변경한다

✅예시코드

export default function Clock({ time }) {
  return (
    <>
      <h1>{time}</h1>
      <input />
    </>
  );
}

위의 예시와 동일한 코드를 가지고 왔다. React가 <h1>의 내용만 새로운 time으로 업데이트하기 때문에 <input>이 JSX에서 이전과 같은 위치로 확인되므로 React는 <input> 또는 value를 건드리지 않는다. => <input>에 텍스트를 입력하여 value를 업데이트 하지만 컴포넌트가 리렌더링될 때 텍스트가 사라지지 않는다.

📌리액트 동작원리

✅Trigger

모든 작업의 시작 Trigger부터 시작하게 된다. 초기 Mount든 State Hook으로 인한 Re-render든 동일하게, 앱의 어느 부분을 렌더링해야 하는지(scheduleUpdateOnFiber()), 어떻게 렌더링해야 하는지 리액트 런타임에 알려준다.

이 단계는 “태스크를 생성”하는 단계로, ensureRootIsScheduled()는 이러한 태스크 생성의 마지막 단계이고, 이후 scheduleCallback()에 의해 태스크가 Scheduler로 전송됩니다.

✅Schedule

React Scheduler로, 기본적으로 우선순위에 따라 작업을 처리하는 우선순위 큐이다. 런타임 코드에서 scheduleCallback()을 호출하여 렌더링이나 Effects 실행과 같은 작업을 예약한다. 스케줄러 내부의 workLoop()는 작업이 실제로 실행되는 방식이다.

✅Render

Render는 예약된 작업(performConcurrentWorkOnRoot())으로, 새로운 Fiber Tree를 계산하고, Host DOM에 적용하기 위해 필요한 업데이트를 파악하는 것을 의미한다.

🧐Fiber Tree가 뭐야?

Fiber Tree는 리액트가 효율적인 렌더링을 관리하기 위해 사용하는 새로운 재조정(Reconciliation) 알고리즘과 그 구조를 나타내는 트리이다.

Fiber Tree는 기본적으로 앱의 현재 상태를 나타내는 내부 트리와 같은 구조이므로 여기서 자세히 알 필요는 없습니다. 이전에는 Virtual DOM이라고 불렸지만, 지금은 DOM에만 사용되는 것이 아니며 React 팀에서는 더 이상 Virtual DOM이라고 부르지 않는다고 한다.

따라서 performConcurrentWorkOnRoot()Trigger 단계에서 생성되고 Scheduler에서 우선순위를 지정한 다음, Render 단계에서 실행된다. 마치 작은 사람이 Fiber Tree를 돌아다니며 다시 렌더링해야 하는지 확인하고 Host DOM에서 필요한 업데이트를 알아내는 것처럼 생각하면 된다.

Concurrent mode로 인해 Render 단계가 중단되었다가 다시 시작될 수 있으므로 상당히 복잡한 단계이기도 하다.

  1. ReactDOMRoot.render() → 우리가 작성하는 사용자 측 코드이며, 먼저 createRoot()를 호출한 다음 render().
  2. scheduleUpdateOnFiber() → 초기 마운트에는 이전 버전이 없으므로 루트에서 호출되며, React에게 렌더링할 위치를 알려준다.
  3. ensureRootIsScheduled() → 이 호출은 performConcurrentWorkOnRoot()가 스케줄되도록 ‘ensure’하는 것.
  4. scheduleCallback()React Scheduler의 일부인 실제 스케줄링은 스크린샷에서 postMessage()에 의해 비동기화되는 것을 확인할 수 있습니다.
  5. workLoop()React Scheduler가 작업을 처리하는 방식이다.
  6. performConcurrentWorkOnRoot() → 스케줄 작업이 실행되고 있으며, 여기서 컴포넌트가 실제로 렌더링된다.

✅Commit

commitRoot() → 이전 렌더링 단계에서 파생된, 필요한 DOM 업데이트를 커밋하며, 이펙트 처리와 같은 더 많은 작업을 수행하기도 한다.
commitMutationEffects()Host DOM의 실제 업데이트.
물론 DOM을 조작하는 것(commitMutationEffects()) 이상의 작업이 존재하기도 한다. 예를 들어, 모든 종류의 Effects도 여기서 처리된다. (flushPassiveEffects(), commitLayoutEffects())

📌Fiber

Fiber는 React의 새로운 재조정(reconciliation) 엔진으로, React 16에서 도입되었다. Fiber는 UI 업데이트를 더 효율적으로 처리하고, 애플리케이션의 반응성을 높이기 위해 만들어졌다. 이전 React의 재조정 알고리즘은 "stack reconciler"라고 불리며, 한 번에 전체 트리를 탐색하는 방식으로 작동했다. 이 방식은 대규모 애플리케이션에서 렌더링 성능 문제를 야기할 수 있었기때문에 Fiber가 등장하게 되었다.

✅Fiber의 주요 특징

1. 우선순위 기반 작업 처리:

Fiber는 작업의 우선순위를 설정할 수 있어, 중요한 작업이 우선적으로 처리되고 덜 중요한 작업은 나중에 처리될 수 있도록 한다. 이를 통해 사용자 상호작용을 차단하지 않고 애플리케이션이 부드럽게 작동할 수 있게 한다.

2. 비동기 렌더링:

Fiber는 렌더링 작업을 작은 청크로 나누어 비동기적으로 실행한다. 이를 통해 React는 작업을 중단하고 다른 중요한 작업(예: 사용자 입력)에 반응한 후 다시 작업을 이어갈 수 있다. 이 접근 방식을 통해 메인 스레드의 차단을 방지하고 렌더링 중에도 애플리케이션이 반응할 수 있도록 한다.

3. 백그라운드 작업 처리:

Fiber는 백그라운드에서 느린 작업을 처리할 수 있어, 화면에 즉시 나타나지 않아도 되는 업데이트는 성능에 영향을 덜 미친다.

4. 동시성 모드:

Fiber는 React의 동시성 기능을 지원하며, 이를 통해 React 18의 Concurrent ModeSuspense 등의 기능이 가능해졌다. 이 모드는 애플리케이션의 반응성을 높이고 사용자 경험을 개선하는 데 중요한 역할을 한다.

5. Stack Reconciler 대비 더 세밀한 작업 제어:

Fiber는 각 작업의 상태를 기억하고, 트리의 다른 부분을 탐색해야 할 때도 작업을 이어나갈 수 있다. 이를 통해 애플리케이션은 더욱 유연하게 업데이트를 제어할 수 있다.

✅Fiber의 작동 방식

Fiber는 재조정 과정에서 수행해야 하는 모든 작업을 청크로 나누고, 이 청크들을 하나씩 실행하는 것이다. 브라우저의 요청에 따라 일정 시간 동안 청크들을 실행하고, 시간이 초과되면 잠시 중단하고 다른 작업을 처리한 후 다시 청크 실행을 이어가게 된다.

profile
공유할 때 행복을 느끼는 프론트엔드 개발자

3개의 댓글

comment-user-thumbnail
2024년 11월 6일

안녕하세요! 유서연입니다.
리액트 렌더링의 원리가 자세히 설명되어 있어 이해하기 편했습니다!
여러 개념을 동일한 예시 코드로 설명해주셔서 더 좋았던 거 같아요.
건휘님 아티클 덕분에 렌더링 할 때 리액트가 Props의 변경 여부는 신경쓰지 않고,
부모 컴포넌트가 렌더링되면 자식 컴포넌트도 무조건 렌더링 된다는 사실을 새롭게 알아가네요…
리액트 어렵다…
아티클 작성하시느라 수고하셨습니다:)

답글 달기
comment-user-thumbnail
2024년 11월 6일

작성해주신 아티클 잘 읽었습니다! 간단한 코드 예시를 같이 작성해주셔서 훨씬 이해하기가 편했던 것 같습니다. 특히 props관련 내용에서 단순히 이전까지는 props를 넘겨주고 그 값이 바뀌니 리렌더링 된다고 생각을 했었는데 그게 아니라 props가 변하게 되면 결국 부모 컴포넌트가 리렌더링 되고, 그로 인해서 자식 컴포넌트도 리렌더링 된다는 관점이 맞다는 것을 처음 알게 되었습니다!

어떻게 보면 비슷한 내용 같지만 관점을 다르게 봐야 더 확실한 이해를 할 수 있다는 것을 알 수 있었습니다..! 이 내용을 보면서 그렇다면 setState(setter) 함수를 props로 넘기는 것도 자식에서 set함수로 state를 변경하면 부모의 state 값이 변경되면서 스냅샷, 리렌더링이 필요하고 이로 인해서 자식 컴포넌도 다시 재렌더링이 되어야 한다고 보면 되는 것인지 궁금하네요!

그리고 리액트 자체의 동작 원리에 대해서 자세히 과정을 알려주셔서 이해하기 편했던 것 같습니다! 수고하셨습니다 :)

1개의 답글