Eloquent: API 리소스
업데이트됨번역일: 2026년 9월 30일
이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.
- 원문 수정
- 2026년 9월 30일
- 번역 갱신
- 2026년 9월 30일
Eloquent: API 리소스
소개
API를 개발하다 보면 Eloquent 모델을 실제로 JSON 응답으로 변환하는 "변환 계층(transformation layer)"이 필요할 때가 많습니다. 예를 들어 사용자에게만 특정 속성을 보여주고 싶거나, 모델의 관계를 JSON에 포함시키고 싶을 수 있습니다. 이런 변환 로직을 매번 컨트롤러에서 직접 처리하다 보면, 같은 모델을 여러 응답에서 서로 다른 형태로 직렬화해야 할 때 코드가 금방 지저분해집니다.
Laravel의 Eloquent 리소스 클래스를 사용하면 모델과 모델 컬렉션을 JSON으로 표현하는 방식을 명확하고 재사용 가능한 형태로 정의할 수 있습니다. 즉, 모델(또는 컬렉션)을 리소스로 감싸기만 하면, 그 리소스가 정의한 규칙에 따라 일관된 JSON을 만들어 줍니다.
Laravel은 두 가지 방식의 리소스 클래스를 제공합니다.
- 기본 Eloquent 리소스 — 자유로운 형태로 JSON 구조를 직접 설계할 때 사용합니다.
- JSON:API 리소스 — JSON:API 명세를 따르는 응답을 만들 때 사용합니다.
두 가지 모두 필요에 맞게 선택할 수 있으며, 한 프로젝트 안에서 API 스타일에 따라 혼용해도 괜찮습니다. 이 문서에서는 먼저 기본 Eloquent 리소스의 개념과 작성법을 살펴본 뒤, JSON:API 리소스를 다룹니다.
NOTE
이 챕터는 Eloquent 리소스 클래스의 개요를 다룹니다. Laravel 애플리케이션에서 API를 구축하는 전 과정을 심도 있게 다루는 튜토리얼이 필요하다면 Laravel API 구축 완전 가이드를 참고하세요.
리소스 생성하기
리소스 클래스는 make:resource Artisan 명령어로 생성할 수 있습니다. 생성된 리소스는 기본적으로 app/Http/Resources 디렉터리에 위치합니다. 리소스는 Illuminate\Http\Resources\Json\JsonResource 클래스를 상속합니다.
php artisan make:resource UserResource리소스 컬렉션
개별 모델을 변환하는 리소스뿐 아니라, 모델 컬렉션을 변환하는 역할을 담당하는 리소스도 만들 수 있습니다. 이렇게 하면 응답에 해당 리소스 전체와 관련된 링크나 기타 메타 정보를 포함시킬 수 있습니다.
리소스 컬렉션을 생성하려면 리소스를 만들 때 --collection 플래그를 사용하면 됩니다. 또는 리소스 이름에 Collection이라는 단어를 포함시키면 Laravel이 자동으로 컬렉션 리소스를 생성해야 한다고 인식합니다. 컬렉션 리소스는 Illuminate\Http\Resources\Json\ResourceCollection 클래스를 상속합니다.
php artisan make:resource User --collectionphp artisan make:resource UserCollectionEloquent API 리소스
소개
API를 개발하다 보면 Eloquent 모델과 실제로 사용자에게 반환되는 JSON 응답 사이에 변환 레이어가 필요할 때가 많습니다. 예를 들어 특정 사용자 그룹에게만 일부 속성을 보여주고 싶거나, 모델을 JSON으로 표현할 때 항상 특정 연관관계를 포함하고 싶은 경우가 있을 수 있습니다. Eloquent의 리소스 클래스를 사용하면 모델과 모델 컬렉션을 JSON으로 변환하는 과정을 표현력 있고 손쉽게 처리할 수 있습니다.
물론 toJson 메서드를 사용해서 Eloquent 모델이나 컬렉션을 바로 JSON으로 변환할 수도 있습니다. 하지만 Eloquent 리소스를 사용하면 모델과 그 연관관계를 JSON으로 직렬화하는 과정을 훨씬 더 세밀하고 견고하게 제어할 수 있습니다.
NOTE
이 문서를 읽기 전에, 실무에서 자주 사용되는 예시로 "게시글(Post)과 작성자(User) 정보를 함께 API로 노출하는 상황"을 떠올려 보세요. 게시글 목록 API에서는 작성자의 닉네임만 보여주고, 게시글 상세 API에서는 작성자의 프로필 정보까지 포함해야 하는 경우처럼, 응답 형태가 상황에 따라 달라져야 할 때 리소스 클래스가 특히 유용합니다.
리소스 생성하기
리소스 클래스는 make:resource Artisan 명령어로 생성할 수 있습니다. 기본적으로 생성된 리소스는 애플리케이션의 app/Http/Resources 디렉터리에 위치하며, Illuminate\Http\Resources\Json\JsonResource 클래스를 상속받습니다:
php artisan make:resource UserResource리소스 컬렉션
개별 모델을 변환하는 리소스뿐만 아니라, 모델의 컬렉션 전체를 변환하는 역할을 하는 리소스도 생성할 수 있습니다. 이런 컬렉션 리소스를 사용하면 JSON 응답에 특정 링크나, 컬렉션 전체와 관련된 메타 정보를 함께 포함시킬 수 있습니다.
리소스 컬렉션을 만들려면 리소스를 생성할 때 --collection 플래그를 사용하면 됩니다. 또는 리소스 이름에 Collection이라는 단어를 포함시키면, Laravel이 이를 컬렉션 리소스로 인식하고 자동으로 생성해줍니다. 컬렉션 리소스는 Illuminate\Http\Resources\Json\ResourceCollection 클래스를 상속받습니다:
php artisan make:resource User --collectionphp artisan make:resource UserCollectionNOTE
클래스 이름 끝에 Collection을 붙이면 별도로 --collection 옵션을 지정하지 않아도 컬렉션 리소스로 생성된다는 점을 기억해두면 편리합니다. 예를 들어 UserResource는 단일 모델을 다루는 리소스이고, UserCollection은 여러 개의 UserResource를 감싸는 컬렉션 리소스가 됩니다.
개념 개요
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));
});더 편리하게는, 모델의 toResource 메서드를 사용할 수도 있습니다. 이 메서드는 프레임워크의 규칙에 따라 모델에 해당하는 리소스를 자동으로 찾아줍니다.
return User::findOrFail($id)->toResource();toResource 메서드가 호출되면 Laravel은 모델의 네임스페이스와 가장 가까운 Http\Resources 네임스페이스 내에서, 모델 이름과 일치하거나 뒤에 Resource가 붙은 이름의 리소스 클래스를 찾으려고 시도합니다.
만약 리소스 클래스가 이러한 네이밍 규칙을 따르지 않거나 다른 네임스페이스에 위치해 있다면, UseResource 속성(attribute)을 사용해 모델의 기본 리소스를 명시적으로 지정할 수 있습니다.
<?php
namespace App\Models;
use App\Http\Resources\CustomUserResource;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\UseResource;
#[UseResource(CustomUserResource::class)]
class User extends Model
{
// ...
}또는 toResource 메서드에 리소스 클래스를 직접 전달하는 방법도 있습니다.
return User::findOrFail($id)->toResource(CustomUserResource::class);리소스 컬렉션
리소스의 컬렉션이나 페이지네이션된 응답을 반환하려면, 라우트나 컨트롤러에서 리소스 인스턴스를 생성할 때 리소스 클래스가 제공하는 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();toResourceCollection 메서드가 호출되면 Laravel은 모델의 네임스페이스와 가장 가까운 Http\Resources 네임스페이스 내에서, 모델 이름 뒤에 Collection이 붙은 이름의 리소스 컬렉션 클래스를 찾으려고 시도합니다.
리소스 컬렉션 클래스가 이 네이밍 규칙을 따르지 않거나 다른 네임스페이스에 위치해 있다면, UseResourceCollection 속성을 사용해 모델의 기본 리소스 컬렉션을 지정할 수 있습니다.
<?php
namespace App\Models;
use App\Http\Resources\CustomUserCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\UseResourceCollection;
#[UseResourceCollection(CustomUserCollection::class)]
class User extends Model
{
// ...
}또는 toResourceCollection 메서드에 리소스 컬렉션 클래스를 직접 전달할 수도 있습니다.
return User::all()->toResourceCollection(CustomUserCollection::class);커스텀 리소스 컬렉션
기본적으로 리소스 컬렉션은 컬렉션과 함께 반환해야 할 별도의 메타 데이터를 추가할 수 없습니다. 만약 리소스 컬렉션의 응답을 커스터마이징하고 싶다면, 컬렉션을 대표하는 전용 리소스 클래스를 직접 생성할 수 있습니다.
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());
});또는 더 편리하게, Eloquent 컬렉션의 toResourceCollection 메서드를 사용해도 됩니다. 이 메서드는 프레임워크 규칙에 따라 모델에 해당하는 리소스 컬렉션을 자동으로 찾아줍니다.
return User::all()->toResourceCollection();toResourceCollection 메서드가 호출되면 Laravel은 모델의 네임스페이스와 가장 가까운 Http\Resources 네임스페이스 내에서, 모델 이름 뒤에 Collection이 붙은 이름의 리소스 컬렉션 클래스를 찾으려고 시도합니다.
컬렉션 키 유지하기
라우트에서 리소스 컬렉션을 반환하면 Laravel은 기본적으로 컬렉션의 키를 초기화해서 숫자 순서로 재정렬합니다. 하지만 리소스 클래스에 PreserveKeys 속성을 사용하면 컬렉션의 원래 키를 유지할지 여부를 지정할 수 있습니다.
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Attributes\PreserveKeys;
use Illuminate\Http\Resources\Json\JsonResource;
#[PreserveKeys]
class UserResource extends JsonResource
{
// ...
}preserveKeys 속성 값이 true로 설정되어 있으면, 컬렉션이 라우트나 컨트롤러에서 반환될 때 원래의 키가 그대로 유지됩니다.
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\Attributes\Collects;
use Illuminate\Http\Resources\Json\ResourceCollection;
#[Collects(Member::class)]
class UserCollection extends ResourceCollection
{
// ...
}Eloquent API 리소스
리소스 작성하기
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\Models\User;
Route::get('/user/{id}', function (string $id) {
return User::findOrFail($id)->toResource();
});연관관계 포함하기
응답에 연관된 리소스를 함께 포함하고 싶다면, 리소스의 toArray 메서드가 반환하는 배열에 추가하면 됩니다. 아래 예제에서는 PostResource의 collection 메서드를 사용해 사용자의 게시글 목록을 함께 응답에 담아봅니다:
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
연관관계가 실제로 로드되어 있을 때만 포함시키고 싶다면 조건부 연관관계 부분을 참고하세요.
리소스 컬렉션
지금까지 살펴본 리소스는 모델 하나를 배열로 변환하는 역할이었다면, 리소스 컬렉션은 여러 모델의 집합(컬렉션)을 배열로 변환합니다. 다만 모델마다 매번 리소스 컬렉션 클래스를 따로 정의할 필요는 없습니다. 모든 Eloquent 모델 컬렉션은 toResourceCollection 메서드를 제공하며, 이를 통해 즉석에서("ad-hoc") 리소스 컬렉션을 생성할 수 있기 때문입니다:
use App\Models\User;
Route::get('/users', function () {
return User::all()->toResourceCollection();
});하지만 컬렉션과 함께 반환되는 메타 데이터를 직접 커스터마이징하고 싶다면, 별도의 리소스 컬렉션 클래스를 정의해야 합니다:
<?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());
});또는 편의를 위해 Eloquent 컬렉션의 toResourceCollection 메서드를 사용할 수도 있습니다. 이 메서드는 프레임워크의 명명 규칙에 따라 모델에 맞는 리소스 컬렉션 클래스를 자동으로 찾아줍니다:
return User::all()->toResourceCollection();toResourceCollection 메서드를 호출하면, Laravel은 모델과 가장 가까운 Http\Resources 네임스페이스 안에서 모델 이름 뒤에 Collection을 붙인 이름의 리소스 컬렉션 클래스를 찾으려고 시도합니다.
데이터 래핑(Wrapping)
기본적으로 리소스 응답을 JSON으로 변환할 때, 가장 바깥쪽(최상위) 리소스는 data 키로 감싸집니다. 예를 들어 일반적인 리소스 컬렉션 응답은 다음과 같은 형태입니다:
{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "therese28@example.com"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "evandervort@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 키로 감싸고 싶다면, 각 리소스마다 리소스 컬렉션 클래스를 정의하고 그 안에서 컬렉션을 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 키로 감쌉니다. 페이지네이션된 응답에는 항상 페이지네이터의 상태 정보를 담은 meta와 links 키가 함께 포함되기 때문입니다:
{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "therese28@example.com"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "evandervort@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());
});또는 편의를 위해 페이지네이터의 toResourceCollection 메서드를 사용할 수도 있습니다. 이 메서드는 프레임워크 규칙에 따라 페이지네이션된 모델에 맞는 리소스 컬렉션을 자동으로 찾아줍니다:
return User::paginate()->toResourceCollection();페이지네이션된 응답에는 항상 페이지네이터의 상태를 나타내는 meta와 links 키가 포함됩니다:
{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "therese28@example.com"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "evandervort@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
}
}페이지네이션 정보 커스터마이징
페이지네이션 응답의 links나 meta 키에 담기는 정보를 직접 변경하고 싶다면, 리소스에 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 키는 응답에서 제거된 채로 클라이언트에 전달됩니다.
조건부 연관관계 개수
연관관계 자체를 조건부로 포함시키는 것 외에도, 모델에 연관관계의 "개수(count)"가 로드되어 있는지에 따라 이를 조건부로 응답에 포함시킬 수 있습니다:
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->email,
'posts_count' => $this->whenCounted('posts'),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}이 예제에서 posts 연관관계의 개수가 로드되어 있지 않다면, posts_count 키는 클라이언트에 전달되기 전에 응답에서 제거됩니다.
avg, sum, min, max와 같은 다른 집계(aggregate) 값들도 whenAggregated 메서드를 사용해 조건부로 로드할 수 있습니다:
'words_avg' => $this->whenAggregated('posts', 'words', 'avg'),
'words_sum' => $this->whenAggregated('posts', 'words', 'sum'),
'words_min' => $this->whenAggregated('posts', 'words', 'min'),
'words_max' => $this->whenAggregated('posts', 'words', 'max'),조건부 피벗(Pivot) 정보
연관관계 정보를 조건부로 포함시키는 것 외에도, 다대다(many-to-many) 연관관계의 중간 테이블(피벗 테이블) 데이터를 whenPivotLoaded 메서드를 사용해 조건부로 포함시킬 수 있습니다. whenPivotLoaded 메서드는 첫 번째 인자로 피벗 테이블의 이름을 받습니다. 두 번째 인자는 피벗 정보가 모델에 로드되어 있을 때 반환할 값을 계산하는 클로저입니다:
/**
* 리소스를 배열로 변환합니다.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'expires_at' => $this->whenPivotLoaded('role_user', function () {
return $this->pivot->expires_at;
}),
];
}연관관계가 커스텀 중간 테이블 모델을 사용하고 있다면, whenPivotLoaded 메서드의 첫 번째 인자로 해당 중간 테이블 모델의 인스턴스를 전달할 수 있습니다:
'expires_at' => $this->whenPivotLoaded(new Membership, function () {
return $this->pivot->expires_at;
}),만약 중간 테이블에서 pivot이 아닌 다른 이름의 접근자(accessor)를 사용하고 있다면, whenPivotLoadedAs 메서드를 사용하면 됩니다:
/**
* 리소스를 배열로 변환합니다.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'expires_at' => $this->whenPivotLoadedAs('subscription', 'role_user', function () {
return $this->subscription->expires_at;
}),
];
}메타 데이터 추가하기
일부 JSON API 표준에서는 리소스나 리소스 컬렉션 응답에 메타 데이터를 함께 포함할 것을 요구합니다. 여기에는 흔히 리소스나 연관 리소스를 가리키는 links, 혹은 리소스 자체에 대한 메타 정보 등이 포함됩니다. 리소스에 추가적인 메타 데이터를 넣고 싶다면 toArray 메서드 안에 포함시키면 됩니다. 예를 들어 리소스 컬렉션을 변환할 때 links 정보를 함께 넣을 수 있습니다:
/**
* 리소스를 배열로 변환합니다.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'links' => [
'self' => 'link-value',
],
];
}리소스에서 추가 메타 데이터를 반환할 때, 페이지네이션된 응답을 반환하면서 Laravel이 자동으로 추가하는 links나 meta 키를 실수로 덮어쓸 걱정은 하지 않아도 됩니다. 여러분이 정의한 추가 links는 페이지네이터가 제공하는 링크 정보와 자동으로 병합됩니다.
최상위 메타 데이터
경우에 따라 해당 리소스가 응답에서 가장 바깥쪽(최상위) 리소스일 때만 특정 메타 데이터를 포함시키고 싶을 수 있습니다. 보통 이런 메타 데이터는 응답 전체에 대한 정보를 담습니다. 이를 정의하려면 리소스 클래스에 with 메서드를 추가하면 됩니다. 이 메서드는 해당 리소스가 변환 대상 중 가장 바깥쪽 리소스일 때만 응답에 포함될 메타 데이터 배열을 반환해야 합니다:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* 리소스 컬렉션을 배열로 변환합니다.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
/**
* 리소스 배열과 함께 반환할 추가 데이터를 가져옵니다.
*
* @return array<string, mixed>
*/
public function with(Request $request): array
{
return [
'meta' => [
'key' => 'value',
],
];
}
}리소스 생성 시 메타 데이터 추가하기
라우트나 컨트롤러에서 리소스 인스턴스를 생성할 때도 최상위 데이터를 추가할 수 있습니다. 모든 리소스에서 사용할 수 있는 additional 메서드는 응답에 추가할 데이터 배열을 인자로 받습니다:
return User::all()
->load('roles')
->toResourceCollection()
->additional(['meta' => [
'key' => 'value',
]]);JSON:API 리소스
라라벨은 JSON:API 스펙을 준수하는 응답을 생성해주는 JsonApiResource 클래스를 기본으로 제공합니다. 이 클래스는 표준 JsonResource를 확장한 것으로, 리소스 객체 구조, 관계(relationships), 스파스 필드셋(sparse fieldsets), 포함(include) 처리, 속성의 지연 평가(lazy evaluation)를 자동으로 처리해 주며, Content-Type 헤더도 application/vnd.api+json으로 자동 설정해 줍니다.
NOTE
라라벨의 JSON:API 리소스는 응답을 직렬화(serialization)하는 역할만 담당합니다. 만약 클라이언트가 보낸 필터, 정렬 같은 JSON:API 쿼리 파라미터까지 파싱해야 한다면, Spatie의 Laravel Query Builder 패키지를 함께 사용하는 것을 추천합니다.
JSON:API 리소스 생성하기
JSON:API 리소스를 생성하려면 make:resource Artisan 명령어에 --json-api 옵션을 추가하면 됩니다.
php artisan make:resource PostResource --json-api이렇게 생성된 클래스는 Illuminate\Http\Resources\JsonApi\JsonApiResource를 상속하며, 정의할 수 있도록 $attributes와 $relationships 프로퍼티가 미리 포함되어 있습니다.
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
class PostResource extends JsonApiResource
{
/**
* 리소스의 속성 목록
*/
public $attributes = [
// ...
];
/**
* 리소스의 관계 목록
*/
public $relationships = [
// ...
];
}JSON:API 리소스도 일반 리소스와 마찬가지로 라우트나 컨트롤러에서 그대로 반환할 수 있습니다.
use App\Http\Resources\PostResource;
use App\Models\Post;
Route::get('/api/posts/{post}', function (Post $post) {
return new PostResource($post);
});또는 모델의 toResource 메서드를 사용하면 더 간결하게 작성할 수 있습니다.
Route::get('/api/posts/{post}', function (Post $post) {
return $post->toResource();
});위 코드는 다음과 같이 JSON:API 규격을 따르는 응답을 만들어 냅니다.
{
"data": {
"id": "1",
"type": "posts",
"attributes": {
"title": "Hello World",
"body": "This is my first post."
}
}
}JSON:API 리소스 컬렉션을 반환하려면 collection 메서드나, 편의 메서드인 toResourceCollection을 사용하면 됩니다.
return PostResource::collection(Post::all());
return Post::all()->toResourceCollection();속성(Attribute) 정의하기
JSON:API 리소스에 포함될 속성을 정의하는 방법은 두 가지입니다.
가장 간단한 방법은 리소스에 $attributes 프로퍼티를 정의하는 것입니다. 속성 이름을 값으로 나열하기만 하면, 라라벨이 연결된 모델에서 해당 값을 직접 읽어옵니다.
public $attributes = [
'title',
'body',
'created_at',
];만약 어떤 속성 값을 계산하는 데 비용이 많이 든다면, toAttributes 안에서 해당 속성을 클로저로 반환하세요. 그러면 응답에서 실제로 이 속성이 필요할 때만 클로저가 실행됩니다.
또는 리소스의 속성을 더 세밀하게 제어하고 싶다면, toAttributes 메서드를 오버라이드할 수도 있습니다.
/**
* 리소스의 속성 목록을 반환합니다.
*
* @return array<string, mixed>
*/
public function toAttributes(Request $request): array
{
return [
'title' => $this->title,
'body' => $this->body,
'is_published' => fn () => $this->published_at !== null,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}관계(Relationship) 정의하기
JSON:API 리소스는 JSON:API 스펙을 따르는 관계 정의를 지원합니다. 관계는 클라이언트가 include 쿼리 파라미터로 요청했을 때만 직렬화됩니다.
`$relationships` 프로퍼티
리소스에서 포함(include) 가능한 관계는 $relationships 프로퍼티로 정의할 수 있습니다.
public $relationships = [
'author',
'comments',
];관계 이름만 값으로 나열하면, 라라벨이 해당 이름에 대응하는 Eloquent 관계를 자동으로 찾아내고, 적절한 리소스 클래스도 자동으로 탐지합니다. 사용할 리소스 클래스를 직접 지정하고 싶다면, 키/클래스 쌍으로 정의하면 됩니다.
use App\Http\Resources\UserResource;
public $relationships = [
'author' => UserResource::class,
'comments',
];또는 toRelationships 메서드를 오버라이드해서 정의할 수도 있습니다.
/**
* 리소스의 관계 목록을 반환합니다.
*/
public function toRelationships(Request $request): array
{
return [
'author' => UserResource::class,
'comments' => fn () => CommentResource::collection(
$request->user()->is($this->resource)
? $this->comments
: $this->comments->where('is_public', true),
),
];
}이처럼 클로저를 사용하면 관계 데이터를 좀 더 세밀하게 제어할 수 있으면서도, 클라이언트가 실제로 요청했을 때만 관계가 해석(resolve)되도록 만들 수 있습니다.
관계 포함하기 (Including Relationships)
클라이언트는 include 쿼리 파라미터를 사용해 연관된 리소스를 함께 요청할 수 있습니다.
GET /api/posts/1?include=author,comments
이 요청에 대한 응답에서는 relationships 키 안에 리소스 식별자 객체(resource identifier object)가 담기고, 최상위의 included 배열에는 전체 리소스 객체가 포함됩니다.
{
"data": {
"id": "1",
"type": "posts",
"attributes": {
"title": "Hello World"
},
"relationships": {
"author": {
"data": {
"id": "1",
"type": "users"
}
},
"comments": {
"data": [
{
"id": "1",
"type": "comments"
}
]
}
}
},
"included": [
{
"id": "1",
"type": "users",
"attributes": {
"name": "Taylor Otwell"
}
},
{
"id": "1",
"type": "comments",
"attributes": {
"body": "Great post!"
}
}
]
}중첩된 관계는 점(dot) 표기법으로 포함시킬 수 있습니다.
GET /api/posts/1?include=comments.author
관계 깊이 제한
기본적으로 중첩된 관계 include는 최대 깊이 제한이 걸려 있습니다. 이 제한값은 maxRelationshipDepth 메서드로 변경할 수 있으며, 보통 애플리케이션의 서비스 프로바이더 안에서 호출합니다.
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
JsonApiResource::maxRelationshipDepth(3);리소스 타입(Type)과 ID
기본적으로 리소스의 type 값은 리소스 클래스 이름을 기반으로 결정됩니다. 예를 들어 PostResource는 posts라는 타입을, BlogPostResource는 blog_posts라는 타입을 만들어 냅니다. 리소스의 id는 모델의 기본 키(primary key)에서 가져옵니다.
이 값을 직접 지정하고 싶다면, 리소스에서 toType과 toId 메서드를 오버라이드하면 됩니다.
/**
* 리소스의 타입을 반환합니다.
*/
public function toType(Request $request): string
{
return 'articles';
}
/**
* 리소스의 ID를 반환합니다.
*/
public function toId(Request $request): string
{
return (string) $this->uuid;
}이 기능은 리소스 클래스 이름과 실제 타입을 다르게 지정해야 할 때 특히 유용합니다. 예를 들어 User 모델을 감싸는 AuthorResource가 있는데, 응답에서는 authors라는 타입으로 노출하고 싶은 경우가 이에 해당합니다.
스파스 필드셋과 include
JSON:API 리소스는 스파스 필드셋을 지원합니다. 이를 통해 클라이언트는 fields 쿼리 파라미터로 각 리소스 타입별로 원하는 속성만 골라서 요청할 수 있습니다.
GET /api/posts?fields[posts]=title,created_at&fields[users]=name
위 요청은 posts 리소스에서는 title과 created_at 속성만, users 리소스에서는 name 속성만 응답에 포함시킵니다.
쿼리 스트링 무시하기
특정 리소스 응답에서 스파스 필드셋 필터링을 비활성화하고 싶다면, ignoreFieldsAndIncludesInQueryString 메서드를 호출하면 됩니다.
return $post->toResource()
->ignoreFieldsAndIncludesInQueryString();이미 로드된(eager-loaded) 관계 포함하기
기본적으로 관계는 include 쿼리 파라미터로 명시적으로 요청했을 때만 응답에 포함됩니다. 쿼리 스트링과 상관없이, 이미 즉시 로딩(eager loading)해 둔 관계를 모두 응답에 포함시키고 싶다면 includePreviouslyLoadedRelationships 메서드를 사용하세요.
return $post->load('author', 'comments')
->toResource()
->includePreviouslyLoadedRelationships();링크(Links)와 메타(Meta) 정보
리소스에서 toLinks와 toMeta 메서드를 오버라이드하면, JSON:API 리소스 객체에 링크와 메타 정보를 추가할 수 있습니다.
/**
* 리소스의 링크 목록을 반환합니다.
*/
public function toLinks(Request $request): array
{
return [
'self' => route('api.posts.show', $this->resource),
];
}
/**
* 리소스의 메타 정보를 반환합니다.
*/
public function toMeta(Request $request): array
{
return [
'readable_created_at' => $this->created_at->diffForHumans(),
];
}이렇게 정의하면 응답의 리소스 객체에 links와 meta 키가 추가됩니다.
{
"data": {
"id": "1",
"type": "posts",
"attributes": {
"title": "Hello World"
},
"links": {
"self": "https://example.com/api/posts/1"
},
"meta": {
"readable_created_at": "2 hours ago"
}
}
}리소스 응답
앞서 살펴본 것처럼, 리소스는 라우트나 컨트롤러에서 바로 반환할 수 있습니다:
use App\Models\User;
Route::get('/user/{id}', function (string $id) {
return User::findOrFail($id)->toResource();
});하지만 클라이언트로 응답을 보내기 전에 HTTP 응답 자체를 커스터마이징해야 할 때가 있습니다. 이를 위한 방법은 두 가지입니다.
첫 번째 방법은 리소스에 response 메서드를 체이닝하는 것입니다. 이 메서드는 Illuminate\Http\JsonResponse 인스턴스를 반환하므로, 응답 헤더를 자유롭게 제어할 수 있습니다:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/user', function () {
return User::find(1)
->toResource()
->response()
->header('X-Value', 'True');
});두 번째 방법은 리소스 클래스 안에 withResponse 메서드를 직접 정의하는 것입니다. 이 메서드는 해당 리소스가 응답의 최상위(가장 바깥쪽) 리소스로 반환될 때 호출됩니다:
<?php
namespace App\Http\Resources;
use Illuminate\Http\JsonResponse;
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,
];
}
/**
* 리소스에 대한 응답을 커스터마이징합니다.
*/
public function withResponse(Request $request, JsonResponse $response): void
{
$response->header('X-Value', 'True');
}
}NOTE
withResponse 메서드는 해당 리소스가 응답의 최상위 리소스일 때만 호출됩니다. 만약 이 리소스가 컬렉션이나 다른 리소스 안에 중첩되어 반환되는 경우에는 호출되지 않으니 주의하세요.
이렇게 두 가지 방법을 활용하면 응답 본문뿐만 아니라 상태 코드, 헤더 등 응답 전반을 원하는 대로 제어할 수 있습니다. 일반적으로 특정 라우트에서만 일회성으로 헤더를 추가하고 싶다면 response() 체이닝 방식이, 리소스 자체의 기본 동작으로 고정하고 싶다면 withResponse 메서드 정의 방식이 더 적합합니다.