라우팅

라우팅 (Routing)

Laravel 라우팅은 URL과 처리 로직을 연결하는 프레임워크의 심장이에요. 요청이 들어오면 어떤 코드를 실행할지 정해주는 '주소록'이라고 생각하면 쉬워요. 복잡한 설정 파일 없이도 URI와 콜백만으로 깔끔하게 정의할 수 있어서, 빠르게 시작할 수 있죠. 이 문서에서는 기본 라우팅부터 파라미터, 그룹, 모델 바인딩, 레이트 리밋까지 Laravel 12 기준으로 차근차근 살펴볼게요.

출처: Laravel 공식 문서 — Routing

기본 라우팅 (Basic Routing)

가장 기본적인 라우팅은 URI와 클로저를 받아, 설정 파일 복잡성 없이 아주 단순하고 표현력 있게 동작을 정의해요. 아래 코드는 /greeting으로 GET 요청이 오면 "Hello World"를 돌려주는 라우트예요.

use Illuminate\Support\Facades\Route;

Route::get('/greeting', function () {
    return 'Hello World';
});

라우트 파일의 기본 구조

모든 라우트는 프로젝트의 routes 디렉터리에 있는 파일에 정의돼요. 이 파일들은 bootstrap/app.php에 적힌 설정에 따라 자동으로 로드되죠. 그중 routes/web.php는 브라우저에서 접근하는 웹 인터페이스용 라우트를 담고, web 미들웨어 그룹이 적용되어 세션 상태와 CSRF 보호 같은 기능을 기본으로 얹어줘요.

컨트롤러를 쓸 땐 클로저 대신 [Controller::class, '메서드'] 배열 형태로 연결해요.

use App\Http\Controllers\UserController;

Route::get('/user', [UserController::class, 'index']);

API 라우트

무상태(stateless) API도 제공한다면 install:api Artisan 커맨드로 API 라우팅을 켤 수 있어요. 이 커맨드는 인증 가드로 유용하게 쓸 수 있는 Laravel Sanctum을 설치하고 routes/api.php 파일을 만들어줘요. routes/api.php의 라우트는 무상태이며 api 미들웨어 그룹과 /api URI 프리픽스가 자동 적용돼요.

php artisan install:api

프리픽스를 바꾸고 싶다면 bootstrap/app.php에서 apiPrefix 값을 조정하면 돼요.

라우터가 지원하는 HTTP 메서드

라우터는 모든 HTTP 동사에 응답할 수 있게 해줘요.

Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);

여러 메서드에 동시에 응답해야 한다면 match를, 모든 메서드에 응답해야 한다면 any를 써요.

Route::match(['get', 'post'], '/', function () {
    // ...
});

Route::any('/', function () {
    // ...
});

의존성 주입과 CSRF

라우트 클로저 시그니처에 필요한 타입을 힌트해 주면 Laravel 서비스 컨테이너가 자동으로 의존성을 주입해요. 예를 들어 현재 HTTP 요청을 받고 싶다면 Request를 타입 힌트하면 돼요.

use Illuminate\Http\Request;

Route::get('/users', function (Request $request) {
    // ...
});

web 라우트 파일에 정의된 POST·PUT·PATCH·DELETE 라우트를 가리키는 HTML 폼에는 반드시 CSRF 토큰 필드를 넣어야 해요. 없으면 요청이 거부되니까 주의하세요.

<form method="POST" action="/profile">
    @csrf
    ...
</form>

리다이렉트와 뷰 라우트

단순히 다른 URI로 리다이렉트만 하면 될 때는 Route::redirect로 간단하게 처리해요. 기본 상태 코드는 302이고, 세 번째 인자로 바꿀 수 있어요.

Route::redirect('/here', '/there');

Route::redirect('/here', '/there', 301);

Route::permanentRedirect('/here', '/there');

라우트가 단순히 뷰를 돌려주기만 한다면 Route::view를 써요. 뷰 이름과 함께 전달할 데이터 배열도 받을 수 있어요.

Route::view('/welcome', 'welcome');

Route::view('/welcome', 'welcome', ['name' => 'Taylor']);

라우트 목록 확인하기

애플리케이션에 정의된 모든 라우트를 훑어보고 싶을 때는 route:list 커맨드를 쓰면 돼요. -v 옵션을 붙이면 각 라우트에 적용된 미들웨어까지 보여주고, --path=api처럼 프리픽스로 필터링할 수도 있어요.

php artisan route:list
php artisan route:list -v
php artisan route:list --path=api

라우트 파라미터 (Route Parameters)

필수 파라미터

URI의 일부를 캡처해야 할 때는 중괄호 {}로 파라미터를 정의해요. 파라미터는 클로저/컨트롤러 인자에 순서대로 주입되기 때문에 인자 이름은 중요하지 않아요.

Route::get('/user/{id}', function (string $id) {
    return 'User '.$id;
});

요청 객체 같은 의존성도 함께 필요하다면 의존성을 앞에, 파라미터를 뒤에 나열해요.

use Illuminate\Http\Request;

Route::get('/user/{id}', function (Request $request, string $id) {
    return 'User '.$id;
});

선택(Optional) 파라미터

항상 URI에 존재하지 않을 수도 있는 값이라면 파라미터 이름 뒤에 ?를 붙이고, 변수에 기본값을 주면 돼요.

Route::get('/user/{name?}', function (?string $name = null) {
    return $name;
});

정규식 제약 (Regular Expression Constraints)

파라미터 형식을 제한하고 싶다면 where 메서드로 정규식을 지정해요. 형식이 맞지 않는 요청은 404가 반환돼요.

Route::get('/user/{id}/{name}', function (string $id, string $name) {
    // ...
})->where(['id' => '[0-9]+', 'name' => '[a-z]+']);

자주 쓰는 패턴은 헬퍼 메서드로 더 간단하게 쓸 수 있어요. whereNumber, whereAlpha, whereAlphaNumeric, whereUuid, whereUlid, whereIn이 대표적이에요.

Route::get('/user/{id}/{name}', function (string $id, string $name) {
    // ...
})->whereNumber('id')->whereAlpha('name');

전역으로 항상 어떤 제약을 적용하고 싶다면 AppServiceProvider::boot에서 Route::pattern으로 정의하면, 그 이름의 파라미터를 쓰는 모든 라우트에 자동 적용돼요.

public function boot(): void
{
    Route::pattern('id', '[0-9]+');
}

이름 있는 라우트 (Named Routes)

이름 있는 라우트는 특정 라우트를 가리키는 URL이나 리다이렉트를 편리하게 생성하게 해줘요. 라우트 정의에 name 메서드를 연결해서 이름을 붙이고, route() 헬퍼로 URL을 만들어요. 파라미터가 있으면 두 번째 인자로 값을 넘기면 올바른 자리에 자동 삽입돼요.

Route::get('/user/profile', function () {
    // ...
})->name('profile');

// URL 생성
$url = route('profile');

// 리다이렉트 생성
return to_route('profile');
Route::get('/user/{id}/profile', function (string $id) {
    // ...
})->name('profile');

$url = route('profile', ['id' => 1, 'photos' => 'yes']);
// http://example.com/user/1/profile?photos=yes

라우트 이름은 항상 유일해야 해요.

라우트 그룹 (Route Groups)

라우트 그룹을 쓰면 미들웨어 같은 공통 속성을 수백 개 라우트에 일일이 적용하지 않고 한 번에 공유할 수 있어요. 중첩된 그룹은 부모 그룹의 속성을 지능적으로 병합하는데, 미들웨어와 where 조건은 합쳐지고 이름과 프리픽스는 이어 붙어요.

Route::middleware(['first', 'second'])->group(function () {
    Route::get('/', function () {
        // first & second 미들웨어가 적용됨
    });
});

URI 프리픽스는 prefix, 라우트 이름 앞에 공통 문자열을 붙일 땐 name 메서드를 써요. 이름 프리픽스는 점(.)을 포함해 정확히 지정된 대로 붙으니 주의하세요.

Route::prefix('admin')->group(function () {
    Route::get('/users', function () {
        // "/admin/users" URL과 매칭
    });
});

Route::name('admin.')->group(function () {
    Route::get('/users', function () {
        // 라우트 이름: "admin.users"
    })->name('users');
});

서브도메인 라우팅이 필요하면 domain 메서드로 서브도메인에 파라미터를 정의할 수 있어요. 이때 루트 도메인 라우트보다 먼저 등록해야 같은 URI 경로의 루트 라우트가 덮어쓰지 않아요.

Route::domain('{account}.example.com')->group(function () {
    Route::get('/user/{id}', function (string $account, string $id) {
        // ...
    });
});

라우트 모델 바인딩 (Route Model Binding)

URL의 모델 ID를 라우트에 주입할 때마다 매번 DB 조회를 하는 대신, Laravel은 그 ID에 해당하는 모델 인스턴스를 라우트에 직접 주입해줘요. 이걸 모델 바인딩이라고 해요.

묵시적 바인딩

타입 힌트한 변수 이름이 URI 세그먼트 이름과 일치하면 Laravel이 자동으로 Eloquent 모델을 조회해 주입해요. 모델을 못 찾으면 404가 자동 반환되죠.

use App\Models\User;

Route::get('/users/{user}', function (User $user) {
    return $user->email;
});

id가 아닌 다른 컬럼으로 조회하고 싶다면 {post:slug}처럼 파라미터 정의에 컬럼을 명시하거나, 모델의 getRouteKeyName을 오버라이드해요. 소프트 삭제된 모델까지 가져오려면 withTrashed()를 연결하세요.

Route::get('/posts/{post:slug}', function (Post $post) {
    return $post;
})->withTrashed();

중첩 모델 바인딩에서는 관례에 따라 부모의 관계 이름을 추측해 자식 모델을 범위(scope) 안에서 조회해요. 아래 예시는 특정 User의 게시글만 slug로 찾아요. 관계를 자동으로 추론하고 싶지 않다면 scopeBindings()withoutScopedBindings()로 명시적으로 조절할 수 있어요.

Route::get('/users/{user}/posts/{post:slug}', function (User $user, Post $post) {
    return $post;
});

묵시적 바인딩에서 모델을 못 찾았을 때의 동작은 missing 메서드로 커스터마이즈할 수 있어요. 예를 들어 목록 페이지로 리다이렉트시키는 식으로요.

Route::get('/locations/{location:slug}', [LocationsController::class, 'show'])
    ->name('locations.view')
    ->missing(function (Request $request) {
        return Redirect::route('locations.index');
    });

명시적 바인딩

관례 기반이 싫다면 Route::model로 파라미터와 모델 클래스를 직접 연결하고, 더 세밀한 로직이 필요하면 Route::bind로 해석 로직을 직접 정의해요. 둘 다 AppServiceProvider::boot에서 설정해요.

public function boot(): void
{
    Route::model('user', User::class);
}
public function boot(): void
{
    Route::bind('user', function (string $value) {
        return User::where('name', $value)->firstOrFail();
    });
}

폴백 라우트

어떤 라우트도 매칭되지 않을 때 실행할 라우트는 Route::fallback으로 정의해요. 일반적으로 처리되지 않은 요청은 예외 핸들러가 404 페이지를 렌더링하지만, 폴백 라우트를 두면 그 동작을 직접 제어할 수 있죠.

Route::fallback(function () {
    // ...
});

레이트 리밋 (Rate Limiting)

Laravel은 특정 라우트나 그룹의 트래픽을 제한하는 강력하고 유연한 레이트 리밋 서비스를 제공해요. RateLimiter 파사드의 for 메서드로 리미터를 정의하고, Limit 클래스의 빌더 메서드로 한도를 구성해요.

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });
}

by 메서드로 기준을 세분화(IP 주소별, 사용자 ID별 등)할 수 있고, 가능한 값을 중복되지 않게 하려면 프리픽스를 붙여줘요. 한도 초과 시 기본으로 429 응답이 반환되고, response 메서드로 직접 응답을 정의할 수도 있어요. 요청이 아니라 응답을 기준으로 제한하고 싶다면 after 메서드로 특정 상태 코드만 카운트하게 할 수 있어요.

RateLimiter::for('resource-not-found', function (Request $request) {
    return Limit::perMinute(10)
        ->by($request->user()?->id ?: $request->ip())
        ->after(function (Response $response) {
            // 열거 공격을 막기 위해 404 응답만 카운트
            return $response->status() === 404;
        });
});

정의한 리미터는 throttle 미들웨어로 라우트(그룹)에 붙여요. Redis를 캐시 드라이버로 쓴다면 withMiddleware에서 throttleWithRedis()를 호출해 레디스 기반 레이트 리밋을 쓸 수 있어요.

Route::middleware(['throttle:uploads'])->group(function () {
    Route::post('/audio', function () {
        // ...
    });
});

폼 메서드 스푸핑과 라우트 캐시

HTML 폼은 PUT·PATCH·DELETE를 지원하지 않아서, 숨김 _method 필드로 실제 HTTP 메서드를 흉내 내야 해요. Blade의 @method 지시어로 편리하게 만들 수 있어요.

<form action="/example" method="POST">
    @method('PUT')
    @csrf
</form>

프로덕션 배포 시에는 route:cache로 라우트 캐시를 만들어 라우트 등록 시간을 크게 줄일 수 있어요. 새 라우트를 추가하면 캐시를 다시 생성해야 하므로, 배포 과정에서만 실행하는 게 좋아요.

php artisan route:cache
php artisan route:clear

더 알아보기 (Learn more)

  • Laravel 공식 문서의 Routing 페이지
  • Controllers 문서에서 컨트롤러 기반 라우트 살펴보기
  • Middleware 문서에서 라우트에 붙이는 미들웨어 이해하기
  • Blade 문서에서 @csrf·@method 지시어 활용법 익히기