> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Vue.js 입문 — Inertia × Laravel에서 사용하는 기초 지식

> Laravel 사용자에게 가장 익숙한 JS 프레임워크 Vue.js를 소개합니다. Options API / Composition API의 개요부터, Inertia v3 × Vue 3에서의 페이지 컴포넌트 · useForm · 공유 데이터까지 실전적으로 설명합니다.

## Vue.js란

Vue.js(이하 Vue)는 사용자 인터페이스를 구축하기 위한 프로그레시브 JavaScript 프레임워크입니다. "프로그레시브"란 작은 부분부터 시작하여 필요에 따라 기능을 추가해 갈 수 있다는 의미로, 기존 HTML 페이지로의 부분적인 편입도, 대규모 SPA의 구축에도 대응할 수 있습니다.

Vue의 핵심은 **리액티비티**입니다. 데이터가 바뀌면 DOM이 자동으로 갱신되므로, 개발자는 "언제, 어느 요소를 갱신할지"를 수동으로 관리할 필요가 없습니다.

<Info>
  이 페이지에서 설명하는 것은 Vue 3와 Inertia v3의 조합입니다. Laravel 13의 스타터 킷은 이 구성을 기본으로 사용합니다.
</Info>

### Options API와 Composition API

Vue 3는 컴포넌트를 작성하는 스타일로서 **Options API**와 **Composition API**의 두 가지를 제공하고 있습니다.

**Options API**는 Vue 2부터 이어지는 기존 스타일입니다. `data` · `methods` · `computed` · `mounted` 등의 옵션 오브젝트로 컴포넌트를 정의합니다.

```vue theme={null}
<!-- 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와의 궁합도 뛰어납니다.

```vue theme={null}
<!-- 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>
```

<Tip>
  Inertia × Laravel 스타터 킷에서는 `<script setup>`을 사용한 Composition API 스타일이 표준입니다. 본 페이지의 예시도 모두 `<script setup>`으로 기술합니다.
</Tip>

***

## Laravel에서의 포지션

### 역사

Vue와 Laravel의 관계는 오래되었으며, \*\*Laravel 5.3(2016년)\*\*에 Vue가 기본 프론트엔드 프레임워크로 채용된 것에서 시작합니다. 당시의 `package.json`에는 Vue가 포함되어 있었고, `resources/js/components/ExampleComponent.vue`라는 샘플 컴포넌트도 동봉되어 있었습니다.

```mermaid theme={null}
timeline
    title Laravel과 Vue의 여정
    2016 : Laravel 5.3 — Vue를 기본 채용
    2019 : Laravel 6 — laravel/ui 패키지로 Vue 스캐폴드 분리
    2021 : Laravel 8 — Jetstream + Inertia (Vue) 스타터 킷 등장
    2022 : Laravel 9 — Vite로 이행
    2025 : Laravel 12 — 스타터 킷 쇄신 (Vue / React)
    2026 : Laravel 13 — Inertia v3 대응 스타터 킷
```

\*\*Laravel 6(2019년)\*\*에서 인증 스캐폴드가 `laravel/ui` 패키지로 분리되어, Vue의 스캐폴드도 동 패키지로 이관되었습니다. 현재는 `laravel new`의 스타터 킷 경유로 Inertia + Vue 구성을 선택하는 것이 주류 스타일입니다.

Laravel 사용자에게 있어 Vue는 가장 익숙한 JS 프레임워크이며, 한국어 학습 리소스도 풍부합니다.

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

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

```mermaid theme={null}
graph LR
    Browser["브라우저"]
    Inertia["Inertia.js<br>(어댑터 레이어)"]
    Laravel["Laravel<br>(컨트롤러)"]
    Vue["Vue<br>(페이지 컴포넌트)"]

    Browser <-->|XHR / 풀 페이지 로드| Inertia
    Inertia <-->|Inertia 응답| Laravel
    Inertia -->|props| Vue
    Vue -->|렌더링| Browser
```

***

## 셋업

### 스타터 킷 경유 (권장)

신규 프로젝트에서 시작하는 경우에는 스타터 킷을 사용하는 것이 가장 손쉽습니다.

```shell theme={null}
laravel new my-app
```

대화식 프롬프트에서 **Vue**를 선택하면 이하가 모두 자동으로 셋업됩니다.

* `inertiajs/inertia-laravel` (서버사이드 어댑터)
* `@inertiajs/vue3` (클라이언트 어댑터)
* `vue` (Vue 3 본체)
* `@vitejs/plugin-vue` (Vite 플러그인)
* `HandleInertiaRequests` 미들웨어
* 로그인 · 회원가입 등의 인증 화면 (Inertia + Vue로 구현 완료)

### 수동 설치

기존 프로젝트에 추가하는 경우에는 서버사이드와 클라이언트사이드를 별도로 설치합니다.

```shell theme={null}
# 서버사이드 (PHP)
composer require inertiajs/inertia-laravel

# 클라이언트사이드 (JavaScript)
npm install @inertiajs/vue3 vue
npm install --save-dev @vitejs/plugin-vue
```

다음으로, `vite.config.js`에 Vue 플러그인을 추가합니다.

```js theme={null}
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 앱을 기동합니다.

```js theme={null}
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)
    },
})
```

<Info>
  수동 설치의 세부 내용(루트 템플릿의 설정이나 미들웨어의 등록 등)은 [Inertia 공식 문서](https://inertiajs.com/installation)를 참고하세요.
</Info>

***

## 디렉터리 구조

스타터 킷에서는 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의 값이나 식을 템플릿에 삽입합니다.

```vue theme={null}
<script setup>
const name = '세계'
const count = 3
</script>

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

#### `v-if` — 조건 분기

```vue theme={null}
<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` — 리스트 렌더링

```vue theme={null}
<template>
    <ul>
        <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
</template>
```

효율적인 차분 갱신을 위해 `:key`는 반드시 지정합니다. React의 `Array.map()`에 해당합니다.

#### `v-model` — 양방향 바인딩

`v-model`을 사용하면 폼 요소의 값과 리액티브 변수를 양방향으로 동기화할 수 있습니다.

```vue theme={null}
<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`의 생략형)

```vue theme={null}
<template>
    <!-- 동적인 속성 바인딩 -->
    <img :src="imageUrl" :alt="imageAlt" />

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

***

## 페이지 컴포넌트의 기본

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

### 컨트롤러

```php theme={null}
// 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 페이지 컴포넌트

```vue theme={null}
<!-- 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를 정의할 필요는 없습니다.

***

## `Link` 컴포넌트

`@inertiajs/vue3`가 제공하는 `<Link>` 컴포넌트를 사용하면, 페이지 전환이 XHR로 이루어져, 브라우저의 풀 리로드를 회피할 수 있습니다.

```vue theme={null}
<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>` 컴포넌트는, 스타터 킷의 인증 화면에서 사용되고 있는 폼 송신의 권장 스타일입니다. `action`과 `method`를 props로 지정하고, `v-slot`으로 `errors`와 `processing`에 접근합니다.

### 기본 사용법

```vue theme={null}
<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](/ko/blog/wayfinder-introduction)를 사용하여 라우트를 오브젝트로 관리하고 있습니다. `store.form()`은 라우트 오브젝트의 `action`과 `method`를 포함하는 오브젝트를 반환하고, `v-bind`로 `<Form>`에 spread합니다.

```vue theme={null}
<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`에 지정한 필드는 송신 성공 시에 자동으로 리셋됩니다. 패스워드 필드 등 송신 후에 비우고 싶은 필드에 지정합니다.

<Info>
  Wayfinder를 사용하지 않는 경우는 `action="/login"`처럼 직접 URL을 전달하면 같은 방식으로 동작합니다.
</Info>

***

## `useForm` 헬퍼

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

### 컨트롤러 측

```php theme={null}
// 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 폼 컴포넌트

```vue theme={null}
<!-- 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()` 메서드로 정의합니다.

```php theme={null}
// 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()`를 사용합니다.

```vue theme={null}
<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>
```

<Info>
  공유 데이터는 모든 요청에 포함되므로, 필요 최소한의 데이터로 좁힐 것을 권장합니다. `fn()`을 사용한 지연 평가로 하면, 실제로 접근되었을 때만 평가됩니다.
</Info>

***

## Vue 3의 리액티비티 기초

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

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

```vue theme={null}
<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` — 산출 프로퍼티

```vue theme={null}
<script setup>
import { ref, computed } from 'vue'

const posts = ref([])

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

### `onMounted` — 마운트 후의 처리

```vue theme={null}
<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 양쪽을 향유할 수 있습니다. 스타터 킷으로 프로젝트를 생성하면 인증 화면도 포함해서 바로 개발을 시작할 수 있습니다.

<Card title="Inertia.js 공식 문서" icon="book-open" href="https://inertiajs.com">
  Inertia v3의 전체 기능에 대해서는 공식 문서를 참고하세요.
</Card>


## Related topics

- [React 입문 — Inertia × Laravel에서 사용하는 기초 지식](/ko/blog/react-introduction.md)
- [Svelte 입문 — Inertia × Laravel에서 사용하는 기초 지식](/ko/blog/svelte-introduction.md)
- [Laravel을 시작하기 전에 필요한 지식](/ko/true-tutorial.md)
- [프런트엔드](/ko/frontend.md)
- [Precognition](/ko/precognition.md)
