우리는 리액트를 이용해 웹을 개발하며 렌더링, 리렌더링이라는 표현을 굉장히 자주 사용합니다. 하지만 리액트 컴포넌트의 렌더링과 브라우저의 렌더링은 분명히 서로 다릅니다. 이 글은 그 차이에 대해서 설명합니다.
브라우저에서의 렌더링 과정을 설명해주세요.
위 질문은 아마 프론트엔드에서 기본적인 면접 질문 중 하나일 것입니다. 아마 많은 분들이 이미 알고 계시듯이, 브라우저에서의 렌더링 과정은 다음과 같습니다.
브라우저 렌더링 과정은 CRP(Critical Redering Path)라는 일련의 과정을 통해 진행됩니다. 그 과정을 요약하면 다음과 같습니다.
HTML 파싱을 통한 DOM 생성
CSS 파싱을 통한 CSSOM 생성
DOM 트리와 CSSOM 트리를 결합해 렌더 트리 생성
layout 과정 실행
paint 및 composite 과정 실행
위에서 살펴본 브라우저의 렌더링과 리액트에서의 렌더링은 그 의미와 사용되는 상황이 사뭇 다릅니다.
일반적으로 우리가 "렌더링" 이라는 단어를 생각할 때, 무엇인가를 화면에 그리는 작업을 떠올릴 수 있지만, React에서의 렌더링은 오히려 화면에 무엇인가를 그리기 전 어떤 과정을 시작하는 듯한 느낌에 가깝습니다.
💡 React에서의 렌더링이란
=> 컴포넌트가 state와 props를 기반으로 어떻게 UI를 구성하고 이를 바탕으로 어떤 dom 결과를 브라우저에게 제공할 것인지를 계산하는 일련의 과정을 말합니다.
React에서의 렌더링은 DOM 업데이트를 의미하지 않습니다. 즉, React에서의 렌더링은 실질적 화면 업데이트를 말하는 것이 아닙니다.
React가 UI를 어떻게 구성할 지 컴포넌트에 요청하는 과정, 즉 React에서의 렌더링 과정은 총 3단계로 나눌 수 있습니다.
React가 데이터가 변경되었는지를 확인하는 과정입니다. 컴포넌트의 state가 변경되었다면 렌더링이 Trigger됩니다. 이는 Object.is()
메서드를 통해 state 값이 불변성을 지키고 있는지를 확인하는 과정입니다.
렌더링을 Trigger하는 경우는 다음과 같습니다.
state 변경
컴포넌트의 state가 변경될 때마다 해당 컴포넌트는 리렌더링됩니다. setState
함수를 호출하여 state를 변경하면, React는 새로운 state를 이용해 컴포넌트를 리렌더링합니다.
props 변경
컴포넌트가 받는 props가 변경되면 해당 컴포넌트는 리렌더링됩니다. 이것은 부모 컴포넌트로부터 props를 받는 모든 자식 컴포넌트에 적용됩니다.
부모 컴포넌트 리렌더링
부모 컴포넌트가 리렌더링되면 그의 모든 자식 컴포넌트들도 리렌더링 됩니다. 부모 컴포넌트의 state나 props가 변경되면 부모 컴포넌트는 리렌더링되며, 이에 따라 모든 자식 컴포넌트들도 리렌더링 됩니다.
Context 변경
React Context API를 사용하면 컴포넌트 트리 내에서 전역적으로 데이터를 공유할 수 있습니다. Context에 변경이 생기면 그를 구독하는 모든 컴포넌트는 리렌더링됩니다.
Force Update
forceUpdate
메서드를 사용하면 컴포넌트는 강제로 리렌더링 될 수 있습니다. 하지만 이 방법은 가능한 한 피해야 합니다. 대부분의 경우 state
나 props
의 변경으로 리렌더링을 처리할 수 있기 때문입니다.
크게 Reconciliation과 Batching 과정으로 나뉩니다.
렌더링이 Trigger되면, 화면에 표시될 새로운 가상 DOM을 만듭니다. 그리고 이전 가상 DOM과 새로운 가상 DOM을 비교해 달라진 부분을 찾아내 어떤 부분을 실제 DOM에 업데이트할 지 결정합니다.
이전 가상 DOM과 새로운 가상 DOM을 비교할 때, Diffing 알고리즘이 사용됩니다. Diffing 알고리즘은 아래와 같은 작업을 합니다.
💡 Diffing 알고리즘을 사용해 이전 가상 DOM과 새로운 가상 DOM을 비교하는 이유는 ?
=> 굳이 리렌더링 되지 않아도 되는 부분은 제외시키고 변경되어야 하는 부분만 변경하기 위해서입니다.
Reconciliation에서 상태 업데이트로 인해 실제 DOM에 업데이트할 작업들을 결정한 후 이를 다음 단계인 Commit 단계로 넘길 때, 한 번에 묶어서 넘깁니다.
즉, React는 렌더링을 최소한으로 발생시키기 위해 작업을 한 번에 묶어서 Batching(일괄 처리) 합니다.
💡 Batching을 통해 얻을 수 있는 이점은?
Commit 단계 이후, 브라우저의 렌더링 과정이 이루어집니다. 브라우저의 렌더링 과정 중 layout, painting 과정은 요소가 변경되거나 레이아웃이 변경되었을 때 다시 실행되는 과정인데 이는 비용이 비싼 작업입니다.
layout이 다시 발생하는 것을 reflow라고 부르는데 이는 paint가 다시 일어나는 과정인 repaint보다 비싼 작업입니다. 따라서 만약 React에서
setState
를 호출할 때마다 리렌더링이 일어나게 되면, repaint와 reflow가 계속해서 발생되고 이는 비효율적일 것입니다.따라서, 연속적인 상태의 변화가 있을 때 Batching을 통해 리렌더링이 한 번만 일어나도록 하는 것입니다.
Rendering에서 재조정된 가상 DOM을 실제 DOM에 적용하고 라이프사이클을 실행하는 단계입니다.
여기서도 DOM에 마운트 된다는 뜻이지 paint 된다는 뜻이 아닙니다. 이 단계는 항상 일관적 화면 업데이트를 위해 동기적으로 실행됩니다. 동기적으로 실행된다는 것은 콜 스택을 비우지 않고, DOM 조작을 Batching 처리 한다는 뜻입니다.
Commit 단계가 끝나면 그제서야 브라우저는 실제 DOM을 업데이트하며 화면을 그려줍니다. 즉, 이 과정이 바로 브라우저에서의 렌더링 과정입니다.
💡 해석에 따라 Commit 단계 이후에 브라우저 렌더링 과정이 일어난다고 보기도 하고, Commit 단계 내에 브라우저 렌더링 과정을 포함해서 보는 관점도 있는 것 같습니다.
💡 리액트의 렌더링이 일어난다고 해서 무조건 브라우저 렌더링(DOM 엡데이트)가 일어나는 것은 아닙니다.
- 렌더링을 수행했으나 Commit 단계까지 갈 필요가 없다면, 즉 변경 사항을 계산했는데 아무런 변경 사항이 감지되지 않는다면 이 Commit 단계는 생략될 수 있습니다.
- 즉 리액트의 렌더링은 꼭 가시적인 변경이 일어나지 않아도 발생할 수 있습니다. 렌더링 과정 중 Render 단계에서 변경 사항을 감지할 수 없다면 Commit 단계가 생략되어 브라우저의 DOM 업데이트가 일어나지 않을 수 있습니다.
즉, 이를 정리하면 다음과 같습니다.
- 리액트의 렌더링 과정에는 Trigger, Rendering, Commit 단계로 이루어져 있습니다.
- state가 변경되어 DOM 업데이트가 필요한 작업만 Batching 처리되어 Commit 단계로 전달됩니다.
- Commit 후에 브라우저 렌더링이 일어납니다.
잘 보고 갑니다!