TIL: Svelte는 왜 빠를까?
Svelte가 사실 예언가라고?
Angular, React, Vue.js가 치고받던 시절을 지나 React가 대세가 되었고, 모두가 사용함에도 뭐랄까 레거시처럼 느껴지는 요즘. 이 바닥에 긴장감을 불어넣는 프레임워크가 있다. 바로 Svelte다.
수년 전 회사 홈페이지를 제작하는 데 Svelte를 사용한 경험이 있다. React를 사용하는 회사였지만 다음 이유로 Svelte가 적절한 기술이라 판단했다.
- 작은 번들 사이즈
- 단순 소개 페이지에 장황한 코드를 작성하고 싶지 않음
- 너무 쉬워 평소 사용하던 기술 스택과 달라도 바로 적응 가능
- 혹여나 퍼블리셔에게 외주를 맡기더라도 코드를 옮겨 작성하기 쉬움
- 내가 써보고 싶었다! 그러니 검토했지!
비록 당시엔 webpack 플러그인 지원이 부족해 다른 기술을 쓰게 되었지만, 코드를 작성하는 데 있어서는 좋은 기억을 가지고 있다. 그런데 언젠가 그런 생각이 들었다. Svelte는 앞으로 일어날 변경을 다 알고 있다던데 어떻게?
DOM을 조작하는 것은 비용이 크다. 단순 연산은 웬만큼 큰 작업이 아니고서야 눈에 띄는 성능 저하가 없는 반면, 렌더링만큼은 조심해야 한다. 고로 실시간으로 빠르고 복잡한 인터랙션을 UI로 표현하고자 하나부터 열까지 다 구현하려 한다면 골치가 아플 것이다. 브라우저의 렌더링 과정은 구글링하면 많이 나오는데 내가 또 썼다.
아무튼 그래서 나온 게 Virtual DOM이다. Virtual DOM의 원리는 간단하다. 변경이 발생할 때마다 가상의 DOM 트리를 만들어 이전 트리 스냅샷과 최신 트리 스냅샷을 비교(diff)하고 변경된 내용만 DOM에 반영하겠다는 거다. 그리고 이를 적당히 batching 했을 뿐이고. 뭐라도 눈으로 확인하고 싶다면 냅다 children
로그 찍으면 나오는 객체가 Virtual DOM이다(작동 원리까지 볼 순 없겠지만).
(대충 Virtual DOM 설명하는 그림)
하지만 Virtual DOM은 DOM 업데이트 범위와 주기를 줄여줄 뿐, 실제로 잦은 렌더링이 일어나야 하는 경우 발생하는 성능 저하는 해결하지 못한다. 오히려 재조정으로 인한 오버헤드가 발생해 영향받는 컴포넌트의 범위를 줄이기 위한 최적화가 필요하다.
Virtual DOM이 어떻게 구현되었는지는 아래 링크에서 더 알아볼 수 있다.
이렇게 원할 때, 필요한 부분만, 적절한 시점에, 다시 렌더링하는 좋은 기술을 버리고 이들은 어떻게 더 큰 성능 향상을 이뤘을까?
Most obviously, diffing isn’t free. You can’t apply changes to the real DOM without first comparing the new virtual DOM with the previous snapshot.
- Virtual DOM is pure overhead
Svelte 팀은 Virtual DOM이 일반적으로 충분히 빠르지만, 이를 확신할 수는 없다고 이야기한다. 정말 항상 빠르다면 shouldComponentUpdate
같은 건 필요하지 않았을 거라 말하며.
물론 Virtual DOM의 diffing 알고리즘은 문제가 될 만큼 느리지 않다. 정말 큰 오버헤드는 변경된 값만 확인해 업데이트가 필요한 부분만 다시 계산하는 게 아닌, 컴포넌트 내부의 모든 변경에 의해 전체를 다시 계산한다는 점이다. 게다가 지금처럼 가상 DOM 트리를 하향식으로 읽어 전체를 업데이트한다면 비용은 더욱 커질 수 밖에.
그래서 Svelte는 View를 동기화하기 위해 인터프리터와 같이 런타임에서 변경할 요소를 찾는 대신, 컴파일러로서 빌드 시점에 어떤 요소가 어떻게 변경되어야 하는지 찾아내 동기화 로직을 작성한다고 한다.
You can’t write serious applications in vanilla JavaScript without hitting a complexity wall. But a compiler can do it for you.
- Frameworks without the framework: why didn’t we think of this sooner?
쉽게 말해 Svelte는 컴파일러라는 뜻이다. 실제로 이를 구현해보진 않고 읽기만 했으나, .svelte 파일의 JS를 파싱할 때 AST로 분석해 exports statements와 reactive statements를 추출한다고 한다. 이렇게 추출할 수 있었기에 앞으로의 변경을 예측할 수 있었던 것.
<!– component.svelte -->
<script>
export let name;
function handleClick(e) {
e.preventDefault()
alert(`Hello ${name}!`)
}
</script>
<h1 class="snazzy" on:click=handleClick>Hello {name}!</h1>/* component.js */
export default function component({ target, props }) {
// defined with `export let`
let { name } = props;
function handleClick(e) {
e.preventDefault();
alert(`Hello ${name}!`);
}
let e0, t1, b2, t3;
return {
create() {
e0 = document.createElement("h1")
t1 = document.createTextNode("Hello ")
b2 = document.createTextNode(name)
t3 = document.createTextNode("!")
e0.setAttribute("class", "snazzy")
e0.addEventListener("click", handleClick)
},
mount() {
e0.appendChild(t1)
e0.appendChild(b2)
e0.appendChild(t3)
target.append(e0)
},
update(changes) {
if (changes.name) {
// update `name` variable and all binding to `name`
b2.data = name = changes.name
}
},
detach() {
e0.removeEventListener("click", handleClick)
target.removeChild(e0)
}
};
}
자세한 내용은 아래 링크에 잘 정리되어 있다.
정작 Svelte를 선택했을 때는 DX에만 매몰되어 리서치가 부족했음을 반성하며, 기회가 된다면 컴파일러도 직접 구현해봐야겠다.
별 내용 없는데도 제 성격 못 이겨 글을 계획보다 길게 써버렸다.
쓰다 지쳐 그만두느니 작은 글이라도 습관이 되어야 한다.
줄이는 것도 훈련이 필요할 듯.