프로그래밍 언어 & 프레임워크/PHP & Laravel

[Laravel] 라라벨 라우팅과 컨트롤러

이번 포스트에서는 라라벨에서 라우팅에 대해 간단하게 알아본다. 컨트롤러는 다음 포스트에 알아보도록 하자. 블로그의 특성상 글이 시리즈로 이어지는 것은 썩 좋지 않기 때문에 기능에 대한 사전식 나열이나 일부 설명이 첨부되는 형식으로만 작성될 것이다.

MVC(Model, View, Controller)

라라벨은 MVC 아키텍쳐를 따르는 프레임워크다. Model 은 어플리케이션에서 사용자에게 보여주고 싶은것, 일반적으로 데이터 또는 데이터베이스이며, View 는 이러한 모델을 사용자에게 어떠한 인터페이스로 보여줄 것인지를 말하는 것이며, Controller 는 그 중간에서 모델의 데이터를 얻어오거나 저장하여 뷰에게 이 사실을 통지한다. 여기서 통지라는 것은 Observer 패턴의 Notify 로 통지하는 것과 다르며 소켓을 통해 실시간으로 통지한다는 개념은 아니다.

라우팅

여기서 라우팅의 개념은, 사용자에게 특정한 URL 주소로 접근하였을 때 그에 해당하는 컨트롤러로 접근할 수 있게하는 것을 말한다. 라라벨은 기본적으로 프론트 컨트롤러 패턴을 따르므로 public/index.php 에 모든 요청을 수집한 뒤, 라우터가 이를 적절한 컨트롤러 로직으로 보낸다. 라라벨이 아닌 일반적인 PHP 로 MVC 직접 구성해보고 싶다면 PHP: MVC(Model, View, Controller)를 참고하자.

RouteServiceProvider

이부분은 일반적으로 책에는 잘 안나오는 부분이긴 하다만, 알아두면 좋기때문에 따로 추가했다. 라라벨은 서비스 프로바이더라는 기능으로 프레임워크에 필요한 기능, 예를 들어 라우트, 로그, 이벤트 같은 것을 등록한다. 이러한 것은 라라벨 서비스 컨테이너가 처리할 텐데, 여기서 라우트에 관한 서비스 프로바이더가 바로 RouteServiceProvider 다.

 

핵심 코드는 대략 아래와 같으며 이곳에서 routes/web.php, routes/api.php 에서 라우트가 처리된다는 사실을 알 수 있다. web.php 에서는 주로 브라우저로 접근하는 일반적인 요청을 말하고, api.php 는 API 서비스를 제공할 때 사용하는 것을 말한다.

namespace App\Providers;

class RouteServiceProvider extends ServiceProvider
{
    /**
     * Define your route model bindings, pattern filters, etc.
     *
     * @return void
     */
    public function boot()
    {
        $this->configureRateLimiting();

        $this->routes(function () {
            Route::prefix('api')
                ->middleware('api')
                ->namespace($this->namespace)
                ->group(base_path('routes/api.php'));

            Route::middleware('web')
                ->namespace($this->namespace)
                ->group(base_path('routes/web.php'));
        });
    }
}

조금 더 들어가면 서비스 프로바이더에 대한 내용이 될 수 있기 때문에 라우트에서는 여기까지만 하기로 하고 이제 다음으로 넘어가보도록 하자.

Routes/*.php

web.php 만 살펴보더라도 충분하다. 여기에서는 일반적인 브라우저에서 접속할 때에 대한 라우트를 지정한다. 아래의 코드는 / 경로에 대한 로직을 지정하는데, welcome 에 해당하는 뷰를 반환하라는 의미다.

// Only for Get
Route::get('/', function () {
    return view('welcome');
});

// For any methods.
Route::any('/', function () {
    return view('welcome');
});

// Only for 'Get', 'Post' methods
Route::match(['get', 'post'], '/', function () {
    return view('welcome');
});

// Not matched
Route::fallback(function () {
    return view('welcome');
});

Roue::get() 은 HTTP Mehod 중 하나인 Get 에 대해 처리하며 그에 따라 Route::post(), Route::put(), Route::delete() 등의 메서드가 존재한다. 보기에 정적메서드인 것처럼 보이지만 이는 라라벨의 파사드 기능을 사용한 것이다. 파사드는 사용자가 라라벨에서 제공하는 기능을 조금 더 사용하기 쉽게하도록 제공한다. Route::any() 는 모든 HTTP Method 에 대응, Route::match() 는 특정 메서드에만, 마지막으로 Route::fallback() 은 라우트에 매치가 되지 않았을때를 이야기한다.

이름

라우트에는 이름이 있다. 이름을 지정하면 나중에 라우트가 변경되더라도 그저 이름으로 접근하기 때문에 유지보수성이 향상된다. 라우트를 지정할 때 이름을 주려면 어떻게 해야할까? 단순히 name() 메서드를 써주면 그만이다.

Route::get('/', function () { return view('welcome'); })->name('home');

이 다음 이름이 있는 라우트에 접근하고 싶다면 route() 헬퍼함수를 사용하여 접근할 수 있다. 또한 두번째 파라매터로 키값 배열을 사용하여 라우트 파라매터를 넘길 수도 있다.

route('home'); // '/'

그룹

라우트 그룹은 여러 개의 라우트를 묶을때 사용한다. 같은 네임스페이스를 사용하거나, 주소에 같은 접미사가 붙거나 또는 나중에 이야기할 미들웨어를 사용하게 될 때 라우트 하나하나에 그것들을 적용시키는 것은 코드가 길어지고 가독성이 떨어지기 때문에 라우트의 그룹을 만들어서 처리할 수 있게 되어있다.

Route::group(function () {
    Route::get('/', function () {
        return view('welcome');
    });
});

// Route prefix 'users'
Route::prefix('users')->group(function () {
    Route::get('/', function () {
        return view('welcome');
    });
});

// With 'auth' middleware
Route::middleware('auth')->group(function () {
    Route::get('/', function () {
        return view('welcome');
    });
});

처음 선언한 코드는 사실상 의미가 없고, Route::prefix() 와 함께 되어있는 것은 URL 의 접두사를 붙인다. 그리고, 마지막으로 auth 미들웨어를 그룹에 적용시킨다. 여기서 auth 미들웨어는 로그인이 되어있는 유저만 접근할 수 있도록 하기 위한 장치다.

지금까지 라우트를 선언한 것을 보면 전부 내부에 view() 헬퍼함수를 사용하여 views/welcome.blade.php 를 반환한 것을 볼 수 있는데, 이러한 간단한 형태라면 Route::view() 를 사용하면 클로저를 사용하지 않고도 바로 할 수 있게된다.

//Route::get('/', function () {
//    return view('welcome');
//});

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

물론 세번째 파라매터를 통해 키값 배열로써 뷰에 아주 단순한 데이터를 보내는 것도 가능하다.

폼 메서드 스푸핑

클라이언트에서 을 통해 라라벨 어플리케이션에 요청할 때 Get, Post 이외에 Delete, Put 등 다른 메서드로 요청하고 싶은 경우가 있다. 이러한 경우 라라벨에서는 _method 라는 이름을 가진 input tag 를 사용하여 메서드를 속여 라라벨에서 해당 메서드로 인식하도록 만들 수 있다. 즉, 실제로는 Post 요청이어도 라라벨에서 Put 처럼 인식하도록 만들 수 있다는 이야기다.

 

두 가지 방법으로 가능한데, 하나는 직접 input tag 에 쓰는 것과, 다른 하나는 @method 지시어를 쓰는 것이다.

<form method="POST" action="/">
    <input type="hidden" name="_method" value="PUT">
    @method('PUT')
</form>

CSRF

CSRF(Cross-Site Request Forgery, 교차 사이트 요청 위조)라고 하며, 공격 방법의 일종인데, 어느 한 사이트, 또는 페이지가 다른 사람의 권한을 도용하여 마치 그 사람이 요청한 것처럼 다른 페이지를 속이는 것을 말한다. PHP: HTML 폼 (GET, POST)에 일반적인 PHP 에서 그것을 막는 방법이 기술되어 있고, 라라벨에서 이를 막으려면 더욱 간단하게 다음과 같이 csrf_field() 헬퍼를 사용하거나, input tag 에 _token 추가, 그리고 마지막으로 @csrf 를 사용할 수 있다.

<form method="POST" action="/">
    <?php echo csrf_field() ?>
    <input type="hidden" name="_token" value="<?php echo csrf_token() ?>">
    @csrf
</form>

캐싱

여기서 말하는 캐싱은 라우트가 정의되어있는 파일들을 캐싱하는 것이다. 라라벨 어플리케이션이 실행되면 라우트를 읽어가며 하나하나 등록하게 될텐데, 여기에서 시간이 다소 걸릴 수 있기때문에 미리 직렬화 및 캐싱하여 실행될때 보다 빠르게 응답을 줄 수 있다.

 

라우트 캐시가 있다면 라라벨은 캐싱된 것을 우선적으로 읽으므로 기존 라우트 파일에서 수정사항이 있어도 적용되지 않는다. 따라서 개발시에는 캐싱을 가급적 꺼두거나 실행하지 말고, 운영환경에서 하길 권한다. 캐싱을 하려면 다음과 같은 명령어로 할 수 있다.

php aritsan route:cache

컨트롤러

컨트롤러는 MVC(Model, View, Controller)에서 C(Controller)에 해당한다. 컨트롤러는 어플리케이션이 가지고 있는 대부분의 비지니스 로직을 구현하는 부분이다. 라라벨에서 컨트롤러는 라우트에서 지정하고 별도의 컨트롤러 클래스를 두는 것이 가장 일반적이다.

 

라우팅 파트에서 지정한 것은 클로저 형태인데, 라라벨 어플리케이션을 설치하면 기본적으로 아래와 같이 작성되어있는 것을 볼 수 있다. 현재 / 에 해당하는 컨트롤러가 하는 일은 welcome 뷰를 반환하는 일이다.

Route::get('/', function () {
    return view('welcome');
});

라라벨에서 가장 일반적인 컨트롤러를 만들어보자. 먼저 아래의 명령으로 기본적인 컨트롤러 클래스를 만들 수 있다. 

php artisan make:controller HomeController

실행하고 나면 App\Http\Controllers 디렉토리 안에 HomeController.php 가 생성되며 다음과 같이 코드를 바꿔보자. 라라벨은 PSR-4 Autoload 규약을 따르기 때문에 디렉토리 경로와 네임스페이스를 매핑한다.

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class HomeController extends Controller
{
    public function index() 
    {
        return view('welcome');
    }
}

라우트에서는 다음과 같이 설정할 수 있다. 네임스페이스를 포함한 컨트롤러의 경로와 메서드의 이름을 튜플로 전달하면 된다. 

Route::get('/', [App\Http\Controllers\HomeController::class, 'index']);

리소스 컨트롤러

리소스 컨트롤러는 라우트 구성을 RESTful 한 URL 인터페이스로 구성할 수 있다. RESTful 한 구조에서는 하나의 리소스에 대해 라우트를 사람이 알기쉽도록 구성한다. 주로 뷰, 리액트 등 SPA(Single Page Application) 프레임워크와 사용할 때 API 라우트를 구성할 때 사용하지만, 브라우저용으로 구성하는 것도 좋다. 리소스 라우트는 라라벨의 관례에 따라 복수형의 단어를 사용한다. 예를 들어 아래와 같이 리소스 라우트를 생성하고 등록한다.

php arisan make:controller TaskController --resource
Route::resource('tasks', App\Http\Controllers\TaskController::class);​

이 다음 등록된 라우트를 확인하면 다음과 같이 등록된다. HTTP Method 에 따라 라우트가 등록되어 있고 그에 따라 RESTful 한 형태로 URL 인터페이스가 되어있음을 확인하자. Name 은 라우트의 이름이고 Action 은 해당 라우트에 해당하는 컨트롤러다.

$ php artisan route:list
+--------+-----------+-------------------+---------------+---------------------------------------------+------------+
| Domain | Method    | URI               | Name          | Action                                      | Middleware |
+--------+-----------+-------------------+---------------+---------------------------------------------+------------+
|        | GET|HEAD  | tasks             | tasks.index   | App\Http\Controllers\TaskController@index   | web        |
|        | POST      | tasks             | tasks.store   | App\Http\Controllers\TaskController@store   | web        |
|        | GET|HEAD  | tasks/create      | tasks.create  | App\Http\Controllers\TaskController@create  | web        |
|        | GET|HEAD  | tasks/{task}      | tasks.show    | App\Http\Controllers\TaskController@show    | web        |
|        | PUT|PATCH | tasks/{task}      | tasks.update  | App\Http\Controllers\TaskController@update  | web        |
|        | DELETE    | tasks/{task}      | tasks.destroy | App\Http\Controllers\TaskController@destroy | web        |
|        | GET|HEAD  | tasks/{task}/edit | tasks.edit    | App\Http\Controllers\TaskController@edit    | web        |
+--------+-----------+-------------------+---------------+---------------------------------------------+------------+

추가적으로 API 리소스 라우트도 있기는 한데, API 의 경우 브라우저를 클라이언트로 하여 접근하는 경우는 많이 없기 때문에 tasks.index, tasks.create 와 같은 일부 브라우저를 위한 라우트는 불필요하게 된다.

파라매터

라우트에는 파라매터를 받을 수 있는데, 예를 들어 위와 같은 라우트에서 {task} 가 바로 파라매터에 해당한다. tasks.show 에 해당하는 컨트롤러 코드에 보면 파라매터로 $id 가 있는 것을 볼 수 있는데, GET tasks/14 로 요청했다면 14 가 할당 된다.

/**
 * Display the specified resource.
 *
 * @param  int  $id
 * @return \Illuminate\Http\Response
 */
public function show($id)
{
    //
}

여러 개의 파라매터가 존재한다면, 함수에 들어가는 파라매터의 순서는 반드시 지켜줄 필요가 있다. 파라매터의 이름은 task 이고 컨트롤러 메서드의 파라매터는 $id 인데, 이는 나중에 나올 라우트 모델 바인딩을 제외하면 일치시킬 필요는 없다.

 

파라매터를 정의할 때 정규표현식을 사용하여 패턴에 일치하는 경우에만 일치시킬 수도 있다.

// Regualer Expression
Route::get('posts/{id}/{slug}', function ($id, $slug) {})->where(['id' => '[0-9]+', 'slug' => '[a-Za-z]+']);

그 외에 whereNumber(), whereAlpha(), whereAlphaNumber() 도 존재하여 라우트 파라매터에 따라 사용할 수 있다.

더 읽을거리

PHP: MVC(Model, View, Controller)

PHP: HTML 폼 (GET, POST)

https://laravel.com/docs/8.x/routing

https://laravel.kr/docs/8.x/routing