Laravel 중급 태스크 목록

Jmnote (토론 | 기여)님의 2024년 6월 16일 (일) 07:29 판 (→‎인증)

1 개요

Intermediate Task List
중급 태스크 목록

https://laravel.com/docs/5.2/quickstart-intermediate

2 소개

이 빠른 시작 가이드는 Laravel 프레임워크에 대한 중급 소개를 제공하며, 데이터베이스 마이그레이션, Eloquent ORM, 라우팅, 인증, 인, 의존성 주입, 검증, 뷰, Blade 템플릿에 대한 내용을 포함하고 있습니다. Laravel 프레임워크나 PHP 프레임워크에 대한 기본 지식이 있는 경우, 이 가이드를 시작하기에 적합합니다.

Laravel의 기본 기능들을 샘플로 다루어 보기 위해, 태스크들을 다루는 태스크 목록 애플리케이션을 만들어 보겠습니다. 즉, 전형적인 "할 일" 목록 예제를 다루게 됩니다. "기본" 빠른 시작과 달리, 이 튜토리얼에서는 사용자가 계정을 만들고 애플리케이션에 인증할 수 있도록 할 것입니다. 이 프로젝트의 완성된 소스코드는 GitHub에서 확인할 수 있습니다.

3 도커 환경

testuser@lcoalhost:~$ mkdir ~/workspace && cd ~/workspace
testuser@localhost:~/workspace$ docker run --name laravel --rm -it -v ${PWD}:/workspace -w /workspace --entrypoint="" --network host bitnami/laravel:latest bash
Unable to find image 'bitnami/laravel:latest' locally
latest: Pulling from bitnami/laravel
3b20dd80ae86: Pull complete 
Digest: sha256:d16af27a0d4c2f6f3f71c730d7def97de91e0b2ea0233e84dc758340521b1629
Status: Downloaded newer image for bitnami/laravel:latest
root@localhost:/workspace#

4 설치

우선, Laravel 프레임워크을 새로 설치해야 합니다. Homestead 가상머신을 사용하거나 선택한 로컬 PHP 환경을 사용하여 프레임워크를 실행할 수 있습니다. 로컬 환경이 준비되면 Composer를 사용하여 Laravel 프레임워크를 설치할 수 있습니다:

composer create-project laravel/laravel quickstart --prefer-dist
root@localhost:/workspace# composer create-project laravel/laravel quickstart --prefer-dist
Creating a "laravel/laravel" project at "./quickstart"
Installing laravel/laravel (v11.1.1)
...
   INFO  Preparing database.

  Creating migration table ................................................... 7.50ms DONE

   INFO  Running migrations.

  0001_01_01_000000_create_users_table ...................................... 25.95ms DONE
  0001_01_01_000001_create_cache_table ....................................... 8.63ms DONE
  0001_01_01_000002_create_jobs_table ....................................... 21.42ms DONE
root@localhost:/workspace# cd quickstart/
root@localhost:/workspace/quickstart# find app/ routes/ resources/ | grep .php
app/Providers/AppServiceProvider.php
app/Http/Controllers/Controller.php
app/Models/User.php
routes/console.php
routes/web.php
resources/views/welcome.blade.php

참고: composer create-project laravel/laravel quickstart --prefer-dist

5 데이터베이스 준비

5.1 데이터베이스 마이그레이션

먼저, 데이터베이스 테이블을 정의하기 위해 마이그레이션을 사용해 보겠습니다. Laravel의 데이터베이스 마이그레이션은 유창하고 표현적인 PHP 코드를 사용하여 데이터베이스 테이블 구조와 수정 사항을 쉽게 정의할 수 있는 방법을 제공합니다. 팀원들에게 데이터베이스의 로컬 복사본에 수동으로 열을 추가하라고 지시하는 대신, 소스 컨트롤에 푸시한 마이그레이션을 실행하기만 하면 됩니다.

users 테이블

애플리케이션 내에서 사용자가 계정을 생성할 수 있도록 할 예정이므로 모든 사용자를 저장할 테이블이 필요합니다. 다행히도 Laravel은 기본 users 테이블을 생성하기 위한 마이그레이션을 이미 제공하고 있으므로 직접 생성할 필요가 없습니다. 기본 마이그레이션은 database/migrations 디렉터리에 위치해 있습니다.

tasks 테이블

다음으로, 모든 태스크을 저장할 데이터베이스 테이블을 생성해 보겠습니다. Artisan CLI는 다양한 클래스를 생성하는 데 사용할 수 있으며, Laravel 프로젝트를 구축할 때 많은 타이핑을 절약해 줍니다. 이번에는 make:migration 명령어를 사용하여 tasks 테이블에 대한 새로운 데이터베이스 마이그레이션을 생성해 보겠습니다:

php artisan make:migration create_tasks_table --create=tasks
$ php artisan make:migration create_tasks_table --create=tasks

   INFO  Migration [database/migrations/2024_06_15_101234_create_tasks_table.php] created successfully.

이 마이그레이션 파일은 프로젝트의 database/migrations 디렉토리에 생성됩니다. make:migration 명령어는 자동 증가 ID와 타임스탬프를 이미 마이그레이션 파일에 추가했습니다. 이 파일을 편집하여 태스크의 이름을 위한 name 문자열 컬럼과, tasks 테이블과 users 테이블을 연결할 user_id 컬럼을 추가해봅시다:

database/migrations/2024_06_15_101234_create_tasks_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->integer('user_id')->unsigned()->index();
            $table->string('name');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('tasks');
    }
};

마이그레이션을 실행하려면 migrate Artisan 명령어를 사용합니다. Homestead를 사용 중이라면, 호스트 머신이 데이터베이스에 직접 접근할 수 없으므로, 가상머신 내에서 이 명령어를 실행해야 합니다:

php artisan migrate
$ php artisan migrate

   INFO  Running migrations.

  2024_06_15_101234_create_tasks_table ....................................... 9.24ms DONE

이 명령어는 모든 데이터베이스 테이블을 생성합니다. 사용 중인 데이터베이스 클라이언트를 통해 데이터베이스 테이블을 확인하면, 마이그레이션에서 정의한 컬럼이 포함된 새로운 tasksusers 테이블을 볼 수 있을 것입니다. 이제 Eloquent ORM 모델을 정의할 준비가 되었습니다!

5.2 Eloquent 모델

Eloquent는 Laravel의 기본 ORM(객체 관계 매퍼)입니다. Eloquent를 사용하면 명확하게 정의된 "모델"을 통해 데이터베이스에서 데이터를 쉽게 검색하고 저장할 수 있습니다. 일반적으로 각 Eloquent 모델은 단일 데이터베이스 테이블과 직접적으로 대응됩니다.

User 모델

먼저, users 데이터베이스 테이블에 대응하는 모델이 필요합니다. 그러나 프로젝트의 app 디렉토리를 살펴보면 Laravel에 이미 User 모델이 포함되어 있으므로 수동으로 생성할 필요가 없습니다.

Task 모델

이제 tasks 데이터베이스 테이블에 대응하는 Task 모델을 정의해봅시다. Artisan 명령어를 사용하여 이 모델을 생성할 수 있습니다. 이번에는 make:model 명령어를 사용합니다:

php artisan make:model Task
$ php artisan make:model Task

   INFO  Model [app/Models/Task.php] created successfully.

모델은 애플리케이션의 app 디렉토리에 배치됩니다. 기본적으로 모델 클래스는 비어 있습니다. Eloquent 모델에 어떤 테이블과 대응되는지 명시적으로 알릴 필요는 없습니다. 왜냐하면 Eloquent는 모델 이름의 복수형을 데이터베이스 테이블로 가정하기 때문입니다. 따라서, Task 모델은 tasks 데이터베이스 테이블과 대응되는 것으로 간주됩니다.

이제 이 모델에 몇 가지를 추가해봅시다. 먼저, name 속성이 "mass-assignable(대량 할당가능)"하도록 명시하겠습니다. 이를 통해 Eloquent의 create 메소드를 사용하여 name 속성을 채울 수 있습니다:

app/Models/Task.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name'];
}

라우트를 애플리케이션에 추가하면서 Eloquent 모델을 사용하는 방법에 대해 더 알아볼 것입니다. 물론, 더 많은 정보를 원한다면 전체 Eloquent 문서를 참조해도 좋습니다.

5.3 Eloquent 관계

이제 모델이 정의되었으므로, 이들을 연결해야 합니다. 예를 들어, User는 여러 개의 Task 인스턴스를 가질 수 있으며, Task는 단일 User에게 할당됩니다. 관계를 정의하면 다음과 같이 관계를 통해 유연하게 조회할 수 있습니다:

$user = App\User::find(1);

foreach ($user->tasks as $task) {
    echo $task->name;
}
tasks 관계

먼저 User 모델에 tasks 관계를 정의해보겠습니다. Eloquent 관계는 모델의 메소드로 정의됩니다. Eloquent는 여러 가지 관계를 지원하므로 전체 Eloquent 문서를 참조하여 더 많은 정보를 확인하세요. 이 경우에는, User 모델에 hasMany 메소드를 호출하는 tasks 함수를 정의합니다:

app/Models/User.php
<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    // Other Eloquent Properties...
 
    /**
     * Get all of the tasks for the user.
     */
    public function tasks()
    {
        return $this->hasMany(Task::class);
    }
}
user 관계

다음으로, Task 모델에 user 관계를 정의해보겠습니다. 이번에도 모델에 메소드로 관계를 정의합니다. 이 경우에는, belongsTo 메소드를 사용하여 관계를 정의합니다:

app/Models/Task.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Task extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name'];

    /**
     * Get the user that owns the task.
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

훌륭합니다! 이제 관계가 정의되었으므로 컨트롤러를 만들기 시작할 수 있습니다!

6 라우팅

태스크 목록 애플리케이션의 기본 버전에서는 routes/web.php 파일 내에서 클로저를 사용하여 모든 로직을 정의했습니다. 여기서는 주로 컨트롤러를 사용하여 라우트를 조직할 것입니다. 컨트롤러를 사용하면 HTTP 요청 처리 로직을 여러 파일에 걸쳐 분산시켜 더 나은 조직화를 할 수 있습니다.

6.1 뷰 표시하기

단일 라우트를 사용하여 클로저를 구현할 것입니다. 이는 애플리케이션의 게스트를 위한 랜딩 페이지가 될 / 라우트입니다. 그럼, / 라우트를 작성해 봅시다. 이 라우트에서는 "welcome" 페이지를 포함하는 HTML 템플릿을 렌더링하고자 합니다.

Laravel에서는 모든 HTML 템플릿은 resources/views 디렉토리에 저장되며, view 헬퍼를 사용하여 이 템플릿 중 하나를 라우트에서 반환할 수 있습니다:

routes/web.php
Route::get('/', function () {
    return view('welcome');
});

물론, 이 뷰를 실제로 정의해야 합니다. 이는 조금 있다가 할 것입니다!

6.2 인증

사용자가 계정을 만들고 애플리케이션에 로그인할 수 있도록 하는 것도 필요합니다. 일반적으로 웹 애플리케이션에 전체 인증 레이어를 구축하는 것은 번거로운 작업일 수 있습니다. 그러나 이는 매우 일반적인 요구사항입니다.

라우트 설정

routes/web.php 파일에 라우트를 추가합니다.

routes/web.php
<?php

use App\Http\Controllers\AuthController;
use Illuminate\Support\Facades\Route;

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

Route::get('/login', [AuthController::class, 'showLoginForm'])->name('login');
Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');

Route::get('/register', [AuthController::class, 'showRegistrationForm'])->name('register');
Route::post('/register', [AuthController::class, 'register']);

Route::get('/dashboard', function () {
    return view('dashboard');
})->middleware('auth');
인증 컨트롤러 생성
php artisan make:controller AuthController
app/Http/Controllers/AuthController.php
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;

class AuthController extends Controller
{
    public function showLoginForm()
    {
        return view('auth.login');
    }

    public function login(Request $request)
    {
        $credentials = $request->only('email', 'password');

        if (Auth::attempt($credentials)) {
            $request->session()->regenerate();
            return redirect()->intended('dashboard');
        }

        return back()->withErrors([
            'email' => 'The provided credentials do not match our records.',
        ]);
    }

    public function logout(Request $request)
    {
        Auth::logout();

        $request->session()->invalidate();
        $request->session()->regenerateToken();

        return redirect('/');
    }

    public function showRegistrationForm()
    {
        return view('auth.register');
    }

    public function register(Request $request)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:8|confirmed',
        ]);

        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

        Auth::login($user);

        return redirect()->intended('dashboard');
    }
}
로그인 뷰 작성
resources/views/auth/login.blade.php
<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
</head>
<body>
    <h2>Login</h2>

    @if($errors->any())
        <div>
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    <form method="POST" action="{{ route('login') }}">
        @csrf
        <div>
            <label for="email">Email:</label>
            <input type="email" name="email" id="email" required>
        </div>

        <div>
            <label for="password">Password:</label>
            <input type="password" name="password" id="password" required>
        </div>

        <div>
            <button type="submit">Login</button>
        </div>
    </form>
</body>
</html>
회원가입 뷰 작성
resources/views/auth/register.blade.php
<!DOCTYPE html>
<html>
<head>
    <title>Register</title>
</head>
<body>
    <h2>Register</h2>

    @if($errors->any())
        <div>
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    <form method="POST" action="{{ route('register') }}">
        @csrf
        <div>
            <label for="name">Name:</label>
            <input type="text" name="name" id="name" value="{{ old('name') }}" required>
        </div>

        <div>
            <label for="email">Email:</label>
            <input type="email" name="email" id="email" value="{{ old('email') }}" required>
        </div>

        <div>
            <label for="password">Password:</label>
            <input type="password" name="password" id="password" required>
        </div>

        <div>
            <label for="password_confirmation">Confirm Password:</label>
            <input type="password" name="password_confirmation" id="password_confirmation" required>
        </div>

        <div>
            <button type="submit">Register</button>
        </div>
    </form>
</body>
</html>
대시보드 뷰 작성
resources/views/dashboard.blade.php
<!DOCTYPE html>
<html>
<head>
    <title>Dashboard</title>
</head>
<body>
    <h2>Dashboard</h2>
    <p>Welcome, {{ Auth::user()->name }}!</p>

    <form method="POST" action="{{ route('logout') }}">
        @csrf
        <button type="submit">Logout</button>
    </form>
</body>
</html>

6.3 태스크 컨트롤러

태스크를 조회하고 저장할 필요가 있으므로, Artisan CLI를 사용하여 TaskController를 생성합시다. 그러면 컨트롤러가 app/Http/Controllers 디렉토리에 생성됩니다:

php artisan make:controller TaskController
$ php artisan make:controller TaskController

   INFO  Controller [app/Http/Controllers/TaskController.php] created successfull

컨트롤러가 생성되었으니, routes/web.php 파일에 컨트롤러를 가리키는 몇 가지 라우트를 추가해 봅시다:

routes/web.php
<?php

use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;

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

Route::get('/tasks', [TaskController::class, 'index']);
Route::post('/task', [TaskController::class, 'store']);
Route::delete('/task/{task}', [TaskController::class, 'destroy']);
모든 태스크 라우트 인증하기

이 애플리케이션에서는 모든 태스크 라우트가 인증된 사용자만 접근할 수 있도록 하고 싶습니다. 즉, 사용자가 태스크를 생성하려면 애플리케이션에 "로그인"해야 합니다. Laravel에서는 미들웨어를 사용하여 이러한 작업을 간단하게 할 수 있습니다.

컨트롤러의 모든 액션에 대해 인증된 사용자만 접근할 수 있도록 하려면, 컨트롤러 생성자에서 middleware 메소드를 호출하면 됩니다. 사용가능한 모든 라우트 미들웨어는 app/Http/Kernel.php 파일에 정의되어 있습니다. 이 경우에는, 모든 액션에 auth 미들웨어를 할당하고자 합니다:

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Requests;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
 
class TaskController extends Controller
{
    /**
     * 새 컨트롤러 인스턴스를 생성합니다.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }
}

7 레이아웃 및 뷰 구축

이 애플리케이션의 주요 부분은 새 태스크를 추가하는 양식과 현재 모든 태스크 목록을 포함하는 단일 뷰만을 가지고 있습니다. 이 뷰를 시각적으로 이해할 수 있도록, 기본 Bootstrap CSS 스타일이 적용된 완성된 애플리케이션의 스크린샷을 여기에 첨부합니다:

Basic-overview.png

7.1 레이아웃 정의하기

거의 모든 웹 애플리케이션은 페이지마다 동일한 레이아웃을 공유합니다. 예를 들어, 이 애플리케이션에는 모든 페이지에 공통으로 존재하는 상단 탐색 바가 있습니다. (만약 페이지가 둘 이상이라면) Laravel은 Blade 레이아웃을 사용하여 이러한 공통 기능을 각 페이지에 쉽게 공유할 수 있도록 합니다.

앞서 논의한 것처럼, 모든 Laravel 뷰는 resources/views에 저장됩니다. 따라서 resources/views/layouts/app.blade.php에 새로운 레이아웃 뷰를 정의해보겠습니다. .blade.php 확장자는 프레임워크에 Blade 템플릿 엔진을 사용하여 뷰를 렌더링하도록 지시합니다. 물론 Laravel에서 일반 PHP 템플릿을 사용할 수도 있습니다. 그러나 Blade는 깔끔하고 간결한 템플릿 작성을 위한 편리한 단축키를 제공합니다.

우리의 app.blade.php 뷰는 다음과 같이 생겼습니다:

<!-- resources/views/layouts/app.blade.php -->
 
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Laravel Quickstart - Intermediate</title>
 
        <!-- CSS And JavaScript -->
    </head>
 
    <body>
        <div class="container">
            <nav class="navbar navbar-default">
                <!-- Navbar Contents -->
            </nav>
        </div>
 
        @yield('content')
    </body>
</html>

이 레이아웃의 @yield('content') 부분을 주목하세요. 이는 자식 페이지가 레이아웃을 확장하여 자신의 콘텐츠를 주입할 수 있는 특별한 Blade 지시어입니다. 다음으로, 이 레이아웃을 사용할 자식 뷰를 정의하고 주요 콘텐츠를 제공해보겠습니다.

7.2 자식 뷰 정의하기

애플리케이션 레이아웃이 완료되었습니다. 다음으로, 새 태스크를 생성하는 폼과 기존 작업을 나열하는 테이블을 포함하는 뷰를 정의해야 합니다. 이 뷰는 TaskControllerindex 메소드에 대응하는 resources/views/tasks/index.blade.php에 정의할 것입니다.

Bootstrap CSS 보일러플레이트 코드는 생략하고 중요한 부분에 집중하겠습니다. 애플리케이션의 전체 소스 코드는 GitHub에서 다운로드할 수 있습니다:

<!-- resources/views/tasks/index.blade.php -->
 
@extends('layouts.app')
 
@section('content')
 
    <!-- Bootstrap Boilerplate... -->
 
    <div class="panel-body">
        <!-- Display Validation Errors -->
        @include('common.errors')
 
        <!-- New Task Form -->
        <form action="{{ url('task') }}" method="POST" class="form-horizontal">
            {{ csrf_field() }}
 
            <!-- Task Name -->
            <div class="form-group">
                <label for="task-name" class="col-sm-3 control-label">Task</label>
 
                <div class="col-sm-6">
                    <input type="text" name="name" id="task-name" class="form-control">
                </div>
            </div>
 
            <!-- Add Task Button -->
            <div class="form-group">
                <div class="col-sm-offset-3 col-sm-6">
                    <button type="submit" class="btn btn-default">
                        <i class="fa fa-plus"></i> Add Task
                    </button>
                </div>
            </div>
        </form>
    </div>
 
    <!-- TODO: Current Tasks -->
@endsection
설명

이 템플릿에 대해 조금 더 설명하겠습니다. 먼저, @extends 지시어는 우리가 resources/views/layouts/app.blade.php에 정의한 레이아웃을 사용하고 있음을 Blade에 알립니다. @section('content')@endsection 사이의 모든 내용은 app.blade.php 레이아웃의 @yield('content') 지시어 위치에 삽입됩니다.

@include('common.errors') 지시어는 resources/views/common/errors.blade.php에 위치한 템플릿을 로드합니다. 이 템플릿은 아직 정의하지 않았지만, 곧 정의할 것입니다!

이제 애플리케이션을 위한 기본 레이아웃과 뷰를 정의했습니다. 다음으로, TaskControllerindex 메소드에서 이 뷰를 반환하도록 하겠습니다:

/**
 * 사용자의 모든 태스크 목록을 표시합니다.
 *
 * @param  Request  $request
 * @return Response
 */
public function index(Request $request)
{
    return view('tasks.index');
}

이제, 양식 입력을 처리하고 데이터베이스에 새 태스크를 추가하는 POST /task 라우트의 컨트롤러 메소드에 코드를 추가할 준비가 되었습니다.

8 태스크 추가

8.1 유효성 검증

이제 뷰에 폼이 만들었으니, TaskControllerstore 메소드에 코드를 추가하여 들어오는 폼 입력을 유효성 검증하고 새 태스크를 생성해야 합니다. 먼저 입력을 검증해봅시다.

이 폼에서는 name 필드를 필수로 하고, 255자 이하로 입력되도록 해야 합니다. 검증에 실패하면 사용자를 /tasks URL로 리디렉션하고, 이전 입력과 오류를 세션에 플래시해야 합니다.

/**
 * 새 태스크를 생성합니다.
 *
 * @param  Request  $request
 * @return Response
 */
public function store(Request $request)
{
    $this->validate($request, [
        'name' => 'required|max:255',
    ]);
 
    // 태스크 생성...
}

기본 빠른 시작와 비교해보면, 이 검증 코드가 상당히 다르게 보일 것입니다! 컨트롤러 내에서는 기본 Laravel 컨트롤러에 포함된 ValidatesRequests 트레이트의 편리함을 활용할 수 있습니다. 이 트레이트는 요청과 검증 규칙 배열을 받는 간단한 validate 메소드를 노출합니다.

검증이 실패했는지 수동으로 확인하거나 수동으로 리디렉션할 필요도 없습니다. 주어진 규칙에 대해 검증이 실패하면, 사용자는 자동으로 원래 있던 곳으로 리디렉션되고 오류는 자동으로 세션에 플래시됩니다. 참 편리하죠!

$errors 변수

뷰 내에서 @include('common.errors') 지시어를 사용하여 폼의 검증 오류를 렌더링한 것을 기억하세요. common.errors 뷰는 모든 페이지에서 동일한 형식으로 검증 오류를 쉽게 표시할 수 있게 해줍니다. 이제 이 뷰의 내용을 정의해봅시다:

<!-- resources/views/common/errors.blade.php -->
 
@if (count($errors) > 0)
    <!-- Form Error List -->
    <div class="alert alert-danger">
        <strong>Whoops! Something went wrong!</strong>
 
        <br><br>
 
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

참고: $errors 변수는 모든 Laravel 뷰에서 사용할 수 있습니다. 검증 오류가 없으면 단순히 빈 ViewErrorBag 인스턴스가 됩니다.

8.2 태스크 생성

이제 입력 유효성 검증이 처리되었으므로, 실제로 새 태스크를 생성하여 라우트를 계속 채워보겠습니다. 새 태스크가 생성된 후, 사용자를 /tasks URL로 리디렉션할 것입니다. 태스크를 생성하기 위해 Eloquent의 관계를 활용할 것입니다.

Laravel의 대부분의 관계는 create 메소드를 노출하며, 이 메소드는 속성 배열을 받아 관련 모델의 외래 키 값을 데이터베이스에 저장하기 전에 자동으로 설정합니다. 이 경우에는, create 메소드는 주어진 태스크의 user_id 속성을 현재 인증된 사용자의 ID로 자동 설정할 것입니다. 현재 사용자를 $request->user()를 사용하여 접근하고 있습니다.

/**
 * 새 태스크를 생성합니다.
 *
 * @param  Request  $request
 * @return Response
 */
public function store(Request $request)
{
    $this->validate($request, [
        'name' => 'required|max:255',
    ]);
 
    $request->user()->tasks()->create([
        'name' => $request->name,
    ]);
 
    return redirect('/tasks');
}

훌륭합니다! 이제 우리는 태스크를 성공적으로 생성할 수 있습니다. 다음으로, 모든 기존 태스크 목록을 작성하여 뷰를 계속 추가해 보겠습니다.

9 기존 태스크 표시하기

먼저, TaskController@index 메소드를 수정하여 모든 기존 태스크를 뷰로 전달해야 합니다. view 함수는 뷰에서 사용가능한 데이터 배열을 두 번째 인수로 받아들이며, 배열의 각 키는 뷰 내에서 변수로 사용됩니다. 예를 들어, 다음과 같이 할 수 있습니다:

/**
 * 사용자의 모든 태스크 목록을 표시합니다.
 *
 * @param  Request  $request
 * @return Response
 */
public function index(Request $request)
{
    $tasks = $request->user()->tasks()->get();
 
    return view('tasks.index', [
        'tasks' => $tasks,
    ]);
}

하지만, Laravel의 의존성 주입 기능을 활용하여 TaskRepositoryTaskController에 주입하는 방법을 탐구해 보겠습니다. 이를 통해 모든 데이터 접근을 처리하도록 하겠습니다.

9.1 의존성 주입

Laravel의 서비스 컨테이너는 프레임워크 전체에서 가장 강력한 기능 중 하나입니다. 이 빠른 시작을 읽은 후에는 컨테이너의 문서를 모두 읽어보세요.

리포지토리 생성

앞서 언급했듯이 Task 모델의 모든 데이터 접근 로직을 담고 있는 TaskRepository를 정의하고자 합니다. 이는 애플리케이션이 성장하여 Eloquent 쿼리를 애플리케이션 전체에서 공유해야 할 경우 특히 유용할 것입니다.

그러면, app/Repositories 디렉토리를 생성하고 TaskRepository 클래스를 추가합시다. Laravel의 모든 app 폴더는 PSR-4 자동로딩 표준을 사용하여 자동으로 로드되므로, 필요한 만큼의 추가 디렉토리를 자유롭게 생성할 수 있습니다:

<?php
 
namespace App\Repositories;
 
use App\User;
 
class TaskRepository
{
    /**
     * 주어진 사용자의 모든 작업을 가져옵니다.
     *
     * @param  User  $user
     * @return Collection
     */
    public function forUser(User $user)
    {
        return $user->tasks()
                    ->orderBy('created_at', 'asc')
                    ->get();
    }
}
레포지토리 주입

레포지토리가 정의되면, 단순히 TaskController의 생성자에서 이를 "타입 힌트"로 지정하고 index 라우트 내에서 활용할 수 있습니다. Laravel은 모든 컨트롤러를 해결하기 위해 컨테이너를 사용하므로, 의존성은 자동으로 컨트롤러 인스턴스에 주입될 것입니다:

<?php
 
namespace App\Http\Controllers;
 
use App\Task;
use App\Http\Requests;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Repositories\TaskRepository;
 
class TaskController extends Controller
{
    /**
     * 태스크 리포지토리 인스턴스.
     *
     * @var TaskRepository
     */
    protected $tasks;
 
    /**
     * 새 컨트롤러 인스턴스를 생성합니다.
     *
     * @param  TaskRepository  $tasks
     * @return void
     */
    public function __construct(TaskRepository $tasks)
    {
        $this->middleware('auth');
 
        $this->tasks = $tasks;
    }
 
    /**
     * 사용자의 모든 태스크 목록을 표시합니다.
     *
     * @param  Request  $request
     * @return Response
     */
    public function index(Request $request)
    {
        return view('tasks.index', [
            'tasks' => $this->tasks->forUser($request->user()),
        ]);
    }
}

9.2 태스크 표시

데이터가 전달되면 tasks/index.blade.php 뷰에서 태스크를 테이블에 표시할 수 있습니다. @foreach Blade 구문을 사용하면 간결한 루프를 작성할 수 있으며, 이는 매우 빠른 순수 PHP 코드로 컴파일됩니다:

@extends('layouts.app')
 
@section('content')
    <!-- 태스크 생성 ... -->
 
    <!-- 현재 태스크들 -->
    @if (count($tasks) > 0)
        <div class="panel panel-default">
            <div class="panel-heading">
                Current Tasks
            </div>
 
            <div class="panel-body">
                <table class="table table-striped task-table">
 
                    <!-- 테이블 헤더 -->
                    <thead>
                        <th>Task</th>
                        <th>&nbsp;</th>
                    </thead>
 
                    <!-- 테이블 바디 -->
                    <tbody>
                        @foreach ($tasks as $task)
                            <tr>
                                <!-- 태스크 이름 -->
                                <td class="table-text">
                                    <div>{{ $task->name }}</div>
                                </td>
 
                                <td>
                                    <!-- TODO: 삭제 버튼 -->
                                </td>
                            </tr>
                        @endforeach
                    </tbody>
                </table>
            </div>
        </div>
    @endif
@endsection

태스크 애플리케이션이 거의 완성되었습니다. 하지만, 완료된 작업을 삭제할 방법이 없습니다. 다음으로 삭제 기능을 추가해 보겠습니다!

10 태스크 삭제

10.1 삭제 버튼 추가

tasks/index.blade.php 뷰에 각 행에 삭제 버튼을 추가해야 할 곳에 "TODO" 메모를 남겼습니다. 이제 각 행에 작은 단일 버튼 폼을 만들어 삭제 버튼을 추가해 보겠습니다. 버튼이 클릭되면 애플리케이션에 DELETE /task 요청이 전송되어 TaskController@destroy 메소드를 트리거하게 됩니다:

<tr>
    <!-- 태스크 이름 -->
    <td class="table-text">
        <div>{{ $task->name }}</div>
    </td>
 
    <!-- 삭제 버튼 -->
    <td>
        <form action="{{ url('task/'.$task->id) }}" method="POST">
            {{ csrf_field() }}
            {{ method_field('DELETE') }}
 
            <button type="submit" id="delete-task-{{ $task->id }}" class="btn btn-danger">
                <i class="fa fa-btn fa-trash"></i>Delete
            </button>
        </form>
    </td>
</tr>
메소드 스푸핑에 대한 참고사항

삭제 버튼의 폼 메서드는 POST로 지정되어 있지만, 우리는 Route::delete 라우트를 사용하여 요청에 응답하고 있습니다. HTML 폼은 GETPOST HTTP 메소드만 허용하므로, 폼에서 DELETE 요청을 스푸핑할 방법이 필요합니다.

폼 내에서 method_field('DELETE') 함수를 사용하여 DELETE 요청을 스푸핑할 수 있습니다. 이 함수는 Laravel이 인식하고 실제 HTTP 요청 메서드를 덮어쓸 숨겨진 폼 입력을 생성합니다. 생성되는 필드는 다음과 같이 같습니다:

<input type="hidden" name="_method" value="DELETE">

10.2 라우트 모델 바인딩

이제 TaskControllerdestroy 메소드를 정의할 준비가 거의 끝났습니다. 먼저, 이 라우트에 대한 라우트 선언과 컨트롤러 메소드를 다시 확인해봅시다:

Route::delete('/task/{task}', 'TaskController@destroy');
 
/**
 * 주어진 태스크를 삭제합니다.
 *
 * @param  Request  $request
 * @param  Task  $task
 * @return Response
 */
public function destroy(Request $request, Task $task)
{
    //
}

라우트의 {task} 변수가 컨트롤러 메소드에 정의된 $task 변수와 일치하므로, Laravel의 암시적 모델 바인딩이 자동으로 해당 Task 모델 인스턴스를 주입합니다.

10.3 인가

이제 destroy 메소드에 Task 인스턴스가 주입되었지만, 인증된 사용자가 해당 태스크를 실제로 "소유"하고 있는지 확실하지 않습니다. 예를 들어, 악의적인 요청이 랜덤 태스크 ID를 /tasks/{task} URL에 전달되어 다른 사용자의 태스크를 삭제하려는 시도로 있을 수 있습니다. 따라서 Laravel의 인증 기능을 사용하여 인증된 사용자가 주입된 Task 인스턴스를 실제로 소유하고 있는지 확인해야 합니다.

정책 생성

Laravel은 "정책"을 사용하여 인증 로직을 간단하고 작은 클래스에 조직합니다. 일반적으로 각 정책은 모델에 대응됩니다. Artisan CLI를 사용하여 TaskPolicy를 생성해보겠습니다. 생성된 파일은 app/Policies/TaskPolicy.php에 배치됩니다:

php artisan make:policy TaskPolicy

다음으로, 정책에 destroy 메소드를 추가해보겠습니다. 이 메소드는 User 인스턴스와 Task 인스턴스를 받습니다. 메소드는 단순히 사용자의 ID가 태스크의 user_id와 일치하는지 확인합니다. 실제로 모든 정책 메소드는 true 또는 false를 반환해야 합니다:

<?php

namespace App\Policies;

use App\Models\User;
use App\Models\Task;
use Illuminate\Auth\Access\HandlesAuthorization;

class TaskPolicy
{
    use HandlesAuthorization;

    /**
     * 주어진 사용자가 주어진 태스크를 삭제할 수 있는지 결정합니다.
     *
     * @param  User  $user
     * @param  Task  $task
     * @return bool
     */
    public function destroy(User $user, Task $task)
    {
        return $user->id === $task->user_id;
    }
}

마지막으로, Task 모델을 TaskPolicy와 연계해야 합니다. 이는 app/Providers/AuthServiceProvider.php 파일의 $policies 속성에 한 줄을 추가하여 수행할 수 있습니다. 이를 통해 Laravel은 Task 인스턴스에서 태스크를 인증할 때 어떤 정책을 사용할지 알 수 있습니다:

/**
 * 애플리케이션의 정책 매핑.
 *
 * @var array
 */
protected $policies = [
    'App\Models\Task' => 'App\Policies\TaskPolicy',
];
액션 인가

정책이 작성되었으므로, 이를 destroy 메소드에서 사용해 보겠습니다. 모든 Laravel 컨트롤러는 AuthorizesRequest 트레이트를 통해 노출된 authorize 메소드를 호출할 수 있습니다:

/**
 * 주어진 태스크를 삭제합니다.
 *
 * @param  Request  $request
 * @param  Task  $task
 * @return Response
 */
public function destroy(Request $request, Task $task)
{
    $this->authorize('destroy', $task);

    // 태스크를 삭제합니다...
}

이 메소드 호출을 잠시 살펴보겠습니다. authorize 메소드에 전달된 첫 번째 인수는 호출하려는 정책 메소드의 이름입니다. 두 번째 인수는 현재 관심 있는 모델 인스턴스입니다. 조금 전에 Task 모델이 TaskPolicy에 대응된다고 Laravel에 알렸기 때문에, 프레임워크는 어떤 정책에서 destroy 메소드를 실행할지 알고 있습니다. 현재 사용자는 자동으로 정책 메소드에 전달되므로 이를 수동으로 전달할 필요가 없습니다.

액션이 인증되면 코드는 정상적으로 계속 실행됩니다. 그러나 액션이 인증되지 않은 경우(즉, 정책의 destroy 메소드가 false를 반환하는 경우) 403 예외가 발생하고 사용자에게 오류 페이지가 표시됩니다.

참고: Laravel이 제공하는 인가 서비스와 상호작용하는 방법은 여러 가지가 있습니다. 전체 인가 문서를 꼭 살펴보시기 바랍니다.

10.4 해당 태스크 삭제

마지막으로, 주어진 태스크를 실제로 삭제하는 로직을 destroy 메소드에 추가해 보겠습니다. Eloquent의 delete 메소드를 사용하여 데이터베이스에서 주어진 모델 인스턴스를 삭제할 수 있습니다. 레코드가 삭제된 후, 사용자를 /tasks URL로 리디렉션할 것입니다:

/**
 * 주어진 태스크를 삭제합니다.
 *
 * @param  Request  $request
 * @param  Task  $task
 * @return Response
 */
public function destroy(Request $request, Task $task)
{
    $this->authorize('destroy', $task);
 
    $task->delete();
 
    return redirect('/tasks');
}
문서 댓글 ({{ doc_comments.length }})
{{ comment.name }} {{ comment.created | snstime }}