본문으로 건너뛰기

Cursor에서 Svelte 및 SvelteKit 개발: 완전 가이드

Cursor에서 Svelte 개발

Svelte는 브라우저에 최소한의 JavaScript를 전송하는 컴파일러 기반 UI 프레임워크이며, SvelteKit은 그 풀스택 애플리케이션 프레임워크입니다. Svelte 5는 이전 let 기반 반응성보다 더 명시적이고 강력한 새로운 반응성 모델인 runes를 도입했습니다. Cursor는 VS Code 호환 기반과 통합 AI 어시스턴트로 Svelte 작업에 탁월한 에디터입니다 — .svelte 파일을 이해하고, 풍부한 IntelliSense를 제공하며, 자연어 프롬프트에서 컴포넌트를 생성할 수 있습니다.

이 가이드는 Cursor에서의 완전한 Svelte 개발 워크플로를 다룹니다: 툴체인 설정, Svelte 5 runes 마스터하기, 파일 기반 라우팅으로 SvelteKit 앱 구축, stores로 공유 상태 관리, AI를 활용한 컴포넌트 생성 및 리팩토링.

Cursor에서 Svelte 개발 환경 설정​

전제 조건​

Svelte 5와 SvelteKit은 Node.js 18 이상이 필요합니다. 설치를 확인하세요:

node --version
npm --version

더 새 버전이 필요한 경우 fnm 또는 nvm을 사용합니다:

fnm install 20
fnm use 20
팁

Cursor의 통합 터미널(Ctrl+`)은 현재 작업 디렉토리를 기억하므로, 프로젝트를 스캐폴드하고 컨텍스트 전환 없이 즉시 파일을 열 수 있습니다.

Svelte 확장 설치​

Cursor는 언어 지원을 위해 공식 Svelte 확장에 의존합니다. 확장 보기(Ctrl+Shift+X)에서 설치합니다:

  1. "Svelte"를 검색합니다.
  2. Svelte 팀이 게시한 확장을 설치합니다.
  3. 메시지가 표시되면 Cursor를 다시 로드합니다.

이 확장은 다음을 제공합니다:

  • .svelte 파일의 구문 강조.
  • svelte-language-server를 통한 진단 및 타입 검사.
  • Prettier와 prettier-plugin-svelte 패키지를 통한 포맷팅.
  • 컴포넌트 import 및 props에 대한 IntelliSense.
경고

이전에 커뮤니티 Svelte 확장을 사용한 경우, 공식 확장을 설치하기 전에 비활성화하세요. 충돌하는 확장은 중복 진단과 신뢰할 수 없는 포맷팅을 유발합니다.

SvelteKit 프로젝트 스캐폴딩​

공식 sv CLI를 사용하여 새 SvelteKit 애플리케이션을 생성합니다:

npx sv create my-svelte-app

대화형 프롬프트에서 사전 설정을 제공합니다:

? Which template would you like? SvelteKit minimal
? Add type checking with TypeScript? Yes, using TypeScript syntax
? Add ESLint for code linting? Yes
? Add Prettier for code formatting? Yes
? Add Playwright for browser testing? No
? Add Vitest for unit testing? Yes

Cursor에서 프로젝트를 엽니다:

cd my-svelte-app
cursor .
npm install
npm run dev

개발 서버는 http://localhost:5173에서 시작됩니다.

팁

프로젝트를 다시 만들지 않고도 나중에 기능을 추가할 수 있습니다. sv add 명령은 기존 앱에 Tailwind CSS, Storybook 또는 Drizzle과 같은 통합을 설치합니다:

npx sv add tailwindcss
npx sv add storybook

Svelte 5 Runes 마스터하기​

Runes란 무엇인가?​

Runes는 Svelte 5에서 도입된 특별한 컴파일러 인식 함수입니다. Svelte 4의 암시적 반응성을 명시적이고 예측 가능한 모델로 대체합니다. 핵심 runes는 다음과 같습니다:

  • $state — 반응형 상태를 선언합니다.
  • $derived — 다른 반응형 상태에서 계산된 값을 선언합니다.
  • $effect — 의존성이 변경될 때 부작용을 실행합니다.
  • $props — 컴포넌트 props를 선언합니다.
  • $bindable — prop을 양방향 바인딩 가능으로 표시합니다.

$state로 상태 선언​

<script lang="ts">
let count = $state(0);
let user = $state({ name: 'Ada', age: 36 });

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

<button onclick={increment}>
Count: {count}
</button>
<p>{user.name} is {user.age}</p>
팁

Svelte 5에서 이벤트 핸들러는 이전 on:click 대신 onclick 속성(소문자, on: 접두사 없음)을 사용합니다. 새 구문은 표준 DOM 속성에 더 가깝고 TypeScript와 더 잘 작동합니다.

$derived로 파생된 값​

<script lang="ts">
let price = $state(100);
let quantity = $state(2);
let total = $derived(price * quantity);
let formatted = $derived(
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(total)
);
</script>

<p>Price: $ {price}</p>
<p>Quantity: {quantity}</p>
<p>Total: {formatted}</p>

$effect로 부작용 처리​

<script lang="ts">
let theme = $state<'light' | 'dark'>('light');

$effect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
});
</script>

<button onclick={() => (theme = theme === 'light' ? 'dark' : 'light')}>
Toggle theme
</button>
경고

$effect는 DOM이 업데이트된 후 실행됩니다. 렌더링에 영향을 미치는 값을 계산하는 데 사용하지 마세요 — 대신 $derived를 사용하세요. Effects는 외부 시스템(API, localStorage, 서드파티 라이브러리)과 동기화하는 데 사용됩니다.

$props로 컴포넌트 Props 선언​

<script lang="ts">
interface Props {
title: string;
count?: number;
onIncrement?: (value: number) => void;
}

let { title, count = 0, onIncrement }: Props = $props();

function handleClick() {
count++;
onIncrement?.(count);
}
</script>

<h2>{title}</h2>
<button onclick={handleClick}>Clicked {count} times</button>

$bindable로 양방향 바인딩​

<!-- TextInput.svelte -->
<script lang="ts">
interface Props {
value?: string;
}
let { value = $bindable('') }: Props = $props();
</script>

<input bind:value />

부모에서 사용:

<script lang="ts">
import TextInput from './TextInput.svelte';
let name = $state('');
</script>

<TextInput bind:value={name} />
<p>Hello, {name}!</p>
팁

$bindable은 Svelte 5에서 export let + bind:value 패턴을 대체합니다. 양방향 바인딩 계약을 자식 컴포넌트의 타입 시그니처에서 명시적으로 만듭니다.

SvelteKit 애플리케이션 구축​

파일 기반 라우팅​

SvelteKit은 src/routes 디렉토리 내에서 파일 기반 라우팅을 사용합니다. 각 +page.svelte 파일은 페이지가 되고, +layout.svelte 파일은 중첩된 페이지를 감쌉니다.

src/routes/
├── +layout.svelte
├── +page.svelte → /
├── about/
│ └── +page.svelte → /about
├── blog/
│ ├── +page.svelte → /blog
│ └── [slug]/
│ └── +page.svelte → /blog/:slug
└── +layout.svelte

+page.server.ts로 데이터 로드​

// src/routes/blog/[slug]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

interface Post {
slug: string;
title: string;
body: string;
}

export const load: PageServerLoad = async ({ params }) => {
const response = await fetch(`https://api.example.com/posts/${params.slug}`);
if (!response.ok) {
throw error(404, 'Post not found');
}
const post: Post = await response.json();
return { post };
};

페이지에서 데이터 소비:

<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>

<article>
<h1>{data.post.title}</h1>
<p>{data.post.body}</p>
</article>
팁

$types 모듈은 SvelteKit에 의해 자동 생성됩니다. 완전히 타입이 지정된 load 함수와 data props를 제공합니다. Cursor의 IntelliSense는 이러한 타입을 자동으로 읽으므로 data.post.title에 대한 자동 완성을 받을 수 있습니다.

폼 액션​

SvelteKit은 +page.server.ts에 정의된 액션으로 폼 제출을 처리합니다:

// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';

export const actions: Actions = {
default: async ({ request, cookies }) => {
const formData = await request.formData();
const email = formData.get('email');
const password = formData.get('password');

if (!email || !password) {
return fail(400, { message: 'Missing credentials' });
}

const token = await authenticate(String(email), String(password));
if (!token) {
return fail(401, { message: 'Invalid credentials' });
}

cookies.set('session', token, { path: '/' });
throw redirect(303, '/dashboard');
},
};

해당 페이지는 점진적 향상을 위해 enhance 액션을 사용합니다:

<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageData } from './$types';
let { form }: { form: PageData } = $props();
</script>

<form method="POST" use:enhance>
<label>Email <input name="email" type="email" /></label>
<label>Password <input name="password" type="password" /></label>
{#if form?.message}
<p class="error">{form.message}</p>
{/if}
<button type="submit">Log in</button>
</form>
경고

서버 측 +page.server.ts 파일은 서버에서만 실행됩니다. 브라우저 전용 API(예: localStorage)를 import하지 마시고, 비밀을 +page.ts(유니버설 load)에 두지 마세요. 이것도 브라우저에서 실행될 수 있습니다.

레이아웃 및 레이아웃 그룹​

<!-- src/routes/+layout.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
import Nav from '../components/Nav.svelte';

let { children }: { children: Snippet } = $props();
</script>

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

Svelte 5에서 children snippet이 더 이상 사용되지 않는 <slot /> 구문을 대체합니다.

Stores로 공유 상태 관리​

Svelte Stores 기본​

runes가 있어도 Svelte stores는 컴포넌트 간 상태에 유용합니다. store는 subscribe 메서드가 있는 모든 객체입니다.

// lib/stores/user.ts
import { writable } from 'svelte/store';

interface User {
id: string;
name: string;
}

export const currentUser = writable<User | null>(null);

$ 접두사로 store 소비:

<script lang="ts">
import { currentUser } from '$lib/stores/user';
</script>

{#if $currentUser}
<p>Welcome, {$currentUser.name}</p>
{:else}
<p>Please log in.</p>
{/if}

Readable 및 Derived Stores​

import { readable, derived } from 'svelte/store';

export const time = readable(new Date(), (set) => {
const interval = setInterval(() => set(new Date()), 1000);
return () => clearInterval(interval);
});

export const formattedTime = derived(time, ($time) =>
$time.toLocaleTimeString('en-US')
);

Runes와 Stores 결합​

$effect 내에서 stores를 사용하여 레거시 코드와 runes를 브리지할 수 있습니다:

<script lang="ts">
import { currentUser } from '$lib/stores/user';
import { get } from 'svelte/store';

let userSnapshot = $state(get(currentUser));

$effect(() => {
const unsubscribe = currentUser.subscribe((value) => {
userSnapshot = value;
});
return unsubscribe;
});
</script>
팁

새 프로젝트의 경우 $state 필드가 있는 클래스와 Svelte context API를 사용하는 runes 기반 공유 상태 패턴을 권장합니다. 이는 반응성을 유지하면서 store 보일러플레이트를 피합니다:

// lib/state/counter.svelte.ts
export class CounterState {
count = $state(0);
increment() {
this.count++;
}
}

AI 지원 Svelte 컴포넌트 생성​

프롬프트에서 컴포넌트 생성​

Cursor의 AI는 runes를 사용하는 Svelte 5 컴포넌트 생성에 탁월합니다. 채팅을 열거나(Ctrl+Shift+P → "Cursor: New Chat") 인라인 편집(Ctrl+K)을 사용하고 필요한 것을 설명합니다:

Create a Svelte 5 component (script lang="ts") for an accessible accordion.
It should accept an items prop: array of { id, title, content }. Use runes
for state (which panel is open). Include keyboard navigation (arrow keys)
and scoped styles.

Cursor는 완전한 .svelte 파일을 생성합니다. 출력을 검토하고 반복합니다 — allowMultiple prop이나 애니메이션 전환 추가를 요청합니다.

Svelte 4에서 Svelte 5로 마이그레이션​

오래된 코드베이스를 유지 관리하는 경우, Cursor는 컴포넌트를 runes 구문으로 마이그레이션할 수 있습니다. 파일을 강조 표시하고 Ctrl+K를 누르고 프롬프트를 입력합니다:

Migrate this Svelte 4 component to Svelte 5 runes syntax. Replace `let`
reactivity with $state, `$:` with $derived/$effect, `export let` with $props,
and `createEventDispatcher` with callback props. Preserve all behavior.
경고

Runes 마이그레이션은 미묘한 방식으로 반응성 타이밍을 변경합니다. 리팩토링 후 테스트 스위트를 실행하고 라이프사이클 훅이나 와처를 사용하는 모든 컴포넌트를 수동으로 확인하세요. $effect는 이전 afterUpdate와 다른 조건에서 발생합니다.

엔드투엔드 테스트 생성​

Playwright를 AI 생성 테스트와 페어링합니다:

// tests/accordion.spec.ts
import { test, expect } from '@playwright/test';

test('accordion expands on click', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Section 1' }).click();
await expect(page.getByText('Content of section 1')).toBeVisible();
});

Cursor에 프롬프트: "Generate Playwright tests for Accordion.svelte covering expand/collapse and keyboard navigation."

Svelte 프로젝트를 위한 Cursor 구성​

워크스페이스 설정​

팀 전체에서 포맷팅을 표준화하기 위해 .vscode/settings.json을 생성합니다:

{
"editor.formatOnSave": true,
"editor.defaultFormatter": "svelte.svelte-vscode",
"svelte.plugin.svelte.format.config.singleQuote": true,
"svelte.plugin.svelte.format.config.semi": true,
"svelte.plugin.svelte.note-new-transformation": false,
"typescript.tsdk": "node_modules/typescript/lib"
}

Svelte 플러그인과 Prettier​

npm install -D prettier prettier-plugin-svelte

.prettierrc:

{
"singleQuote": true,
"semi": true,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": { "parser": "svelte" }
}
]
}
팁

Prettier는 <script> 및 <style> 블록을 포맷하지만 디렉티브를 손상시키지 않도록 Svelte 템플릿 마크업은 의도적으로 그대로 둡니다. 템플릿 포맷팅은 Svelte 확장의 내장 포매터에 의존하세요.

ESLint 구성​

// eslint.config.js
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import tsParser from 'typescript-eslint';

export default [
js.configs.recommended,
...svelte.configs['flat/recommended'],
{
languageOptions: {
parserOptions: {
extraFileExtensions: ['.svelte'],
},
},
},
];

디버깅 및 성능​

Svelte DevTools​

컴포넌트 계층과 상태를 검사하기 위해 Svelte DevTools 브라우저 확장을 설치합니다. Cursor가 에디터를 호스트하지만, 브라우저 DevTools는 runes 디버깅에 중요한 런타임 값을 표시합니다.

프로덕션에서의 Source Maps​

SvelteKit은 기본적으로 source maps를 생성합니다. svelte.config.js에서 확인:

import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
},
};

export default config;
경고

서버리스 플랫폼에 배포할 때 adapter가 source maps를 올바르게 처리하는지 확인하세요. adapter-node와 adapter-vercel은 다르게 동작합니다 — source maps를 프로덕션으로 보내기 전에 adapter 문서를 검토하세요.

번들 분석​

번들 크기를 검사하기 위해 rollup-plugin-visualizer를 추가합니다:

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

export default defineConfig({
plugins: [
sveltekit(),
visualizer({
filename: 'stats.html',
open: true,
gzipSize: true,
}),
],
});

Cursor에서 Svelte 개발을 위한 모범 사례​

프로젝트 구조​

src/
├── lib/
│ ├── components/
│ │ ├── Accordion.svelte
│ │ └── TextInput.svelte
│ ├── stores/
│ │ └── user.ts
│ ├── state/
│ │ └── counter.svelte.ts
│ └── utils/
├── routes/
│ ├── +layout.svelte
│ └── +page.svelte
└── app.html

Runes 파일 명명 규칙​

Svelte 5는 컴포넌트 외부에서 runes를 사용하는 모듈을 위해 .svelte.ts(및 .svelte.js) 확장자를 도입했습니다. 공유 상태 파일을 그에 따라 명명합니다:

// src/lib/state/cart.svelte.ts
export class CartState {
items = $state<{ id: number; qty: number }[]>([]);

get total() {
return this.items.reduce((sum, i) => sum + i.qty, 0);
}

add(id: number, qty = 1) {
this.items.push({ id, qty });
}
}
팁

.svelte.ts 접미사는 Svelte 컴파일러에게 일반 TypeScript 모듈 내에서 runes를 처리하도록 지시합니다. 이것 없이 $state는 정의되지 않은 함수로 처리되고 빌드가 실패합니다.

기본 접근성​

Svelte의 컴파일러는 누락된 alt, aria-*, 레이블 연결에 대해 경고합니다. CI에서 이러한 경고를 오류로 처리합니다:

// svelte.config.js
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

const config = {
preprocess: vitePreprocess(),
kit: { adapter: adapter() },
compilerOptions: {
accessors: false,
},
};

export default config;

일반적인 함정 및 피하는 방법​

레거시 이벤트 구문 사용​

Svelte 5는 on:click을 onclick으로 대체하여 더 이상 사용하지 않습니다. 전환 중에 둘을 혼합하면 작동하지만 경고가 생성됩니다. 새 구문으로 표준화하세요:

<!-- Svelte 4 (더 이상 사용되지 않음) -->
<button on:click={handler}>Click</button>

<!-- Svelte 5 (권장) -->
<button onclick={handler}>Click</button>

Props 수정​

Props는 기본적으로 읽기 전용입니다. 양방향 바인딩에는 $bindable을 사용하거나 콜백을 내보냅니다:

<script lang="ts">
interface Props {
value: string;
onChange?: (v: string) => void;
}
let { value, onChange }: Props = $props();

function handleInput(e: Event) {
const target = e.target as HTMLInputElement;
onChange?.(target.value);
}
</script>

<input value={value} oninput={handleInput} />

$effect 과도한 사용​

Effects는 외부 시스템과 동기화해야 하며, 렌더링 값을 계산해서는 안 됩니다. 템플릿에서 사용되는 변수를 업데이트하기 위해 $effect를 작성하고 있다면 $derived로 전환하세요:

<script lang="ts">
let a = $state(1);
let b = $state(2);

// 잘못됨 — 파생된 값에 effect 사용
// let sum = $state(0);
// $effect(() => { sum = a + b; });

// 올바름 — 파생
let sum = $derived(a + b);
</script>
경고

자체에 의존하는 effect는 무한 루프를 만들 수 있습니다. Svelte 컴파일러는 일부 경우를 감지하지만, 항상 effect의 의존성이 유한하고 종료되는지 확인하세요.

요약​

Cursor는 Svelte 및 SvelteKit 개발을 위한 강력한 환경입니다. 공식 Svelte 확장은 타입 안전한 작성과 포맷팅을 제공하고, Svelte 5 runes는 명시적이고 예측 가능한 반응성을 가져오며, SvelteKit의 파일 기반 라우팅과 타입이 지정된 load 함수는 풀스택 작업을 가속화합니다. 이러한 도구를 컴포넌트 생성, 리팩토링, 테스트 스캐폴딩을 위한 Cursor의 AI 어시스턴트와 결합하여 현대적인 Svelte 애플리케이션을 효율적으로 구축할 수 있습니다. sv create로 시작하고, Prettier 및 ESLint 구성을 고정하고, runes와 AI가 반복적인 스캐폴딩을 처리하도록 하는 동안 세련된 사용자 경험을 제공하는 데 집중하세요.