메인 콘텐츠로 건너뛰기

Svelte란

Svelte는 JavaScript 프레임워크 중에서도 독자적인 위치를 차지하고 있습니다. React나 Vue가 런타임 라이브러리로 동작하는 것에 비해, Svelte는 컴파일러로 동작합니다. 빌드 시에 컴포넌트를 순수한 JavaScript로 변환하므로, 브라우저에 여분의 프레임워크 코드를 보낼 필요가 없습니다. Svelte의 최대 특징은 Virtual DOM을 사용하지 않는다는 점입니다. 상태가 변화하면, Svelte가 컴파일 타임에 생성한 코드가 직접 DOM을 갱신합니다. 이로 인해 매우 경량이고 고속인 UI를 실현할 수 있습니다.
이 페이지에서 설명하는 것은 Svelte 5와 Inertia v3의 조합입니다. Laravel 13의 스타터 킷은 이 구성을 기본으로 사용합니다.

Svelte 5의 룬

Svelte 5(2024년 릴리스)에서는 **룬(Runes)**이라 불리는 새로운 리액티비티 시스템이 도입되었습니다. $state · $derived · $effect 등의 특별한 함수(룬)를 사용하여 리액티브한 상태를 선언합니다. Svelte 4 이전의 암묵적인 리액티비티와는 달리, 명시적이고 예측하기 쉬운 설계로 되어 있습니다.
<script lang="ts">
    let count = $state(0)

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

<button onclick={increment}>{count}</button>
Laravel의 Svelte 스타터 킷은 Svelte 5 + TypeScript가 표준입니다. 본 페이지의 예시도 모두 TypeScript(lang="ts")로 기술합니다.

Laravel에서의 포지션

역사

Svelte와 Laravel의 관계는 Vue · React에 비해 매우 새로우며, 공식 스타터 킷으로의 채용이 최초의 공식 지원입니다. 2026년의 Laravel 13에서 스타터 킷에 Svelte가 정식 추가된 것에 의해, Svelte는 Laravel 에코시스템에서의 공식 프론트엔드 선택지 중 하나가 되었습니다. Vue · React와 동렬로 취급되며, laravel new의 대화 프롬프트에서 선택할 수 있습니다. Laravel 사용자에게는 아직 그다지 알려지지 않은 프레임워크이지만, 컴파일러에 의한 경량 번들심플한 구문이 특징으로, Vue나 React에서 갈아타면 쓰는 양이 적은 것에 놀랄지도 모릅니다.

현재의 주류 스타일: Inertia × Svelte

현재 Laravel에서 Svelte 사용법의 중심은 Inertia × Svelte입니다. Inertia는 API를 설계하지 않고 Laravel의 컨트롤러에서 직접 Svelte 컴포넌트에 데이터를 전달할 수 있는 “모던 모놀리스” 아키텍처를 실현합니다.

셋업

스타터 킷 경유 (권장)

신규 프로젝트에서 시작하는 경우에는 스타터 킷을 사용하는 것이 가장 손쉽습니다.
laravel new my-app
대화식 프롬프트에서 Svelte를 선택하면 이하가 모두 자동으로 셋업됩니다.
  • inertiajs/inertia-laravel (서버사이드 어댑터)
  • @inertiajs/svelte (클라이언트 어댑터)
  • svelte + @sveltejs/vite-plugin-svelte (Svelte 5 본체와 Vite 플러그인)
  • TypeScript + svelte-check
  • Tailwind CSS + shadcn-svelte 컴포넌트 라이브러리
  • HandleInertiaRequests 미들웨어
  • 로그인 · 회원가입 등의 인증 화면 (Inertia + Svelte + TypeScript로 구현 완료)

수동 설치

기존 프로젝트에 추가하는 경우에는 서버사이드와 클라이언트사이드를 별도로 설치합니다.
# 서버사이드 (PHP)
composer require inertiajs/inertia-laravel

# 클라이언트사이드 (JavaScript)
npm install @inertiajs/svelte @inertiajs/vite svelte
npm install --save-dev @sveltejs/vite-plugin-svelte svelte-check typescript
다음으로, vite.config.ts에 Svelte 플러그인과 Inertia Vite 플러그인을 추가합니다.
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import inertia from '@inertiajs/vite'

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.ts'],
            refresh: true,
        }),
        svelte(),
        inertia(),
    ],
})
resources/js/app.ts에서 Inertia 앱을 기동합니다. @inertiajs/vite 플러그인이 페이지의 자동 해결과 마운트를 수행하므로, 최소한의 엔트리 포인트만으로 충분합니다.
import { createInertiaApp } from '@inertiajs/svelte'

createInertiaApp()
수동 설치의 세부 내용(루트 템플릿의 설정이나 미들웨어의 등록 등)은 Inertia 공식 문서를 참고하세요.

디렉터리 구조

스타터 킷에서는 Svelte의 페이지 컴포넌트를 resources/js/pages/ 디렉터리에 배치합니다.
resources/js/
├── app.ts             # Inertia 앱의 기점
├── components/        # 재이용 가능한 UI 컴포넌트
│   └── ui/            # shadcn-svelte 컴포넌트
├── layouts/           # 레이아웃 컴포넌트
│   ├── AppLayout.svelte
│   └── AuthLayout.svelte
├── lib/               # 유틸리티 함수 · Svelte 룬 모듈
├── pages/             # Inertia 페이지 컴포넌트 (컨트롤러 이름에 대응)
│   ├── auth/
│   │   ├── Login.svelte
│   │   └── Register.svelte
│   ├── Dashboard.svelte
│   └── posts/
│       ├── Index.svelte
│       ├── Create.svelte
│       └── Show.svelte
└── types/             # TypeScript 타입 정의
Inertia::render('posts/Index', [...])이라고 쓰면 resources/js/pages/posts/Index.svelte가 대응하는 컴포넌트가 됩니다.

Svelte 파일의 기본 구조

.svelte 파일은 **<script> · 템플릿 · <style>**의 3개 블록으로 구성됩니다.
<script lang="ts">
    // 로직 (TypeScript)
    let name = $state('Laravel')
</script>

<!-- 템플릿 (HTML 라이크한 표기법) -->
<h1>Hello, {name}!</h1>

<style>
    /* 스코프가 부여된 CSS (이 컴포넌트에만 적용) */
    h1 {
        color: #ff2d20;
    }
</style>
Vue의 Single File Component(SFC)와 비슷한 구조이지만, 템플릿과 스크립트의 기술량이 적은 것이 특징입니다. <style>은 기본으로 스코프가 한정되므로, 클래스 이름의 충돌을 걱정할 필요가 없습니다.

템플릿 구문

변수 전개와 식

템플릿 내에서는 {}를 사용하여 JavaScript의 값이나 식을 삽입합니다.
<script lang="ts">
    let name = $state('세계')
    let count = $state(3)
</script>

<p>안녕하세요, {name}!</p>
<p>2배는 {count * 2}</p>

{#if} — 조건 분기

<script lang="ts">
    let isLoggedIn = $state(false)
    let role = $state('editor')
</script>

{#if isLoggedIn}
    <p>어서 오세요!</p>
{:else if role === 'admin'}
    <p>관리자로 로그인 중</p>
{:else}
    <a href="/login">로그인</a>
{/if}
Vue의 v-if / v-else, React의 삼항 연산자에 해당합니다.

{#each} — 리스트 렌더링

<script lang="ts">
    type Post = { id: number; title: string }
    let posts = $state<Post[]>([
        { id: 1, title: '첫 번째 게시물' },
        { id: 2, title: '두 번째 게시물' },
    ])
</script>

<ul>
    {#each posts as post (post.id)}
        <li>{post.title}</li>
    {/each}
</ul>
(post.id)는 키 지정이며, 효율적인 차분 갱신에 사용됩니다. Vue의 v-for / React의 Array.map()에 해당합니다.

bind: — 양방향 바인딩

bind:value를 사용하면, 폼 요소의 값과 리액티브 변수를 양방향으로 동기화할 수 있습니다.
<script lang="ts">
    let title = $state('')
    let agreed = $state(false)
    let role = $state('viewer')
</script>

<!-- 텍스트 입력 -->
<input bind:value={title} type="text" />
<p>입력 중: {title}</p>

<!-- 체크박스 -->
<input bind:checked={agreed} type="checkbox" />
<p>동의: {agreed}</p>

<!-- 셀렉트박스 -->
<select bind:value={role}>
    <option value="viewer">열람자</option>
    <option value="editor">편집자</option>
    <option value="admin">관리자</option>
</select>
Vue의 v-model에 해당합니다. React에서는 onChange 핸들러를 수기로 작성해야 했지만, Svelte에서는 bind:로 선언적으로 작성할 수 있습니다.

페이지 컴포넌트의 기본

Inertia의 페이지 컴포넌트는 일반적인 Svelte 컴포넌트입니다. Laravel의 컨트롤러에서 전달된 데이터를 props로 받을 수 있습니다.

컨트롤러

// app/Http/Controllers/PostController.php
use Inertia\Inertia;
use App\Models\Post;

class PostController extends Controller
{
    public function index()
    {
        return Inertia::render('posts/Index', [
            'posts' => Post::latest()->paginate(10),
        ]);
    }
}

Svelte 페이지 컴포넌트

Svelte 5에서는 $props() 룬을 사용하여 props를 받습니다.
<!-- resources/js/pages/posts/Index.svelte -->
<script lang="ts">
    import { Link } from '@inertiajs/svelte'

    type Post = {
        id: number
        title: string
        created_at: string
    }

    type Props = {
        posts: {
            data: Post[]
        }
    }

    let { posts }: Props = $props()
</script>

<div>
    <h1>게시물 목록</h1>
    {#each posts.data as post (post.id)}
        <article>
            <h2>
                <Link href={`/posts/${post.id}`}>{post.title}</Link>
            </h2>
            <p>{post.created_at}</p>
        </article>
    {/each}
</div>
$props()로 props를 받는 것만으로, 컨트롤러에서 전달된 데이터를 템플릿에서 사용할 수 있습니다. REST API를 정의할 필요는 없습니다.
@inertiajs/svelte가 제공하는 <Link> 컴포넌트를 사용하면, 페이지 전환이 XHR로 이루어져, 브라우저의 풀 리로드를 회피할 수 있습니다.
<script lang="ts">
    import { Link } from '@inertiajs/svelte'
</script>

<!-- 기본 링크 -->
<Link href="/posts">게시물 목록</Link>

<!-- POST 메서드로 링크 (삭제 등) -->
<Link href="/posts/1" method="delete" as="button">
    삭제
</Link>

<!-- 프리로드 (호버 시 사전 취득) -->
<Link href="/posts/1" preload>게시물 보기</Link>
일반적인 <a> 태그와 같은 방식으로 작성할 수 있지만, 뒤에서 Inertia가 페이지 컴포넌트만을 교체하므로 SPA 같은 조작감이 됩니다.

Form 컴포넌트

@inertiajs/svelte가 제공하는 <Form> 컴포넌트는, 스타터 킷의 인증 화면에서 사용되고 있는 폼 송신의 권장 스타일입니다. actionmethod를 props로 지정하고, 내부를 **{#snippet}**으로 기술합니다.

기본 사용법

<script lang="ts">
    import { Form } from '@inertiajs/svelte'
</script>

<Form action="/posts" method="post" class="flex flex-col gap-4">
    {#snippet children({ errors, processing })}
        <div>
            <label for="title">제목</label>
            <input id="title" name="title" type="text" required />
            {#if errors.title}
                <p class="error">{errors.title}</p>
            {/if}
        </div>

        <div>
            <label for="content">본문</label>
            <textarea id="content" name="content"></textarea>
            {#if errors.content}
                <p class="error">{errors.content}</p>
            {/if}
        </div>

        <button type="submit" disabled={processing}>
            {processing ? '전송 중...' : '게시하기'}
        </button>
    {/snippet}
</Form>
{#snippet children({ errors, processing })}는 Svelte의 스니펫 구문으로, 컴포넌트에 전달할 콘텐츠 블록입니다 (Vue의 슬롯이나 React의 render props에 해당). errorsprocessingForm 컴포넌트가 자동으로 산출하여 전달해 줍니다. 폼 필드에는 bind:value가 아니라 HTML 네이티브의 name 속성을 사용하며, 브라우저의 표준 폼 데이터 수집이 기능합니다.

스타터 킷의 패턴

스타터 킷은 Wayfinder를 사용하여 라우트를 오브젝트로 관리하고 있습니다. store.form()은 라우트 오브젝트의 actionmethod를 포함하는 오브젝트를 반환하고, <Form>에 spread합니다.
<script lang="ts">
    import { Form } from '@inertiajs/svelte'
    import { store } from '@/routes/login'
</script>

<Form
    {...store.form()}
    resetOnSuccess={['password']}
    class="flex flex-col gap-6"
>
    {#snippet children({ errors, processing })}
        <!-- 폼 내용 -->
    {/snippet}
</Form>
resetOnSuccess에 지정한 필드는 송신 성공 시에 자동으로 리셋됩니다. 패스워드 필드 등 송신 후에 비우고 싶은 필드에 지정합니다.
Wayfinder를 사용하지 않는 경우는 action="/login"처럼 직접 URL을 전달하면 같은 방식으로 동작합니다.

useForm

폼 처리에는 @inertiajs/svelteuseForm 훅을 사용합니다. 폼의 상태 관리 · 송신 · 유효성 검증 오류 표시가 심플하게 구현됩니다.

컨트롤러 측

// app/Http/Controllers/PostController.php
class PostController extends Controller
{
    public function store(Request $request)
    {
        $validated = $request->validate([
            'title'   => ['required', 'string', 'max:255'],
            'content' => ['required', 'string'],
        ]);

        Post::create($validated + ['user_id' => auth()->id()]);

        return redirect()->route('posts.index')
            ->with('success', '게시물을 작성했습니다.');
    }
}

Svelte 폼 컴포넌트

<!-- resources/js/pages/posts/Create.svelte -->
<script lang="ts">
    import { useForm } from '@inertiajs/svelte'

    const form = useForm({
        title: '',
        content: '',
    })

    function submit(e: SubmitEvent) {
        e.preventDefault()
        form.post('/posts')
    }
</script>

<form onsubmit={submit}>
    <div>
        <label>제목</label>
        <input type="text" bind:value={form.data.title} />
        {#if form.errors.title}
            <p class="error">{form.errors.title}</p>
        {/if}
    </div>

    <div>
        <label>본문</label>
        <textarea bind:value={form.data.content}></textarea>
        {#if form.errors.content}
            <p class="error">{form.errors.content}</p>
        {/if}
    </div>

    <button type="submit" disabled={form.processing}>
        {form.processing ? '전송 중...' : '게시하기'}
    </button>
</form>
useForm이 반환하는 오브젝트의 주요 프로퍼티를 정리합니다.
프로퍼티 / 메서드설명
form.data폼의 데이터 오브젝트
form.errors유효성 검증 오류 (필드명으로 접근)
form.processing송신 중은 true (버튼 무효화에 사용)
form.isDirty초기값에서 변경된 경우 true
form.post(url)POST 요청으로 송신
form.put(url)PUT 요청으로 송신 (갱신)
form.delete(url)DELETE 요청으로 송신
form.reset()폼을 초기값으로 리셋
유효성 검증 오류가 반환되었을 때, useForm은 입력 내용을 유지한 채 오류를 표시합니다. bind:value와의 조합으로 심리스한 폼 경험을 실현할 수 있습니다.

공유 데이터 (Shared Data)

모든 페이지에서 공통으로 필요한 데이터(로그인 중인 사용자 정보 · 플래시 메시지 등)는 HandleInertiaRequests 미들웨어의 share() 메서드로 정의합니다.
// app/Http/Middleware/HandleInertiaRequests.php
use Illuminate\Http\Request;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    public function share(Request $request): array
    {
        return array_merge(parent::share($request), [
            'auth' => [
                'user' => $request->user()
                    ? $request->user()->only('id', 'name', 'email')
                    : null,
            ],
            'flash' => [
                'success' => $request->session()->get('success'),
                'error'   => $request->session()->get('error'),
            ],
        ]);
    }
}
Svelte 컴포넌트에서 공유 데이터에 접근하려면 usePage()를 사용합니다.
<script lang="ts">
    import { usePage } from '@inertiajs/svelte'

    type SharedProps = {
        auth: {
            user: { id: number; name: string; email: string } | null
        }
        flash: {
            success: string | null
            error: string | null
        }
    }

    const page = usePage<SharedProps>()
</script>

<header>
    {#if page.props.auth.user}
        <span>{page.props.auth.user.name}</span>
    {:else}
        <span>게스트</span>
    {/if}
</header>

{#if page.props.flash.success}
    <div class="alert-success">{page.props.flash.success}</div>
{/if}
공유 데이터는 모든 요청에 포함되므로, 필요 최소한의 데이터로 좁힐 것을 권장합니다. fn()을 사용한 지연 평가로 하면, 실제로 접근되었을 때만 평가됩니다.

Svelte 5의 리액티비티 (룬)

Inertia × Svelte로 개발할 때 알아 두어야 할 Svelte 5의 룬 구문을 소개합니다.

$state — 리액티브한 상태

<script lang="ts">
    let count = $state(0)
    let isOpen = $state(false)
    let items = $state<string[]>([])
</script>

<p>{count}</p>
<button onclick={() => count++}>+1</button>
<button onclick={() => isOpen = !isOpen}>토글</button>
$state로 선언한 변수는 자동으로 리액티브가 됩니다. 값이 바뀌면 DOM이 자동 갱신됩니다. Vue의 ref나 React의 useState에 해당하지만, .value 프로퍼티로의 접근이 불필요하며, 대입하는 것만으로 상태를 갱신할 수 있습니다.

$derived — 파생된 값

<script lang="ts">
    let posts = $state<{ title: string; published: boolean }[]>([])

    // published가 true인 게시물만 추출
    let publishedPosts = $derived(posts.filter(post => post.published))

    // 여러 의존관계가 있는 경우
    let summary = $derived.by(() => {
        const total = posts.length
        const published = publishedPosts.length
        return `전 ${total}건 중 ${published}건 공개`
    })
</script>

<p>{summary}</p>
$derived는 의존하는 값이 바뀌면 자동으로 재계산됩니다. Vue의 computed나 React의 useMemo에 해당합니다.

$effect — 부수 효과 처리

<script lang="ts">
    let query = $state('')

    // query가 바뀔 때마다 실행
    $effect(() => {
        console.log('검색 쿼리가 바뀌었습니다:', query)

        // 클린업 함수를 반환할 수 있음
        return () => {
            console.log('클린업')
        }
    })
</script>

<input bind:value={query} placeholder="검색..." />
$effect는 의존하는 $state 값이 변화할 때마다 실행됩니다. React의 useEffect에 해당하지만, 의존 배열을 명시할 필요가 없고, 사용한 $state 변수가 자동으로 추적됩니다.

shadcn-svelte 컴포넌트

스타터 킷에는 shadcn-svelte가 편입되어 있습니다. shadcn-svelte는 React용 shadcn/ui와 같은 설계 사상을 가진 컴포넌트 라이브러리로, 코드를 프로젝트에 복사하여 자유롭게 커스터마이즈할 수 있습니다.

컴포넌트 추가

npx shadcn-svelte@latest add button
npx shadcn-svelte@latest add input
npx shadcn-svelte@latest add card
명령을 실행하면 resources/js/components/ui/에 컴포넌트의 소스 코드가 배치됩니다.

사용법

<script lang="ts">
    import { Button } from '@/components/ui/button'
    import { Input } from '@/components/ui/input'
    import * as Card from '@/components/ui/card'
    import { useForm } from '@inertiajs/svelte'

    const form = useForm({ email: '', password: '' })

    function submit(e: SubmitEvent) {
        e.preventDefault()
        form.post('/login')
    }
</script>

<Card.Root class="w-96">
    <Card.Header>
        <Card.Title>로그인</Card.Title>
    </Card.Header>
    <Card.Content>
        <form onsubmit={submit} class="space-y-4">
            <Input
                type="email"
                bind:value={form.data.email}
                placeholder="메일 주소"
            />
            {#if form.errors.email}
                <p class="text-sm text-red-500">{form.errors.email}</p>
            {/if}

            <Input
                type="password"
                bind:value={form.data.password}
                placeholder="패스워드"
            />

            <Button type="submit" disabled={form.processing} class="w-full">
                {form.processing ? '로그인 중...' : '로그인'}
            </Button>
        </form>
    </Card.Content>
</Card.Root>
스타터 킷에는 Button · Input · Card · Dialog · Dropdown 등 자주 사용하는 컴포넌트가 미리 포함되어 있습니다. 추가하고 싶은 컴포넌트는 npx shadcn-svelte@latest add 명령으로 그때그때 추가할 수 있습니다.

정리

Svelte는 Laravel과의 조합에서, 특히 Inertia를 경유한 “모던 모놀리스” 구성에서 힘을 발휘합니다. 컴파일러 기반 설계에 의해 번들 크기가 작고, 룬 구문에 의해 리액티비티가 명시적이고 이해하기 쉬운 것이 특징입니다.
요소역할
Laravel 컨트롤러라우팅 · 데이터 취득 · 유효성 검증
Inertia::render()컨트롤러에서 Svelte 컴포넌트로 데이터 전달
Svelte 페이지 컴포넌트$props()로 props를 받고 UI를 렌더링
useForm폼의 상태 관리 · 송신 · 오류 표시
Link 컴포넌트풀 리로드 없는 페이지 전환
usePage().props공유 데이터로의 접근
shadcn-svelte표준 컴포넌트 라이브러리
Inertia × Svelte를 사용하면, Laravel 백엔드의 심플함과 Svelte의 컴팩트한 기술 스타일을 조합한 개발 경험을 얻을 수 있습니다. 스타터 킷으로 프로젝트를 생성하면 인증 화면도 포함해서 바로 개발을 시작할 수 있습니다.

Inertia.js 공식 문서

Inertia v3의 전체 기능에 대해서는 공식 문서를 참고하세요.
마지막 수정일 2026년 7월 13일