TIL: Svelte는 왜 빠를까?

TIL: Svelte는 왜 빠를까?
Svelte가 사실 예언가라고?

Angular, React, Vue.js가 치고받던 시절을 지나 React가 대세가 되었고, 모두가 사용함에도 뭐랄까 레거시처럼 느껴지는 요즘. 이 바닥에 긴장감을 불어넣는 프레임워크가 있다. 바로 Svelte다.

State of JavaScript 2022: Front-end Frameworks
The 2022 edition of the annual survey about the latest trends in the JavaScript ecosystem.
The State of JS 2022 결과, Retention, Interest 모두 1위를 다투고 있는 Svelte.

수년 전 회사 홈페이지를 제작하는 데 Svelte를 사용한 경험이 있다. React를 사용하는 회사였지만 다음 이유로 Svelte가 적절한 기술이라 판단했다.

  • 작은 번들 사이즈
  • 단순 소개 페이지에 장황한 코드를 작성하고 싶지 않음
  • 너무 쉬워 평소 사용하던 기술 스택과 달라도 바로 적응 가능
  • 혹여나 퍼블리셔에게 외주를 맡기더라도 코드를 옮겨 작성하기 쉬움
  • 내가 써보고 싶었다! 그러니 검토했지!

비록 당시엔 webpack 플러그인 지원이 부족해 다른 기술을 쓰게 되었지만, 코드를 작성하는 데 있어서는 좋은 기억을 가지고 있다. 그런데 언젠가 그런 생각이 들었다. Svelte는 앞으로 일어날 변경을 다 알고 있다던데 어떻게?

‘Write less code’ 이 어찌나 아름다운 문장인가? 내 사랑 Svelte, 언젠가 React가 사라진 세상이 온다면 너를 꼭 사용하고 말 테야.

DOM을 조작하는 것은 비용이 크다. 단순 연산은 웬만큼 큰 작업이 아니고서야 눈에 띄는 성능 저하가 없는 반면, 렌더링만큼은 조심해야 한다. 고로 실시간으로 빠르고 복잡한 인터랙션을 UI로 표현하고자 하나부터 열까지 다 구현하려 한다면 골치가 아플 것이다. 브라우저의 렌더링 과정은 구글링하면 많이 나오는데 내가 또 썼다.

브라우저의 렌더링 과정
렌더링, 그거 어떻게 하는 건데. 브라우저의 구조는 저마다 다르지만 대체로 이런 구성 요소들로 이루어져 있다. * UI (당신의 동료 디자이너가 디자인한 것 말고 진짜 브라우저 UI) * UI Backend (Backend 개발자 아니다) * 브라우저 엔진 * 렌더링 엔진 * 네트워킹 인터페이스 * 자바스크립트 인터프리터 * 데이터 저장소 오늘의 주인공은 바로 Rendering Engine이다. 말 그대로 렌더링이 일이라 요청한

아무튼 그래서 나온 게 Virtual DOM이다. Virtual DOM의 원리는 간단하다. 변경이 발생할 때마다 가상의 DOM 트리를 만들어 이전 트리 스냅샷과 최신 트리 스냅샷을 비교(diff)하고 변경된 내용만 DOM에 반영하겠다는 거다. 그리고 이를 적당히 batching 했을 뿐이고. 뭐라도 눈으로 확인하고 싶다면 냅다 children 로그 찍으면 나오는 객체가 Virtual DOM이다(작동 원리까지 볼 순 없겠지만).

(대충 Virtual DOM 설명하는 그림)

하지만 Virtual DOM은 DOM 업데이트 범위와 주기를 줄여줄 뿐, 실제로 잦은 렌더링이 일어나야 하는 경우 발생하는 성능 저하는 해결하지 못한다. 오히려 재조정으로 인한 오버헤드가 발생해 영향받는 컴포넌트의 범위를 줄이기 위한 최적화가 필요하다.

Virtual DOM이 어떻게 구현되었는지는 아래 링크에서 더 알아볼 수 있다.

Vanilla Javascript로 가상돔(VirtualDOM) 만들기 | 개발자 황준일
React와 Vue에서 사용되고 있는 가상돔(VirtualDOM)을 Vanilla JS로 직접 만드는 과정에 대해 소개합니다.

이렇게 원할 때, 필요한 부분만, 적절한 시점에, 다시 렌더링하는 좋은 기술을 버리고 이들은 어떻게 더 큰 성능 향상을 이뤘을까?


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 코드 컴파일러는 어떻게 동작할까?
스벨트는 내부적으로 Virtual Dom을 사용하지 않는 것으로 알고 있습니다. 그렇다면 컴파일러가 상당히 많은 일을 해줘야 할텐데, 어떻게 이것이 가능할 것일까요? 요새 Vue3을 이용해 신규 프로젝트를 진행하고 있습니다. 춣시되면 아마 한국에서 Vue3을 이용한 하이브리드 앱 중 가장 큰 규모의 엔터프라이즈 애플리케이션이 될 것 같습니다. (이 글을 읽는 어려분도 제가 개발하고 있는 앱을 신규 버전으로 사용하게 될 확률이 상당히 높습니다. 이미 엄청 유명한 앱이거든요) 원래 리액트만 활용했던 만큼, 리액트의 불변 모델과 온리 코…

정작 Svelte를 선택했을 때는 DX에만 매몰되어 리서치가 부족했음을 반성하며, 기회가 된다면 컴파일러도 직접 구현해봐야겠다.


별 내용 없는데도 제 성격 못 이겨 글을 계획보다 길게 써버렸다.
쓰다 지쳐 그만두느니 작은 글이라도 습관이 되어야 한다.
줄이는 것도 훈련이 필요할 듯.

Reference