본문 바로가기

크래프톤 정글/WIL

정글[Week_04]: React가 렌더링 비용을 획기적으로 절약한 방법

렌더링이란 무엇인가?

보통 렌더링(Rendering)이라 하면 코드와 같은 컴퓨터 내부의 데이터를 사람이 볼 수 있도록 가시적인 이미지, 영상 등으로 변환하는 작업을 말한다. 이 렌더링이라는 단어는 다양한 곳에서 사용되고 있는데, 웹 분야에서도 단어의 의미에 맞게, HTML/CSS 등 코드들을 읽어 우리가 볼 수 있는 웹 페이지로 바꾸는 작업을 의미한다.

렌더링 과정 경로

렌더링 과정은 위의 이미지를 참고하면 좋다. 모두 당장 정확히 이해하기 보다는, HTML, CSS 등 문서를 DOM으로 만들고, 이를 몇몇 과정을 거쳐 최종적으로 화면에 그린다(paint). 문제는, 이 과정을 거치는데 상당한 비용을 소비한다는 점이다. 
초창기 SSR 렌더링 방식은, 매 서버에다가 요청할 때마다 페이지 전채를 새로 그려야 했다(리렌더링). 하지만, 언제나 길은 있는 법. 개발자들은 페이지에서 새롭게 추가/삭제해야할 일부 요소만 렌더링을 하는 방식을 개발해 냈는데, 이를 쉽게 말해 부분 업데이트라고 생각하면 좋겠다.

 

 

렌더링의 구조의 핵심, DOM

그렇다면, 위 문단에서 강조된 DOM은 무엇일까? 사실 React을 이해하기에 앞서 이 Dom에 대해 선행되어야 한다. 웹 브라우저가 HTML을 단순 텍스트로서 해석하는 것이 아닌, Javascript로도 동시에 조작할 수 있는 트리 구조로 만든 것이 DOM이다. 트리 구조란, 부모-자식 관계의 위계를 뜬 화면 설계도라 생각하면 좋다.

HTML과 DOM 비교

위계를 띄는 이유는 HTML의 구조와 DOM의 트리 구조의 비교를 통해 알 수 있다. 들여쓰기 된 라인은 위에 종속된 부모-자식 관계이고, 상위의 객체를 변경을 하면 아래 하부 전체에 영향을 주는 구조이다. 물론 DOM에는 단순히 HTML 정보만 담고 있지 않다. 각 객체의 id, class, CSS에서 불러오는 style 등 화면을 그리는데 필요한 핵심 정보를 담고 있다.

 

 

DOM을 조작하여 페이지 렌더링 방식을 결정한다

페이지 전체를 새로고침 하지 않고 부분 업데이트 하는 기법은 React가 등장하기 이전에도 존재하였다. 이른바 AJAX(비동기적 정보 교환 기법)이라 하며, DOM의 특정 객체를 Javascript를 통해 조작하는 방식이다. 문제는.. 과거에는 이런 DOM을 직접 조작해야 하는 일이 빈번했다는 것이다. 게다가 2000년대 초반까지만 해도, DOM을 해석하는 방식이 브라우저마다 달라서, 지금보다 고 난이도 작업이었다. 무엇보다 실제 DOM은 매우 무겁다.

React는 무거운 DOM을 대신하여 가상 돔(Virtual DOM)이라는 개념을 만들어냈다. 가상 돔은 말 그대로, 실제 돔과 다르게 React 상에서 만들어낸 별개의 돔 객체이다. 생성은 웹 페이지에서 상태 변화가 일어났을 경우, JS 메모리 내 새로운 가상 돔 트리를 만드는 방식이다.

실제 돔 가상 돔
위치 계산 함수를 비롯해 수백 개의 무거운 속성을 가짐 태그 명, 속성, 자식 노드 등 간략한 정보만 포함
매우 무거우며 브라우저의 메모리 독차지의 원인  일반 JS 객체로 가벼움
수정할 때마다 레이아웃을 재계산 해야 함 메모리상 객체의 값만 변동이 일어남

가상 돔이 실제 돔에 적용되는 방식

이렇게 만든 가상 돔은, 상태 변화(카드 추가, 서버 응답 등)이 발생하여 새로운 화면을 만들기 전, 새 화면에 맞는 가상 돔을 만든다. 새 가상 돔, 그리고 이전 legacy 가상 돔과 비교(Diffing)를 하면서, 수정할 객체를 탐색하는데, 여기서는 Diff 알고리즘을 통해서 이루어진다. 이 과정은 순수 JS 객체끼리 비교하는 것이기 때문에 가볍고 빠르다.

비교가 끝나면, 바꿔야 할 부위만 딱 꼬집어서 실제 돔에게 수정 명령을 가한다. 이 명령을 패치(Patch), 혹은 커밋(Commit)이라고 한다.

 

 

Diff 알고리즘을 효율적으로 사용하는 React

천 개의 노드가 있는 돔 트리 두 개를 일일이 비교를 하려면, 이론 상 O(n^3), 즉 최대 10억번의 연산이 필요하다. 새 화면을 부분적으로 그리기 위한 비교(Diff) 알고리즘이 비대해지만, 본래 시간/자원 절약의 목표와는 멀어지는 배 보다 배꼽이 커지는 문제가 발생할 수 있게 된다. 그러나 React는 이 부분도 매우 현명한 방식으로 극복해 냈다.

이른바 휴리스틱(Heuristic) 알고리즘, 최적해가 될 가능성이 없는 경우의 수는 미리 가지를 잘라내어 탐색 비용을 O(n)으로 줄였다고 한다. 경험적 추측을 기반으로 하여 잠재 요소를 식별한다고 봐도 좋겠다. React가 사용하는 휴리스틱 로직은 대략 다음과 같다:

- 컴포넌트 종류 우선 비교: 예시로, <div>에서 <span>으로 바뀌었다고 하자. 타입이 바뀌었는데 내부가 동일할 확률은 매우 낮으니, 일일이 비교하지 않는다.
- 키 기반 탐색: index를 기반으로 List를 탐색할 시, 삽입/삭제로 뒤에 있는 모든 인덱스가 뒤로 밀려나 전부 확인해야 하는 수고로움을 덜 수 있다.
- BFS 탐색: 트리 기반 자료구조인 만큼, level 단윌 탐색한다. 부모 노드를 기반으로 자식 노드를 추정을 하는 방식이다.

 

이걸로 이번 글을 마치고자 한다.

그럼 끝!