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

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

> Laravel + Inertia.js에서 React를 사용하는 입문 가이드. laravel/ui부터 현행 스타터 킷까지의 역사적 경위와 useForm · usePage 등의 Inertia React 훅을 실전적으로 설명합니다.

## React란

React는 Meta(구 Facebook)가 개발 · 유지하는 사용자 인터페이스 구축을 위한 JavaScript 라이브러리입니다. 선언적인 UI 기술과 **컴포넌트 기반**의 아키텍처가 특징으로, 소규모 위젯부터 풀 SPA까지 폭넓게 활용되고 있습니다.

React의 핵심은 **가상 DOM**을 통한 효율적인 재렌더링입니다. 상태(state)가 변화하면 React는 차분만을 DOM에 반영하므로, 수동으로 DOM을 조작할 필요가 없습니다.

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

### JSX와 TSX

React 컴포넌트는 **JSX** (JavaScript XML)라는 구문으로 작성합니다. HTML과 비슷한 표기법을 JavaScript 안에 직접 쓸 수 있습니다.

```jsx theme={null}
// JSX 예
function Greeting({ name }) {
    return <h1>안녕하세요, {name}님</h1>
}
```

스타터 킷에서는 **TypeScript**(`.tsx`)가 표준 채택되어 있습니다. 타입 정의에 의해 IDE의 자동완성이 강화되고, 버그를 조기에 발견할 수 있습니다.

```tsx theme={null}
// TSX 예 (TypeScript)
type Props = {
    name: string
}

function Greeting({ name }: Props) {
    return <h1>안녕하세요, {name}님</h1>
}
```

<Tip>
  Laravel의 React 스타터 킷은 TypeScript + TSX가 표준입니다. 본 페이지의 예시도 모두 TSX로 기술합니다.
</Tip>

***

## Laravel에서의 포지션

### 역사

React와 Laravel의 관계는 Vue보다 다소 짧지만, 현재는 동등 이상의 대우가 되어 있습니다.

```mermaid theme={null}
timeline
    title Laravel과 React의 여정
    2019 : Laravel 6 — laravel/ui에 React 스캐폴드 추가
    2021 : Laravel 8 — Breeze에 Inertia React 스택 추가
    2022 : Laravel 9 — Vite로 이행
    2025 : Laravel 12 — 스타터 킷 쇄신 (React / Vue)
    2026 : Laravel 13 — Inertia v3 대응 스타터 킷
```

\*\*Laravel 6 (2019년)\*\*에서 인증 스캐폴드가 `laravel/ui` 패키지로 분리되어, Vue와 함께 React 버전 스캐폴드도 제공되었습니다. 다만 당시에는 Vue가 주류였으며 React 버전의 존재감은 약한 상황이었습니다.

\*\*Laravel Breeze (2021년)\*\*에 Inertia + React 스택이 추가되면서 본격적인 채용이 시작되었고, \*\*Laravel 12 (2025년)\*\*의 스타터 킷 쇄신으로 React가 Vue와 완전히 동렬(오히려 먼저 표시됨)의 대우가 되었습니다.

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

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

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

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

***

## 셋업

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

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

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

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

* `inertiajs/inertia-laravel` (서버사이드 어댑터)
* `@inertiajs/react` (클라이언트 어댑터)
* `react` + `react-dom` (React 19 본체)
* `@vitejs/plugin-react` (Vite 플러그인)
* TypeScript + `@types/react`
* Tailwind CSS + shadcn/ui 컴포넌트 라이브러리
* `HandleInertiaRequests` 미들웨어
* 로그인 · 회원가입 등의 인증 화면 (Inertia + React + TypeScript로 구현 완료)

### 수동 설치

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

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

# 클라이언트사이드 (JavaScript)
npm install @inertiajs/react react react-dom
npm install --save-dev @vitejs/plugin-react @types/react @types/react-dom typescript
```

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

```ts theme={null}
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import react from '@vitejs/plugin-react'

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.tsx'],
            refresh: true,
        }),
        react(),
    ],
})
```

`resources/js/app.tsx`에서 Inertia 앱을 기동합니다.

```tsx theme={null}
import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'

createInertiaApp({
    resolve: (name) =>
        resolvePageComponent(
            `./pages/${name}.tsx`,
            import.meta.glob('./pages/**/*.tsx'),
        ),
    setup({ el, App, props }) {
        createRoot(el).render(<App {...props} />)
    },
})
```

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

***

## 디렉터리 구조

스타터 킷에서는 React의 페이지 컴포넌트를 `resources/js/pages/` 디렉터리에 배치합니다.

```
resources/js/
├── app.tsx            # Inertia 앱의 기점
├── bootstrap.ts
├── components/        # 재이용 가능한 UI 컴포넌트
│   ├── ui/            # shadcn/ui 컴포넌트
│   └── ...
├── hooks/             # 커스텀 React 훅
├── layouts/           # 레이아웃 컴포넌트
│   ├── app-layout.tsx
│   └── auth-layout.tsx
├── lib/               # 유틸리티 함수 · 설정
├── pages/             # Inertia 페이지 컴포넌트 (컨트롤러 이름에 대응)
│   ├── auth/
│   │   ├── login.tsx
│   │   └── register.tsx
│   ├── dashboard.tsx
│   └── posts/
│       ├── index.tsx
│       ├── create.tsx
│       └── show.tsx
└── types/             # TypeScript 타입 정의
```

`Inertia::render('posts/index', [...])`이라고 쓰면 `resources/js/pages/posts/index.tsx`가 대응하는 컴포넌트가 됩니다.

***

## JSX 구문의 기초

React는 JSX를 사용합니다. JavaScript 안에 HTML 라이크한 구문을 쓰는 스타일로, 스타터 킷의 코드를 읽기 위해 최소한 알아 두고 싶은 패턴을 소개합니다.

#### `{}` — 변수 전개

JSX 내에서는 `{}`를 사용하여 JavaScript의 값이나 식을 삽입합니다.

```tsx theme={null}
const name = '세계'
const count = 3

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

#### 조건 분기 — `&&`와 삼항 연산자

React에는 `v-if`에 해당하는 디렉티브가 없습니다. 심플한 조건에는 `&&` 연산자, if/else에는 삼항 연산자 `? :`를 사용합니다.

```tsx theme={null}
{/* 조건이 true일 때만 표시 */}
{isLoggedIn && <p>어서 오세요!</p>}

{/* if / else */}
{isLoggedIn ? <p>어서 오세요!</p> : <a href="/login">로그인</a>}

{/* if / else if / else */}
{isLoggedIn
    ? <p>어서 오세요!</p>
    : role === 'admin'
        ? <p>관리자로 로그인 중</p>
        : <a href="/login">로그인</a>
}
```

#### 리스트 렌더링 — `.map()`

리스트 렌더링에는 `Array.map()`을 사용합니다. 효율적인 차분 갱신을 위해 `key` prop은 반드시 지정합니다.

```tsx theme={null}
<ul>
    {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
    ))}
</ul>
```

Vue의 `v-for :key`나 Svelte의 `{#each}`에 해당합니다.

#### `className` — CSS 클래스명

JSX는 JavaScript로 컴파일되므로, `class`는 예약어입니다. CSS 클래스에는 `className`을 사용합니다.

```tsx theme={null}
{/* HTML: <div class="container"> */}
<div className="container">...</div>
```

#### 이벤트 핸들러 — 캐멀 케이스

JSX의 이벤트 속성은 캐멀 케이스이며, 함수의 참조를 전달합니다.

```tsx theme={null}
<button onClick={handleClick}>클릭</button>

{/* 폼 송신의 기본 동작 취소 */}
<form onSubmit={(e) => { e.preventDefault(); handleSubmit() }}>
    ...
</form>
```

***

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

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

### React 페이지 컴포넌트

```tsx theme={null}
// resources/js/pages/posts/index.tsx
import { Link } from '@inertiajs/react'

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

type Props = {
    posts: {
        data: Post[]
        // paginate(10)은 페이지네이션 정보도 포함하는 오브젝트를 반환
        // current_page, last_page, per_page, total 등도 이용 가능
    }
}

export default function PostsIndex({ posts }: Props) {
    return (
        <div>
            <h1>게시물 목록</h1>
            {posts.data.map((post) => (
                <article key={post.id}>
                    <h2>
                        <Link href={`/posts/${post.id}`}>{post.title}</Link>
                    </h2>
                    <p>{post.created_at}</p>
                </article>
            ))}
        </div>
    )
}
```

컴포넌트의 인자로 props를 받는 것만으로, 컨트롤러에서 전달된 데이터를 그대로 사용할 수 있습니다. REST API를 정의할 필요는 없습니다.

***

## `Link` 컴포넌트

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

```tsx theme={null}
import { Link } from '@inertiajs/react'

export default function PostsIndex() {
    return (
        <div>
            {/* 기본 링크 */}
            <Link href="/posts">게시물 목록</Link>

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

            {/* 프리로드 (호버 시 사전 취득) */}
            <Link href="/posts/1" preload>
                게시물 보기
            </Link>
        </div>
    )
}
```

일반적인 `<a>` 태그와 동일하게 작성할 수 있지만, 뒤에서 Inertia가 페이지 컴포넌트만을 교체하므로 SPA와 같은 조작감이 됩니다.

***

## `Form` 컴포넌트

`@inertiajs/react`가 제공하는 `<Form>` 컴포넌트는 스타터 킷의 인증 화면에서 사용되는 폼 송신의 권장 스타일입니다. `action`과 `method`를 props로 지정하고, children 함수(render prop)로 `errors`와 `processing`을 받습니다.

### 기본 사용법

```tsx theme={null}
import { Form } from '@inertiajs/react'

export default function PostCreate() {
    return (
        <Form action="/posts" method="post" className="flex flex-col gap-4">
            {({ errors, processing }) => (
                <>
                    <div>
                        <label htmlFor="title">제목</label>
                        <input id="title" name="title" type="text" required />
                        {errors.title && <p className="error">{errors.title}</p>}
                    </div>

                    <div>
                        <label htmlFor="content">본문</label>
                        <textarea id="content" name="content" />
                        {errors.content && <p className="error">{errors.content}</p>}
                    </div>

                    <button type="submit" disabled={processing}>
                        {processing ? '전송 중...' : '게시하기'}
                    </button>
                </>
            )}
        </Form>
    )
}
```

`<Form>`의 children은 `({ errors, processing }) => JSX`라는 함수(render prop 패턴)입니다. `Form` 컴포넌트가 이 값들을 자동으로 산출하여 전달해 줍니다. 폼 필드에는 `onChange` 핸들러가 아니라 HTML 네이티브의 `name` 속성을 사용하며, 브라우저의 표준 폼 데이터 수집이 기능합니다.

### 스타터 킷의 패턴

스타터 킷은 [Wayfinder](/ko/blog/wayfinder-introduction)를 사용하여 라우트를 오브젝트로 관리하고 있습니다. `store.form()`은 라우트 오브젝트의 `action`과 `method`를 포함하는 오브젝트를 반환하고, `<Form>`에 spread합니다.

```tsx theme={null}
import { Form } from '@inertiajs/react'
import { store } from '@/routes/login'

export default function Login() {
    return (
        <Form
            {...store.form()}
            resetOnSuccess={['password']}
            className="flex flex-col gap-6"
        >
            {({ errors, processing }) => (
                <>
                    {/* 폼 내용 */}
                </>
            )}
        </Form>
    )
}
```

`resetOnSuccess`에 지정한 필드는 송신 성공 시에 자동으로 리셋됩니다. 패스워드 필드 등 송신 후에 비우고 싶은 필드에 지정합니다.

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

***

## `useForm` 훅

폼 처리에는 `@inertiajs/react`의 `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', '게시물을 작성했습니다.');
    }
}
```

### React 폼 컴포넌트

```tsx theme={null}
// resources/js/pages/posts/create.tsx
import { useForm } from '@inertiajs/react'
import { FormEventHandler } from 'react'

export default function PostCreate() {
    const { data, setData, post, processing, errors } = useForm({
        title: '',
        content: '',
    })

    const submit: FormEventHandler = (e) => {
        e.preventDefault()
        post('/posts')
    }

    return (
        <form onSubmit={submit}>
            <div>
                <label>제목</label>
                <input
                    type="text"
                    value={data.title}
                    onChange={(e) => setData('title', e.target.value)}
                />
                {errors.title && <p className="error">{errors.title}</p>}
            </div>

            <div>
                <label>본문</label>
                <textarea
                    value={data.content}
                    onChange={(e) => setData('content', e.target.value)}
                />
                {errors.content && <p className="error">{errors.content}</p>}
            </div>

            <button type="submit" disabled={processing}>
                {processing ? '전송 중...' : '게시하기'}
            </button>
        </form>
    )
}
```

`useForm`이 반환하는 오브젝트의 주요 프로퍼티를 정리합니다.

| 프로퍼티 / 메서드              | 설명                         |
| ----------------------- | -------------------------- |
| `data`                  | 폼의 데이터 오브젝트                |
| `setData(field, value)` | 필드의 값 갱신                   |
| `errors`                | 유효성 검증 오류 (필드명으로 접근)       |
| `processing`            | 송신 중에는 `true` (버튼 무효화에 사용) |
| `isDirty`               | 초기값에서 변경되어 있는 경우 `true`    |
| `post(url)`             | POST 요청으로 송신               |
| `put(url)`              | PUT 요청으로 송신 (갱신)           |
| `delete(url)`           | DELETE 요청으로 송신             |
| `reset()`               | 폼을 초기값으로 리셋                |

유효성 검증 오류가 반환되었을 때, `useForm`은 입력 내용을 유지한 채 오류를 표시합니다.

***

## 공유 데이터 (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'),
            ],
        ]);
    }
}
```

React 컴포넌트에서 공유 데이터에 접근하려면 `usePage()` 훅을 사용합니다.

```tsx theme={null}
import { usePage } from '@inertiajs/react'

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

export default function AppHeader() {
    const { auth, flash } = usePage<SharedProps>().props

    return (
        <>
            <header>
                {auth.user ? (
                    <span>{auth.user.name}</span>
                ) : (
                    <span>게스트</span>
                )}
            </header>

            {flash.success && (
                <div className="alert-success">{flash.success}</div>
            )}
        </>
    )
}
```

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

***

## React 훅 기초

Inertia × React로 개발하는 데 있어 알아 두어야 할 React의 기본 훅을 소개합니다.

### `useState` — 로컬 상태 관리

```tsx theme={null}
import { useState } from 'react'

export default function Counter() {
    const [count, setCount] = useState(0)
    const [isOpen, setIsOpen] = useState(false)

    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>+1</button>
            <button onClick={() => setIsOpen(!isOpen)}>토글</button>
        </div>
    )
}
```

### `useEffect` — 부수 효과 처리

```tsx theme={null}
import { useState, useEffect } from 'react'

export default function Timer() {
    const [seconds, setSeconds] = useState(0)

    useEffect(() => {
        const timer = setInterval(() => {
            setSeconds((s) => s + 1)
        }, 1000)

        // 클린업 함수
        return () => clearInterval(timer)
    }, []) // 빈 배열 = 마운트 시 한 번만 실행

    return <p>경과 시간: {seconds}초</p>
}
```

### `useMemo`와 `useCallback` — 퍼포먼스 최적화

```tsx theme={null}
import { useMemo, useCallback } from 'react'
import { router } from '@inertiajs/react'

type Post = { id: number; title: string; published: boolean }
type Props = { posts: Post[] }

export default function PostsList({ posts }: Props) {
    // 값을 메모이제이션 (posts가 바뀌지 않는 한 재계산하지 않음)
    const publishedPosts = useMemo(
        () => posts.filter((post) => post.published),
        [posts],
    )

    // 함수를 메모이제이션 (의존 값이 바뀌지 않는 한 재생성하지 않음)
    const handleClick = useCallback((id: number) => {
        router.visit(`/posts/${id}`)
    }, [])

    return (
        <ul>
            {publishedPosts.map((post) => (
                <li key={post.id} onClick={() => handleClick(post.id)}>
                    {post.title}
                </li>
            ))}
        </ul>
    )
}
```

***

## TypeScript 지원

스타터 킷의 React 버전은 TypeScript가 기본입니다. Inertia의 타입 정의와 조합하면 props의 타입 안전을 확보할 수 있습니다.

### 전역 타입 정의

스타터 킷에서는 `resources/js/types/index.d.ts`에 공유 데이터의 타입을 정의합니다.

```ts theme={null}
// resources/js/types/index.d.ts
export interface User {
    id: number
    name: string
    email: string
    email_verified_at?: string
}

export type PageProps<T extends Record<string, unknown> = Record<string, unknown>> = T & {
    auth: {
        user: User
    }
}
```

### 페이지 컴포넌트에서의 타입 이용

```tsx theme={null}
import { PageProps } from '@/types'

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

export default function PostsIndex({ auth, posts }: PageProps<{ posts: Post[] }>) {
    return (
        <div>
            <p>로그인 중: {auth.user.name}</p>
            {posts.map((post) => (
                <article key={post.id}>
                    <h2>{post.title}</h2>
                </article>
            ))}
        </div>
    )
}
```

***

## 정리

React는 Laravel과의 조합에서, 특히 Inertia를 경유한 "모던 모놀리스" 구성에서 힘을 발휘합니다. TypeScript와의 궁합도 뛰어나며, 대규모 애플리케이션 개발에 적합합니다.

| 요소                  | 역할                         |
| ------------------- | -------------------------- |
| Laravel 컨트롤러        | 라우팅 · 데이터 취득 · 유효성 검증      |
| `Inertia::render()` | 컨트롤러에서 React 컴포넌트로 데이터 전달  |
| React 페이지 컴포넌트      | props를 받아 UI를 렌더링          |
| `useForm`           | 폼의 상태 관리 · 송신 · 오류 표시      |
| `Link` 컴포넌트         | 풀 리로드 없는 페이지 전환            |
| `usePage().props`   | 공유 데이터로의 접근                |
| TypeScript          | props의 타입 안전 · IDE 자동완성 강화 |

Inertia × React를 사용하면, Laravel 백엔드의 심플함과 React의 강력한 에코시스템을 조합한 개발 경험을 얻을 수 있습니다. 스타터 킷으로 프로젝트를 생성하면 인증 화면도 포함해서 바로 개발을 시작할 수 있습니다.

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


## Related topics

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