본문으로 건너뛰기

16. 성능 최적화

Svelte는 컴파일 타임 최적화를 통해 기본적으로 뛰어난 성능을 제공하지만, 대규모 애플리케이션에서는 추가적인 최적화가 필요합니다. 번들 크기 최소화, 런타임 성능 향상, 서버 사이드 렌더링 등 다양한 최적화 기법을 통해 사용자 경험을 크게 개선할 수 있습니다. 이 장에서는 Svelte와 SvelteKit 애플리케이션의 성능을 극대화하는 실전 최적화 기법들을 완전히 마스터해보겠습니다.


16.1 빌드 최적화

코드 스플리팅

코드 스플리팅은 JavaScript 번들을 작은 청크로 나누어 필요한 시점에만 로드하는 최적화 기법입니다. SvelteKit은 라우트 기반 코드 스플리팅을 자동으로 처리하여 초기 로드 시간을 크게 단축시킵니다. 동적 import를 활용하면 컴포넌트 레벨에서도 세밀한 코드 스플리팅이 가능합니다.

동적 컴포넌트 로딩

LazyComponent.svelte
<script>
import { onMount } from 'svelte';

let Component = $state(null);
let isLoading = $state(true);

onMount(async () => {
// 컴포넌트를 동적으로 import
const module = await import('./HeavyComponent.svelte');
Component = module.default;
isLoading = false;
});
</script>

{#if isLoading}
<div class="loading">컴포넌트 로딩 중...</div>
{:else if Component}
<svelte:component this="{Component}" />
{/if}

<style>
.loading {
padding: 2rem;
text-align: center;
background: #f3f4f6;
}
</style>

실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!

조건부 로딩 패턴

ConditionalLoading.svelte
<script>
let showChart = $state(false);
let ChartComponent = $state(null);

async function loadChart() {
if (!ChartComponent) {
const module = await import('./DataChart.svelte');
ChartComponent = module.default;
}
showChart = true;
}

function hideChart() {
showChart = false;
}
</script>

<div>
<button onclick="{loadChart}">
차트 {showChart ? '새로고침' : '보기'}
</button>

{#if showChart && ChartComponent}
<button onclick="{hideChart}">차트 숨기기</button>
<svelte:component this="{ChartComponent}" />
{/if}
</div>

실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!

트리 셰이킹

트리 셰이킹은 사용하지 않는 코드를 번들에서 제거하는 최적화 기법입니다. Svelte의 컴파일러는 뛰어난 트리 셰이킹을 자동으로 수행하지만, 올바른 import 패턴을 사용해야 효과적입니다. ES 모듈을 사용하고 필요한 부분만 명시적으로 import하여 번들 크기를 최소화할 수 있습니다.

효율적인 Import 패턴

utils.js
// ❌ 비효율적: 전체 라이브러리 import
import _ from 'lodash';

export function processData(data) {
return _.map(data, item => item.value);
}

// ✅ 효율적: 필요한 함수만 import
import { map } from 'lodash-es';

export function processData(data) {
return map(data, item => item.value);
}

// ✅ 더 나은 방법: 네이티브 메서드 사용
export function processData(data) {
return data.map(item => item.value);
}

사이드 이펙트 방지

optimized-module.js
// sideEffects: false를 package.json에 설정하여
// 트리 셰이킹 최적화

// ✅ 순수 함수 - 트리 셰이킹 가능
export function calculateSum(a, b) {
return a + b;
}

export function formatCurrency(amount) {
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: 'KRW',
}).format(amount);
}

// ❌ 사이드 이펙트 있음 - 트리 셰이킹 어려움
console.log('Module loaded'); // 피하세요
window.globalVar = 'value'; // 피하세요

번들 분석

번들 크기를 분석하여 최적화 포인트를 찾는 것이 중요합니다. rollup-plugin-visualizer나 vite-bundle-visualizer를 사용하면 번들 구성을 시각적으로 확인할 수 있습니다. 큰 의존성을 식별하고 대체 가능한 경량 라이브러리를 찾아 번들 크기를 줄일 수 있습니다.

Vite 설정으로 번들 분석

vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
import { visualizer } from 'rollup-plugin-visualizer';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [
sveltekit(),
visualizer({
filename: './stats.html',
open: true,
gzipSize: true,
brotliSize: true,
}),
],
build: {
// 번들 크기 경고 임계값 설정
chunkSizeWarningLimit: 500,
rollupOptions: {
output: {
// 청크 분리 전략
manualChunks: {
vendor: ['svelte'],
utils: ['./src/lib/utils'],
},
},
},
},
});

16.2 런타임 최적화

가상화된 리스트

긴 리스트를 렌더링할 때 뷰포트에 보이는 항목만 DOM에 렌더링하는 가상화 기법을 사용합니다. 수천 개의 항목도 부드럽게 스크롤할 수 있으며 메모리 사용량을 크게 줄일 수 있습니다. Svelte에서는 svelte-virtual-list 같은 라이브러리를 활용하거나 직접 구현할 수 있습니다.

가상 스크롤 구현

VirtualList.svelte
<script>
let items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `아이템 ${i + 1}`,
value: Math.random() * 100,
}));

let containerHeight = 400;
let itemHeight = 50;
let scrollTop = $state(0);

let visibleStart = $derived(
Math.floor(scrollTop / itemHeight)
);
let visibleEnd = $derived(
Math.ceil((scrollTop + containerHeight) / itemHeight)
);
let visibleItems = $derived(
items.slice(visibleStart, visibleEnd + 1)
);

function handleScroll(event) {
scrollTop = event.target.scrollTop;
}
</script>

<div
class="container"
onscroll="{handleScroll}"
style="height: {containerHeight}px"
>
<div style="height: {items.length * itemHeight}px">
<div
style="transform: translateY({visibleStart * itemHeight}px)"
>
{#each visibleItems as item (item.id)}
<div class="item">
{item.name}: {item.value.toFixed(2)}
</div>
{/each}
</div>
</div>
</div>

<style>
.container {
overflow-y: auto;
border: 1px solid #e5e7eb;
}

.item {
height: 50px;
padding: 0 1rem;
display: flex;
align-items: center;
border-bottom: 1px solid #f3f4f6;
}
</style>

실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!

프로덕션 팁: 실제 프로젝트에서는 svelte-virtual-list 또는 @humanspeak/svelte-virtual-list 같은 검증된 라이브러리 사용을 권장합니다. 이들은 동적 높이, 양방향 스크롤, 무한 스크롤 등 고급 기능을 제공합니다.

지연 로딩

이미지, 비디오, 무거운 컴포넌트를 뷰포트에 진입할 때만 로드하는 지연 로딩을 구현합니다. Intersection Observer API를 활용하면 효율적인 지연 로딩을 구현할 수 있습니다. 초기 페이지 로드 시간을 크게 단축하고 불필요한 네트워크 트래픽을 줄일 수 있습니다.

이미지 지연 로딩

LazyImage.svelte
<script>
import { onMount } from 'svelte';

let {
src,
alt,
placeholder = '/placeholder.jpg',
} = $props();

let imageElement;
let isLoaded = $state(false);
let isInView = $state(false);

onMount(() => {
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
isInView = true;
observer.disconnect();
}
});
},
{ threshold: 0.1 }
);

observer.observe(imageElement);

return () => observer.disconnect();
});

$effect(() => {
if (isInView && !isLoaded) {
const img = new Image();
img.onload = () => {
isLoaded = true;
};
img.src = src;
}
});
</script>

<div class="image-wrapper" bind:this="{imageElement}">
<img
src="{isLoaded"
?
src
:
placeholder}
{alt}
class:loaded="{isLoaded}"
/>
</div>

<style>
.image-wrapper {
position: relative;
overflow: hidden;
}

img {
width: 100%;
transition: opacity 0.3s;
opacity: 0.5;
}

img.loaded {
opacity: 1;
}
</style>

실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!

메모이제이션

비용이 큰 계산을 캐싱하여 불필요한 재계산을 방지하는 메모이제이션을 활용합니다. Svelte 5의 $derived는 자동으로 메모이제이션을 제공하지만, 복잡한 경우 커스텀 메모이제이션이 필요할 수 있습니다. 특히 재귀적 계산이나 무거운 데이터 변환에서 성능 향상이 큽니다.

메모이제이션 패턴

MemoizedComputation.svelte
<script>
let numbers = $state([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
let filterThreshold = $state(5);

// 자동 메모이제이션 (Svelte 5 $derived)
let expensiveResult = $derived(() => {
console.log('비용이 큰 계산 실행');
return numbers
.filter(n => n > filterThreshold)
.map(n => ({
value: n,
square: n ** 2,
factorial: factorial(n),
}));
});

// 수동 메모이제이션
let memoCache = new Map();

function factorial(n) {
if (memoCache.has(n)) {
return memoCache.get(n);
}

const result = n <= 1 ? 1 : n * factorial(n - 1);
memoCache.set(n, result);
return result;
}

function addNumber() {
const newNum = Math.max(...numbers) + 1;
numbers = [...numbers, newNum];
}
</script>

<div>
<label>
필터 임계값: {filterThreshold}
<input
type="range"
min="1"
max="10"
bind:value="{filterThreshold}"
/>
</label>

<button onclick="{addNumber}">숫자 추가</button>

<div class="results">
{#each expensiveResult as item}
<div>
{item.value}: 제곱={item.square},
팩토리얼={item.factorial}
</div>
{/each}
</div>
</div>

<style>
.results {
margin-top: 1rem;
padding: 1rem;
background: #f9fafb;
}
</style>

실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!


16.3 SSR과 프리렌더링

서버 사이드 렌더링 설정

SvelteKit의 SSR은 초기 페이지 로드 성능과 SEO를 크게 향상시킵니다. 서버에서 HTML을 생성하여 클라이언트에 전송하므로 First Contentful Paint가 빨라집니다. 하이드레이션을 통해 클라이언트에서 상호작용이 가능한 페이지로 전환됩니다.

SSR 설정과 최적화

+page.server.js
// 서버에서만 실행되는 load 함수
export async function load({ fetch, params }) {
// 데이터베이스나 외부 API 호출
const response = await fetch('/api/products');
const products = await response.json();

// 스트리밍으로 점진적 렌더링
return {
products,
// 지연 로딩 데이터
streamed: {
reviews: loadReviews(params.id),
},
};
}

async function loadReviews(productId) {
// 느린 데이터는 스트리밍으로 처리
await new Promise(r => setTimeout(r, 1000));
const response = await fetch(`/api/reviews/${productId}`);
return response.json();
}
+page.svelte
<script>
export let data;

let { products, streamed } = data;
</script>

<div class="products">
{#each products as product}
<div class="product">
<h3>{product.name}</h3>
<p>{product.price}원</p>
</div>
{/each}
</div>

{#await streamed.reviews}
<div class="loading">리뷰 로딩 중...</div>
{:then reviews}
<div class="reviews">
{#each reviews as review}
<div>{review.content}</div>
{/each}
</div>
{/await}

<style>
.products {
display: grid;
gap: 1rem;
}

.loading {
padding: 2rem;
text-align: center;
}
</style>

실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!

정적 사이트 생성

정적 콘텐츠는 빌드 시점에 미리 생성하여 CDN에서 제공하면 최고의 성능을 얻을 수 있습니다. SvelteKit의 프리렌더링 기능을 사용하면 특정 페이지나 전체 사이트를 정적으로 생성할 수 있습니다. 동적 콘텐츠와 정적 콘텐츠를 혼합하여 최적의 성능과 유연성을 달성할 수 있습니다.

프리렌더링 설정

+page.js
// 개별 페이지 프리렌더링
export const prerender = true;

// 동적 매개변수가 있는 페이지의 프리렌더링
export async function entries() {
const posts = await fetchPosts();
return posts.map(post => ({
slug: post.slug,
}));
}

export async function load({ params }) {
const post = await fetchPost(params.slug);
return { post };
}
svelte.config.js
import adapter from '@sveltejs/adapter-static';

export default {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: null,
precompress: true,
strict: true,
}),
prerender: {
handleMissingId: 'warn',
handleHttpError: ({ path, referrer, message }) => {
console.warn(`${path} 프리렌더링 실패: ${message}`);
},
},
},
};

하이브리드 렌더링

SSR, SSG, CSR을 페이지별로 선택적으로 적용하는 하이브리드 렌더링 전략을 구현합니다. 마케팅 페이지는 정적으로, 대시보드는 CSR로, 상품 페이지는 SSR로 렌더링하는 등 최적화가 가능합니다. 이를 통해 각 페이지의 특성에 맞는 최적의 렌더링 전략을 적용할 수 있습니다.

페이지별 렌더링 전략

+layout.js
// 루트 레이아웃 - 기본 SSR
export const ssr = true;
export const csr = true;
routes/dashboard/+page.js
// 대시보드 - CSR 전용 (SPA 모드)
export const ssr = false;
export const csr = true;
routes/blog/+page.js
// 블로그 - 완전 정적 생성
export const prerender = true;
export const ssr = true;
export const csr = false; // JavaScript 없음
routes/product/[id]/+page.js
// 상품 페이지 - ISR (Incremental Static Regeneration)
export const prerender = 'auto';

export async function load({ params, setHeaders }) {
// 캐시 제어
setHeaders({
'cache-control': 'max-age=60, s-maxage=3600',
});

const product = await fetchProduct(params.id);
return { product };
}

정리

Svelte와 SvelteKit의 성능 최적화 기법을 완전히 마스터했습니다! 이제 다음과 같은 핵심 개념들을 이해했습니다:

핵심 요약

  • 빌드 최적화: 코드 스플리팅, 트리 셰이킹, 번들 분석을 통한 번들 크기 최소화
  • 런타임 최적화: 가상화, 지연 로딩, 메모이제이션으로 실행 성능 향상
  • 렌더링 전략: SSR, SSG, 하이브리드 렌더링으로 최적의 성능과 SEO 달성

실무 활용 팁

  • 번들 분석 도구로 정기적으로 번들 크기 모니터링
  • 긴 리스트는 반드시 가상화 적용
  • 페이지 특성에 맞는 렌더링 전략 선택

다음 단계: 17장 "테스팅"에서는 Vitest를 사용한 단위 테스트와 Playwright를 활용한 E2E 테스트 방법을 알아보겠습니다. 안정적이고 신뢰할 수 있는 애플리케이션을 구축하는 방법을 마스터해보세요!