cadenzah's hideOut

~ 정처없이 떠돌 수 없어 속이 베베 꼬인 영혼을 위로하며 ~



React.js의 가상 DOM이 주는 진정한 이점

이 글은 The Real Benefits of the Virtual DOM in React.js 을 번역한 것입니다. 의역이 있을 수 있습니다.

React.js에서 가상 DOM을 사용함에 따른 장점이 무엇인지에 관하여 혼란과 오해가 존재한다.

문서 객체 모델(이하 DOM)을 직접 갱신하는 것은 비효율적이고 느리다는 사실은 우리 모두가 들은 바 있다. 하지만, 이를 뒷받침하는 데이터는 대부분 갖고 있지 않다. React의 가상 DOM에 대한 흔한 이야기는 바로 웹 어플리케이션의 화면(View)를 갱신하는 보다 효율적인 방법이라는 것이지만, 정확하게 왜, 그리고 이러한 효율성이 빠른 페이지 렌더 시간을 만들어내는가에 대하여는 대부분 알지 못한다.

React를 사용함에 따른 다른 장점들, 단방향 데이터 바인딩과 컴포넌트 등은 제쳐두고, 나는 가상 DOM이 정확히 무엇인지, 그리고 가상 DOM으로 하여금 다른 UI 라이브러리(혹은 UI 라이브러리를 사용하지 않는 것)가 아닌 React를 사용하는 것을 정당화할 수 있는지에 대하여 논하도록 하겠다.

왜 UI 라이브러리가 필요한가?

반응형 프로그래밍의 중요한 2가지 발상은 바로 시스템이 이벤트 기반(Event-driven)이고 상태 변화에 대응하여야 한다는 것이다.

DOM의 사용자 인터페이스 컴포넌트는 내부 상태를 가지며, 브라우저를 갱신한다는 것은 무언가 변화할 때마다 DOM을 재생성하는 식의 단순한 것이 아니다. 만약 예를 들어 Gmail이 그런 식이라면, 새로운 메세지를 표시하기 위하여 브라우저 윈도우 전체가 새로고침될 것이고, 작성하고 있던 이메일을 모두 날려버리는 식으로 당신을 짜증나게 할 것이다.

DOM이 상태를 갖는다는 점은 키/값 관측(Ember에서 사용됨), Dirty checking(Angular에서 사용됨) 등의 사용자 인터페이스 라이브러리가 필요한 이유를 말해준다. UI 라이브러리는 데이터 모델에 변화가 있는지 감시하고, 변화가 발생하였을 때 DOM에서 올바른 부분을 갱신한다. 또는 반대로, DOM에 변화가 있는지 감시하고, 변화가 발생하였을 때 데이터 모델을 갱신한다.

이러한 형태의 감시-갱신은 양방향 바인딩이라고 하며, 이러한 방식은 종종 UI 작업을 복잡하고 혼란스럽게 만든다.

React는 무엇이 다른가?

React와 React의 가상 DOM은 개발자의 관점에서 볼 때, 반응형 자바스크립트를 만드는 다른 접근 방식에 비하여 훨신 간단하다. 당신은 React 컴포넌트를 갱신하는 순수 자바스크립트를 작성하고, React는 DOM을 갱신해준다. 데이터 바인딩은 어플리케이션과 복잡하게 얽혀있지 않다.

React는 단방향 바인딩을 사용하여 상황을 간단하게 만든다. 예를 들어 React UI 상의 입력 란에 무언가를 입력하면, 해당 컴포넌트의 상태는 직접적으로 바뀌지 않는다. 대신, 데이터 모델이 갱신되며, 이로 인하여 뒤이어서 UI가 갱신되고, 입력된 글자는 입력 란에 표시된다. (역자 주: 입력하는 행위와 입력한 글자가 입력 란에 표시되는 것을 구분하고 있습니다)

DOM은 느린가?

가상 DOM을 다루는 강연과 글들이 지적하길, 오늘날의 자바스크립트 엔진은 아주 빠르지만, 브라우저의 DOM에 대한 읽기/쓰기 작업은 느리다.

이것은 엄밀히 말하자면, 틀렸다. DOM은 빠르다. DOM 노드를 더하거나 제거하는 작업은 자바스크립트 객체에 속성값을 부여하는 것보다도 훨씬 적은 시간이 든다. 단순 작업이라는 것이다.

하지만 여기서 느린 부분은, DOM이 변화할 때마다 발생하는 브라우저의 배치(Layout) 작업이다. DOM이 변화할 때마다, 브라우저는 CSS를 다시 계산하고, 이를 기반으로 다시 배치하고, 화면을 다시 색칠해야 한다. 이 부분에서 시간이 걸린다.

브라우저 개발사들은 화면을 다시 색칠하는 데에 걸리는 시간을 줄이기 위하여 끊임없이 노력하고 있다. 여기에 가장 크게 기여할 수 있는 요인은 다시 그리는 작업이 필요한 DOM 변화를 최소화하고, 일괄 작업하는 것이다.

DOM 변화를 줄이고 일괄 작업하는 이러한 전략은 또다른 추상화 단계로 넘어갔고, 이것이 React의 가상 DOM에 깔린 발상이다.

가상 DOM은 어떻게 작동하는가?

실제 DOM과 같이, 가상 DOM은 요소, 특성, 컨텐츠들객체와 속성으로 표현한 노드 트리아다. React의 render() 메서드는 React 컴포넌트로부터 노드 트리를 만들고, 데이터 모델 내의 변화에 대응하여 이 트리를 갱신한다. 이 변화는 사용자의 액션에 의하여 발생한다.

React 어플리케이션에서 하부의 데이터가 변화할 때마다, UI에 대한 새로운 가상 DOM 표현이 생성된다.

이 부분이 아주 흥미로운 부분이다. React에서 브라우저의 DOM을 갱신하는 것은 3단계 과정으로 이루어진다.

  1. 무엇 하나라도 변화하면, 그 때마다 가상 DOM 표현의 모든 UI가 다시 렌더링된다.
  2. 바로 직전의 가상 DOM 표현새로운 가상 DOM 표현 간의 차이점이 계산된다.
  3. 실제 DOM은 실제 달라진 점만을 반영하여 갱신한다. 이는 패치를 적용하는 것과 아주 비슷하다.

가상 DOM은 느린가?

혹자는 가상 DOM을 매번 다시 렌더링하게 되면, 무언가 바뀐다는 사실 자체가 낭비로 이어질 가능성이 있다고 말한다. 두말 할 것 없이, React는 항상 메모리 상에 2개의 가상 DOM 트리를 유지해야 한다.

하지만, 가상 DOM을 다시 렌더링하는 것실제 브라우저 상의 DOM에 존재하는 UI를 렌더링하는 것보다 항상 훨씬 더 빠르다. 이것은 어떤 브라우저를 사용하든 변하지 않는 사실이다. 하지만 이는 별 상관 없는 일이기도 하다.

문제는 바로 사용자들은 가상 DOM을 눈으로 확인할 수 없다는 것이다. 이는 마치 10,000개의 타코를 해외의 다른 나라에서 가지고 있는 것과 같다. 언젠가는 이 타코들을 국내로 가져와야만 할 것이고, 이는 느릴 것이며 값비싼 비용을 치뤄야 할 것이다.

위의 타코 비유를 계속 가지고 이야기를 해보자. 모든 타코를 한번에 배송하는 것이 빠른가, 아니면 현재 필요한 타코의 수량과 현재 가지고 있는 타코의 수량의 차이를 계산하여, 해외로부터 최소한의 분량만큼을 배송하는 것이 빠른가? 단지 타코 4개만이 필요하다면, 4개만 배송하는 것이 저렴할 것이다.

그 다음 질문은, 타코 주문을 어떻게 할 것인가? "타코 4개를 보내줘"인가, 혹은 "타코 현황이 이런 식으로 변하면 좋겠으니, 자세한 건 너가 알아서 해"인가?

후자의 접근 방식이 가상 DOM이 작동하는 방식이다. UI가 이런 식으로 변하도록 만드는 코드를 작성하면, 가상 DOM이 현재 UI변화 후의 UI 간의 차이점을 계산하고, 갱신되어야 하는 부분만 갱신하는 것이다.

React는 문서 내 요소들에 특성을 부여하고, diff를 통하여 무엇이 갱신되어야 하는지 결정한 뒤, 앞서 부여한 특별한 ID 특성을 활용하여 각각의 요소들을 조작하는 것으로 이 신비로운 작업을 해낸다. 가상 DOM은 비록 과정 상에 단계를 추가하는 것이기는 하지만, 브라우저를 최소한도로 조작할 수 있는 우아한 방법을 제공한다. 여기서는 실제로 사용되는 메서드가 무엇인지, 무엇이 언제 갱신되어야 하는지 등을 개발자가 걱정할 필요가 없다.

어디, 숫자를 한번 봅시다!

벤치마킹 검사는 하지 않을 것이다. 수많은 사람들이 React의 가상 DOM 접근 방식이 실제로 빠른지를 확인하기 위한 다양한 검사들을 고안해왔다. 가장 흔한 결론은 "빠르지 않다"인 듯 하지만, 검사들이 현실적이지 않기 때문에 그건 큰 의미가 없다.

가상 DOM은 DOM 조작을 최소화해주는 브라우저의 숨겨진 최적화 위에 스크립트 계층을 형성한다. 이 추가적인 추상화의 계층은 DOM을 갱신하는 다른 어떤 메서드들보다 React를 CPU 집약적으로 만든다.

여기 네이티브 자바스크립트 DOM 조작을 사용하는 “Hello, world!” 예시가 있다.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello JavaScript!</title>

</head>
<body>
<div id="example"></div>
<script>
document.getElementById("example").innerHTML = "<h1>Hello, world!</h1>";
</script>
</body>
</html>

그리고 다음 예시가 똑같은 작업을 React에서 수행하는 것이다. React, ReactDOM, babel을 포함해야 하는 데에 유의한다. 그래야 JSX를 일반 자바스크립트로 변환할 수 있다.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<script src="build/react.js"></script>
<script src="build/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
</head>
<body>
<div id="example"></div>
<script type="text/babel">
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);
</script>
</body>
</html>

네이티브 접근이 항상 훨씬 더 빠를 것이다. 재미 삼아 그 증명을 살펴본다.

“Hello, world!” 페이지의 로드 및 직접 DOM 조작 렌더링의 시간 흐름도이다. (Chrome 브라우저 사용)


그리고 동일한 내용의 React 어플리케이션을 로드하고 출력하는 데의 시간 흐름도이다. (Chrome 브라우저 사용)

다른 모든 것이 본질적으로 같은데, Scripting에 든 시간만이 다르다. React는 DOM 메서드를 직접 사용한 것에 비하여 상당히 느리다! 하지만, jQuery에 비교한다면 어떨까?

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello jQuery!</title>
<script type="text/javascript" src="scripts/vendor/jquery-1.12.3.min.js"></script>

</head>
<body>
<div id="example"></div>
<script>
$(document).ready(function(){
$("#example").html("<h1>Hello, world!</h1>");
});
</script>
</body>
</html>

jQuery가 “Hello, world!” 페이지를 표시하는 데에 든 총 시간은 네이티브 자바스크립트보다 50ms 정도 느렸으며, 두 경우 모두 React보다는 3배 가까이 빨랐다.

따라서, 확실히, 빠르기만 생각하면 네이티브 자바스크립트와 jQuery가 쉽게 이긴다.

하지만 이것은 지극히 상식적인 생각이다. 라이브러리를 사용하는 것은 그렇지 않은 것보다 느리다. 그리고 실제로 DOM을 조작하기 이전에 DOM을 메모리 상에 표현하는 것은 단지 DOM을 직접 조작하기만 하는 것에 비하면 느리다. 제대로 수행만 된다면 말이다.

확실하게 아닌 경우를 봤으므로, 이제 가상 DOM을 빠르게 사용할 수 있는 방법을 알아본다.

가상 DOM을 사용하는 방법

위의 “Hello, world!” 예제는 React에게 불리한 예제였다. 왜냐하면 위에서는 페이지의 최초 렌더링만을 다루기 때문이다. React는 페이지의 갱신을 관리하기 위하여 설계되었다.

가상 DOM으로 인하여, 데이터 모델이 변화할 때마다 가상 UI 전체가 새로고침된다. 이것은 다른 라이브러리가 사용하는 시스템과는 아주 다른데, 다른 라이브러리의 경우 문서를 계속 주시하다가 필요할 때마다 갱신한다. 가상 DOM은 실제로 종종 다른 시스템에 비하여 적은 메모리를 사용하는데, 다른 시스템의 경우 문서를 계속 주시하기 위해 필요한 메모리 사용(Observable)이 가상 DOM에서는 필요하지 않기 때문이다.

하지만, 어떤 액션이 발생할 때마다 두 가상 DOM을 통째로 비교하는 부분에서 비효율이 존재한다. 복잡한 UI의 경우 CPU 요구사항이 상당할 수 있다.

이런 이유 때문에, React 개발자들은 무엇을 렌더링할지에 대하여 결정하는 데에 수동적이어선 안 된다. 어떤 액션이 어떤 컴포넌트에 영향을 주지 않을 것이라는 것을 확실히 알고 있다면, React로 하여금 해당 컴포넌트를 분석하지 않도록 지시할 수 있다. 이를 통하여 자원을 상당히 아끼고, 어플리케이션의 속도를 상당히 향상시킬 수 있다. 대개의 경우, React의 성능을 설명할 때에는 높은 CPU 사용량을 보여주는 수치, 여기에 이어서 메모이제이션과 같은 좋은 개발 사례를 통한 성능 향상 등을 포함한다.

사실, DOM을 직접 갱신하는 것보다 가상 DOM을 사용하는 것이 훨씬 빠른지를 확인할 방법은 없다. 왜냐하면 수백만 개의 다양한 요인에 따라 달라지겠지만, 대부분의 경우 어플리케이션을 어떻게 최적화하는지가 더 중요하기 때문이다.

이것은 놀랍거나 혁신적인 것이 아니다. 어떤 툴이든, 사용자의 역량만큼의 효용을 보인다. 하지만 React와 가상 DOM이 우리에게 주는 것은 브라우저는 갱신하는 것에 대한 간단한 방법이다. 이 간단함은 상당한 정신적 자원을 절약하여 UI 최적화에 활용할 수 있도록 해준다. 이것이 React에 깔려있는 진정한 - 성능과 생산성 모두에 도움이 되는 이점이다.


덧글

댓글 입력 영역