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

# Eloquent API 리소스

> Eloquent 모델을 일관된 JSON API 응답으로 변환하는 방법을 설명합니다. 리소스 클래스의 작성부터 조건부 필드, 페이지네이션 연계까지 실전적으로 소개합니다.

## API 리소스란

API를 구축할 때, Eloquent 모델을 그대로 JSON으로 반환하면 숨기고 싶은 컬럼이 새어 나가거나 클라이언트가 필요로 하지 않는 대량의 데이터를 보내 버리는 경우가 있습니다.

**Eloquent API 리소스**는 모델과 JSON 응답 사이에 변환 레이어를 끼우는 구조입니다. `toArray()` 메서드로 "무엇을 어떤 형식으로 응답에 포함시킬지"를 명시적으로 정의할 수 있습니다.

```mermaid theme={null}
flowchart LR
  A["Eloquent 모델<br>(User)"] --> B["리소스 클래스<br>(UserResource)"]
  B --> C["JSON 응답<br>{ id, name, email }"]
```

주된 장점은 다음과 같습니다.

* 응답에 포함시킬 필드를 완전히 컨트롤할 수 있다
* 필드 이름의 변환이나 값의 가공을 한 곳에 정리할 수 있다
* 조건에 따라 필드를 구분해서 출력할 수 있다
* 릴레이션을 중첩해 일관된 구조를 유지할 수 있다

## 리소스 작성

`make:resource` Artisan 명령어로 리소스 클래스를 생성합니다.

```shell theme={null}
php artisan make:resource UserResource
```

생성된 클래스는 `app/Http/Resources` 디렉터리에 놓입니다.

```php theme={null}
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}
```

`$this`로 모델의 프로퍼티에 직접 접근할 수 있습니다. 이것은 리소스 클래스가 내부에서 모델에의 접근을 프록시하고 있기 때문입니다.

## 컨트롤러에서의 사용

정의한 리소스는 컨트롤러나 라우트에서 반환할 수 있습니다.

```php theme={null}
use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/users/{user}', function (User $user) {
    return new UserResource($user);
});
```

또는 모델의 `toResource()` 메서드를 사용하는 방법도 있습니다.

```php theme={null}
Route::get('/users/{user}', function (User $user) {
    return $user->toResource();
});
```

`toResource()`는 모델명에 기반해 대응하는 리소스 클래스(`UserResource`)를 자동으로 찾습니다.

기본적으로 응답은 `data` 키로 래핑됩니다.

```json theme={null}
{
  "data": {
    "id": 1,
    "name": "山田 太郎",
    "email": "yamada@example.com",
    "created_at": "2024-01-15T10:00:00.000000Z",
    "updated_at": "2024-01-15T10:00:00.000000Z"
  }
}
```

## 리소스 컬렉션

여러 모델을 반환하는 경우 `collection()` 메서드를 사용합니다.

```php theme={null}
use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/users', function () {
    return UserResource::collection(User::all());
});
```

또는 Eloquent 컬렉션의 `toResourceCollection()`을 사용합니다.

```php theme={null}
return User::all()->toResourceCollection();
```

### 커스텀 컬렉션 리소스

컬렉션 전체에 메타데이터를 부가하고 싶은 경우, 전용 컬렉션 리소스를 작성합니다.

```shell theme={null}
php artisan make:resource UserCollection
```

```php theme={null}
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'links' => [
                'self' => route('users.index'),
            ],
        ];
    }
}
```

```php theme={null}
Route::get('/users', function () {
    return new UserCollection(User::all());
});
```

## 필드의 가공과 변환

`toArray()` 내에서 필드 이름의 변경이나 값의 가공을 할 수 있습니다.

```php theme={null}
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'full_name' => $this->name,                         // 필드 이름을 변환
        'email_address' => $this->email,                    // 필드 이름을 변환
        'role' => strtoupper($this->role),                  // 값을 가공
        'registered_at' => $this->created_at->toDateString(), // 날짜를 포맷팅
    ];
}
```

## 조건부 필드

### when() — 조건에 따라 필드를 추가

특정 조건을 만족할 때만 필드를 포함시키고 싶은 경우 `when()`을 사용합니다.

```php theme={null}
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        // 인증 사용자가 관리자일 때만 포함
        'secret_token' => $this->when(
            $request->user()?->isAdmin(),
            $this->secret_token
        ),
    ];
}
```

`when()`의 조건이 `false`일 때는 그 키 자체가 응답에서 제거됩니다.

### mergeWhen() — 여러 필드를 함께 조건부 추가

같은 조건으로 여러 필드를 함께 구분해서 출력하려면 `mergeWhen()`을 사용합니다.

```php theme={null}
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        $this->mergeWhen($request->user()?->isAdmin(), [
            'admin_note' => $this->admin_note,
            'internal_id' => $this->internal_id,
        ]),
    ];
}
```

### whenLoaded() — 로드된 릴레이션만 포함

릴레이션이 Eager 로드되어 있을 때만 포함시킴으로써, N+1 문제를 방지하면서 유연한 응답을 만들 수 있습니다.

```php theme={null}
use App\Http\Resources\PostResource;

public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        // posts 가 with() 로 로드되어 있을 때만 포함
        'posts' => PostResource::collection($this->whenLoaded('posts')),
    ];
}
```

컨트롤러 측에서 릴레이션을 로드할지 여부를 제어할 수 있습니다.

```php theme={null}
// posts 를 포함해서 반환
return new UserResource($user->load('posts'));

// posts 없이 반환
return new UserResource($user);
```

### whenCounted() — 카운트를 조건부로 포함

`loadCount()`로 취득한 릴레이션의 카운트를 조건부로 포함시킵니다.

```php theme={null}
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'posts_count' => $this->whenCounted('posts'),
    ];
}
```

```php theme={null}
return new UserResource($user->loadCount('posts'));
```

## 중첩된 리소스

릴레이션을 다른 리소스 클래스로 중첩함으로써 일관된 구조를 유지할 수 있습니다.

```php theme={null}
// app/Http/Resources/PostResource.php
class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
            'author' => new UserResource($this->whenLoaded('user')),
            'comments' => CommentResource::collection($this->whenLoaded('comments')),
            'published_at' => $this->published_at?->toDateString(),
        ];
    }
}
```

```php theme={null}
// 컨트롤러
$post = Post::with(['user', 'comments'])->findOrFail($id);

return new PostResource($post);
```

```json theme={null}
{
  "data": {
    "id": 1,
    "title": "Laravel로 배우는 API 리소스",
    "author": {
      "id": 5,
      "name": "山田 太郎",
      "email": "yamada@example.com"
    },
    "comments": [
      { "id": 10, "body": "참고가 되었습니다!" }
    ]
  }
}
```

## 메타데이터의 추가

### with() — 톱 레벨의 메타데이터

컬렉션 전체에 메타데이터를 부가하려면 `with()` 메서드를 오버라이드합니다.

```php theme={null}
class UserCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }

    public function with(Request $request): array
    {
        return [
            'meta' => [
                'version' => '1.0',
                'generated_at' => now()->toIso8601String(),
            ],
        ];
    }
}
```

응답 예시:

```json theme={null}
{
  "data": [...],
  "meta": {
    "version": "1.0",
    "generated_at": "2024-01-15T10:00:00+00:00"
  }
}
```

### additional() — 동적으로 메타데이터 추가

컨트롤러 측에서 동적으로 메타데이터를 추가하고 싶은 경우 `additional()`을 사용합니다.

```php theme={null}
return User::all()
    ->load('roles')
    ->toResourceCollection()
    ->additional(['meta' => [
        'total_admins' => User::where('role', 'admin')->count(),
    ]]);
```

## 페이지네이션과의 조합

페이지네이션 결과를 리소스에 전달하기만 하면 `meta`와 `links`가 자동으로 부가됩니다.

```php theme={null}
Route::get('/users', function () {
    return UserResource::collection(User::paginate(15));
});
```

또는:

```php theme={null}
return User::paginate(15)->toResourceCollection();
```

응답 예시:

```json theme={null}
{
  "data": [
    { "id": 1, "name": "山田 太郎" },
    { "id": 2, "name": "鈴木 花子" }
  ],
  "links": {
    "first": "https://example.com/users?page=1",
    "last": "https://example.com/users?page=5",
    "prev": null,
    "next": "https://example.com/users?page=2"
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 5,
    "per_page": 15,
    "to": 15,
    "total": 72
  }
}
```

<Info>
  페이지네이션 응답에서는 `withoutWrapping()`을 호출하고 있어도 `data` 키는 반드시 붙습니다. 페이지네이션의 `meta`나 `links` 키와 공존시키기 위해서입니다.
</Info>

## 데이터 래핑의 무효화

기본적으로 최외곽 리소스가 `data` 키로 래핑됩니다. 이것을 무효화하려면 `AppServiceProvider`의 `boot()` 내에서 `withoutWrapping()`을 호출합니다.

```php theme={null}
use Illuminate\Http\Resources\Json\JsonResource;

public function boot(): void
{
    JsonResource::withoutWrapping();
}
```

<Warning>
  `withoutWrapping()`은 최외곽 래핑에만 영향을 줍니다. 스스로 정의한 `data` 키는 삭제되지 않습니다.
</Warning>

## 실전 예시: 사용자 API 의 구현

사용자 관리 API를 예로, 리소스를 사용한 일관된 응답 설계를 보여 드립니다.

```mermaid theme={null}
flowchart TD
  A["GET /api/users"] --> B["UserController@index"]
  C["GET /api/users/:id"] --> D["UserController@show"]
  B --> E["UserResource::collection(paginate)"]
  D --> F["new UserResource(user)"]
  E --> G["{ data: [...], meta, links }"]
  F --> H["{ data: { id, name, email, ... } }"]
```

### UserResource

```php theme={null}
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'avatar_url' => $this->avatar_url,
            'role' => $this->role,
            // 관리자에게만 표시
            'created_at' => $this->when(
                $request->user()?->isAdmin(),
                $this->created_at->toDateString()
            ),
            // 로드되어 있을 때만 포함
            'posts' => PostResource::collection($this->whenLoaded('posts')),
            'posts_count' => $this->whenCounted('posts'),
        ];
    }
}
```

### UserController

```php theme={null}
<?php

namespace App\Http\Controllers\Api;

use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class UserController extends Controller
{
    public function index(): AnonymousResourceCollection
    {
        $users = User::withCount('posts')->paginate(20);

        return UserResource::collection($users);
    }

    public function show(User $user): UserResource
    {
        $user->loadCount('posts')->load('posts');

        return new UserResource($user);
    }
}
```

## 관련 페이지

<Card title="Eloquent 릴레이션 입문" icon="link" href="/ko/eloquent-relationships">
  릴레이션의 정의 방법과 Eager 로딩을 확인합니다.
</Card>

<Card title="페이지네이션" icon="list" href="/ko/pagination">
  페이지네이션 결과를 API 리소스와 조합하는 방법을 확인합니다.
</Card>


## Related topics

- [Laravel 13 신기능 정리](/ko/blog/laravel-13-new-features.md)
- [2026년 3월 Laravel 업데이트](/ko/blog/changelog/202603.md)
- [컨트롤러](/ko/controllers.md)
- [페이지네이션](/ko/pagination.md)
- [Eloquent 액세서·뮤테이터·캐스트](/ko/eloquent-mutators.md)
