본문 바로가기

Eloquent: API 리소스

번역일: 2026년 6월 21일

Eloquent: API 리소스

소개

API를 개발하다 보면, Eloquent 모델과 실제 클라이언트에 반환되는 JSON 응답 사이에 변환 계층이 필요한 경우가 생깁니다. 예를 들어, 특정 사용자 역할에 따라 일부 속성만 노출하거나, 항상 특정 관계 데이터를 JSON에 포함시키고 싶을 수 있습니다. Eloquent의 리소스 클래스를 사용하면 모델과 모델 컬렉션을 JSON으로 변환하는 로직을 명확하고 간결하게 작성할 수 있습니다.

물론 모델이나 컬렉션에 toJson 메서드를 직접 호출해 JSON으로 변환할 수도 있습니다. 하지만 API 리소스는 모델과 관계 데이터의 JSON 직렬화 방식을 훨씬 더 세밀하게 제어할 수 있게 해줍니다.

리소스 생성

리소스 클래스는 make:resource Artisan 명령어로 생성합니다. 생성된 파일은 기본적으로 app/Http/Resources 디렉터리에 위치하며, Illuminate\Http\Resources\Json\JsonResource 클래스를 상속합니다.

php artisan make:resource UserResource

리소스 컬렉션

단일 모델을 변환하는 리소스 외에, 모델 컬렉션 전체를 변환하는 리소스 컬렉션도 생성할 수 있습니다. 컬렉션 리소스를 사용하면 링크나 페이지네이션 메타 정보처럼 컬렉션 전체에 관련된 부가 데이터도 응답에 포함시킬 수 있습니다.

컬렉션 리소스를 생성하려면 --collection 플래그를 사용하거나, 클래스 이름에 Collection을 붙이면 됩니다. 컬렉션 리소스는 Illuminate\Http\Resources\Json\ResourceCollection 클래스를 상속합니다.

php artisan make:resource User --collectionphp artisan make:resource UserCollection

개념 개요

NOTE

이 섹션은 리소스와 리소스 컬렉션에 대한 전체적인 흐름을 설명합니다. 커스터마이징 방법과 리소스의 다양한 기능을 깊이 이해하려면 이후 섹션들도 꼼꼼히 읽어보시길 권장합니다.

본격적으로 리소스 작성 방법을 살펴보기 전에, Laravel에서 리소스가 어떻게 동작하는지 큰 그림을 먼저 파악해 보겠습니다.

리소스 클래스는 단일 모델을 JSON 구조로 변환하는 역할을 합니다. 다음은 간단한 UserResource 예시입니다.

<?php namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { /** * 리소스를 배열로 변환합니다. * * @return array<string, mixed> */ 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, ]; } }

모든 리소스 클래스는 toArray 메서드를 정의합니다. 이 메서드는 라우트나 컨트롤러에서 응답을 반환할 때 JSON으로 변환될 속성 배열을 반환합니다.

$this를 통해 모델 프로퍼티에 직접 접근할 수 있는 것은, 리소스 클래스가 내부적으로 모델의 프로퍼티와 메서드 접근을 자동으로 프록시해 주기 때문입니다. 리소스를 정의했다면 라우트나 컨트롤러에서 바로 반환할 수 있습니다. 리소스 생성자에 모델 인스턴스를 전달하면 됩니다.

use App\Http\Resources\UserResource; use App\Models\User; Route::get('/user/{id}', function (string $id) { return new UserResource(User::findOrFail($id)); });

리소스 컬렉션

리소스 컬렉션이나 페이지네이션된 응답을 반환할 때는 라우트 또는 컨트롤러에서 리소스 클래스의 collection 메서드를 사용합니다.

use App\Http\Resources\UserResource; use App\Models\User; Route::get('/users', function () { return UserResource::collection(User::all()); });

단, 이 방법으로는 컬렉션 응답에 커스텀 메타 데이터를 추가할 수 없습니다. 메타 데이터를 함께 반환하려면 전용 컬렉션 리소스 클래스를 별도로 생성해야 합니다.

php artisan make:resource UserCollection

컬렉션 리소스 클래스를 생성한 뒤, 응답에 포함할 메타 데이터를 자유롭게 정의할 수 있습니다.

<?php namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection { /** * 리소스 컬렉션을 배열로 변환합니다. * * @return array<int|string, mixed> */ public function toArray(Request $request): array { return [ 'data' => $this->collection, 'links' => [ 'self' => 'link-value', ], ]; } }

정의한 리소스 컬렉션은 라우트나 컨트롤러에서 바로 반환할 수 있습니다.

use App\Http\Resources\UserCollection; use App\Models\User; Route::get('/users', function () { return new UserCollection(User::all()); });

컬렉션 키 유지

라우트에서 리소스 컬렉션을 반환하면, Laravel은 기본적으로 컬렉션의 키를 0부터 시작하는 숫자 순서로 재설정합니다. 원래의 키를 유지하고 싶다면 리소스 클래스에 preserveKeys 프로퍼티를 추가하세요.

<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { /** * 컬렉션의 원래 키를 유지할지 여부를 지정합니다. * * @var bool */ public $preserveKeys = true; }

preserveKeystrue이면, 라우트나 컨트롤러에서 컬렉션을 반환할 때 원래 키가 그대로 유지됩니다.

use App\Http\Resources\UserResource; use App\Models\User; Route::get('/users', function () { return UserResource::collection(User::all()->keyBy->id); });

기본 리소스 클래스 커스터마이징

리소스 컬렉션의 $this->collection 프로퍼티는 자동으로 컬렉션의 각 항목을 단일 리소스 클래스에 매핑한 결과로 채워집니다. 단일 리소스 클래스 이름은 컬렉션 클래스 이름에서 Collection 접미사를 제거한 이름으로 가정하며, 필요에 따라 Resource 접미사가 붙을 수도 있습니다.

예를 들어, UserCollection은 각 유저 인스턴스를 UserResource로 매핑하려고 시도합니다. 이 동작을 변경하려면 $collects 프로퍼티를 재정의하세요.

<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection { /** * 이 컬렉션이 사용할 리소스 클래스를 지정합니다. * * @var string */ public $collects = Member::class; }

리소스 작성

NOTE

개념 개요를 아직 읽지 않으셨다면, 먼저 읽고 이 섹션을 진행하시길 권장합니다.

리소스의 핵심 역할은 주어진 모델을 배열로 변환하는 것입니다. 각 리소스는 toArray 메서드를 통해 모델의 속성을 API 응답에 맞는 배열로 변환하며, 이 배열이 라우트나 컨트롤러에서 반환됩니다.

<?php namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { /** * 리소스를 배열로 변환합니다. * * @return array<string, mixed> */ 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, ]; } }

리소스를 정의하면 라우트나 컨트롤러에서 직접 반환할 수 있습니다.

use App\Http\Resources\UserResource; use App\Models\User; Route::get('/user/{id}', function (string $id) { return new UserResource(User::findOrFail($id)); });

관계 포함

관련 리소스를 응답에 포함하려면 toArray 메서드가 반환하는 배열에 추가합니다. 아래 예시에서는 PostResourcecollection 메서드를 사용하여 사용자의 블로그 게시글을 응답에 추가합니다.

use App\Http\Resources\PostResource; use Illuminate\Http\Request; /** * 리소스를 배열로 변환합니다. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'posts' => PostResource::collection($this->posts), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; }

NOTE

이미 로드된 관계만 조건부로 포함하려면 조건부 관계 문서를 참고하세요.

리소스 컬렉션

단일 모델 변환과 달리 리소스 컬렉션은 모델 컬렉션 전체를 배열로 변환합니다. 모든 리소스는 collection 메서드를 제공하기 때문에, 별도의 컬렉션 클래스 없이 즉석에서 "임시 컬렉션 리소스"를 생성할 수 있습니다.

use App\Http\Resources\UserResource; use App\Models\User; Route::get('/users', function () { return UserResource::collection(User::all()); });

컬렉션과 함께 반환되는 메타 데이터를 커스터마이징하려면 별도의 컬렉션 리소스 클래스를 정의해야 합니다.

<?php namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection { /** * 리소스 컬렉션을 배열로 변환합니다. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ 'data' => $this->collection, 'links' => [ 'self' => 'link-value', ], ]; } }

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

use App\Http\Resources\UserCollection; use App\Models\User; Route::get('/users', function () { return new UserCollection(User::all()); });

데이터 래핑

기본적으로 리소스 응답이 JSON으로 변환될 때, 최상위 리소스 데이터는 data 키로 감싸집니다. 예를 들어, 일반적인 리소스 컬렉션 응답은 다음과 같은 형태입니다.

{ "data": [ { "id": 1, "name": "홍길동", "email": "hong@example.com" }, { "id": 2, "name": "김철수", "email": "kim@example.com" } ] }

최상위 리소스의 data 래핑을 비활성화하려면, 기본 Illuminate\Http\Resources\Json\JsonResource 클래스에서 withoutWrapping 메서드를 호출하세요. 보통 모든 요청에서 로드되는 AppServiceProvider 또는 다른 서비스 프로바이더boot 메서드에서 호출하는 것이 일반적입니다.

<?php namespace App\Providers; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { /** * 애플리케이션 서비스를 등록합니다. */ public function register(): void { // ... } /** * 애플리케이션 서비스를 부트스트랩합니다. */ public function boot(): void { JsonResource::withoutWrapping(); } }

WARNING

withoutWrapping 메서드는 최상위 응답의 래핑에만 영향을 미칩니다. 직접 리소스 컬렉션에 수동으로 추가한 data 키는 제거되지 않습니다.

중첩 리소스의 래핑

관계 리소스를 어떻게 래핑할지는 전적으로 개발자가 결정할 수 있습니다. 중첩 수준에 관계없이 모든 리소스 컬렉션을 data 키로 감싸고 싶다면, 각 리소스에 대해 컬렉션 리소스 클래스를 별도로 정의하고 toArray 에서 data 키를 반환하면 됩니다.

이렇게 하면 최상위 리소스가 data 키로 이중 래핑되지 않을지 걱정할 수 있습니다. Laravel은 리소스가 실수로 이중 래핑되는 것을 방지하므로, 변환하는 컬렉션의 중첩 수준을 신경 쓸 필요가 없습니다.

<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class CommentsCollection extends ResourceCollection { /** * 리소스 컬렉션을 배열로 변환합니다. * * @return array<string, mixed> */ public function toArray(Request $request): array { return ['data' => $this->collection]; } }

데이터 래핑과 페이지네이션

리소스 응답으로 페이지네이션된 컬렉션을 반환할 때는, withoutWrapping 메서드를 호출하더라도 Laravel이 리소스 데이터를 data 키로 감쌉니다. 페이지네이션 응답에는 항상 페이지네이터 상태 정보가 담긴 metalinks 키가 포함되어야 하기 때문입니다.

{ "data": [ { "id": 1, "name": "홍길동", "email": "hong@example.com" }, { "id": 2, "name": "김철수", "email": "kim@example.com" } ], "links": { "first": "http://example.com/users?page=1", "last": "http://example.com/users?page=1", "prev": null, "next": null }, "meta": { "current_page": 1, "from": 1, "last_page": 1, "path": "http://example.com/users", "per_page": 15, "to": 10, "total": 10 } }

페이지네이션

Laravel 페이지네이터 인스턴스를 리소스의 collection 메서드나 커스텀 리소스 컬렉션에 직접 전달할 수 있습니다.

use App\Http\Resources\UserCollection; use App\Models\User; Route::get('/users', function () { return new UserCollection(User::paginate()); });

페이지네이션 응답에는 항상 페이지네이터 상태 정보가 담긴 metalinks 키가 포함됩니다.

{ "data": [ { "id": 1, "name": "홍길동", "email": "hong@example.com" }, { "id": 2, "name": "김철수", "email": "kim@example.com" } ], "links": { "first": "http://example.com/users?page=1", "last": "http://example.com/users?page=1", "prev": null, "next": null }, "meta": { "current_page": 1, "from": 1, "last_page": 1, "path": "http://example.com/users", "per_page": 15, "to": 10, "total": 10 } }

페이지네이션 정보 커스터마이징

페이지네이션 응답의 linksmeta 키에 포함되는 정보를 커스터마이징하려면 리소스에 paginationInformation 메서드를 정의하세요. 이 메서드는 $paginated 데이터와 links, meta 키를 담은 $default 배열을 인자로 받습니다.

/** * 리소스의 페이지네이션 정보를 커스터마이징합니다. * * @param \Illuminate\Http\Request $request * @param array $paginated * @param array $default * @return array */ public function paginationInformation($request, $paginated, $default) { $default['links']['custom'] = 'https://example.com'; return $default; }

조건부 속성

특정 조건이 충족될 때만 리소스 응답에 속성을 포함하고 싶을 때가 있습니다. 예를 들어, 현재 사용자가 관리자일 때만 특정 값을 반환하는 경우입니다. Laravel은 이런 상황을 처리할 수 있는 다양한 헬퍼 메서드를 제공합니다. when 메서드를 사용하면 조건에 따라 속성을 추가할 수 있습니다.

/** * 리소스를 배열로 변환합니다. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'secret' => $this->when($request->user()->isAdmin(), 'secret-value'), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; }

이 예시에서 secret 키는 인증된 사용자의 isAdmin 메서드가 true를 반환할 때만 응답에 포함됩니다. false이면 클라이언트에 전송되기 전에 secret 키가 응답에서 제거됩니다. when 메서드를 사용하면 배열을 구성할 때 조건문을 따로 작성하지 않아도 됩니다.

when 메서드의 두 번째 인자로 클로저를 전달하면, 조건이 true일 때만 값을 계산합니다.

'secret' => $this->when($request->user()->isAdmin(), function () { return 'secret-value'; }),

whenHas 메서드는 해당 속성이 모델에 실제로 존재할 때만 포함시킵니다.

'name' => $this->whenHas('name'),

whenNotNull 메서드는 속성 값이 null이 아닐 때만 포함시킵니다.

'name' => $this->whenNotNull($this->name),

조건부 속성 병합

동일한 조건에 따라 여러 속성을 한꺼번에 포함시켜야 할 때는 mergeWhen 메서드를 사용하세요. 주어진 조건이 true일 때만 해당 속성들이 응답에 포함됩니다.

/** * 리소스를 배열로 변환합니다. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, $this->mergeWhen($request->user()->isAdmin(), [ 'first-secret' => 'value', 'second-secret' => 'value', ]), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; }

조건이 false이면 해당 속성들은 클라이언트에 전송되기 전에 응답에서 제거됩니다.

WARNING

mergeWhen 메서드는 문자열 키와 숫자 키가 혼합된 배열 내에서 사용하면 안 됩니다. 또한, 순차적이지 않은 숫자 키를 가진 배열에서도 사용하지 마세요.

조건부 관계

속성을 조건부로 포함하는 것과 마찬가지로, 모델에 관계 데이터가 이미 로드된 경우에만 관계를 응답에 포함시킬 수 있습니다. 이를 활용하면 컨트롤러에서 어떤 관계를 로드할지 결정하고, 리소스에서는 실제로 로드된 관계만 포함시키는 방식으로 깔끔하게 N+1 쿼리 문제를 방지할 수 있습니다.

whenLoaded 메서드를 사용하면 관계가 로드된 경우에만 포함시킬 수 있습니다. 이 메서드는 관계 객체 자체가 아닌 관계의 이름을 인자로 받아, 불필요한 관계 로드를 방지합니다.

use App\Http\Resources\PostResource; /** * 리소스를 배열로 변환합니다. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'posts' => PostResource::collection($this->whenLoaded('posts')), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; }

이 예시에서 posts 관계가 로드되지 않은 경우, posts 키는 클라이언트에 전송되기 전에 응답에서 제거됩니다.

조건부 관계 카운트

관계 자체뿐만 아니라, 관계 카운트가 모델에 로드되어 있을 때만 조건부로 포함시킬 수도 있습니다.

new UserResource($user->loadCount('posts'));

whenCounted 메서드는 관계 카운트가 로드된 경우에만 응답에 포함시킵니다. 카운트가 없으면 해당 속성은 응답에서 제외됩니다.

/** * 리소스를 배열로 변환합니다. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this

이 문서는 Laravel 공식 문서(MIT)를 한국 개발자를 위해 번역·재구성한 것입니다.

번역일: 2026년 6월 21일