메인 콘텐츠로 건너뛰기

API 리소스란

API를 구축할 때, Eloquent 모델을 그대로 JSON으로 반환하면 숨기고 싶은 컬럼이 새어 나가거나 클라이언트가 필요로 하지 않는 대량의 데이터를 보내 버리는 경우가 있습니다. Eloquent API 리소스는 모델과 JSON 응답 사이에 변환 레이어를 끼우는 구조입니다. toArray() 메서드로 “무엇을 어떤 형식으로 응답에 포함시킬지”를 명시적으로 정의할 수 있습니다. 주된 장점은 다음과 같습니다.
  • 응답에 포함시킬 필드를 완전히 컨트롤할 수 있다
  • 필드 이름의 변환이나 값의 가공을 한 곳에 정리할 수 있다
  • 조건에 따라 필드를 구분해서 출력할 수 있다
  • 릴레이션을 중첩해 일관된 구조를 유지할 수 있다

리소스 작성

make:resource Artisan 명령어로 리소스 클래스를 생성합니다.
php artisan make:resource UserResource
생성된 클래스는 app/Http/Resources 디렉터리에 놓입니다.
<?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로 모델의 프로퍼티에 직접 접근할 수 있습니다. 이것은 리소스 클래스가 내부에서 모델에의 접근을 프록시하고 있기 때문입니다.

컨트롤러에서의 사용

정의한 리소스는 컨트롤러나 라우트에서 반환할 수 있습니다.
use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/users/{user}', function (User $user) {
    return new UserResource($user);
});
또는 모델의 toResource() 메서드를 사용하는 방법도 있습니다.
Route::get('/users/{user}', function (User $user) {
    return $user->toResource();
});
toResource()는 모델명에 기반해 대응하는 리소스 클래스(UserResource)를 자동으로 찾습니다. 기본적으로 응답은 data 키로 래핑됩니다.
{
  "data": {
    "id": 1,
    "name": "山田 太郎",
    "email": "[email protected]",
    "created_at": "2024-01-15T10:00:00.000000Z",
    "updated_at": "2024-01-15T10:00:00.000000Z"
  }
}

리소스 컬렉션

여러 모델을 반환하는 경우 collection() 메서드를 사용합니다.
use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/users', function () {
    return UserResource::collection(User::all());
});
또는 Eloquent 컬렉션의 toResourceCollection()을 사용합니다.
return User::all()->toResourceCollection();

커스텀 컬렉션 리소스

컬렉션 전체에 메타데이터를 부가하고 싶은 경우, 전용 컬렉션 리소스를 작성합니다.
php artisan make:resource UserCollection
<?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'),
            ],
        ];
    }
}
Route::get('/users', function () {
    return new UserCollection(User::all());
});

필드의 가공과 변환

toArray() 내에서 필드 이름의 변경이나 값의 가공을 할 수 있습니다.
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()을 사용합니다.
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()을 사용합니다.
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 문제를 방지하면서 유연한 응답을 만들 수 있습니다.
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')),
    ];
}
컨트롤러 측에서 릴레이션을 로드할지 여부를 제어할 수 있습니다.
// posts 를 포함해서 반환
return new UserResource($user->load('posts'));

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

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

loadCount()로 취득한 릴레이션의 카운트를 조건부로 포함시킵니다.
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'posts_count' => $this->whenCounted('posts'),
    ];
}
return new UserResource($user->loadCount('posts'));

중첩된 리소스

릴레이션을 다른 리소스 클래스로 중첩함으로써 일관된 구조를 유지할 수 있습니다.
// 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(),
        ];
    }
}
// 컨트롤러
$post = Post::with(['user', 'comments'])->findOrFail($id);

return new PostResource($post);
{
  "data": {
    "id": 1,
    "title": "Laravel로 배우는 API 리소스",
    "author": {
      "id": 5,
      "name": "山田 太郎",
      "email": "[email protected]"
    },
    "comments": [
      { "id": 10, "body": "참고가 되었습니다!" }
    ]
  }
}

메타데이터의 추가

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

컬렉션 전체에 메타데이터를 부가하려면 with() 메서드를 오버라이드합니다.
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(),
            ],
        ];
    }
}
응답 예시:
{
  "data": [...],
  "meta": {
    "version": "1.0",
    "generated_at": "2024-01-15T10:00:00+00:00"
  }
}

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

컨트롤러 측에서 동적으로 메타데이터를 추가하고 싶은 경우 additional()을 사용합니다.
return User::all()
    ->load('roles')
    ->toResourceCollection()
    ->additional(['meta' => [
        'total_admins' => User::where('role', 'admin')->count(),
    ]]);

페이지네이션과의 조합

페이지네이션 결과를 리소스에 전달하기만 하면 metalinks가 자동으로 부가됩니다.
Route::get('/users', function () {
    return UserResource::collection(User::paginate(15));
});
또는:
return User::paginate(15)->toResourceCollection();
응답 예시:
{
  "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
  }
}
페이지네이션 응답에서는 withoutWrapping()을 호출하고 있어도 data 키는 반드시 붙습니다. 페이지네이션의 metalinks 키와 공존시키기 위해서입니다.

데이터 래핑의 무효화

기본적으로 최외곽 리소스가 data 키로 래핑됩니다. 이것을 무효화하려면 AppServiceProviderboot() 내에서 withoutWrapping()을 호출합니다.
use Illuminate\Http\Resources\Json\JsonResource;

public function boot(): void
{
    JsonResource::withoutWrapping();
}
withoutWrapping()은 최외곽 래핑에만 영향을 줍니다. 스스로 정의한 data 키는 삭제되지 않습니다.

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

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

UserResource

<?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

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);
    }
}

관련 페이지

Eloquent 릴레이션 입문

릴레이션의 정의 방법과 Eager 로딩을 확인합니다.

페이지네이션

페이지네이션 결과를 API 리소스와 조합하는 방법을 확인합니다.
마지막 수정일 2026년 7월 13일