라우팅은 애플리케이션의 첫 번째 설계도다
브라우저에서 http://localhost:8000/posts에 접속하면 Laravel은 먼저 "이 URL을 처리할 규칙이 있는가?"를 확인한다. 이 규칙이 라우트다. 라우트는 URL과 HTTP 메서드를 어떤 코드에 연결할지 정한다.
대부분의 웹 화면 라우트는 routes/web.php에 작성한다. 이 파일에 있는 라우트에는 세션, 쿠키, CSRF 보호 같은 웹 기능을 위한 기본 미들웨어 그룹이 적용된다.1 그래서 로그인 상태를 기억하거나 폼 전송을 보호하는 기능도 나중에 자연스럽게 연결할 수 있다.
새 프로젝트의 routes/web.php를 열면 보통 다음과 비슷한 코드가 있다.
<?php
use IlluminateSupportFacadesRoute;
Route::get('/', function () {
return view('welcome');
});이 코드는 다음 뜻이다.
GET /요청이 들어오면익명 함수, 즉 클로저를 실행하고
welcome뷰를 응답으로 반환한다
여기서 중요한 점은 URL만 보는 것이 아니라 HTTP 메서드까지 함께 본다는 것이다. 같은 /posts 주소라도 목록을 보는 요청과 새 글을 저장하는 요청은 다르게 처리되어야 한다.
AI 생성 이미지
HTTP 메서드로 사용자의 의도 구분하기
웹 애플리케이션에서 URL은 자원, 즉 다루려는 대상을 나타내고 HTTP 메서드는 사용자의 의도를 나타낸다. 게시글 기능을 만든다면 다음처럼 생각할 수 있다.
| 메서드 | URL | 의미 |
|---|---|---|
GET | /posts | 게시글 목록 보기 |
GET | /posts/create | 새 게시글 작성 화면 보기 |
POST | /posts | 새 게시글 저장하기 |
GET | /posts/{post} | 특정 게시글 보기 |
GET | /posts/{post}/edit | 특정 게시글 수정 화면 보기 |
PATCH 또는 PUT | /posts/{post} | 특정 게시글 수정하기 |
DELETE | /posts/{post} | 특정 게시글 삭제하기 |
Laravel에서는 이를 다음처럼 작성한다.
use IlluminateSupportFacadesRoute;
Route::get('/posts', function () {
return '게시글 목록';
});
Route::get('/posts/create', function () {
return '게시글 작성 화면';
});
Route::post('/posts', function () {
return '게시글 저장';
});
Route::get('/posts/{post}', function (string $post) {
return "게시글 상세: {$post}";
});
Route::get('/posts/{post}/edit', function (string $post) {
return "게시글 수정 화면: {$post}";
});
Route::patch('/posts/{post}', function (string $post) {
return "게시글 수정: {$post}";
});
Route::delete('/posts/{post}', function (string $post) {
return "게시글 삭제: {$post}";
});Route::get(), Route::post(), Route::put(), Route::patch(), Route::delete()는 Laravel이 제공하는 대표적인 라우팅 메서드다.1 이름 그대로 어떤 HTTP 요청을 처리할지 정한다.
여기서 PUT과 PATCH는 둘 다 수정에 사용된다. 엄밀히 구분하면 PUT은 전체 교체, PATCH는 일부 수정에 가깝다. 실무에서는 게시글 제목과 내용 일부를 고치는 일이 많으므로 Laravel 예제에서 PATCH를 자주 사용한다.
단, 브라우저의 HTML 폼은 기본적으로 GET과 POST만 직접 전송할 수 있다. 그래서 Laravel에서는 폼 안에 숨은 값으로 "실제로는 PATCH 요청처럼 처리해 달라"고 표시하는 방식을 사용한다. Blade에서는 나중에 @method('PATCH'), @method('DELETE') 같은 형태를 보게 된다. 지금은 라우트가 PATCH, DELETE 같은 의도를 구분할 수 있다는 점만 기억하면 된다.
클로저 라우트와 컨트롤러 라우트
방금 작성한 예제는 모두 클로저로 처리했다. 클로저 라우트는 작고 빠르게 확인할 때 좋다.
Route::get('/about', function () {
return '소개 페이지';
});하지만 기능이 커지면 routes/web.php에 모든 코드를 넣기 어렵다. 게시글 목록을 가져오고, 권한을 확인하고, 화면에 넘길 데이터를 준비하는 코드가 모두 라우트 파일에 쌓이면 구조를 파악하기 힘들어진다.
그래서 실제 애플리케이션에서는 라우트를 컨트롤러에 연결한다.
use AppHttpControllersPostController;
use IlluminateSupportFacadesRoute;
Route::get('/posts', [PostController::class, 'index']);
Route::get('/posts/{post}', [PostController::class, 'show']);이 코드는 다음 뜻이다.
GET /posts요청은PostController의index메서드로 보낸다GET /posts/{post}요청은PostController의show메서드로 보낸다
컨트롤러는 이어지는 실습에서 더 자세히 다룬다. 이번 장에서는 라우트가 "어느 코드가 실행될지 결정하는 입구"라는 점에 집중하자.
동적 파라미터로 변하는 URL 받기
게시글마다 URL을 따로 만들 수는 없다. /posts/1, /posts/2, /posts/100처럼 뒤의 값만 달라지는 URL은 동적 파라미터로 처리한다.
Route::get('/posts/{post}', function (string $post) {
return "게시글 번호: {$post}";
});{post} 부분은 요청 URL에서 값을 꺼내 함수의 인자로 전달한다. 예를 들어 다음처럼 동작한다.
/posts/1→$post값은1/posts/25→$post값은25
파라미터가 여러 개일 수도 있다.
Route::get('/posts/{post}/comments/{comment}', function (string $post, string $comment) {
return "게시글 {$post}의 댓글 {$comment}";
});이 라우트는 /posts/3/comments/10 같은 URL을 처리한다. 파라미터 이름은 나중에 코드를 읽는 사람도 의미를 알 수 있도록 구체적으로 붙이는 편이 좋다.
선택 파라미터도 사용할 수 있다. 값이 있을 수도 있고 없을 수도 있는 경우다.
Route::get('/hello/{name?}', function (?string $name = null) {
return '안녕하세요, ' . ($name ?? '방문자') . '님';
});여기서 {name?}의 물음표는 선택 값이라는 뜻이다. 선택 파라미터를 사용할 때는 함수 인자에 기본값을 지정해야 한다.1
/hello→안녕하세요, 방문자님/hello/mina→안녕하세요, mina님
다만 선택 파라미터는 꼭 필요할 때만 사용하는 편이 좋다. URL이 너무 유연해지면 어떤 요청이 어떤 의미인지 흐려질 수 있다. 초보 단계에서는 명확한 URL을 먼저 설계하는 습관이 더 중요하다.
제약 조건으로 잘못된 URL 걸러내기
/posts/{post}에서 {post}는 게시글 번호로 쓰려는 값이다. 그렇다면 /posts/abc 같은 요청은 처리하지 않는 편이 자연스럽다. 이때 라우트 제약 조건을 사용할 수 있다.
Route::get('/posts/{post}', function (string $post) {
return "게시글 번호: {$post}";
})->whereNumber('post');whereNumber('post')는 {post}가 숫자일 때만 이 라우트와 일치하게 한다. 숫자가 아니면 Laravel은 해당 라우트를 찾지 못한 것으로 보고 보통 404 응답을 반환한다.
정규표현식으로 더 직접 지정할 수도 있다.
Route::get('/users/{name}', function (string $name) {
return "사용자: {$name}";
})->where('name', '[A-Za-z]+');이 라우트는 알파벳 이름만 허용한다. 제약 조건은 보안 장치라기보다 URL의 모양을 명확히 하는 도구에 가깝다. 실제 데이터 접근 권한이나 입력값 검증은 컨트롤러, 폼 요청, 정책 같은 다른 계층에서도 처리해야 한다.
라우트 순서도 중요하다. Laravel은 등록된 라우트를 위에서 아래로 비교한다. 다음 두 라우트를 보자.
Route::get('/posts/create', function () {
return '작성 화면';
});
Route::get('/posts/{post}', function (string $post) {
return "게시글 상세: {$post}";
});/posts/create는 고정 URL이고, /posts/{post}는 변하는 URL이다. 보통 고정 URL을 더 위에 두는 것이 안전하다. 그래야 create가 게시글 번호처럼 해석되는 혼란을 줄일 수 있다. 특히 아직 제약 조건을 붙이지 않은 동적 라우트가 있다면 순서를 더 신경 써야 한다.
이름 있는 라우트로 링크를 안전하게 만들기
화면에서 게시글 목록으로 이동하는 링크를 만든다고 하자. HTML에 직접 /posts를 적을 수도 있다.
<a href="/posts">게시글 목록</a>처음에는 문제가 없어 보인다. 하지만 나중에 URL을 /board/posts로 바꾸면 어떻게 될까? 프로젝트 전체에서 /posts를 찾아 고쳐야 한다. 빠뜨린 링크가 생기면 사용자는 잘못된 페이지로 이동한다.
Laravel에서는 라우트에 이름을 붙일 수 있다.
Route::get('/posts', function () {
return '게시글 목록';
})->name('posts.index');
Route::get('/posts/{post}', function (string $post) {
return "게시글 상세: {$post}";
})->name('posts.show');이제 URL을 직접 쓰지 않고 route() 헬퍼로 생성한다.
$url = route('posts.index');
$showUrl = route('posts.show', ['post' => 1]);결과는 각각 다음과 같다.
/posts
/posts/1Blade 템플릿에서는 다음처럼 사용할 수 있다.
<a href="{{ route('posts.index') }}">게시글 목록</a>
<a href="{{ route('posts.show', ['post' => 1]) }}">첫 번째 게시글</a>리다이렉트에도 이름 있는 라우트를 쓰는 편이 좋다.
Route::post('/posts', function () {
// 저장했다고 가정한다.
return redirect()->route('posts.index');
})->name('posts.store');이 방식의 장점은 분명하다. URL 구조가 바뀌어도 라우트 이름이 그대로라면 링크와 리다이렉트 코드를 크게 바꾸지 않아도 된다. URL은 외부 모양이고, 라우트 이름은 애플리케이션 내부에서 기능을 가리키는 안정적인 이름이다.
라우트 이름은 보통 자원.동작 형태로 짓는다.
posts.indexposts.createposts.storeposts.showposts.editposts.updateposts.destroy
이 이름은 뒤에서 RESTful 설계를 배울 때 다시 등장한다.
라우트 그룹으로 관련 기능 묶기
애플리케이션이 커지면 비슷한 라우트가 많아진다. 예를 들어 관리자 화면은 모두 /admin으로 시작할 수 있다.
Route::get('/admin/dashboard', function () {
return '관리자 대시보드';
});
Route::get('/admin/users', function () {
return '사용자 관리';
});중복이 많다. 이때 접두어 그룹을 사용한다.
Route::prefix('admin')->group(function () {
Route::get('/dashboard', function () {
return '관리자 대시보드';
});
Route::get('/users', function () {
return '사용자 관리';
});
});이 그룹 안의 라우트는 자동으로 /admin 접두어를 가진다.
/admin/dashboard/admin/users
라우트 이름에도 접두어를 붙일 수 있다.
Route::prefix('admin')
->name('admin.')
->group(function () {
Route::get('/dashboard', function () {
return '관리자 대시보드';
})->name('dashboard');
Route::get('/users', function () {
return '사용자 관리';
})->name('users.index');
});최종 라우트 이름은 다음과 같다.
admin.dashboardadmin.users.index
이제 링크를 만들 때도 의미가 분명하다.
<a href="{{ route('admin.dashboard') }}">관리자 홈</a>미들웨어도 그룹에 적용할 수 있다. 미들웨어는 요청이 실제 기능 코드에 도착하기 전후로 실행되는 검사 계층이다. 예를 들어 로그인한 사용자만 접근 가능한 페이지를 묶을 수 있다.
Route::middleware('auth')->group(function () {
Route::get('/profile', function () {
return '내 프로필';
})->name('profile');
Route::get('/my-posts', function () {
return '내 게시글';
})->name('my-posts.index');
});이렇게 하면 그룹 안의 라우트는 모두 auth 미들웨어를 통과해야 한다. 로그인하지 않은 사용자는 기능 코드에 도착하기 전에 다른 응답을 받게 된다. 권한과 인증은 뒤의 장에서 더 자세히 다루지만, 라우팅 단계에서 관련 URL을 묶어 둘 수 있다는 점은 지금부터 익숙해지는 것이 좋다.
현재 라우트 확인하기
라우트가 늘어나면 내가 어떤 URL을 등록했는지 헷갈릴 수 있다. 이때 터미널에서 다음 명령을 실행한다.
php artisan route:list이 명령은 등록된 라우트의 메서드, URL, 이름, 연결된 코드, 미들웨어를 보여 준다. 예상한 라우트가 동작하지 않을 때는 먼저 이 목록을 확인하자.
특히 다음을 점검하면 좋다.
URL 오타가 없는가?
HTTP 메서드가 맞는가?
라우트 이름이 중복되거나 잘못되지 않았는가?
동적 파라미터가 너무 넓게 잡혀 다른 라우트를 가로채지 않는가?
미들웨어 때문에 컨트롤러나 클로저에 도달하지 못하는가?
라우팅 오류는 대부분 "Laravel이 내 요청을 어떤 규칙과 연결했는가"를 확인하면 실마리가 보인다.
게시글 기능의 라우트 초안 만들기
이번 장의 내용을 바탕으로 게시글 기능의 라우트 초안을 정리해 보자. 아직 컨트롤러를 본격적으로 만들지는 않았으므로 클로저로 응답만 확인한다.
use IlluminateSupportFacadesRoute;
Route::prefix('posts')
->name('posts.')
->group(function () {
Route::get('/', function () {
return '게시글 목록';
})->name('index');
Route::get('/create', function () {
return '게시글 작성 화면';
})->name('create');
Route::post('/', function () {
return redirect()->route('posts.index');
})->name('store');
Route::get('/{post}', function (string $post) {
return "게시글 상세: {$post}";
})->whereNumber('post')->name('show');
Route::get('/{post}/edit', function (string $post) {
return "게시글 수정 화면: {$post}";
})->whereNumber('post')->name('edit');
Route::patch('/{post}', function (string $post) {
return redirect()->route('posts.show', ['post' => $post]);
})->whereNumber('post')->name('update');
Route::delete('/{post}', function (string $post) {
return redirect()->route('posts.index');
})->whereNumber('post')->name('destroy');
});이 코드는 완성된 게시판은 아니다. 하지만 중요한 설계는 이미 들어 있다.
게시글 기능은
/posts아래에 모여 있다목록, 작성, 저장, 상세, 수정, 삭제의 의도가 HTTP 메서드로 나뉜다
게시글 번호는 동적 파라미터로 받는다
숫자가 아닌 게시글 번호는 라우트에서 걸러진다
링크와 리다이렉트에 사용할 이름이 정해져 있다
즉, 라우팅은 단순히 URL을 등록하는 작업이 아니다. 사용자가 애플리케이션을 어떻게 사용할지 정리하는 첫 번째 설계다. 이후에는 이 라우트들을 컨트롤러와 화면, 데이터베이스 처리로 조금씩 연결하면서 실제 기능으로 확장해 나가게 된다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.