리액트에서 렌더링(rendering)이란, 컴포넌트의 내용을 화면에 표시하거나 업데이트하는 과정을 의미한다.
리액트에서는 크게 아래의 3단계를 거쳐 렌더링 과정이 이루어진다.
1. 렌더링 트리거
2. 컴포넌트 렌더링
3. DOM에 커밋
리액트 공식문서에서는 리액트를 웨이터에 비유한다. 1.렌더링 트리거 (setState와 같은 상태 변경)는 손님의 주문을 주방으로 전달. 2. 컴포넌트 렌더링 : 주방에서 요리를 준비. 3.DOM에 변경사항을 커밋 : 테이블에 전달.
=> 위의 과정이 이루어질 수 있도록 리액트는 중간에서 웨이터와 같이 요청, 준비, 전달의 역할을 한다고 할 수 있다.
컴포넌트 렌더링이 일어나는 데에는 두 가지 이유가 있다.
컴포넌트가 처음으로 화면에 나타날 때, 리액트는 컴포넌트 트리를 바탕으로 가상 DOM을 생성하고, 이를 실제 DOM에 반영하여 화면에 처음으로 렌더링하는 과정.
컴포넌트의 state가 변경될 때마다, 리액트는 해당 컴포넌트를 다시 렌더링하여 변경된 내용을 가상 DOM에 반영하고, 변경된 부분만 실제 DOM에 업데이트하는 과정.
렌더링을 트리거한 후 React는 컴포넌트를 호출하여 화면에 표시할 내용을 파악한다. “렌더링”은 React에서 컴포넌트를 호출하는 것이다.
React는 기본적으로 부모 컴포넌트가 렌더링되면, 그 안에 있는 모든 자식 컴포넌트를 재귀적으로 렌더링한다. => 일반적으로 컴포넌트가 렌더링되면 그 안에 있는 모든 컴포넌트 역시 렌더링 된다.
또 주목해야할 점은 일반적인 렌더링 과정에서, React는 "Props가 변경되었는지 여부"는 신경쓰지 않는다. 그저 부모 컴포넌트가 렌더링되었기 때문에 자식 컴포넌트도 무조건 렌더링하는 것이다.
export default function Clock({ time }) {
return (
<>
<h1>{time}</h1>
<input />
</>
);
}
위의 코드와 같이 time이 +1초씩 증가하는 코드가 있다고 생각해보자. 우리들은 보통 props로 전달받은 time이 변경되기 때문에 자식 컴포넌트인 Clock 컴포넌트에서 렌더링 된다고 생각할 수 있지만, 정확하게 말하면 부모 컴포넌트에서 time 값이 변경되면서 부모가 다시 렌더링되고, 그로 인해 자식 컴포넌트인 Clock 컴포넌트도 다시 렌더링된다는 관점으로 해석해야 적절하다는 것이다.
리액트는 컴포넌트 계층을 리렌더링하는 기본 동작을 통해 데이터 흐름을 유지하고 단방향 데이터 바인딩을 보장할 수 있다. 부모가 업데이트되면 자식들도 자동으로 갱신되므로, 데이터가 의도치 않게 오래된 상태로 남아 있는 것을 방지할 수 있다.
리액트는 컴포넌트를 언제 렌더링할지 매우 효율적으로 처리하지만, "부모 렌더링 → 자식 렌더링"의 단순 규칙을 따르는 것이 최적화 측면에서 더 낫다고 판단하였다.
리액트의 기본 렌더링 방식에서는 자식 컴포넌트가 props가 변경되지 않아도 렌더링되는 것이 맞지만, 특정 경우에 props 변경 여부를 기반으로 최적화를 할 수 있다.
컴포넌트를 렌더링(호출)한 후 React는 DOM을 수정한다.
appendChild()
DOM API를 사용하여 생성한 모든 DOM 노드를 화면에 표시한다.export default function Clock({ time }) {
return (
<>
<h1>{time}</h1>
<input />
</>
);
}
위의 예시와 동일한 코드를 가지고 왔다. React가 <h1>
의 내용만 새로운 time으로 업데이트하기 때문에 <input>
이 JSX에서 이전과 같은 위치로 확인되므로 React는 <input>
또는 value를 건드리지 않는다. => <input>
에 텍스트를 입력하여 value를 업데이트 하지만 컴포넌트가 리렌더링될 때 텍스트가 사라지지 않는다.
모든 작업의 시작 Trigger
부터 시작하게 된다. 초기 Mount든 State Hook으로 인한 Re-render든 동일하게, 앱의 어느 부분을 렌더링해야 하는지(scheduleUpdateOnFiber()
), 어떻게 렌더링해야 하는지 리액트 런타임에 알려준다.
이 단계는 “태스크를 생성”하는 단계로, ensureRootIsScheduled()
는 이러한 태스크 생성의 마지막 단계이고, 이후 scheduleCallback()
에 의해 태스크가 Scheduler
로 전송됩니다.
React Scheduler
로, 기본적으로 우선순위에 따라 작업을 처리하는 우선순위 큐이다. 런타임 코드에서 scheduleCallback()
을 호출하여 렌더링이나 Effects 실행과 같은 작업을 예약한다. 스케줄러 내부의 workLoop()
는 작업이 실제로 실행되는 방식이다.
Render
는 예약된 작업(performConcurrentWorkOnRoot()
)으로, 새로운 Fiber Tree
를 계산하고, Host DOM에 적용하기 위해 필요한 업데이트를 파악하는 것을 의미한다.
Fiber Tree는 리액트가 효율적인 렌더링을 관리하기 위해 사용하는 새로운 재조정(Reconciliation) 알고리즘과 그 구조를 나타내는 트리이다.
Fiber Tree
는 기본적으로 앱의 현재 상태를 나타내는 내부 트리와 같은 구조이므로 여기서 자세히 알 필요는 없습니다. 이전에는 Virtual DOM
이라고 불렸지만, 지금은 DOM에만 사용되는 것이 아니며 React 팀에서는 더 이상 Virtual DOM
이라고 부르지 않는다고 한다.
따라서 performConcurrentWorkOnRoot()
는 Trigger
단계에서 생성되고 Scheduler
에서 우선순위를 지정한 다음, Render
단계에서 실행된다. 마치 작은 사람이 Fiber Tree
를 돌아다니며 다시 렌더링해야 하는지 확인하고 Host DOM
에서 필요한 업데이트를 알아내는 것처럼 생각하면 된다.
Concurrent mode
로 인해 Render
단계가 중단되었다가 다시 시작될 수 있으므로 상당히 복잡한 단계이기도 하다.
ReactDOMRoot.render()
→ 우리가 작성하는 사용자 측 코드이며, 먼저 createRoot()를 호출한 다음 render().scheduleUpdateOnFiber()
→ 초기 마운트에는 이전 버전이 없으므로 루트에서 호출되며, React에게 렌더링할 위치를 알려준다.ensureRootIsScheduled()
→ 이 호출은 performConcurrentWorkOnRoot()
가 스케줄되도록 ‘ensure’하는 것.scheduleCallback()
→ React Scheduler
의 일부인 실제 스케줄링은 스크린샷에서 postMessage()에 의해 비동기화되는 것을 확인할 수 있습니다.workLoop()
→ React Scheduler
가 작업을 처리하는 방식이다.performConcurrentWorkOnRoot()
→ 스케줄 작업이 실행되고 있으며, 여기서 컴포넌트가 실제로 렌더링된다.commitRoot()
→ 이전 렌더링 단계에서 파생된, 필요한 DOM 업데이트를 커밋하며, 이펙트 처리와 같은 더 많은 작업을 수행하기도 한다.
commitMutationEffects()
→ Host DOM
의 실제 업데이트.
물론 DOM을 조작하는 것(commitMutationEffects()
) 이상의 작업이 존재하기도 한다. 예를 들어, 모든 종류의 Effects
도 여기서 처리된다. (flushPassiveEffects()
, commitLayoutEffects()
)
Fiber
는 React의 새로운 재조정(reconciliation) 엔진으로, React 16에서 도입되었다. Fiber는 UI 업데이트를 더 효율적으로 처리하고, 애플리케이션의 반응성을 높이기 위해 만들어졌다. 이전 React의 재조정 알고리즘은"stack reconciler"
라고 불리며, 한 번에 전체 트리를 탐색하는 방식으로 작동했다. 이 방식은 대규모 애플리케이션에서 렌더링 성능 문제를 야기할 수 있었기때문에Fiber
가 등장하게 되었다.
1. 우선순위 기반 작업 처리:
Fiber
는 작업의 우선순위를 설정할 수 있어, 중요한 작업이 우선적으로 처리되고 덜 중요한 작업은 나중에 처리될 수 있도록 한다. 이를 통해 사용자 상호작용을 차단하지 않고 애플리케이션이 부드럽게 작동할 수 있게 한다.
2. 비동기 렌더링:
Fiber
는 렌더링 작업을 작은 청크로 나누어 비동기적으로 실행한다. 이를 통해 React는 작업을 중단하고 다른 중요한 작업(예: 사용자 입력)에 반응한 후 다시 작업을 이어갈 수 있다. 이 접근 방식을 통해 메인 스레드의 차단을 방지하고 렌더링 중에도 애플리케이션이 반응할 수 있도록 한다.
3. 백그라운드 작업 처리:
Fiber
는 백그라운드에서 느린 작업을 처리할 수 있어, 화면에 즉시 나타나지 않아도 되는 업데이트는 성능에 영향을 덜 미친다.
4. 동시성 모드:
Fiber
는 React의 동시성 기능을 지원하며, 이를 통해 React 18의 Concurrent Mode
나 Suspense
등의 기능이 가능해졌다. 이 모드는 애플리케이션의 반응성을 높이고 사용자 경험을 개선하는 데 중요한 역할을 한다.
5. Stack Reconciler 대비 더 세밀한 작업 제어:
Fiber
는 각 작업의 상태를 기억하고, 트리의 다른 부분을 탐색해야 할 때도 작업을 이어나갈 수 있다. 이를 통해 애플리케이션은 더욱 유연하게 업데이트를 제어할 수 있다.
Fiber
는 재조정 과정에서 수행해야 하는 모든 작업을 청크
로 나누고, 이 청크들을 하나씩 실행하는 것이다. 브라우저의 요청에 따라 일정 시간 동안 청크
들을 실행하고, 시간이 초과되면 잠시 중단하고 다른 작업을 처리한 후 다시 청크
실행을 이어가게 된다.
작성해주신 아티클 잘 읽었습니다! 간단한 코드 예시를 같이 작성해주셔서 훨씬 이해하기가 편했던 것 같습니다. 특히 props관련 내용에서 단순히 이전까지는 props를 넘겨주고 그 값이 바뀌니 리렌더링 된다고 생각을 했었는데 그게 아니라 props가 변하게 되면 결국 부모 컴포넌트가 리렌더링 되고, 그로 인해서 자식 컴포넌트도 리렌더링 된다는 관점이 맞다는 것을 처음 알게 되었습니다!
어떻게 보면 비슷한 내용 같지만 관점을 다르게 봐야 더 확실한 이해를 할 수 있다는 것을 알 수 있었습니다..! 이 내용을 보면서 그렇다면 setState(setter) 함수를 props로 넘기는 것도 자식에서 set함수로 state를 변경하면 부모의 state 값이 변경되면서 스냅샷, 리렌더링이 필요하고 이로 인해서 자식 컴포넌도 다시 재렌더링이 되어야 한다고 보면 되는 것인지 궁금하네요!
그리고 리액트 자체의 동작 원리에 대해서 자세히 과정을 알려주셔서 이해하기 편했던 것 같습니다! 수고하셨습니다 :)
안녕하세요! 유서연입니다.
리액트 렌더링의 원리가 자세히 설명되어 있어 이해하기 편했습니다!
여러 개념을 동일한 예시 코드로 설명해주셔서 더 좋았던 거 같아요.
건휘님 아티클 덕분에 렌더링 할 때 리액트가 Props의 변경 여부는 신경쓰지 않고,
부모 컴포넌트가 렌더링되면 자식 컴포넌트도 무조건 렌더링 된다는 사실을 새롭게 알아가네요…
리액트 어렵다…
아티클 작성하시느라 수고하셨습니다:)