React Virtual DOM 이해하기

gwak2837·2021년 1월 16일
13

React 이해하기

목록 보기
1/4

정의

브라우저는 화면을 그리기 위해서 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과 동일한 상태를 가지고 있고 표현하는 형식만 다를 뿐이다.

  1. 특정 컴포넌트에서 setState 호출 등의 이유로 컴포넌트 상태가 변하면 해당 컴포넌트의 shouldComponentUpdate 함수를 실행한다. 그리고 이 함수가 true를 반환하면 render 함수를 실행한다.

shouldComponentUpdate 함수가 false를 반환하면 이 컴포넌트의 render 함수를 실행하지 않고, 자식 컴포넌트의 shouldComponentUpdate, render 함수도 실행하지 않는다.

  1. 상태가 변한 컴포넌트를 루트 노드로 해서 깊이 우선 탐색 방식으로 각 자식 컴포넌트의 shouldComponentUpdate 함수와 render 함수를 실행한다.

  2. 이렇게 render 함수를 실행하여 얻은 새로운 Virtual DOM을 실제 DOM과 동기화되어 있는 기존 Virtual DOM과 비교해서 변경 사항을 파악한다(reconcilation). 그리고 실제로 변경된 부분만 DOM API를 호출하여 DOM에 반영하면, 브라우저가 변경 사항이 반영된 DOM과 CSSOM으로 새로운 Render Tree를 생성해서 화면을 다시 그린다.

응용

React는 내부적으로 필요한 경우에만 실제 DOM 노드에 접근해 CRUD 작업을 수행하지만, 이런 과정을 shouldComponentUpdate 컴포넌트 생명주기 함수를 재정의해서 커스터마이징 할 수 있다.

shouldComponentUpdate 함수는 기본적으로 true를 반환하고 컴포넌트의 render 함수를 호출하기 이전에 호출된다. 만약 shouldComponentUpdate 함수가 false를 반환하면 해당 컴포넌트와 그의 자식 컴포넌트의 render 함수가 실행되지 않는다. 그리고 렌더링 이후에 일어나는 componentDidUpdateuseEffect도 실행되지 않는다.

함수 컴포넌트의 경우 React.memo를 사용해서 순수 컴포넌트와 같이 컴포넌트 렌더링을 방지하는 효과를 얻을 수 있다. 함수 컴포넌트에서 렌더링이란 함수 컴포넌트 자체를 실행하는 것을 의미한다.

예시

React.PureComponent(순수 컴포넌트)는 shouldComponentUpdate 함수가 props를 얕게 비교하도록 재정의한다. 순수 컴포넌트를 사용하면 props가 변한 경우에만 컴포넌트의 render 함수를 실행하기 때문에 렌더링 시간을 줄일 수 있다.

React에선 shouldComponentUpdate를 직접 재정의하는 것보다 순수 컴포넌트를 사용할 것을 권장하고 있다. 왜냐하면 어떤 컴포넌트의 내부 상태가 변경되거나 구독하는 context의 value가 변경되면, 해당 컴포넌트와 그의 모든 자식 컴포넌트의 shouldComponentUpdate가 실행되기 때문이다. 그래서 이 함수 안에서 무거운 작업을 수행하면 렌더링 시간이 길어질 수 있다. #React

엄밀히 말하면 중간에 특정 컴포넌트의 shouldComponentUpdatefalse를 반환하면 해당 컴포넌트와 그의 자식 컴포넌트의 shouldComponentUpdate는 실행되지 않는다

만약 직접 재정의해도 shouldComponentUpdate에서 props를 깊게 비교하는 것은 비효율적이고 성능도 낮아질 수 있다며 깊게 비교하지 않는 것을 권장하고 있다.

유의점

하지만 매 렌더링이 이뤄질 때마다 매번 props가 변하는 컴포넌트는 순수 컴포넌트로 지정해도 별 의미가 없다. 오히려 shouldComponentUpdate의 얕은 복사 로직이 불필요하게 실행되기 때문에 렌더링 시간이 늘어날 수 있다. 따라서 아래와 같이 적절한 상황에서 사용하는 것이 좋다.

  • 매 렌더링 시 props가 매번 변경되는 컴포넌트 -> (일반) 컴포넌트
  • 매 렌더링 시 props가 거의 변하지 않는 컴포넌트 -> 순수 컴포넌트

그리고 특정 컴포넌트의 useState의 setState가 호출되어 컴포넌트 상태가 변하거나 구독 중인 context의 value prop이 변경되면, shouldComponentUpdate 함수 실행 없이 무조건 렌더링이 이뤄진다.

함수 컴포넌트의 경우 React.memo의 2번째 매개변수가 실행되지 않고 무조건 렌더링이 이뤄진다.

참고

React 공식 문서 - Optimizing Performance
React 공식 문서 - React Top-Level API
React 공식 문서 - Reconciliation

도움

@samiehomie

profile
이유와 방법을 알려주는 메모장 겸 블로그. 블로그 내용에 대한 토의나 질문은 언제나 환영합니다.

5개의 댓글

comment-user-thumbnail
2021년 11월 13일

버추얼돔의 사용목적은 기존 DOM 조작이 무겁기 때문이 아니라 DOM요소의 재조정 여부의 판단과정을 자동화해서 생산성을 높이기 위함이 아닌가요?

2개의 답글
comment-user-thumbnail
2일 전

감사합니다! 이해에 많은 도움 되었어요

1개의 답글