Svelte 핵심 — 컴파일러 기반 반응성과 Virtual DOM 없는 렌더링

Svelte의 컴파일러 동작 원리, Virtual DOM 없는 직접 DOM 조작, Runes 반응성 문법($state/$derived/$effect), Stores, SvelteKit, Svelte 4 vs 5 비교를 정리합니다.

· 6 min read · PALDYN Team

지난 글에서 Vue 3의 Proxy 기반 반응성을 살펴봤습니다. 이번에는 Svelte를 다룹니다. Svelte는 React나 Vue와 근본적으로 다른 철학을 가집니다. **“프레임워크를 런타임에 포함시키지 말고, 컴파일 타임에 바닐라 JS로 변환하자”**는 발상입니다. Virtual DOM 없이, 런타임 Diff 없이, 직접 DOM을 조작하는 코드를 생성합니다.


컴파일러 동작 원리

Svelte — 컴파일러 기반 반응성

.svelte 파일은 빌드 타임에 컴파일됩니다. 컴파일러는 템플릿 변수를 분석해서 어떤 DOM 노드가 어떤 변수에 의존하는지 정적으로 파악합니다. 그 결과 런타임에 “어디가 변경됐는지 찾는” 과정이 필요 없습니다. count++가 실행되면 컴파일된 코드가 직접 해당 텍스트 노드를 업데이트합니다.

// 컴파일 결과 (단순화)
let count = 0
const text = document.createTextNode(count)

button.addEventListener('click', () => {
  count++
  text.data = count   // DOM 직접 업데이트 — Diff 없음
})

Svelte 4 — 기본 문법

<script>
  export let name = 'World'   // props
  let count = 0

  // $: 반응성 선언 (count가 바뀔 때마다 재계산)
  $: doubled = count * 2
  $: {
    // 블록 전체가 반응성
    if (count > 10) console.log('10 초과!')
  }

  function handleClick() {
    count++
  }
</script>

<h1>Hello, {name}!</h1>
<button on:click={handleClick}>
  클릭: {count} (doubled: {doubled})
</button>

<style>
  /* 자동으로 컴포넌트 스코프 적용 */
  button { color: red; }
</style>

Svelte의 CSS는 기본적으로 컴포넌트 스코프입니다. 컴파일러가 각 클래스에 고유 해시를 붙여서 다른 컴포넌트와 충돌하지 않습니다.


Svelte 5 — Runes

Svelte 5는 Runes라는 새로운 반응성 API를 도입했습니다. $state, $derived, $effect, $props는 특수 컴파일러 시그널로, 일반 JS 변수 할당처럼 작성하지만 반응성이 있습니다.

<script>
  let count = $state(0)
  let doubled = $derived(count * 2)

  $effect(() => {
    console.log('count changed:', count)
    return () => console.log('cleanup')   // cleanup
  })

  let { name, age = 0 } = $props()
</script>

Runes는 TypeScript와 훨씬 자연스럽게 통합됩니다. $state<User>({ id: 1 })처럼 제네릭 타입도 지원합니다.


Stores — 전역 상태 관리

Svelte Stores — 전역 상태 관리

Svelte Stores는 subscribe 계약을 구현한 객체입니다. 표준 인터페이스({ subscribe })를 구현하면 Svelte의 $ 자동 구독 문법을 쓸 수 있습니다.

// stores.js
import { writable, readable, derived, get } from 'svelte/store'

// writable: set/update/subscribe
export const count = writable(0)

// readable: 외부 소스 연결 (WebSocket, timer 등)
export const time = readable(new Date(), function start(set) {
  const id = setInterval(() => set(new Date()), 1000)
  return function stop() { clearInterval(id) }
})

// derived: 다른 스토어에서 파생
export const elapsed = derived(
  time,
  $time => Math.round(($time - start) / 1000)
)

// 컴포넌트 밖에서 스토어 값 읽기
const value = get(count)
<script>
  import { count } from './stores'
</script>

<!-- $ 접두사: 자동 구독 + 컴포넌트 언마운트 시 자동 해제 -->
<p>현재 카운트: {$count}</p>
<button on:click={() => count.update(n => n + 1)}>증가</button>
<button on:click={() => count.set(0)}>리셋</button>

컴포넌트 간 통신

<!-- Parent.svelte -->
<script>
  import Child from './Child.svelte'
  let message = ''

  function handleMessage(event) {
    message = event.detail.text
  }
</script>

<Child on:message={handleMessage} />
<p>받은 메시지: {message}</p>
<!-- Child.svelte -->
<script>
  import { createEventDispatcher } from 'svelte'
  const dispatch = createEventDispatcher()

  function send() {
    dispatch('message', { text: 'Hello from Child!' })
  }
</script>

<button on:click={send}>메시지 보내기</button>

SvelteKit — 메타 프레임워크

SvelteKit은 Svelte의 공식 메타 프레임워크로, Next.js에 대응합니다.

// src/routes/products/[id]/+page.server.js
export async function load({ params }) {
  const product = await fetchProduct(params.id)
  if (!product) throw error(404, 'Product not found')
  return { product }
}
<!-- src/routes/products/[id]/+page.svelte -->
<script>
  export let data   // load()의 반환값 자동 주입
</script>

<h1>{data.product.name}</h1>

파일 시스템 기반 라우팅, SSR/SSG/CSR 혼합, Form Actions, 서버 사이드 데이터 로딩을 지원합니다.


React/Vue와 비교

항목SvelteReactVue 3
반응성 방식컴파일 타임 변환런타임 HookProxy 기반
Virtual DOM없음있음있음
런타임 크기~4KB~40KB~30KB
번들 크기 (앱 포함)앱이 커질수록 차이 감소런타임 고정런타임 고정
학습 곡선낮음중간중간
TypeScriptRunes에서 완전 지원완전 지원완전 지원

지난 글: Vue 3 핵심 — Composition API, Reactivity, Virtual DOM

다음 글: Angular 핵심 — Zone.js, DI, 변경 감지, Signal


읽어주셔서 감사합니다. 😊