페이지네이션
번역일: 2026년 6월 21일
페이지네이션
- 소개
- 기본 사용법
- 페이지네이션 결과 출력
- 페이지네이션 뷰 커스터마이징
- Paginator / LengthAwarePaginator 인스턴스 메서드
- CursorPaginator 인스턴스 메서드
소개
다른 프레임워크에서는 페이지네이션 구현이 꽤 번거로울 수 있습니다. Laravel의 페이지네이터는 쿼리 빌더 및 Eloquent ORM과 긴밀하게 통합되어 있어, 별도의 설정 없이도 데이터베이스 레코드를 손쉽게 페이지 단위로 나눌 수 있습니다.
기본적으로 페이지네이터가 생성하는 HTML은 Tailwind CSS와 호환되며, Bootstrap 스타일도 지원합니다.
Tailwind 설정
Tailwind 4.x와 Laravel 기본 Tailwind 페이지네이션 뷰를 함께 사용하는 경우, resources/css/app.css 파일에 아래와 같이 @source 지시어가 이미 포함되어 있어야 합니다:
@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';기본 사용법
쿼리 빌더 결과 페이지네이션
페이지네이션을 적용하는 가장 간단한 방법은 쿼리 빌더 또는 Eloquent 쿼리에서 paginate 메서드를 호출하는 것입니다. paginate 메서드는 사용자가 현재 보고 있는 페이지를 기준으로 SQL의 LIMIT와 OFFSET을 자동으로 설정합니다. 현재 페이지는 HTTP 요청의 page 쿼리 파라미터 값으로 감지되며, Laravel이 자동으로 이 값을 읽어 페이지네이션 링크에도 포함시킵니다.
아래 예시에서 paginate에 전달하는 인자는 페이지당 표시할 항목 수입니다. 여기서는 페이지당 15개를 표시합니다:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class UserController extends Controller
{
/**
* 모든 사용자 목록을 표시합니다.
*/
public function index(): View
{
return view('user.index', [
'users' => DB::table('users')->paginate(15)
]);
}
}간단한 페이지네이션 (simplePaginate)
paginate 메서드는 레코드를 가져오기 전에 전체 레코드 수를 계산하는 쿼리를 한 번 더 실행합니다. 이는 전체 페이지 수를 UI에 표시하기 위해 필요한 과정입니다. 그러나 "다음"과 "이전" 버튼만 있으면 충분하고 전체 페이지 수를 보여줄 필요가 없다면, simplePaginate 메서드를 사용하면 불필요한 COUNT 쿼리 없이 더 효율적으로 동작합니다:
$users = DB::table('users')->simplePaginate(15);Eloquent 결과 페이지네이션
Eloquent 쿼리에도 동일하게 페이지네이션을 적용할 수 있습니다. 아래 예시는 App\Models\User 모델을 페이지당 15개씩 나누어 가져옵니다. 쿼리 빌더 방식과 문법이 거의 동일합니다:
use App\Models\User;
$users = User::paginate(15);물론 where 등의 조건을 추가한 뒤 paginate를 호출할 수도 있습니다:
$users = User::where('votes', '>', 100)->paginate(15);Eloquent 모델에서도 simplePaginate를 사용할 수 있습니다:
$users = User::where('votes', '>', 100)->simplePaginate(15);커서 기반 페이지네이션을 원한다면 cursorPaginate를 사용합니다:
$users = User::where('votes', '>', 100)->cursorPaginate(15);한 화면에 여러 페이지네이터 사용
한 화면에 페이지네이터를 두 개 이상 렌더링해야 할 때가 있습니다. 두 페이지네이터가 모두 기본 page 파라미터를 사용하면 충돌이 발생합니다. 이런 경우 paginate, simplePaginate, cursorPaginate 메서드의 세 번째 인자로 각 페이지네이터가 사용할 쿼리 파라미터 이름을 지정하면 됩니다:
use App\Models\User;
$users = User::where('votes', '>', 100)->paginate(
$perPage = 15, $columns = ['*'], $pageName = 'users'
);커서 페이지네이션
paginate와 simplePaginate는 SQL의 OFFSET 절을 사용하는 반면, 커서 페이지네이션은 정렬 기준 컬럼의 값을 비교하는 WHERE 절로 동작합니다. 이 방식은 Laravel이 제공하는 페이지네이션 중 데이터베이스 성능이 가장 우수하며, 대용량 데이터셋이나 무한 스크롤 UI에 특히 적합합니다.
오프셋 기반 페이지네이션이 URL에 페이지 번호(?page=2)를 포함하는 것과 달리, 커서 페이지네이션은 인코딩된 "커서" 문자열을 쿼리 파라미터에 포함시킵니다. 이 커서에는 다음 쿼리를 시작할 위치와 방향 정보가 담겨 있습니다:
http://localhost/users?cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0쿼리 빌더의 cursorPaginate 메서드를 호출하면 Illuminate\Pagination\CursorPaginator 인스턴스가 반환됩니다:
$users = DB::table('users')->orderBy('id')->cursorPaginate(15);커서 페이지네이터 인스턴스를 얻은 뒤에는 paginate, simplePaginate와 동일하게 결과를 출력할 수 있습니다. 커서 페이지네이터가 제공하는 인스턴스 메서드는 커서 페이지네이터 인스턴스 메서드를 참고하세요.
WARNING
커서 페이지네이션을 사용하려면 쿼리에 반드시 ORDER BY 절이 있어야 합니다. 또한 정렬 기준 컬럼은 페이지네이션 대상 테이블의 컬럼이어야 합니다.
커서 페이지네이션 vs 오프셋 페이지네이션
두 방식의 차이를 SQL로 살펴보겠습니다. 아래 두 쿼리는 모두 users 테이블을 id 기준으로 정렬한 뒤 두 번째 페이지(16~30번째 행)를 가져옵니다:
-- 오프셋 페이지네이션
SELECT * FROM users ORDER BY id ASC LIMIT 15 OFFSET 15;
-- 커서 페이지네이션
SELECT * FROM users WHERE id > 15 ORDER BY id ASC LIMIT 15;커서 페이지네이션의 장점:
- 정렬 기준 컬럼에 인덱스가 있는 경우, 대용량 데이터에서 성능이 훨씬 뛰어납니다.
OFFSET방식은 건너뛰는 행을 모두 스캔해야 하기 때문입니다. - 쓰기가 잦은 데이터셋에서 오프셋 방식은 새 데이터가 추가되거나 삭제될 때 레코드를 건너뛰거나 중복으로 표시하는 문제가 생길 수 있습니다. 커서 방식은 이런 문제에서 자유롭습니다.
커서 페이지네이션의 제한:
simplePaginate와 마찬가지로 "이전"/"다음" 링크만 제공하며, 페이지 번호로 이동하는 링크는 지원하지 않습니다.- 정렬 기준으로 하나 이상의 고유(unique) 컬럼 또는 고유 컬럼 조합이 필요합니다.
null값을 가진 컬럼은 지원하지 않습니다. ORDER BY절에 쿼리 표현식을 사용하는 경우,SELECT절에서도 별칭(alias)을 통해 해당 표현식을 선택해야 합니다.- 파라미터가 포함된 쿼리 표현식은 지원하지 않습니다.
페이지네이터 직접 생성
이미 메모리에 로드된 배열을 페이지네이션해야 할 때는 페이지네이터 인스턴스를 직접 생성할 수 있습니다. 필요에 따라 Illuminate\Pagination\Paginator, Illuminate\Pagination\LengthAwarePaginator, Illuminate\Pagination\CursorPaginator 중 하나를 선택합니다.
Paginator→simplePaginate에 대응. 전체 레코드 수 불필요.LengthAwarePaginator→paginate에 대응. 전체 레코드 수 필요.CursorPaginator→cursorPaginate에 대응. 전체 레코드 수 불필요.
Paginator와 CursorPaginator는 전체 항목 수를 알 필요가 없는 대신, 마지막 페이지 인덱스를 조회하는 메서드를 제공하지 않습니다.
WARNING
페이지네이터를 직접 생성할 때는 전달하는 배열을 현재 페이지에 해당하는 항목만 포함하도록 직접 잘라내야 합니다. 방법을 잘 모르겠다면 PHP의 array_slice 함수를 참고하세요.
페이지네이션 URL 커스터마이징
기본적으로 페이지네이터는 현재 요청 URI를 기반으로 링크를 생성합니다. withPath 메서드를 사용하면 링크 생성에 사용할 URI를 원하는 값으로 변경할 수 있습니다. 예를 들어 http://example.com/admin/users?page=N 형태의 링크를 원한다면 아래와 같이 /admin/users를 전달합니다:
use App\Models\User;
Route::get('/users', function () {
$users = User::paginate(15);
$users->withPath('/admin/users');
// ...
});쿼리 파라미터 추가
appends 메서드를 사용하면 페이지네이션 링크에 추가 쿼리 파라미터를 붙일 수 있습니다. 예를 들어 모든 페이지 링크에 sort=votes를 추가하려면 다음과 같이 합니다:
use App\Models\User;
Route::get('/users', function () {
$users = User::paginate(15);
$users->appends(['sort' => 'votes']);
// ...
});현재 요청의 모든 쿼리 파라미터를 페이지네이션 링크에 그대로 포함시키려면 withQueryString 메서드를 사용합니다:
$users = User::paginate(15)->withQueryString();해시 프래그먼트 추가
페이지네이션 링크 URL 끝에 해시 프래그먼트를 추가하고 싶다면 fragment 메서드를 사용합니다. 예를 들어 모든 링크 끝에 #users를 붙이려면 다음과 같이 호출합니다:
$users = User::paginate(15)->fragment('users');페이지네이션 결과 출력
paginate→Illuminate\Pagination\LengthAwarePaginator반환simplePaginate→Illuminate\Pagination\Paginator반환cursorPaginate→Illuminate\Pagination\CursorPaginator반환
이 객체들은 결과 집합을 설명하는 다양한 메서드를 제공합니다. 또한 이터러블(iterable)이므로 배열처럼 반복할 수 있습니다. 결과를 가져온 뒤에는 Blade를 사용해 아래와 같이 결과를 출력하고 페이지 링크를 렌더링합니다:
<div class="container">
@foreach ($users as $user)
{{ $user->name }}
@endforeach
</div>
{{ $users->links() }}links 메서드는 나머지 페이지로 이동하는 링크들을 렌더링합니다. 각 링크에는 page 쿼리 파라미터가 자동으로 포함됩니다. 생성되는 HTML은 Tailwind CSS와 호환됩니다.
페이지 링크 범위 조정
페이지네이터는 기본적으로 현재 페이지를 중심으로 앞뒤로 3개씩 링크를 표시합니다. onEachSide 메서드로 이 범위를 조정할 수 있습니다:
{{ $users->onEachSide(5)->links() }}결과를 JSON으로 변환
Laravel의 페이지네이터 클래스는 Illuminate\Contracts\Support\Jsonable 인터페이스를 구현하고 있으므로 toJson 메서드를 사용하거나, 라우트나 컨트롤러 액션에서 직접 반환하면 자동으로 JSON으로 변환됩니다:
use App\Models\User;
Route::get('/users', function () {
return User::paginate();
});페이지네이터가 반환하는 JSON에는 total, current_page, last_page 등의 메타 정보가 포함되며, 실제 레코드는 data 키 아래에 위치합니다:
{
"total": 50,
"per_page": 15,
"current_page": 1,
"last_page": 4,
"current_page_url": "http://laravel.app?page=1",
"first_page_url": "http://laravel.app?page=1",
"last_page_url": "http://laravel.app?page=4",
"next_page_url": "http://laravel.app?page=2",
"prev_page_url": null,
"path": "http://laravel.app",
"from": 1,
"to": 15,
"data":[
{
// 레코드...
},
{
// 레코드...
}
]
}페이지네이션 뷰 커스터마이징
기본적으로 페이지네이션 링크는 Tailwind CSS 기반 뷰로 렌더링됩니다. Tailwind를 사용하지 않는다면 links 메서드에 원하는 뷰 이름을 첫 번째 인자로 전달하여 커스텀 뷰를 사용할 수 있습니다:
{{ $paginator->links('view.name') }}
<!-- 뷰에 추가 데이터 전달 -->
{{ $paginator->links('view.name', ['foo' => 'bar']) }}가장 편리한 방법은 vendor:publish 명령어로 기본 뷰 파일을 프로젝트로 가져온 뒤 수정하는 것입니다:
php artisan vendor:publish --tag=laravel-pagination이 명령을 실행하면 resources/views/vendor/pagination 디렉터리에 뷰 파일들이 복사됩니다. 이 중 tailwind.blade.php가 기본 페이지네이션 뷰입니다. 이 파일을 수정하여 HTML 구조를 변경할 수 있습니다.
기본 페이지네이션 뷰를 다른 파일로 교체하려면 App\Providers\AppServiceProvider의 boot 메서드에서 defaultView와 defaultSimpleView를 호출합니다:
<?php
namespace App\Providers;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Paginator::defaultView('view-name');
Paginator::defaultSimpleView('view-name');
}
}Bootstrap 사용
Laravel은 Bootstrap CSS 기반의 페이지네이션 뷰도 내장하고 있습니다. 기본 Tailwind 뷰 대신 Bootstrap 뷰를 사용하려면 App\Providers\AppServiceProvider의 boot 메서드에서 아래 메서드를 호출합니다:
use Illuminate\Pagination\Paginator;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Paginator::useBootstrapFive();
// 또는 Bootstrap 4를 사용하려면:
Paginator::useBootstrapFour();
}Paginator / LengthAwarePaginator 인스턴스 메서드
각 페이지네이터 인스턴스는 다음 메서드를 통해 추가 정보를 제공합니다:
| 메서드 | 설명 |
|---|---|
$paginator->count() | 현재 페이지의 항목 수를 반환합니다. |
$paginator->currentPage() | 현재 페이지 번호를 반환합니다. |
$paginator->firstItem() | 현재 페이지 첫 번째 항목의 결과 번호를 반환합니다. |
$paginator->getOptions() | 페이지네이터 옵션을 반환합니다. |
$paginator->getUrlRange($start, $end) | 지정 범위의 페이지네이션 URL 목록을 생성합니다. |
$paginator->hasPages() | 여러 페이지로 나눌 만큼 항목이 충분한지 확인합니다. |
$paginator->hasMorePages() | 데이터 저장소에 더 많은 항목이 있는지 확인합니다. |
$paginator->items() | 현재 페이지의 항목들을 반환합니다. |
$paginator->lastItem() | 현재 페이지 마지막 항목의 결과 번호를 반환합니다. |
$paginator->lastPage() | 마지막 페이지 번호를 반환합니다. (simplePaginate 사용 시 불가) |
$paginator->nextPageUrl() | 다음 페이지 URL을 반환합니다. |
$paginator->onFirstPage() | 현재 첫 번째 페이지인지 확인합니다. |
$paginator->onLastPage() | 현재 마지막 페이지인지 확인합니다. |
$paginator->perPage() | 페이지당 표시할 항목 수를 반환합니다. |
$paginator->previousPageUrl() | 이전 페이지 URL을 반환합니다. |
$paginator->total() | 전체 일치 항목 수를 반환합니다. (simplePaginate 사용 시 불가) |
$paginator->url($page) | 지정한 페이지 번호의 URL을 반환합니다. |
$paginator->getPageName() | 페이지 저장에 사용되는 쿼리 파라미터 이름을 반환합니다. |
$paginator->setPageName($name) | 페이지 저장에 사용할 쿼리 파라미터 이름을 설정합니다. |
$paginator->through($callback) | 콜백을 사용해 각 항목을 변환합니다. |
CursorPaginator 인스턴스 메서드
각 커서 페이지네이터 인스턴스는 다음 메서드를 통해 추가 정보를 제공합니다:
| 메서드 | 설명 |
|---|---|
$paginator->count() | 현재 페이지의 항목 수를 반환합니다. |
$paginator->cursor() | 현재 커서 인스턴스를 반환합니다. |
$paginator->getOptions() | 페이지네이터 옵션을 반환합니다. |
$paginator->hasPages() | 여러 페이지로 나눌 만큼 항목이 충분한지 확인합니다. |
$paginator->hasMorePages() | 데이터 저장소에 더 많은 항목이 있는지 확인합니다. |
$paginator->getCursorName() | 커서 저장에 사용되는 쿼리 파라미터 이름을 반환합니다. |
$paginator->items() | 현재 페이지의 항목들을 반환합니다. |
$paginator->nextCursor() | 다음 항목 집합의 커서 인스턴스를 반환합니다. |
$paginator->nextPageUrl() | 다음 페이지 URL을 반환합니다. |
$paginator->onFirstPage() | 현재 첫 번째 페이지인지 확인합니다. |
$paginator->onLastPage() | 현재 마지막 페이지인지 확인합니다. |
$paginator->perPage() | 페이지당 표시할 항목 수를 반환합니다. |
$paginator->previousCursor() | 이전 항목 집합의 커서 인스턴스를 반환합니다. |
$paginator->previousPageUrl() | 이전 페이지 URL을 반환합니다. |
$paginator->setCursorName() | 커서 저장에 사용할 쿼리 파라미터 이름을 설정합니다. |
$paginator->url($cursor) | 지정한 커서 인스턴스의 URL을 반환합니다. |