본문으로 건너뛰기
Advertisement

17.1 Svelte 소개

Svelte는 사용자 인터페이스를 만드는 컴파일러 기반 프레임워크입니다. React나 Vue와 달리 런타임 라이브러리를 번들에 포함하지 않고, 빌드 타임에 모든 마법을 일반 JavaScript로 변환합니다. 결과물은 순수한 DOM 조작 코드로, 크기가 작고 실행 속도가 빠릅니다.


Svelte란? 컴파일러 기반 프레임워크

Svelte는 2016년 Rich Harris가 만든 프레임워크입니다. "Svelte"는 "날씬한, 군더더기 없는"이라는 뜻으로, 이름 그대로 불필요한 런타임 오버헤드가 없는 프레임워크를 목표로 합니다.

핵심 차이점

구분React / VueSvelte
동작 방식런타임에 가상 DOM 비교컴파일 타임에 DOM 조작 코드 생성
번들 크기프레임워크 런타임 포함컴파일된 코드만 포함
반응성useState, ref 등 API 필요변수 할당으로 자동 반응
학습 곡선API를 익혀야 함HTML/CSS/JS에 가까운 문법

컴파일 과정

Counter.svelte
↓ (svelte 컴파일러)
Counter.js (순수 JavaScript DOM 조작 코드)

Svelte 파일(.svelte)은 단순한 텍스트 파일이 아닙니다. 빌드 시 Svelte 컴파일러가 이 파일을 읽어 고도로 최적화된 JavaScript로 변환합니다.


가상 DOM이 없는 이유

React의 가상 DOM(Virtual DOM)은 상태 변경 시 이전/이후 가상 트리를 비교(diffing)하여 실제 DOM에 최소한의 변경을 적용하는 기술입니다. 이는 직접 DOM을 조작하는 것보다 빠르지만, 비교 연산 자체에 비용이 발생합니다.

Svelte는 다른 접근 방식을 택합니다. 어떤 변수가 어떤 DOM 요소에 영향을 주는지 컴파일 타임에 이미 알고 있기 때문에, 런타임에서 비교할 필요가 없습니다.

<!-- Counter.svelte -->
<script>
let count = 0;

function increment() {
count++; // 이 할당이 DOM 업데이트를 트리거
}
</script>

<button on:click={increment}>
클릭 횟수: {count}
</button>

위 코드는 컴파일 후 대략 다음과 같이 변환됩니다:

// 컴파일된 결과 (단순화)
function create_fragment(ctx) {
let button;
return {
c() {
button = element("button");
button.textContent = `클릭 횟수: ${ctx[0]}`;
},
m(target, anchor) {
insert(target, button, anchor);
listen(button, "click", ctx[1]);
},
p(ctx, [dirty]) {
if (dirty & 1) { // count가 변경된 경우에만
set_data(button_text, `클릭 횟수: ${ctx[0]}`);
}
}
};
}

가상 DOM 비교 없이, 정확히 count가 바뀔 때 해당 텍스트 노드만 업데이트합니다.


React / Vue / Angular와 번들 크기 비교

간단한 카운터 앱을 각 프레임워크로 만들 때의 번들 크기(gzip 기준):

프레임워크번들 크기 (gzip)특징
Svelte~3 KB런타임 없음, 컴파일된 코드만
React + React DOM~45 KB가상 DOM 런타임 포함
Vue 3~33 KBComposition API 런타임 포함
Angular~60 KB+풀 프레임워크

단, 앱이 커질수록 Svelte의 컴파일 결과물도 증가하여 격차가 줄어들 수 있습니다. 소~중규모 앱에서 Svelte의 번들 크기 이점이 두드러집니다.


Svelte 5의 주요 변경점: Runes 도입

Svelte 5(2024년 말 출시)는 Runes 시스템을 도입하여 반응성 선언 방식을 근본적으로 바꿨습니다.

Svelte 4 vs Svelte 5 비교

<!-- Svelte 4 방식 -->
<script>
let count = 0; // 암시적 반응형
$: doubled = count * 2; // 반응형 구문
</script>

<p>Count: {count}, Doubled: {doubled}</p>
<!-- Svelte 5 방식 (Runes) -->
<script>
let count = $state(0); // 명시적 반응형 상태
let doubled = $derived(count * 2); // 명시적 파생 값
</script>

<p>Count: {count}, Doubled: {doubled}</p>

Svelte 5 주요 Rune

Rune역할설명
$state()반응형 상태이전의 let (암시적 반응성) 대체
$derived()파생 값이전의 $: 반응 구문 대체
$effect()사이드 이펙트이전의 $: { ... } 블록 대체
$props()컴포넌트 props이전의 export let 대체
$bindable()양방향 바인딩 props부모에서 bind: 허용

Runes는 일반 JavaScript 파일(.js, .ts)에서도 사용 가능하여, Svelte 컴포넌트 밖에서도 반응형 로직을 작성할 수 있습니다.


Svelte가 적합한 프로젝트

잘 맞는 경우

  • 소~중규모 애플리케이션: 번들 크기 이점이 극대화됨
  • 성능이 중요한 프로젝트: 런타임 오버헤드가 없어 초기 로딩 빠름
  • 콘텐츠 중심 사이트: SvelteKit의 SSR/SSG 활용
  • 팀 학습 곡선이 낮아야 할 때: HTML/CSS/JS와 유사한 문법
  • 임베디드 위젯: 독립적인 웹 컴포넌트 생성 가능

덜 적합한 경우

  • 대규모 엔터프라이즈 앱: 생태계 크기에서 React/Angular에 비해 작음
  • 풍부한 서드파티 컴포넌트가 필요할 때: React UI 라이브러리 생태계가 더 넓음
  • Svelte 팀 경험자가 없을 때: 채용 시 고려 필요

기본 컴포넌트 예제

Svelte 컴포넌트는 세 섹션으로 구성됩니다:

<!-- Greeting.svelte -->
<script>
// JavaScript 로직 (Svelte 5: Runes 사용)
let { name = '세계' } = $props();
let count = $state(0);
let message = $derived(`안녕하세요, ${name}! (${count}번 인사)`);

function greet() {
count++;
}
</script>

<!-- HTML 템플릿 -->
<div class="greeting">
<h1>{message}</h1>
<button on:click={greet}>인사하기</button>
</div>

<!-- 스타일 (자동으로 컴포넌트 스코프 적용) -->
<style>
.greeting {
font-family: sans-serif;
padding: 1rem;
border: 2px solid #ff3e00;
border-radius: 8px;
}

button {
background-color: #ff3e00;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}

button:hover {
background-color: #dd3700;
}
</style>

Svelte 파일 구조 특징

  1. <script>: 컴포넌트 로직, Runes, imports
  2. HTML 템플릿: 마크업, {표현식}, 로직 블록
  3. <style>: CSS (기본적으로 컴포넌트 스코프, 클래스 이름 해시 처리)

각 섹션은 선택 사항이며, 순서도 자유롭습니다.


Todo 앱 실전 예제

<!-- TodoApp.svelte -->
<script>
let todos = $state([
{ id: 1, text: 'Svelte 배우기', done: false },
{ id: 2, text: 'SvelteKit 배우기', done: false },
{ id: 3, text: 'Runes 마스터하기', done: false },
]);

let newTodo = $state('');

let remaining = $derived(todos.filter(t => !t.done).length);

function addTodo() {
if (!newTodo.trim()) return;
todos.push({
id: Date.now(),
text: newTodo.trim(),
done: false,
});
newTodo = '';
}

function toggleTodo(id) {
const todo = todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}

function removeTodo(id) {
todos = todos.filter(t => t.id !== id);
}

function handleKeydown(event) {
if (event.key === 'Enter') addTodo();
}
</script>

<div class="app">
<h1>할 일 목록 <span class="badge">{remaining}</span></h1>

<div class="input-row">
<input
bind:value={newTodo}
on:keydown={handleKeydown}
placeholder="새 할 일 입력..."
/>
<button on:click={addTodo}>추가</button>
</div>

<ul>
{#each todos as todo (todo.id)}
<li class:done={todo.done}>
<input
type="checkbox"
checked={todo.done}
on:change={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button class="remove" on:click={() => removeTodo(todo.id)}>×</button>
</li>
{/each}
</ul>

{#if todos.length === 0}
<p class="empty">할 일이 없습니다. 새로 추가해보세요!</p>
{/if}
</div>

<style>
.app {
max-width: 400px;
margin: 2rem auto;
font-family: sans-serif;
}

.badge {
background: #ff3e00;
color: white;
border-radius: 999px;
padding: 0.1rem 0.5rem;
font-size: 0.8rem;
}

.input-row {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}

input[type="text"], input:not([type]) {
flex: 1;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}

ul {
list-style: none;
padding: 0;
}

li {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-bottom: 1px solid #eee;
}

li.done span {
text-decoration: line-through;
color: #999;
}

li span {
flex: 1;
}

.remove {
background: none;
border: none;
color: #999;
cursor: pointer;
font-size: 1.2rem;
}

.empty {
text-align: center;
color: #999;
}
</style>

Svelte REPL 소개

Svelte는 공식 온라인 에디터인 **REPL(Read-Eval-Print Loop)**을 제공합니다.

REPL에서 할 수 있는 것:

  • 코드 즉시 실행 및 결과 확인
  • 컴파일된 JavaScript 출력 보기 ("JS output" 탭)
  • 예제 공유 (URL로 공유 가능)
  • 공식 튜토리얼과 연동

REPL 활용 팁

  1. "JS output" 탭에서 컴파일 결과를 보면 Svelte가 어떻게 동작하는지 이해됨
  2. "CSS output" 탭에서 스코프 적용된 CSS 확인 가능
  3. 여러 컴포넌트 파일을 추가하여 컴포넌트 간 통신 테스트 가능

고수 팁

팁 1: Svelte의 컴파일 결과 분석

Svelte를 제대로 이해하려면 컴파일 결과를 꼭 봐야 합니다:

# 번들 분석
npm run build -- --report

# Vite에서 번들 시각화
npx vite-bundle-visualizer

팁 2: Svelte 4와 5 병행 사용

SvelteKit 프로젝트에서 Svelte 4 문법과 5 문법을 혼용할 수 있습니다. runes 모드는 파일 단위로 활성화할 수 있어 점진적 마이그레이션이 가능합니다.

<!-- Svelte 5 Runes 모드 강제 활성화 -->
<svelte:options runes={true} />

<script>
let count = $state(0);
</script>

팁 3: 웹 컴포넌트로 컴파일

Svelte 컴포넌트를 표준 웹 컴포넌트(Custom Element)로 컴파일할 수 있습니다:

<svelte:options customElement="my-counter" />

<script>
let count = $state(0);
</script>

<button on:click={() => count++}>Count: {count}</button>
<!-- 어느 HTML 파일에서도 사용 가능 -->
<script src="./my-counter.js"></script>
<my-counter></my-counter>

팁 4: 성능 벤치마크 이해

js-framework-benchmark 결과에서 Svelte는 일반적으로 상위권을 기록합니다. 단, "Svelte가 항상 React보다 빠르다"는 과도한 일반화는 주의해야 합니다. 실제 앱 성능은 아키텍처와 구현 방식에 더 크게 좌우됩니다.


정리

개념설명
컴파일러 프레임워크빌드 타임에 최적화된 JS 생성
가상 DOM 없음런타임 비교 비용 제거
Runes (Svelte 5)$state, $derived, $effect, $props
컴포넌트 구조<script> + HTML + <style>
스코프 CSS스타일이 컴포넌트에만 적용
번들 크기React/Vue 대비 현저히 작음

다음 장에서는 SvelteKit으로 프로젝트를 설정하는 방법을 알아봅니다.

Advertisement