브라우저는 화면을 그리기 위해서 DOM(Document Object Model)이라는 개념을 사용합니다. DOM은 HTML 파일 내용을 토대로 만들어지는데, JavaScript와 같은 스크립팅 언어로 수정할 수 있도록 만들어진, 웹 페이지의 객체 지향 표현입니다. DOM은 브라우저가 화면을 그리기 위해서 필요한 정보가 트리 형태로 저장된 데이터입니다.
React에서 사용하는 가상 DOM도 실제 DOM 내용에 기반하여 만들어집니다. 그럼 가상 DOM이 왜 존재하나요? 왜냐하면 실제 DOM에는 브라우저가 화면을 그리는데 필요한 모든 정보가 들어있어 실제 DOM을 조작하는 작업은 무겁기 때문입니다. 그래서 React는 실제 DOM의 변경 사항을 빠르게 파악하고 반영하기 위해 내부적으로 가상 DOM을 만들어서 관리합니다. 가상 DOM은 일종의 DOM의 메타데이터, DOM의 요약본이라고 볼 수 있습니다.
React는 성능 향상을 위해 실제 렌더링된 UI를 내부적으로 JavaScript 객체로 따로 관리한다. 왜냐하면 DOM 노드를 생성하거나 기존 DOM 노드에 접근하는 것이 JavaScript 객체로 표현된 트리 노드를 생성하거나 접근하는 거에 비해 느리기 때문이다. 즉, JavaScript 객체로 표현된 트리에 CRUD 작업을 수행하는 것이 DOM 노드에 CRUD 작업을 수행하는 것보다 일반적으로 더 빠르다. (대신 메모리 사용량이 늘어난다는 단점은 있다.)
기존에는 화면을 다시 그릴 때마다 jQuery나 document.getElementById, document.querySelector
등을 통해 DOM 노드를 검색하고 수정하거나 특정 위치에 노드를 추가-삭제했는데, 이렇게 DOM 노드에 CRUD 작업을 수행하는 것은 비싼 작업이기 때문에 Virtual DOM이라는 개념이 등장했다. Virtual DOM은 일종의 DOM 캐싱, DOM 버퍼링이라고 볼 수 있다.
각 컴포넌트가 반환하는 엘리먼트를 이전에 반환했던 엘리먼트와 비교하고(Reconcilation), 다른 경우에만 해당하는 DOM 노드에 CRUD 작업을 수행한다.
기본적으로 Virtual DOM은 실제 DOM과 동일한 상태를 가지고 있고 표현하는 형식만 다를 뿐이다.
setState
호출 등의 이유로 컴포넌트 상태가 변하면 해당 컴포넌트의 shouldComponentUpdate
함수를 실행한다. 그리고 이 함수가 true
를 반환하면 render
함수를 실행한다.
shouldComponentUpdate
함수가false
를 반환하면 이 컴포넌트의render
함수를 실행하지 않고, 자식 컴포넌트의shouldComponentUpdate, render
함수도 실행하지 않는다.
상태가 변한 컴포넌트를 루트 노드로 해서 깊이 우선 탐색 방식으로 각 자식 컴포넌트의 shouldComponentUpdate
함수와 render
함수를 실행한다.
이렇게 render
함수를 실행하여 얻은 새로운 Virtual DOM을 실제 DOM과 동기화되어 있는 기존 Virtual DOM과 비교해서 변경 사항을 파악한다(reconcilation). 그리고 실제로 변경된 부분만 DOM API를 호출하여 DOM에 반영하면, 브라우저가 변경 사항이 반영된 DOM과 CSSOM으로 새로운 Render Tree를 생성해서 화면을 다시 그린다.
React는 내부적으로 필요한 경우에만 실제 DOM 노드에 접근해 CRUD 작업을 수행하지만, 이런 과정을 shouldComponentUpdate
컴포넌트 생명주기 함수를 재정의해서 커스터마이징 할 수 있다.
shouldComponentUpdate
함수는 기본적으로 true
를 반환하고 컴포넌트의 render
함수를 호출하기 이전에 호출된다. 만약 shouldComponentUpdate
함수가 false
를 반환하면 해당 컴포넌트와 그의 자식 컴포넌트의 render
함수가 실행되지 않는다. 그리고 렌더링 이후에 일어나는 componentDidUpdate
나 useEffect
도 실행되지 않는다.
함수 컴포넌트의 경우
React.memo
를 사용해서 순수 컴포넌트와 같이 컴포넌트 렌더링을 방지하는 효과를 얻을 수 있다. 함수 컴포넌트에서 렌더링이란 함수 컴포넌트 자체를 실행하는 것을 의미한다.
React.PureComponent
(순수 컴포넌트)는 shouldComponentUpdate
함수가 props를 얕게 비교하도록 재정의한다. 순수 컴포넌트를 사용하면 props가 변한 경우에만 컴포넌트의 render 함수를 실행하기 때문에 렌더링 시간을 줄일 수 있다.
React에선 shouldComponentUpdate
를 직접 재정의하는 것보다 순수 컴포넌트를 사용할 것을 권장하고 있다. 왜냐하면 어떤 컴포넌트의 내부 상태가 변경되거나 구독하는 context의 value가 변경되면, 해당 컴포넌트와 그의 모든 자식 컴포넌트의 shouldComponentUpdate
가 실행되기 때문이다. 그래서 이 함수 안에서 무거운 작업을 수행하면 렌더링 시간이 길어질 수 있다. #React
엄밀히 말하면 중간에 특정 컴포넌트의
shouldComponentUpdate
가false
를 반환하면 해당 컴포넌트와 그의 자식 컴포넌트의shouldComponentUpdate
는 실행되지 않는다
만약 직접 재정의해도 shouldComponentUpdate
에서 props를 깊게 비교하는 것은 비효율적이고 성능도 낮아질 수 있다며 깊게 비교하지 않는 것을 권장하고 있다.
하지만 매 렌더링이 이뤄질 때마다 매번 props가 변하는 컴포넌트는 순수 컴포넌트로 지정해도 별 의미가 없다. 오히려 shouldComponentUpdate
의 얕은 복사 로직이 불필요하게 실행되기 때문에 렌더링 시간이 늘어날 수 있다. 따라서 아래와 같이 적절한 상황에서 사용하는 것이 좋다.
그리고 특정 컴포넌트의 useState의 setState가 호출되어 컴포넌트 상태가 변하거나 구독 중인 context의 value prop이 변경되면, shouldComponentUpdate
함수 실행 없이 무조건 렌더링이 이뤄진다.
함수 컴포넌트의 경우
React.memo
의 2번째 매개변수가 실행되지 않고 무조건 렌더링이 이뤄진다.
React 공식 문서 - Optimizing Performance
React 공식 문서 - React Top-Level API
React 공식 문서 - Reconciliation
버추얼돔의 사용목적은 기존 DOM 조작이 무겁기 때문이 아니라 DOM요소의 재조정 여부의 판단과정을 자동화해서 생산성을 높이기 위함이 아닌가요?