React 를 공부할 때 맨 처음 나오는 개념들 중 Virtual DOM 이라는 친구가 있습니다. 많은 자료들에서 Virtual DOM이 성능이 좋고 그런 이야기를 하는데, 이게 진짜인지, 정확히 어떤 말인지 알아보겠습니다.
DOM (Document Object Model) 은 HTML / XML 문서에 접근하기 위한 인터페이스입니다. 브라우저는 HTML 문서를 파싱하여 사용자에게 시각화해 줍니다. 이때 우리는 여러 가지 이유로 브라우저가 띄워주는 HTML 문서에 어떤 동적 처리를 해 주고 싶을 텐데, 그 과정을 도와주는 인터페이스가 DOM입니다. 대표적인 DOM 규격 중 하나인 W3C DOM의 API DOC 을 보면 우리가 흔히 아는 getElementsByTagName
과 같은 메소드를 정의하고 있음을 알 수 있습니다.
JavaScript 는 브라우저에서 Web API를 이용하고 DOM을 조작하기 위해 태어난 언어입니다. 따라서 DOM을 조작하기 위해서는 JavaScript를 이용합니다.
// js
function addItem () {
const value = document.getElementById('input').value;
const list = document.getElemendById('list');
const newItem = document.createElement('li');
const text = document.createTextNode(value);
newItem.appendChild(text);
list.appendChild(newItem);
}
// html
...
<ul id="list">
<li>a</li>
<li>b</li>
</ul>
<input id="input"></input>
<button onclick="addItem">+</button>
...
우리는 위와 같이 JavaScript를 통해 DOM을 조작할 수 있었습니다.
관용적으로 "DOM을 조작한다"라는 표현이 이용되긴 하지만, DOM은 인터페이스이고 개발자가 조작하고 싶은 건 화면이기 때문에 DOM을 조작한다는 표현이 개인적으로는 조금 어색하게 느껴집니다. 아마 DOM을 이용한다 정도가 정확한 표현일 것 같습니다.
Web의 복잡도가 점차 증가했고 DOM 조작이 점점 빈번하게 일어나게 되었습니다. 브라우저 렌더 과정 에 따라 DOM의 조작은 브라우저 렌더 과정을 유발합니다. (렌더 트리 생성, 페인트 등등) 그러니까 3000번의 변화가 생기면 렌더도 3000번 진행한다는 거죠.
DOM 조작이 많이 발생할 경우, 이는 상당히 비효율적입니다. Virtual DOM 은 이런 상황을 해결하기 위해 탄생했습니다.
Virtual DOM은 하나의 가상 레이어입니다. Virtual DOM에 적용하는 변경사항은 DOM에 바로 반영되지 않습니다. 다만 그 변경사항들을 모아뒀다가, 한번에 DOM한테 보냅니다. 그러면 DOM은 업데이트를 딱 한 번만 하면 되고, 그러면 렌더도 한 번만 하면 되는 거죠. DOM에 들어온 조작은 딱 한 번이니까요! 따라서 DOM 조작이 아주 빈번할 경우 Virtual DOM은 DOM을 조작하는 것에 비해 효율적으로 동작합니다.
그런데 여기, 함정이 있습니다.
Virtual DOM의 작동 원리는 하나의 레이어를 더 거쳐가는 동작이기 때문에, DOM 조작이 아주 많지 않다면 "당연하게도" DOM을 직접 조작하는 것보다 느립니다.
당연히 전자가 느리겠죠?
그리고 실제로 DOM 조작은 그리 드라마틱하게 많이 발생하지 않습니다. 정상적인 상황에서 3000번의 DOM 조작 같은 건 일어날 일이 없습니다.
그러니까 사실 대부분의 상황에서 웹 개발자들은 Virtual DOM 을 이용할 필요가 없습니다. 그렇게까지 안 해도 충분히 잘 돌아가거든요!
그런데 어느 날 이 Virtual DOM 의 대유행을 유발하는 한 라이브러리가 등장합니다.
Facebook 에서는 프론트엔드 MVC패턴을 사용하고 있었습니다. Facebook 에는 페이스북 메시지가 오지 않았는데 메시지가 와 있다고 뜨는 심각한 버그가 있었고, 이들은 그 버그를 해결하지 못했습니다.
데이터 흐름을 파악하고 버그를 체크하는 데에 신물이 난 이들은 아예 새로운 발상을 하게 됩니다.
그냥 데이터가 바뀌면 싹 다 다시 그려 버리면 안돼?
그러니까, DOM에 변경사항을 조작해서 집어넣지 말고 그냥 데이터가 바뀔 때마다 전부 다 다시 그려 버리면 버그가 생길 일이 없지 않냐는 질문이었습니다. (필자는 이게 React의 핵심 아이디어라고 생각합니다)
옛날이었으면 말도 안 되는 발상이었겠죠? 업데이트될 때마다 DOM에다가 수많은 조작을 가해야 할 테니까요. 하지만 이때 세상에는 Virtual DOM 이라는 기술이 있었습니다. 앞서 말했듯이 "DOM 조작이 드라마틱하게 많이 발생한다면" Virtual DOM이 충분한 가치를 지닙니다. 조금 더 구체화된 아이디어는 다음과 같습니다.
전부 다 Virtual DOM 에 새로 그리고, 기존에 비해 변경된 부분만 DOM에 한 번에 보내버리면 되지 않을까?
상당히 막무가내인 방식이었으나 결국 React 는 성공했습니다. React를 이용하면 데이터 흐름과 DOM의 차이에 의한 버그가 발생하지 않았고, 코드를 선언적으로 깔끔하게 작성할 수 있었습니다.
const [value, setValue] = useState('');
const [list, setList] = useState(['a', 'b', 'c']);
const handleChange = (e) => {
setValue(e.target.value);
}
const handleClick = () => {
setList([ ...list, value ]);
}
return (
<>
<ul>
{list.map((item, i) => <li key={i}>{item}</li>}
</ul>
<input value={value} onChange={handleChange />
<button onClick={handleClick}>+</button>
</>
)
react 코드. 데이터 바인딩이니 흐름이니 하나도 신경 쓸 필요 없고, 상태에만 집중하여 선언적인 코드를 짤 수 있다.
Virtual DOM을 이용하겠다는 건 데이터를 한번 조작하고, DOM을 또 한 번 조작하겠다는 이야기입니다. 당연히 DOM을 바로 조작하는 것보다 느립니다.
다만 극소수의 상황에서 (react가 이용하는) DOM 조작이 너무 빈번하게 일어날 경우, Virtual DOM을 이용해서 업데이트를 모아서 한번에 DOM한테 보내는 게 더 효율적이어집니다.
측정 방식에 따라 다르겠으나 위 사진에서 볼 수 있다시피 React 는 성능이 좋은 라이브러리가 아닙니다. 거의 가장 느린 프레임워크들과 어깨를 나란히 합니다. "데이터가 바뀌면 싹 다 다시 그리겠다"는 우직한 철학 때문입니다. 어마어마한 양의 DOM 조작을 감당하기 위해 Virtual DOM 을 차용했고, "봐 줄 만한 수준"의 성능이 나오는 라이브러리가 된 것일 뿐 성능이 좋지는 않습니다.
성능은 높을수록 좋은 거 아니냐고요? 사실 프론트엔드에서는 "봐 줄 만한 수준" 만 되면 (16.6ms 이내) 됩니다. 어차피 브라우저는 대부분 60fps거든요!
Myth: React is “faster than DOM”.
Reality: it helps create maintainable applications, and is fast enough for most use cases.
- Dan Abramov on twitter
Virtual DOM은 DOM을 직접 조작하지 않고 변경사항을 하나의 가상 돔에 모았다가 DOM에 한 번에 보내는 기술입니다.
일반적인 상황에서 웹의 복잡도가 높다 해도 Virtual DOM을 사용할 이유는 거의 없었습니다. 아이디어는 괜찮지만 실제로 Virtual DOM이 더 중요할 만한 상황이 없었고, 그게 오히려 느렸거든요.
React 의 근본 아이디어는 "다 다시 그려버리겠다"는 것으로 Virtual DOM이 충분한 의미를 가질 만한 무식한 양의 DOM 조작을 요구했습니다.
결국 React 는 Virtual DOM을 통해 대부분의 상황에서 봐 줄 만한 성능을 보여주는 라이브러리가 되었고, 성능을 포기하면서 챙긴 철학을 통해 글을 작성하는 시점 기준 세상에서 가장 인기있는 프론트엔드 툴이 되었습니다.
"그러니까, DOM에 변경사항을 조작해서 집어넣지 말고 그냥 데이터가 바뀔 때마다 전부 다 다시 그려 버리면 버그가 생길 일이 없지 않냐는 질문이었습니다." 라는 말씀이 굉장히 와닿습니다. 좋은 글 잘 읽고 갑니다. 🙂