본문으로 건너뛰기

12. 라우팅 시스템

SvelteKit의 핵심은 파일 시스템 기반 라우팅입니다. URL 경로가 프로젝트의 디렉토리 구조에 의해 자동으로 정의되어 별도의 라우팅 설정 없이도 직관적이고 확장 가능한 웹 애플리케이션을 구축할 수 있습니다. 이 장에서는 기본 라우팅부터 동적 라우팅, 중첩 레이아웃까지 SvelteKit의 강력한 라우팅 시스템을 완전히 마스터해보겠습니다.


12.1 파일 시스템 기반 라우팅

라우팅의 기본 개념

SvelteKit은 파일 시스템 기반 라우팅을 사용합니다. src/routes 디렉토리의 구조가 그대로 URL 경로가 되어 별도의 라우터 설정이나 구성 파일이 필요하지 않습니다. 이는 Next.js, Nuxt.js와 유사한 방식이지만 더 간단하고 직관적인 컨벤션을 제공합니다.

기본 라우팅 구조

디렉토리 구조URL 경로설명
src/routes/+page.svelte/홈페이지
src/routes/about/+page.svelte/aboutAbout 페이지
src/routes/blog/+page.svelte/blog블로그 목록
src/routes/contact/+page.svelte/contact연락처 페이지

+page.svelte 파일

src/routes/+page.svelte
<!-- 홈페이지 -->
<script>
let title = $state('SvelteKit 홈페이지');
</script>

<svelte:head>
<title>{title}</title>
</svelte:head>

<h1>환영합니다!</h1>
<p>SvelteKit으로 만든 웹사이트입니다.</p>
<nav>
<a href="/about">소개</a>
<a href="/blog">블로그</a>
<a href="/contact">연락처</a>
</nav>

<style>
nav {
margin-top: 2rem;
display: flex;
gap: 1rem;
}

a {
color: #3b82f6;
text-decoration: none;
padding: 0.5rem;
border: 1px solid #3b82f6;
border-radius: 4px;
}
</style>
src/routes/about/+page.svelte
<!-- About 페이지 -->
<script>
let company = $state({
name: 'SvelteKit Company',
founded: 2023,
description:
'SvelteKit을 활용한 혁신적인 웹 솔루션 개발',
});
</script>

<svelte:head>
<title>소개 - {company.name}</title>
</svelte:head>

<h1>회사 소개</h1>
<div class="company-info">
<h2>{company.name}</h2>
<p>설립년도: {company.founded}</p>
<p>{company.description}</p>
</div>

<a href="/">홈으로</a>

<style>
.company-info {
background: #f8fafc;
padding: 2rem;
margin: 1rem 0;
border-radius: 8px;
}
</style>

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

중첩 라우팅

디렉토리를 중첩하면 URL 경로도 자동으로 중첩됩니다. 각 레벨에서 독립적인 +page.svelte 파일을 가질 수 있어 계층적인 사이트 구조를 쉽게 만들 수 있습니다.

src/routes/blog/+page.svelte
<!-- 블로그 목록 페이지 -->
<script>
let posts = $state([
{
id: 1,
title: 'SvelteKit 시작하기',
slug: 'getting-started',
},
{
id: 2,
title: '라우팅 마스터하기',
slug: 'routing-guide',
},
{ id: 3, title: '성능 최적화', slug: 'performance' },
]);
</script>

<h1>블로그</h1>
<div class="posts">
{#each posts as post}
<article>
<h2><a href="/blog/{post.slug}">{post.title}</a></h2>
<p>게시글 ID: {post.id}</p>
</article>
{/each}
</div>

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

article {
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 4px;
}
</style>
src/routes/blog/category/+page.svelte
<!-- 블로그 카테고리 페이지 -->
<script>
let categories = $state([
'JavaScript',
'SvelteKit',
'웹 개발',
'성능 최적화',
]);
</script>

<h1>블로그 카테고리</h1>
<ul>
{#each categories as category}
<li>
<a href="/blog/category/{category.toLowerCase()}"
>{category}</a
>
</li>
{/each}
</ul>

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


12.2 동적 라우팅

매개변수 라우트 [slug]

대괄호로 감싸진 폴더명은 동적 라우트 매개변수가 됩니다. URL에서 해당 부분의 값을 params 객체를 통해 접근할 수 있어 데이터베이스나 API에서 동적으로 콘텐츠를 가져올 수 있습니다.

src/routes/blog/[slug]/+page.svelte
<!-- 개별 블로그 포스트 페이지 -->
<script>
let { data } = $props();
</script>

<svelte:head>
<title>{data.post.title} - 블로그</title>
</svelte:head>

<article>
<h1>{data.post.title}</h1>
<div class="meta">
<p>게시일: {data.post.date}</p>
<p>조회수: {data.post.views}</p>
</div>
<div class="content">{data.post.content}</div>
</article>

<nav>
<a href="/blog">블로그 목록으로</a>
</nav>

<style>
.meta {
color: #6b7280;
font-size: 0.9rem;
margin: 1rem 0;
}

.content {
line-height: 1.6;
margin: 2rem 0;
}
</style>
src/routes/blog/[slug]/+page.js
// 블로그 포스트 데이터 로더
export async function load({ params }) {
const posts = {
'getting-started': {
title: 'SvelteKit 시작하기',
date: '2024-01-15',
views: 1250,
content:
'SvelteKit은 Svelte 기반의 풀스택 웹 프레임워크입니다...',
},
'routing-guide': {
title: '라우팅 마스터하기',
date: '2024-01-20',
views: 890,
content:
'SvelteKit의 파일 기반 라우팅 시스템을 알아봅시다...',
},
performance: {
title: '성능 최적화',
date: '2024-01-25',
views: 567,
content:
'SvelteKit 애플리케이션의 성능을 최적화하는 방법...',
},
};

const post = posts[params.slug];

if (!post) {
throw new Error('포스트를 찾을 수 없습니다');
}

return {
post,
};
}

여러 매개변수 활용

하나의 라우트에서 여러 매개변수를 사용할 수 있습니다. 각 매개변수는 최소 하나의 문자로 구분되어야 하며, URL 패턴에 따라 유연하게 데이터를 처리할 수 있습니다.

src/routes/shop/[category]/[product]/+page.svelte
<!-- 상품 상세 페이지 -->
<script>
let { data } = $props();
</script>

<nav class="breadcrumb">
<a href="/shop">쇼핑</a>
<span>></span>
<a href="/shop/{data.category}">{data.categoryName}</a>
<span>></span>
<span>{data.product.name}</span>
</nav>

<div class="product-detail">
<img
src="{data.product.image}"
alt="{data.product.name}"
/>
<div class="product-info">
<h1>{data.product.name}</h1>
<p class="price">
{data.product.price.toLocaleString()}원
</p>
<p>{data.product.description}</p>
<button>장바구니 담기</button>
</div>
</div>

<style>
.breadcrumb {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
color: #6b7280;
}

.product-detail {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}

.price {
font-size: 1.5rem;
font-weight: bold;
color: #ef4444;
}
</style>
src/routes/shop/[category]/[product]/+page.js
export async function load({ params }) {
const categories = {
electronics: '전자제품',
clothing: '의류',
books: '도서',
};

const products = {
'electronics-laptop': {
name: '고성능 노트북',
price: 1500000,
image: '/laptop.jpg',
description:
'최신 프로세서와 고화질 디스플레이를 탑재한 노트북',
},
'clothing-shirt': {
name: '캐주얼 셔츠',
price: 45000,
image: '/shirt.jpg',
description: '편안한 착용감의 면 소재 셔츠',
},
};

const productKey = `${params.category}-${params.product}`;
const product = products[productKey];
const categoryName = categories[params.category];

if (!product || !categoryName) {
throw new Error('상품을 찾을 수 없습니다');
}

return {
category: params.category,
categoryName,
product,
};
}

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

나머지 매개변수 [...rest]

[...rest] 패턴은 임의의 수의 경로 세그먼트를 포착합니다. 파일 경로나 중첩된 카테고리 등 가변 길이의 URL 구조를 처리할 때 유용합니다.

src/routes/docs/[...path]/+page.svelte
<!-- 문서 페이지 -->
<script>
let { data } = $props();
</script>

<div class="docs-layout">
<nav class="sidebar">
<h3>문서</h3>
<ul>
{#each data.sections as section}
<li>
<a href="/docs/{section.path}">{section.title}</a>
</li>
{/each}
</ul>
</nav>

<main class="content">
<h1>{data.title}</h1>
<div class="breadcrumb">
<a href="/docs">문서</a>
{#each data.pathSegments as segment, index}
<span>></span>
<a
href="/docs/{data.pathSegments.slice(0, index + 1).join('/')}"
>{segment}</a
>
{/each}
</div>
<div class="doc-content">{data.content}</div>
</main>
</div>

<style>
.docs-layout {
display: grid;
grid-template-columns: 250px 1fr;
gap: 2rem;
}

.sidebar {
padding: 1rem;
background: #f8fafc;
border-radius: 8px;
}

.breadcrumb {
display: flex;
gap: 0.5rem;
margin: 1rem 0;
color: #6b7280;
}
</style>
src/routes/docs/[...path]/+page.js
export async function load({ params }) {
const path = params.path || '';
const pathSegments = path ? path.split('/') : [];

const docs = {
'': {
title: '문서 홈',
content: '문서 사이트에 오신 것을 환영합니다.',
},
'getting-started': {
title: '시작하기',
content:
'SvelteKit 프로젝트를 시작하는 방법을 알아봅시다.',
},
'advanced/routing': {
title: '고급 라우팅',
content:
'동적 라우팅과 매개변수 활용법을 설명합니다.',
},
'advanced/performance': {
title: '성능 최적화',
content:
'애플리케이션 성능을 향상시키는 기법들입니다.',
},
};

const sections = [
{ path: '', title: '홈' },
{ path: 'getting-started', title: '시작하기' },
{ path: 'advanced/routing', title: '고급 라우팅' },
{ path: 'advanced/performance', title: '성능 최적화' },
];

const doc = docs[path];

if (!doc) {
throw new Error('문서를 찾을 수 없습니다');
}

return {
title: doc.title,
content: doc.content,
pathSegments,
sections,
};
}

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

선택적 매개변수 [[optional]]

이중 대괄호는 선택적 매개변수를 만듭니다. 국제화나 기본값이 있는 라우트에서 유용하며, 매개변수가 있을 때와 없을 때 모두 같은 페이지를 보여줄 수 있습니다.

src/routes/[[lang]]/+page.svelte
<!-- 다국어 홈페이지 -->
<script>
let { data } = $props();
</script>

<div class="language-switcher">
<a href="/" class:active="{data.lang" ="" ="" ="ko" }
>한국어</a
>
<a href="/en" class:active="{data.lang" ="" ="" ="en" }
>English</a
>
<a href="/ja" class:active="{data.lang" ="" ="" ="ja" }
>日本語</a
>
</div>

<main>
<h1>{data.welcome}</h1>
<p>{data.description}</p>
</main>

<style>
.language-switcher {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
}

.language-switcher a {
padding: 0.5rem 1rem;
border: 1px solid #e5e7eb;
border-radius: 4px;
text-decoration: none;
}

.language-switcher a.active {
background: #3b82f6;
color: white;
}
</style>
src/routes/[[lang]]/+page.js
export async function load({ params }) {
const lang = params.lang || 'ko'; // 기본값은 한국어

const translations = {
ko: {
welcome: '환영합니다!',
description:
'SvelteKit으로 만든 다국어 웹사이트입니다.',
},
en: {
welcome: 'Welcome!',
description:
'This is a multilingual website built with SvelteKit.',
},
ja: {
welcome: 'ようこそ!',
description:
'SvelteKitで作成された多言語ウェブサイトです。',
},
};

const content = translations[lang] || translations.ko;

return {
lang,
...content,
};
}

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


12.3 고급 라우팅

라우트 그룹 (group)

괄호로 감싼 폴더명은 URL에 포함되지 않는 라우트 그룹을 만듭니다. 관련된 라우트들을 논리적으로 그룹화하면서도 URL 구조는 깔끔하게 유지할 수 있어 대규모 애플리케이션의 구조 관리에 유용합니다.

src/routes/(app)/dashboard/+page.svelte
<!-- 대시보드 페이지 -->
<script>
let { data } = $props();
</script>

<div class="dashboard">
<h1>대시보드</h1>
<div class="stats-grid">
{#each data.stats as stat}
<div class="stat-card">
<h3>{stat.title}</h3>
<div class="stat-value">{stat.value}</div>
<div
class="stat-change"
class:positive="{stat.change"
>
0}> {stat.change > 0 ? '+' : ''}{stat.change}%
</div>
</div>
{/each}
</div>
</div>

<style>
.stats-grid {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(200px, 1fr)
);
gap: 1rem;
margin-top: 2rem;
}

.stat-card {
padding: 1.5rem;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.stat-value {
font-size: 2rem;
font-weight: bold;
margin: 0.5rem 0;
}

.stat-change.positive {
color: #10b981;
}
</style>
src/routes/(app)/+layout.svelte
<!-- 앱 레이아웃 -->
<script>
let { children } = $props();
</script>

<div class="app-layout">
<nav class="sidebar">
<h2>애플리케이션</h2>
<ul>
<li><a href="/dashboard">대시보드</a></li>
<li><a href="/profile">프로필</a></li>
<li><a href="/settings">설정</a></li>
</ul>
</nav>

<main class="content">{@render children()}</main>
</div>

<style>
.app-layout {
display: grid;
grid-template-columns: 250px 1fr;
min-height: 100vh;
}

.sidebar {
background: #1f2937;
color: white;
padding: 2rem;
}

.sidebar ul {
list-style: none;
padding: 0;
margin-top: 2rem;
}

.sidebar li {
margin: 1rem 0;
}

.sidebar a {
color: #d1d5db;
text-decoration: none;
display: block;
padding: 0.5rem;
border-radius: 4px;
}

.sidebar a:hover {
background: #374151;
color: white;
}

.content {
padding: 2rem;
background: #f9fafb;
}
</style>

매개변수 검증

매개변수 매처(parameter matcher)를 사용하여 라우트 매개변수의 형식을 검증할 수 있습니다. src/params 디렉토리에 매처 함수를 정의하고 [param=matcher] 형식으로 사용합니다.

src/params/integer.js
// 정수 매처
export function match(param) {
return /^\d+$/.test(param);
}
src/routes/user/[id=integer]/+page.svelte
<!-- 사용자 프로필 페이지 (ID는 정수만 허용) -->
<script>
let { data } = $props();
</script>

<div class="user-profile">
<img
src="{data.user.avatar}"
alt="{data.user.name} 프로필"
class="avatar"
/>
<div class="user-info">
<h1>{data.user.name}</h1>
<p>사용자 ID: {data.user.id}</p>
<p>이메일: {data.user.email}</p>
<p>가입일: {data.user.joinDate}</p>
</div>
</div>

<style>
.user-profile {
display: flex;
gap: 2rem;
align-items: center;
}

.avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
}

.user-info h1 {
margin: 0 0 1rem 0;
color: #1f2937;
}

.user-info p {
margin: 0.5rem 0;
color: #6b7280;
}
</style>
src/routes/user/[id=integer]/+page.js
export async function load({ params }) {
const userId = parseInt(params.id);

// 실제로는 데이터베이스나 API에서 가져옴
const users = {
1: {
id: 1,
name: '김개발',
email: 'kim@example.com',
avatar: '/avatars/kim.jpg',
joinDate: '2024-01-15',
},
2: {
id: 2,
name: '박프론트',
email: 'park@example.com',
avatar: '/avatars/park.jpg',
joinDate: '2024-02-01',
},
};

const user = users[userId];

if (!user) {
throw new Error('사용자를 찾을 수 없습니다');
}

return {
user,
};
}

라우트 매칭

SvelteKit은 여러 라우트가 같은 URL과 매치될 때 우선순위에 따라 결정합니다. 더 구체적인 라우트가 높은 우선순위를 가지며, 매개변수가 없는 라우트가 동적 라우트보다 우선됩니다.

src/routes/blog/latest/+page.svelte
<!-- 최신 블로그 (고정 라우트) -->
<script>
let { data } = $props();
</script>

<h1>최신 블로그 포스트</h1>
<div class="latest-posts">
{#each data.posts as post}
<article class="post-preview">
<h2><a href="/blog/{post.slug}">{post.title}</a></h2>
<p class="post-date">{post.date}</p>
<p>{post.excerpt}</p>
</article>
{/each}
</div>

<style>
.latest-posts {
display: grid;
gap: 2rem;
margin-top: 2rem;
}

.post-preview {
padding: 1.5rem;
border: 1px solid #e5e7eb;
border-radius: 8px;
}

.post-date {
color: #6b7280;
font-size: 0.9rem;
}
</style>

이 예제에서 /blog/latest는 고정 라우트이므로 /blog/[slug] 동적 라우트보다 높은 우선순위를 갖습니다.

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


12.4 레이아웃

+layout.svelte 파일

+layout.svelte 파일은 해당 디렉토리와 하위 디렉토리의 모든 페이지에 공통으로 적용되는 레이아웃을 정의합니다. 중복되는 UI 요소를 한 곳에서 관리하여 코드 재사용성을 높이고 일관된 사용자 경험을 제공할 수 있습니다.

src/routes/+layout.svelte
<!-- 루트 레이아웃 -->
<script>
let { children } = $props();

let navigation = $state([
{ href: '/', label: '홈' },
{ href: '/about', label: '소개' },
{ href: '/blog', label: '블로그' },
{ href: '/contact', label: '연락처' },
]);
</script>

<div class="app">
<header>
<nav>
<div class="logo">
<a href="/">SvelteKit 사이트</a>
</div>
<ul class="nav-links">
{#each navigation as link}
<li>
<a href="{link.href}">{link.label}</a>
</li>
{/each}
</ul>
</nav>
</header>

<main>{@render children()}</main>

<footer>
<p>
&copy; 2024 SvelteKit Company. All rights reserved.
</p>
</footer>
</div>

<style>
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}

header {
background: #1f2937;
color: white;
padding: 1rem 0;
}

nav {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}

.logo a {
font-size: 1.5rem;
font-weight: bold;
color: white;
text-decoration: none;
}

.nav-links {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 2rem;
}

.nav-links a {
color: #d1d5db;
text-decoration: none;
transition: color 0.2s;
}

.nav-links a:hover {
color: white;
}

main {
flex: 1;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
width: 100%;
}

footer {
background: #f3f4f6;
padding: 2rem 0;
text-align: center;
color: #6b7280;
}
</style>

중첩 레이아웃

여러 레벨의 레이아웃을 중첩할 수 있습니다. 페이지는 자신이 속한 디렉토리와 상위 디렉토리의 모든 레이아웃을 상속받아 계층적인 UI 구조를 만들 수 있습니다.

src/routes/admin/+layout.svelte
<!-- 관리자 레이아웃 -->
<script>
let { children } = $props();

let adminMenu = $state([
{
href: '/admin/dashboard',
label: '대시보드',
icon: '📊',
},
{
href: '/admin/users',
label: '사용자 관리',
icon: '👥',
},
{ href: '/admin/settings', label: '설정', icon: '⚙️' },
]);
</script>

<div class="admin-layout">
<div class="admin-header">
<h1>관리자 패널</h1>
<div class="user-info">
<span>관리자님 환영합니다</span>
<a href="/logout">로그아웃</a>
</div>
</div>

<div class="admin-content">
<aside class="admin-sidebar">
<nav>
<ul>
{#each adminMenu as item}
<li>
<a href="{item.href}">
<span class="icon">{item.icon}</span>
{item.label}
</a>
</li>
{/each}
</ul>
</nav>
</aside>

<div class="admin-main">{@render children()}</div>
</div>
</div>

<style>
.admin-layout {
background: #f8fafc;
min-height: calc(100vh - 200px);
}

.admin-header {
background: #3b82f6;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}

.user-info {
display: flex;
gap: 1rem;
align-items: center;
}

.user-info a {
color: #dbeafe;
text-decoration: none;
}

.admin-content {
display: grid;
grid-template-columns: 250px 1fr;
min-height: 500px;
}

.admin-sidebar {
background: white;
border-right: 1px solid #e5e7eb;
padding: 2rem 0;
}

.admin-sidebar ul {
list-style: none;
padding: 0;
margin: 0;
}

.admin-sidebar a {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 2rem;
color: #374151;
text-decoration: none;
transition: background-color 0.2s;
}

.admin-sidebar a:hover {
background: #f3f4f6;
}

.admin-main {
padding: 2rem;
}
</style>
src/routes/admin/users/+page.svelte
<!-- 사용자 관리 페이지 -->
<script>
let users = $state([
{
id: 1,
name: '김사용자',
email: 'kim@example.com',
role: '일반',
},
{
id: 2,
name: '박관리자',
email: 'park@example.com',
role: '관리자',
},
{
id: 3,
name: '최개발자',
email: 'choi@example.com',
role: '개발자',
},
]);
</script>

<div class="users-page">
<div class="page-header">
<h2>사용자 관리</h2>
<button class="btn-primary">사용자 추가</button>
</div>

<div class="users-table">
<table>
<thead>
<tr>
<th>ID</th>
<th>이름</th>
<th>이메일</th>
<th>역할</th>
<th>액션</th>
</tr>
</thead>
<tbody>
{#each users as user}
<tr>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
<span class="role-badge">{user.role}</span>
</td>
<td>
<button class="btn-sm">편집</button>
<button class="btn-sm btn-danger">삭제</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>

<style>
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}

.btn-primary {
background: #3b82f6;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}

.users-table {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

table {
width: 100%;
border-collapse: collapse;
}

th {
background: #f9fafb;
padding: 1rem;
text-align: left;
font-weight: 600;
}

td {
padding: 1rem;
border-bottom: 1px solid #f3f4f6;
}

.role-badge {
background: #dbeafe;
color: #1e40af;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.8rem;
}

.btn-sm {
padding: 0.25rem 0.5rem;
margin: 0 0.25rem;
border: none;
border-radius: 4px;
cursor: pointer;
background: #e5e7eb;
}

.btn-danger {
background: #ef4444;
color: white;
}
</style>

레이아웃 상속과 재설정

특정 페이지에서 상위 레이아웃을 건너뛰고 다른 레이아웃을 사용하려면 @ 기호를 사용합니다. 이를 통해 예외적인 페이지 구조나 완전히 다른 디자인을 가진 페이지를 유연하게 구현할 수 있습니다.

src/routes/login/+page@.svelte
<!-- 로그인 페이지 (루트 레이아웃만 사용) -->
<script>
let loginData = $state({
email: '',
password: '',
});

function handleSubmit() {
console.log('로그인 시도:', loginData);
}
</script>

<div class="login-page">
<div class="login-container">
<div class="login-form">
<h1>로그인</h1>
<form on:submit|preventDefault="{handleSubmit}">
<div class="form-group">
<label for="email">이메일</label>
<input
id="email"
type="email"
bind:value="{loginData.email}"
required
/>
</div>

<div class="form-group">
<label for="password">비밀번호</label>
<input
id="password"
type="password"
bind:value="{loginData.password}"
required
/>
</div>

<button type="submit" class="login-btn">
로그인
</button>
</form>

<p class="signup-link">
계정이 없으신가요? <a href="/signup">회원가입</a>
</p>
</div>
</div>
</div>

<style>
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(
135deg,
#667eea 0%,
#764ba2 100%
);
}

.login-container {
background: white;
padding: 3rem;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}

.login-form h1 {
text-align: center;
margin-bottom: 2rem;
color: #1f2937;
}

.form-group {
margin-bottom: 1.5rem;
}

.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #374151;
font-weight: 500;
}

.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 1rem;
}

.login-btn {
width: 100%;
background: #3b82f6;
color: white;
border: none;
padding: 0.75rem;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}

.login-btn:hover {
background: #2563eb;
}

.signup-link {
text-align: center;
margin-top: 2rem;
color: #6b7280;
}

.signup-link a {
color: #3b82f6;
text-decoration: none;
}
</style>

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


정리

SvelteKit의 라우팅 시스템을 완전히 마스터했습니다! 이제 다음과 같은 핵심 개념들을 이해했습니다:

핵심 요약

  • 파일 시스템 기반 라우팅: 디렉토리 구조가 URL 경로를 정의하여 직관적이고 유지보수가 쉬운 라우팅 시스템
  • 동적 라우팅: [slug], [...rest], [[optional]] 패턴을 통한 유연하고 강력한 URL 매개변수 처리
  • 고급 라우팅: 라우트 그룹, 매개변수 검증, 우선순위를 통한 복잡한 애플리케이션 구조 구현
  • 레이아웃 시스템: 중첩 레이아웃과 상속을 통한 일관된 UI 구조와 코드 재사용성 극대화

실무 활용 팁

  • 라우트 그룹 (name)을 활용하여 URL 구조를 깔끔하게 유지하면서 파일 구조 정리
  • 매개변수 매처를 사용하여 URL 검증과 SEO 최적화 구현
  • 레이아웃 상속을 적절히 활용하여 공통 UI 관리와 특별한 페이지 디자인 구현

다음 단계: 13장 "데이터 로딩"에서는 load 함수를 통한 서버 사이드와 클라이언트 사이드 데이터 처리 방법을 알아보겠습니다. 효율적인 데이터 페칭과 상태 관리를 마스터해보세요!