02강: 쿼리 스코프(Query Scopes) & 도서 다중 필터링/정렬 체이닝
실습 프로젝트: 지니샵(JinyShop) 온라인 도서 쇼핑몰
주제: 비즈니스 조건 캡슐화 로컬 스코프(scopePublished,scopeInStock), 다중 조건 플루언트 쿼리(when), 가격/인기순 정렬
등장인물: 🐱 지니(시니어 멘토), 👧 도로시(주니어 개발자), 🐶 토토(개발 보조견)
공식 문서 연계: Eloquent 시작하기 (Eloquent), 쿼리 빌더 (Queries)
💬 1. 지니와 도로시의 티키타카 회의
👧 도로시: “지니! 도서 목록을 가져올 때마다 매번 where('status', 'published')->where('stock_quantity', '>', 0)을 중복해서 적다 보니까 실수가 잦아져요. ‘출판 완료된 도서’나 ‘재고 있는 도서’ 같은 서점의 기본 규칙을 모델에 딱 한 번만 적어두고 재사용할 수는 없나요?”
🐱 지니: “도로시, 바로 그럴 때 사용하는 마법이 로컬 쿼리 스코프(Local Query Scopes)란다! 모델 안에 scopePublished()나 scopeInStock()처럼 정의해두면, 밖에서는 Book::published()->inStock()->get() 처럼 마치 영어를 읽듯이 자연스러운 체이닝으로 쿼리를 작성할 수 있지! 게다가 쿼리 빌더의 when() 메서드를 조합하면 검색어가 있을 때만 조건절을 동적으로 붙이는 명품 컨트롤러를 완성할 수 있단다!”
🐶 토토: “멍멍! if ($keyword) { $query->where(...) } 처럼 지저분한 if문을 덕지덕지 쓰지 않고 $query->when($keyword, ...)을 쓰는 것이 라라벨 고수들의 비법이다멍!”
🎯 2. 실습 목표 및 서점 시나리오
Book모델에 자주 쓰이는 5대 비즈니스 로컬 쿼리 스코프를 정의합니다.when()헬퍼를 활용해 카테고리, 검색어, 가격대, 재고 유무를 조건부 결합하는 유연한 쿼리를 작성합니다.- 인기순, 신간순, 낮은 가격순, 높은 가격순 정렬 스위칭을 구현합니다.
🛠️ 3. 단계별 실습 절차 (Step-by-Step)
[Step 1] Book 모델에 쿼리 스코프 추가
app/Models/Book.php에 다음 스코프 메서드들을 작성합니다:
// app/Models/Book.php 내부에 추가
use Illuminate\Database\Eloquent\Builder;
/**
* 1. 출판 완료된 정상 도서만 조회
*/
public function scopePublished(Builder $query): void
{
$query->where('status', 'published');
}
/**
* 2. 실물 재고가 남아있는 도서만 조회 (또는 무제한 전자책)
*/
public function scopeInStock(Builder $query): void
{
$query->where(function (Builder $q) {
$q->where('stock_quantity', '>', 0)
->orWhere('format', 'ebook');
});
}
/**
* 3. 할인 중인 도서만 조회
*/
public function scopeDiscounted(Builder $query): void
{
$query->whereNotNull('sale_price')
->whereColumn('sale_price', '<', 'price');
}
/**
* 4. 도서명 및 소개글 키워드 검색 스코프
*/
public function scopeSearch(Builder $query, ?string $keyword): void
{
if (empty($keyword)) {
return;
}
$query->where(function (Builder $q) use ($keyword) {
$q->where('title', 'like', "%{$keyword}%")
->orWhere('isbn', 'like', "%{$keyword}%")
->orWhere('description', 'like', "%{$keyword}%");
});
}
/**
* 5. 정렬 기준 적용 스코프
*/
public function scopeSortBy(Builder $query, string $sort): void
{
match ($sort) {
'price_asc' => $query->orderByRaw('COALESCE(sale_price, price) ASC'),
'price_desc' => $query->orderByRaw('COALESCE(sale_price, price) DESC'),
'latest' => $query->orderBy('published_at', 'desc'),
default => $query->orderBy('id', 'desc'), // 기본 최신순
};
}
[Step 2] BookController@index에서 우아한 체이닝 조립
app/Http/Controllers/BookController.php의 index() 메서드를 쿼리 스코프 기반으로 전면 리팩토링합니다:
// app/Http/Controllers/BookController.php
use App\Models\Book;
use Illuminate\Http\Request;
public function index(Request $request)
{
$keyword = $request->string('keyword')->trim()->value();
$categoryId = $request->integer('category_id');
$inStockOnly = $request->boolean('in_stock_only', false);
$discountOnly = $request->boolean('discount_only', false);
$sort = $request->input('sort', 'latest');
// 1. 기본 쿼리 빌더 시작 (출판 완료 도서)
$books = Book::query()
->published()
// 검색어가 있으면 search 스코프 적용
->when($keyword, fn ($q) => $q->search($keyword))
// 카테고리 필터가 있으면 적용
->when($categoryId, fn ($q) => $q->where('category_id', $categoryId))
// 재고 필터 플래그가 켜져 있으면 inStock 적용
->when($inStockOnly, fn ($q) => $q->inStock())
// 할인 도서 플래그
->when($discountOnly, fn ($q) => $q->discounted())
// 정렬 적용
->sortBy($sort)
// 3강에서 배울 12권씩 페이징 적용
->paginate(12)
->withQueryString(); // 검색 쿼리스트링 파라미터 링크 유지!
return view('books.index', compact('books', 'keyword', 'categoryId', 'inStockOnly', 'discountOnly', 'sort'));
}
[Step 3] 브라우저에서 다중 검색 필터링 테스트
로컬 서버에서 다양한 URL 조건을 전달해 봅니다:
- 라라벨 검색어 필터:
http://127.0.0.1:8000/books?keyword=라라벨 - 할인 중인 도서만 낮은 가격순 정렬:
http://127.0.0.1:8000/books?discount_only=1&sort=price_asc - 재고 있는 책 중 높은 가격순:
http://127.0.0.1:8000/books?in_stock_only=1&sort=price_desc
단 하나의 깔끔한 컨트롤러 메서드가 수십 가지의 복합 조합을 에러 없이 초고속으로 처리합니다!
🔍 4. 라라벨 공식 문서 원리 심층 분석 (Deep Dive)
when() 메서드의 조건부 실행 미학
$query->when($value, $callback, $defaultCallback)$value가 참(truthy, 즉 null이나 빈 문자열이 아님)일 때만 콜백을 실행하여 쿼리 조건을 추가합니다.- 복잡한 검색 폼이 주어졌을 때 if-else 블록으로 쿼리를 쪼개지 않고 단일 플루언트 파이프라인으로 연결할 수 있어 가독성과 유지보수성이 극대화됩니다.
🐶 5. 토토의 트러블슈팅 & 주의사항
🐶 토토의 멍멍 가이드!
- 할인가와 정가가 섞여 있을 때 가격순 정렬은 어떻게 하나요?
SQL의COALESCE(sale_price, price)구문을 쓰면 할인가가 있으면 할인가를, 없으면 정가를 기준으로 정확하게 정렬된다멍!
💡 6. 2강 자가진단 과제
scopeBetweenPrices($query, $min, $max)스코프를 만들어 최소/최대 가격 범위 검색 기능을 추가해 보세요.- Telescope의 Queries 탭에서 생성된 실제 SQL 질의문과 바인딩 파라미터를 눈으로 확인해 보세요.