Claude Code로 Svelte와 SvelteKit 개발하기
Claude Code로 Svelte/SvelteKit을 안전하게 개발하는 실전 가이드. runes, 라우팅, form actions, 테스트까지 다룹니다.
Claude Code와 SvelteKit이 잘 맞는 이유
Svelte는 선언형 컴포넌트를 가벼운 JavaScript로 컴파일하는 UI 프레임워크입니다. SvelteKit은 그 위에 파일 기반 라우팅, 서버 사이드 렌더링, load 함수, form actions, API 엔드포인트, 배포 어댑터를 더한 애플리케이션 프레임워크입니다. 작은 UI는 단순하지만, 실제 기능은 src/routes, src/lib/components, $lib/server, 타입, 테스트를 함께 바꾸는 경우가 많습니다.
Claude Code는 코드베이스를 읽고, 파일을 수정하고, 명령을 실행할 수 있는 Anthropic의 agentic coding 도구입니다. SvelteKit 프로젝트에서는 “앱 전체를 만들어 줘”보다 “이 라우트와 이 컴포넌트를 읽고, 수정 범위를 제안한 뒤, 최소 변경으로 고쳐 줘”가 훨씬 안전합니다. 특히 초보자는 Svelte 5의 runes와 SvelteKit의 서버/클라이언트 경계를 명확히 적어야 합니다.
runes는 반응형 코드를 표현하는 현재 방식입니다. $props는 부모 컴포넌트가 넘기는 입력, $state는 바뀌는 상태, $derived는 상태에서 계산되는 값, $effect는 브라우저에서만 필요한 부수 효과에 사용합니다. 이 글은 작은 태스크 관리 앱을 기준으로 프로젝트 생성, 컴포넌트, 공유 상태, 라우팅, form actions, 테스트, Claude Code에 안전하게 요청하는 법을 정리합니다.
flowchart LR
A["작은 요구사항 작성"] --> B["Claude Code가 관련 파일 읽기"]
B --> C["Svelte 컴포넌트 수정"]
C --> D["load/actions 확인"]
D --> E["타입 체크와 테스트 실행"]
E --> F["git diff를 사람이 리뷰"]
프로젝트 생성과 첫 요청
새 SvelteKit 프로젝트는 공식 Svelte CLI인 sv로 시작하는 것이 현재 기본입니다. 공식 문서의 흐름은 npx sv create my-app입니다. 독립적인 Svelte 위젯이나 Vite 앱만 필요하면 Vite의 svelte-ts 템플릿을 사용할 수 있습니다. Vite 공식 가이드는 Vite 자체에 Node.js 20.19 이상 또는 22.12 이상을 요구하므로, 오래된 Node에서 생기는 오류를 Svelte 문제로 착각하지 않는 것이 좋습니다.
npx sv create claude-svelte-demo
cd claude-svelte-demo
npm install
npm run dev
claude
처음에는 Claude Code를 plan mode로 쓰는 편이 좋습니다. 이 모드는 파일을 읽고 계획을 세우지만 소스는 직접 수정하지 않습니다.
/plan
이 SvelteKit 프로젝트에 태스크 목록 기능을 추가하고 싶습니다.
먼저 src/routes와 src/lib 구조를 읽고, 수정할 파일, 데이터 흐름, 테스트 계획을 제안해 주세요.
아직 파일은 수정하지 마세요.
claude --permission-mode plan
프로젝트 규칙은 CLAUDE.md 또는 .claude/CLAUDE.md에 둡니다. 예를 들어 “Svelte 5 runes 우선”, “폼은 SvelteKit actions 사용”, “비밀 값은 $lib/server 밖으로 내보내지 않기”, “완료 전 npm run check 실행”, “요청 없이는 commit 금지”처럼 구체적으로 적습니다.
Svelte 5 컴포넌트를 작게 만들기
아래 코드는 src/lib/components/TaskCard.svelte에 그대로 둘 수 있는 예시입니다. props 타입을 명확히 하고, 표시용 값은 $derived로 계산합니다. 버튼의 aria-pressed도 유지해 접근성을 잃지 않도록 했습니다.
<!-- src/lib/components/TaskCard.svelte -->
<script lang="ts">
type Task = {
id: string;
title: string;
done: boolean;
estimateMinutes: number;
tags: string[];
};
let {
task,
onToggle
}: {
task: Task;
onToggle: (id: string) => void;
} = $props();
let statusLabel = $derived(task.done ? '완료' : '진행 중');
let estimateLabel = $derived(`${Math.ceil(task.estimateMinutes / 15) * 15}분 단위`);
</script>
<article class:done={task.done} class="task-card">
<div>
<p class="status">{statusLabel}</p>
<h3>{task.title}</h3>
<p>{estimateLabel}</p>
</div>
<ul aria-label="태그">
{#each task.tags as tag}
<li>{tag}</li>
{/each}
</ul>
<button type="button" aria-pressed={task.done} onclick={() => onToggle(task.id)}>
{task.done ? '미완료로 변경' : '완료로 변경'}
</button>
</article>
<style>
.task-card {
display: grid;
gap: 0.75rem;
border: 1px solid #ddd;
border-radius: 0.5rem;
padding: 1rem;
}
.done {
background: #f2fff5;
}
.status {
font-size: 0.875rem;
font-weight: 700;
}
ul {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
list-style: none;
padding: 0;
}
li {
border-radius: 999px;
background: #eef2ff;
padding: 0.2rem 0.6rem;
}
</style>
Claude Code에 컴포넌트 수정을 요청할 때는 공개 API를 보호해야 합니다.
src/lib/components/TaskCard.svelte를 개선하되 Svelte 5 runes 문법은 유지해 주세요.
조건:
- Task 타입과 onToggle 시그니처를 바꾸지 않기
- onclick을 유지하고 legacy on:click으로 되돌리지 않기
- 버튼의 aria-pressed 유지
- 레이아웃과 빈 상태 대응만 개선
- 수정 후 컴포넌트 테스트 1개를 제안
공유 상태: runes와 stores 구분하기
Svelte 5에서는 .svelte.ts 파일에서도 runes를 사용할 수 있습니다. 공식 문서에서는 이런 파일이 재사용 가능한 반응형 로직과 공유 상태에 유용하다고 설명합니다. 반면 svelte/store는 복잡한 비동기 스트림, 수동 구독, 기존 store 기반 라이브러리와의 연결이 필요할 때 여전히 좋습니다.
// src/lib/state/taskFilters.svelte.ts
export type TaskStatus = 'all' | 'open' | 'done';
export const taskFilters = $state({
query: '',
status: 'all' as TaskStatus,
tag: ''
});
export function resetTaskFilters() {
taskFilters.query = '';
taskFilters.status = 'all';
taskFilters.tag = '';
}
<!-- src/lib/components/TaskFilterPanel.svelte -->
<script lang="ts">
import { resetTaskFilters, taskFilters } from '$lib/state/taskFilters.svelte';
</script>
<section aria-label="태스크 필터">
<label>
키워드
<input bind:value={taskFilters.query} placeholder="청구서, 글쓰기, 리뷰..." />
</label>
<label>
상태
<select bind:value={taskFilters.status}>
<option value="all">전체</option>
<option value="open">미완료</option>
<option value="done">완료</option>
</select>
</label>
<button type="button" onclick={resetTaskFilters}>초기화</button>
</section>
주의할 점은 SSR입니다. window, document, localStorage는 브라우저에만 있습니다. 브라우저 저장소를 사용해야 한다면 $app/environment의 browser로 보호합니다.
// src/lib/state/theme.svelte.ts
import { browser } from '$app/environment';
export const themeState = $state({
theme: 'system' as 'system' | 'light' | 'dark'
});
export function loadTheme() {
if (!browser) return;
const saved = localStorage.getItem('theme');
if (saved === 'light' || saved === 'dark' || saved === 'system') {
themeState.theme = saved;
}
}
export function saveTheme(nextTheme: typeof themeState.theme) {
themeState.theme = nextTheme;
if (browser) localStorage.setItem('theme', nextTheme);
}
라우팅, load 함수, 서버 전용 코드
SvelteKit은 파일 시스템 기반 라우터를 사용합니다. src/routes/about은 /about, src/routes/tasks/[slug]는 slug 파라미터를 가진 상세 페이지가 됩니다. 데이터를 미리 읽어야 하면 같은 폴더에 +page.server.ts를 둡니다. DB 접근, 비밀 키, 관리자 API는 $lib/server에 두고 컴포넌트에서 직접 import하지 않는 것이 중요합니다.
// src/lib/server/tasks.ts
export type Task = {
id: string;
slug: string;
title: string;
done: boolean;
estimateMinutes: number;
tags: string[];
};
const tasks: Task[] = [
{
id: 'task-1',
slug: 'write-svelte-guide',
title: 'SvelteKit 글 초안 작성',
done: false,
estimateMinutes: 45,
tags: ['writing', 'svelte']
}
];
export async function getTaskBySlug(slug: string) {
return tasks.find((task) => task.slug === slug) ?? null;
}
// src/routes/tasks/[slug]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { getTaskBySlug } from '$lib/server/tasks';
export const load: PageServerLoad = async ({ params }) => {
const task = await getTaskBySlug(params.slug);
if (!task) {
error(404, 'Task not found');
}
return { task };
};
<!-- src/routes/tasks/[slug]/+page.svelte -->
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
</script>
<svelte:head>
<title>{data.task.title} | Tasks</title>
</svelte:head>
<article>
<p>{data.task.done ? '완료' : '미완료'}</p>
<h1>{data.task.title}</h1>
<p>예상 시간: {data.task.estimateMinutes}분</p>
</article>
요청 예시는 다음처럼 범위와 금지사항을 함께 둡니다.
src/routes/tasks/[slug]와 src/lib/server/tasks.ts를 읽어 주세요.
Task에 dueDate 필드를 추가하고 상세 페이지에 표시해 주세요.
서버 전용 데이터 접근은 $lib/server 안에 유지해 주세요.
[slug] 라우트 디렉터리 이름은 바꾸지 마세요.
마지막에 npm run check를 실행해 주세요.
form actions와 점진적 향상
SvelteKit form actions는 +page.server.ts에서 actions를 export하고, HTML의 <form method="POST">로 서버에 제출하는 방식입니다. JavaScript가 없어도 작동하고, use:enhance를 붙이면 JavaScript가 있을 때 사용자 경험을 향상시킬 수 있습니다.
// src/routes/contact/+page.server.ts
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions = {
default: async ({ request }) => {
const formData = await request.formData();
const values = {
name: String(formData.get('name') ?? '').trim(),
email: String(formData.get('email') ?? '').trim(),
message: String(formData.get('message') ?? '').trim()
};
const errors: Record<string, string> = {};
if (values.name.length < 2) errors.name = '이름은 2자 이상 입력해 주세요.';
if (!values.email.includes('@')) errors.email = '이메일 주소를 확인해 주세요.';
if (values.message.length < 10) errors.message = '내용은 10자 이상 입력해 주세요.';
if (Object.keys(errors).length > 0) {
return fail(400, { values, errors });
}
console.log('New inquiry', values);
return { success: true };
}
} satisfies Actions;
<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageProps } from './$types';
let { form }: PageProps = $props();
</script>
{#if form?.success}
<p role="status">전송되었습니다. 1~3영업일 안에 답변드리겠습니다.</p>
{/if}
<form method="POST" use:enhance>
<label>
이름
<input name="name" value={form?.values?.name ?? ''} aria-invalid={!!form?.errors?.name} />
</label>
{#if form?.errors?.name}<p>{form.errors.name}</p>{/if}
<label>
이메일
<input name="email" type="email" value={form?.values?.email ?? ''} aria-invalid={!!form?.errors?.email} />
</label>
{#if form?.errors?.email}<p>{form.errors.email}</p>{/if}
<label>
문의 내용
<textarea name="message" rows="5" aria-invalid={!!form?.errors?.message}>{form?.values?.message ?? ''}</textarea>
</label>
{#if form?.errors?.message}<p>{form.errors.message}</p>{/if}
<button type="submit">보내기</button>
</form>
폼을 Claude Code에 맡길 때는 “서버 검증 유지”, “JavaScript 없이도 POST 가능”, “접근성 속성 삭제 금지”를 꼭 적습니다. GET으로 부수 효과를 만들거나, 비밀 키를 브라우저에 노출하거나, 검증되지 않은 내용을 {@html}로 렌더링하면 안 됩니다.
테스트, 활용 사례, 함정
Svelte 공식 테스트 문서는 Vite와 SvelteKit에서 Vitest가 잘 맞는 선택지라고 설명합니다. 컴포넌트는 Testing Library로 사용자 행동에 가깝게 테스트하고, 실제 라우팅과 폼 제출은 Playwright 같은 E2E 테스트로 확인합니다.
// src/lib/components/TaskCard.test.ts
import { fireEvent, render, screen } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import TaskCard from './TaskCard.svelte';
describe('TaskCard', () => {
it('toggles the task when the button is clicked', async () => {
let toggledId = '';
render(TaskCard, {
task: {
id: 'task-1',
title: 'SvelteKit 글쓰기',
done: false,
estimateMinutes: 45,
tags: ['writing']
},
onToggle: (id) => {
toggledId = id;
}
});
await fireEvent.click(screen.getByRole('button', { name: '완료로 변경' }));
expect(toggledId).toBe('task-1');
});
});
npm run check
npm run test
npm run build
git diff -- src/lib src/routes
| 활용 사례 | Claude Code에 맡기기 좋은 일 | 사람이 확인할 일 |
|---|---|---|
| 관리자 필터 UI | $state와 $derived로 필터 상태 정리 | URL에 남길 조건, 권한으로 숨길 필드 |
| 블로그/CMS 상세 페이지 | [slug], load, SEO 제목, 404 처리 연결 | HTML 정화, 초안 공개 범위, 미리보기 규칙 |
| 문의/리드 폼 | actions, 검증, use:enhance, 테스트 작성 | 개인정보 저장, 알림 대상, 스팸 대응 |
| Svelte 4에서 5로 이전 | 선택한 컴포넌트만 runes로 변환 | 자동 변환이 동작을 바꾸지 않았는지 |
흔한 실패는 다섯 가지입니다. 너무 큰 요청을 한 번에 던지는 것, Svelte 4와 5 문법을 섞는 것, 컴포넌트에서 $lib/server를 import하는 것, $effect를 일반 계산에 남용하는 것, 그리고 버튼 존재만 확인하는 약한 테스트를 그대로 통과시키는 것입니다.
수익화 관점에서는 SvelteKit의 페이지와 폼을 상담 또는 제품 흐름에 연결하기 쉽습니다. ClaudeCodeLab에서는 이 글에서 Claude Code consultation page로 이어질 수 있습니다. 더 기초가 필요하면 Claude Code 시작 가이드, TypeScript 팁, 테스트 전략을 함께 읽어 보세요.
공식 자료와 실제 결과
동작을 확인할 때는 공식 문서를 우선하세요. Svelte docs, SvelteKit docs, SvelteKit form actions, Vite guide, Claude Code docs가 기준입니다.
현재 SvelteKit 구조를 읽은 뒤 아래 범위만 수정해 주세요.
파일: src/routes/contact/+page.svelte, src/routes/contact/+page.server.ts
목표: 문의 폼에 company 필드 추가
조건:
- Svelte 5 runes 문법 유지
- use:enhance 삭제 금지
- 서버 측 검증 추가
- 기존 CTA 문구와 레이아웃 class 변경 금지
- 완료 전 npm run check 실행
마지막에 변경점, 위험, 부족한 테스트를 3줄로 요약해 주세요.
Masa가 작은 SvelteKit 태스크 앱에서 이 절차를 시험한 결과, plan mode로 먼저 구조를 읽힌 경우가 가장 손실이 적었습니다. “runes 문법 유지”, “npm run check 실행”, “commit 금지”를 명시하자 diff가 작아졌고, Svelte 초보자도 변경 이유를 따라가기 쉬웠습니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.