메인 콘텐츠로 건너뛰기

Vue.js란

Vue.js(이하 Vue)는 사용자 인터페이스를 구축하기 위한 프로그레시브 JavaScript 프레임워크입니다. “프로그레시브”란 작은 부분부터 시작하여 필요에 따라 기능을 추가해 갈 수 있다는 의미로, 기존 HTML 페이지로의 부분적인 편입도, 대규모 SPA의 구축에도 대응할 수 있습니다. Vue의 핵심은 리액티비티입니다. 데이터가 바뀌면 DOM이 자동으로 갱신되므로, 개발자는 “언제, 어느 요소를 갱신할지”를 수동으로 관리할 필요가 없습니다.
이 페이지에서 설명하는 것은 Vue 3와 Inertia v3의 조합입니다. Laravel 13의 스타터 킷은 이 구성을 기본으로 사용합니다.

Options API와 Composition API

Vue 3는 컴포넌트를 작성하는 스타일로서 Options APIComposition API의 두 가지를 제공하고 있습니다. Options API는 Vue 2부터 이어지는 기존 스타일입니다. data · methods · computed · mounted 등의 옵션 오브젝트로 컴포넌트를 정의합니다.
<!-- Options API 예 -->
<script>
export default {
    data() {
        return { count: 0 }
    },
    methods: {
        increment() {
            this.count++
        }
    }
}
</script>

<template>
    <button @click="increment">{{ count }}</button>
</template>
Composition API는 Vue 3에서 도입된 새로운 스타일입니다. <script setup> 구문과 조합함으로써 보다 간결하게 쓸 수 있습니다. 로직의 재이용성도 높고, TypeScript와의 궁합도 뛰어납니다.
<!-- Composition API (<script setup>) 예 -->
<script setup>
import { ref } from 'vue'

const count = ref(0)

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

<template>
    <button @click="increment">{{ count }}</button>
</template>
Inertia × Laravel 스타터 킷에서는 <script setup>을 사용한 Composition API 스타일이 표준입니다. 본 페이지의 예시도 모두 <script setup>으로 기술합니다.

Laravel에서의 포지션

역사

Vue와 Laravel의 관계는 오래되었으며, **Laravel 5.3(2016년)**에 Vue가 기본 프론트엔드 프레임워크로 채용된 것에서 시작합니다. 당시의 package.json에는 Vue가 포함되어 있었고, resources/js/components/ExampleComponent.vue라는 샘플 컴포넌트도 동봉되어 있었습니다. **Laravel 6(2019년)**에서 인증 스캐폴드가 laravel/ui 패키지로 분리되어, Vue의 스캐폴드도 동 패키지로 이관되었습니다. 현재는 laravel new의 스타터 킷 경유로 Inertia + Vue 구성을 선택하는 것이 주류 스타일입니다. Laravel 사용자에게 있어 Vue는 가장 익숙한 JS 프레임워크이며, 한국어 학습 리소스도 풍부합니다.

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

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

셋업

스타터 킷 경유 (권장)

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

수동 설치

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

# 클라이언트사이드 (JavaScript)
npm install @inertiajs/vue3 vue
npm install --save-dev @vitejs/plugin-vue
다음으로, vite.config.js에 Vue 플러그인을 추가합니다.
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
        vue({
            template: {
                transformAssetUrls: {
                    base: null,
                    includeAbsolute: false,
                },
            },
        }),
    ],
})
resources/js/app.js에서 Inertia 앱을 기동합니다.
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'

createInertiaApp({
    resolve: (name) =>
        resolvePageComponent(
            `./pages/${name}.vue`,
            import.meta.glob('./pages/**/*.vue'),
        ),
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .mount(el)
    },
})
수동 설치의 세부 내용(루트 템플릿의 설정이나 미들웨어의 등록 등)은 Inertia 공식 문서를 참고하세요.

디렉터리 구조

스타터 킷에서는 Vue의 페이지 컴포넌트를 resources/js/pages/ 디렉터리에 배치합니다.
resources/js/
├── app.js             # Inertia 앱의 기점
├── bootstrap.js
├── components/        # 재이용 가능한 UI 컴포넌트
│   ├── NavBar.vue
│   └── ...
├── layouts/           # 레이아웃 컴포넌트
│   ├── AppLayout.vue
│   └── AuthLayout.vue
└── pages/             # Inertia 페이지 컴포넌트 (컨트롤러 이름에 대응)
    ├── Auth/
    │   ├── Login.vue
    │   └── Register.vue
    ├── Dashboard.vue
    └── Posts/
        ├── Index.vue
        ├── Create.vue
        └── Show.vue
Inertia::render('Posts/Index', [...])이라고 쓰면 resources/js/pages/Posts/Index.vue가 대응하는 컴포넌트가 됩니다.

Vue 템플릿 구문

스타터 킷의 코드를 읽고 쓰기 위해 필요한 기본적인 템플릿 디렉티브를 소개합니다.

{{ }} — 변수 전개

이중 중괄호를 사용하여, JavaScript의 값이나 식을 템플릿에 삽입합니다.
<script setup>
const name = '세계'
const count = 3
</script>

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

v-if — 조건 분기

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

v-for — 리스트 렌더링

<template>
    <ul>
        <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
</template>
효율적인 차분 갱신을 위해 :key는 반드시 지정합니다. React의 Array.map()에 해당합니다.

v-model — 양방향 바인딩

v-model을 사용하면 폼 요소의 값과 리액티브 변수를 양방향으로 동기화할 수 있습니다.
<script setup>
import { ref } from 'vue'

const title = ref('')
const agreed = ref(false)
const role = ref('viewer')
</script>

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

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

    <!-- 셀렉트박스 -->
    <select v-model="role">
        <option value="viewer">열람자</option>
        <option value="editor">편집자</option>
        <option value="admin">관리자</option>
    </select>
</template>

: (v-bind)와 @ (v-on)

  • :attr="value" — HTML 속성에 동적인 값을 바인드 (v-bind:attr의 생략형)
  • @event="handler" — 이벤트 리스너를 등록 (v-on:event의 생략형)
<template>
    <!-- 동적인 속성 바인딩 -->
    <img :src="imageUrl" :alt="imageAlt" />

    <!-- 이벤트 핸들러 -->
    <button @click="handleClick">클릭</button>
    <form @submit.prevent="handleSubmit">...</form>
</template>

페이지 컴포넌트의 기본

Inertia의 페이지 컴포넌트는 일반적인 Vue 컴포넌트입니다. 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),
        ]);
    }
}

Vue 페이지 컴포넌트

<!-- resources/js/pages/Posts/Index.vue -->
<script setup>
import { Link } from '@inertiajs/vue3'

defineProps({
    posts: Object,
})
</script>

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

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

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

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

Form 컴포넌트

@inertiajs/vue3가 제공하는 <Form> 컴포넌트는, 스타터 킷의 인증 화면에서 사용되고 있는 폼 송신의 권장 스타일입니다. actionmethod를 props로 지정하고, v-slot으로 errorsprocessing에 접근합니다.

기본 사용법

<script setup>
import { Form } from '@inertiajs/vue3'
</script>

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

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

        <button type="submit" :disabled="processing">
            {{ processing ? '전송 중...' : '게시하기' }}
        </button>
    </Form>
</template>
v-slot="{ errors, processing }"은 Vue의 스코프 부여 슬롯 구문으로, Form 컴포넌트가 이 값들을 자동으로 산출하여 전달해 줍니다. 폼 필드에는 v-model이 아니라 HTML 네이티브의 name 속성을 사용하고, 브라우저의 표준 폼 데이터 수집이 기능합니다.

스타터 킷의 패턴

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

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

useForm 헬퍼

폼 처리에는 @inertiajs/vue3useForm 헬퍼를 사용합니다. 폼의 상태 관리 · 송신 · 유효성 검증 오류 표시를 심플하게 구현할 수 있습니다.

컨트롤러 측

// 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', '게시물을 작성했습니다.');
    }
}

Vue 폼 컴포넌트

<!-- resources/js/pages/Posts/Create.vue -->
<script setup>
import { useForm } from '@inertiajs/vue3'

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

function submit() {
    form.post('/posts')
}
</script>

<template>
    <form @submit.prevent="submit">
        <div>
            <label>제목</label>
            <input v-model="form.title" type="text" />
            <p v-if="form.errors.title" class="error">{{ form.errors.title }}</p>
        </div>

        <div>
            <label>본문</label>
            <textarea v-model="form.content"></textarea>
            <p v-if="form.errors.content" class="error">{{ form.errors.content }}</p>
        </div>

        <button type="submit" :disabled="form.processing">
            {{ form.processing ? '전송 중...' : '게시하기' }}
        </button>
    </form>
</template>
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은 입력 내용을 유지한 채 오류를 표시합니다. v-model과의 조합으로 심리스한 폼 경험을 실현할 수 있습니다.

공유 데이터 (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'),
            ],
        ]);
    }
}
Vue 컴포넌트에서 공유 데이터에 접근하려면 usePage()를 사용합니다.
<script setup>
import { computed } from 'vue'
import { usePage } from '@inertiajs/vue3'

const page = usePage()

// 공유 데이터로의 접근
const user = computed(() => page.props.auth.user)
const flash = computed(() => page.props.flash)
</script>

<template>
    <header>
        <span v-if="user">{{ user.name }}</span>
        <span v-else>게스트</span>
    </header>

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

Vue 3의 리액티비티 기초

Inertia × Vue로 개발할 때 알아 두어야 할 Vue 3의 리액티비티 API를 소개합니다.

ref — 프리미티브한 리액티브 값

<script setup>
import { ref } from 'vue'

const count = ref(0)
const isOpen = ref(false)

// .value로 접근 (템플릿 내에서는 불필요)
count.value++
</script>

<template>
    <p>{{ count }}</p>
    <button @click="isOpen = !isOpen">토글</button>
</template>

computed — 산출 프로퍼티

<script setup>
import { ref, computed } from 'vue'

const posts = ref([])

const publishedPosts = computed(() =>
    posts.value.filter(post => post.published)
)
</script>

onMounted — 마운트 후의 처리

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
    console.log('컴포넌트가 마운트되었습니다')
})
</script>

정리

Vue.js는 Laravel과의 친화성이 높으며, 특히 Inertia를 경유한 “모던 모놀리스” 구성에서 힘을 발휘합니다.
요소역할
Laravel 컨트롤러라우팅 · 데이터 취득 · 유효성 검증
Inertia::render()컨트롤러에서 Vue 컴포넌트로 데이터 전달
Vue 페이지 컴포넌트props를 받아 UI를 렌더링
useForm폼의 상태 관리 · 송신 · 오류 표시
Link 컴포넌트풀 리로드 없는 페이지 전환
usePage().props공유 데이터로의 접근
Inertia × Vue를 사용하면, Laravel 백엔드의 심플함과 Vue의 리액티브한 UI 양쪽을 향유할 수 있습니다. 스타터 킷으로 프로젝트를 생성하면 인증 화면도 포함해서 바로 개발을 시작할 수 있습니다.

Inertia.js 공식 문서

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