[laravel] 나만보는 라라벨 튜토리얼

라라벨코리아, 라라캐스트를 보고 간단한 프로젝트를 만들면서 정리하는 글

  1. 프로젝트생성
  2. composer 의존성 설치
  3. 프론트앤드 스케폴딩
  4. npm 의존성 설치
  5. laravel-mix 사용하기
  6. model, controller, migration 생성하기
  7. restful한 route 만들기
  8. view 생성하기
  9. 기능구현하기
    • 글쓰기 폼작성
    • 저장하기
    • 글보기
    • 글 수정하기 폼작성
    • 글 업데이트하기
    • 글목록 페이지
    • 삭제하기
  10. 모델을 라우트에 바인딩 시키자
  11. 중복 제거

1.프로젝트 생성

laravel new blog
cd ./blog

2.composer 의존성 설치

composer install

3.프론트앤드 스케폴딩
필요한 부분만 선택하여 설치하면 된다. (본 예제에서는 bootstrap만 스캐폴딩함)

composer require laravel/ui --dev
// Generate basic scaffolding...
php artisan ui bootstrap
php artisan ui vue
php artisan ui react
// Generate login / registration scaffolding...
php artisan ui bootstrap --auth
php artisan ui vue --auth
php artisan ui react --auth

4.npm 의존성 설치
프론트 앤드 스캐폴딩을 하게 되면 package.json에 의존성 목록의 추가된다. 프로젝트에 사용하기 위해서 의존성을 설치해준다.

npm install

5.laravel-mix 사용하기
루트디렉토리의 webpack.mix.js 파일을 열어보면 아래와 같이 기본설정이 되어있다.

mix.js('resources/js/app.js', 'public/js')
    .sass('resources/sass/app.scss', 'public/css');

특별히 추가 할게 없다면 npm run dev 명령어를 실행해서 컴파일 하자. 앞에서 부트스트랩을 스캐폴딩했기 때문에 컴파일 하면 부트스트랩도 같이 컴파일 되어 public/js/app.js, public/css/app.css 로 위치하게 된다. (laravel-mix 문서 보기)

추가로 browsersyc를 webpack.mix.js에 추가해서 브라우저 변경시 새로고침없이 자동으로 리로드되게 해주면 개발할때 편리하다.

//mix.browserSync('mydomain.com');
mix.browserSync('localhost:8000');

6.모델, 컨트롤러, 마이그레이션 생성하기
-r 옵션은 리소스컨트롤러로 컨트롤러에 기본적인 메서드가 자동으로 추가된다.

* 모델과 컨트롤러는 단수로, 마이그레이션은 복수로 표현한다.
모델          Article
컨트롤러      ArticleController
마이그레이션  create_articles_table
php artisan make:model Article
php artisan make:controller ArticleController
php artisan make:migration create_aticles_table
// 한번에 생성하기 -c: controller -r:resource controller -m:migration
php artisan make:model Article -c -m
또는
php artisan make:model Article -r -m

생성된 마이그레이션 파일을 열어 테이블 스키마를 지정해준다.

    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->text('title');
            $table->longText('content');
            $table->timestamps();
            $table->boolean('is_published')->nullable()->default(false);
        });
    }
    public function down()
    {
        Schema::dropIfExists('articles');
    }
//up 메서드는 migrate 명령어가 실행될 때 호출되고 down 메서드는 migrate:rollback 될 때 호출된다.

php artisan migrate 명령어를 실행해서 테이블을 생성하면된다. 이때 한번도 마이그레이션하지 않았으면 laravel에서 제공하는 테이블까지 함께 생성된다.

7. restful한 route 만들기

//인덱스 리스트
route::get('/article/', 'ArticleController@index')->name('article.index');
//글쓰기 폼
route::get('/article/create', 'ArticleController@create')->name('article.create');
//새로운글 저장
route::post('/article', 'ArticleController@store')->name('article.store');
//해당글 보기 
route::get('/article/{id}', 'ArticleController@show')->name('article.show');
//해당글 수정 폼
route::get('/article/{id}/edit', 'ArticleController@edit')->name('article.edit');
//해당글 업데이트
route::put('/article/{id}', 'ArticleController@update')->name('article.update');
//해당글 삭제
route::delete('/article/{id}/destroy', 'ArticleController@destroy')->name('article.destroy');

route파사드의 name메서드를 사용하는 편이 나중에 url 관리하기가 편하다.

8.view 생성하기
/resources/views 디렉토리에 이번프로젝트에 뼈대가 될 layout.blade.php 파일을 생성한다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="{{ asset('css/app.css')}}">
    <script src="{{ asset('js/app.js') }}"></script>
    <title>Document</title>
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <div class="container">
            <a class="navbar-brand" href="{{ route('article.index') }}">Navbar</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav">
                <li class="nav-item {{Request::is('article*')? 'active' : ''}}">
                    <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
                </li>
                </ul>
            </div>
        </div>
    </nav>
    <div class="container">
        @yield('content')
    </div>
    <div class="footer container mt-3 pt-3 text-center">
        <div class="text-muted">@laravel</div>
    </div>
</body>
</html>

/resources/views/article 디렉토리를 생성하고 앞에서 지정한 라우트에 필요한 뷰파일을 생성해준다.

//index.blade.php
@extends('layout')
@section('content')
리스트페이지
@endsection
//create.blade.php
@extends('layout')
@section('content')
글쓰기 폼
@endsection
//show.blade.php
@extends('layout')
@section('content')
글보기
@endsection
//edit.blade.php
@extends('layout')
@section('content')
글쓰기 수정 폼
@endsection
뷰파일은 /resources/views 내에 어디서든 위치해도 상관없는데 일관성을 위해서 article 디렉토리를 추가했다. 

9.기능구현하기

1)글쓰기 폼 (create.blade.php) 작성

  • ArticleController@create
    public function create()
    {
        return view('article.create');
    }
  • create.blade.php
@extends('layout')
@section('content')
    <form action="{{ route('article.store') }}" method="post">
        @csrf
        <div class="form-group">
          <label for="title">제목</label>
          <input 
          type="text"
          name="title"
          id="title"
          class="form-control @error('title') is-invalid @enderror"
          placeholder=""
          aria-describedby="title"
          value="{{ old('title') }}">
          @error('title')
          <p class="invalid-feedback">제목을 입력하세요</p>
          @enderror
        </div>
        <div class="form-group">
          <label for="content">내용</label>
          <textarea class="form-control @error('content') is-invalid  @enderror" name="content" id="content" rows="3">{{ old('content') }}</textarea>
          @error('content')
          <p class="invalid-feedback">내용을 입력하세요</p>
          @enderror
        </div>
        <button type="submit" class="btn btn-primary">저장</button>
    </form>
@endsection

form안에 @csrf 를 사용해서 csrf 토큰을 꼭 생성해야 한다.
validation처리를 위해서 @error, 글로벌 헬퍼함수 old() 부분을 잘 확인하자.
old()는 넘어온값을 세션에 저장해 두는데 validation에 통과하지 못했을 때 양식에 이전에 입력한 값을 다시 넣어주기 위해 사용한다.

2)저장하기 ArticleController@store

use App\Article; 
public function store()
    {
        request()->validate([
            'title' => 'required',
            'content' => 'required'
        ]);
        $article = new Article();
        $article->title = request('title');
        $article->content= request('content');
        $article->save();
        return redirect()->route('article.index');
    }

3)글보기

  • ArticleController@show 작성
    public function show($id)
    {
        $article = Article::find($id);
        return view('article.show', compact('article'));
    }
  • show.blade.php 작성
@extends('layout')
@section('content')
<h1>{{ $article->title }}</h1>
<small class="small text-muted">{{ $article->updated_at }}</small>
<div class="mt-3 pt-3 border-top">{{ $article->content }}</div>
@endsection

4)글 수정하기폼 ArticleController@edit

  • ArticleController@edit
   public function edit($id)
    {   
       $article = Article::find($id); 
        return view('article.edit', compact('article'));
    }
  • edit.blade.php
@extends('layout')
@section('content')
    <form action="{{ route('article.update', $article->id) }}" method="POST">
        @csrf
        @method('PUT')
        <div class="form-group">
          <label for="title">제목</label>
          <input 
          type="text"
          name="title"
          id="title"
          class="form-control @error('title') is-invalid @enderror"
          placeholder=""
          aria-describedby="title"
          value="{{ $erros->has('title') ? old('title') : $article->title }}">
          @error('title')
          <p class="invalid-feedback">제목을 입력하세요</p>
          @enderror
        </div>
        <div class="form-group">
          <label for="content">내용</label>
          <textarea 
          class="form-control @error('content') is-invalid  @enderror" 
          name="content" 
          id="content" 
          rows="3">{{ $erros->has('content') ? old('content') : $article->content}}</textarea>
          @error('content')
          <p class="invalid-feedback">내용을 입력하세요</p>
          @enderror
        </div>
        <button type="submit" class="btn btn-primary">저장</button>
    </form>
    <form action="{{ route('article.destroy', $article->id) }}" method="POST">
      @csrf
      @method('DELETE')
      <button type="submit" class="btn btn-danger">삭제</button>
    </form>
@endsection

@method 지시자를 이용해서 PUT메서드를 스푸핑하는것을 확인하자.

5)글 업데이트 ArticleController@update

public function update($id)
    {
        request()->validate([
            'title' => 'required',
            'content' => 'required'
        ]);
        $article = Article::find($id);
        $article->title = request('title');
        $article->content= request('content');
        $article->save();
        return redirect()->route('article.show', $article->id);
    }

5)글목록 페이지

  • ArticleController@index
public function index(){
        $articles = Article::orderBy('created_at', 'desc')->paginate(15);
        return view('article.index', compact('articles'));
    }
  • inedx.blade.php
@extends('layout')
@section('content')
@foreach($articles as $article)
<div class="card mb-3">
  <div class="row no-gutters">
    <div class="col-md-3">
      <img src="..." class="card-img" alt="">
    </div>
    <div class="col-md-9">
      <div class="card-body">
        <h5 class="card-title">
          <a href="{{ route('article.show', $article->id) }}">{{ $article->title }}</a>
        </h5>
        <p class="card-text">{{ $article->content }}</p>
        <p class="card-text"><small class="text-muted">{{ $article->updated_at }}</small></p>
      </div>
    </div>
  </div>
</div>
@endforeach
@endsection

6)해당글 삭제하기 ArticleController@destroy

    public function destroy($id)
    {
        $article = Article::find($id);
        $article->delete();
        return redirect()->route('article.index');
    }

10. 모델을 라우트에 바인딩 시키자

라우트의 {id} 부분을 {article}로 변경하고 컨트롤러 메서드에 $id로 전달한 파라미터를 Article $article 타입 힌트로 지정한다.

만약 route의 {article}을 {foo}로 지정했다면 컨트롤러의 타입힌트도 Article $foo로 변경해야 한다.

//web.php
route::get('/article/', 'ArticleController@index')->name('article.index');
route::get('/article/create', 'ArticleController@create')->name('article.create');
route::post('/article', 'ArticleController@store')->name('article.store');
route::get('/article/{article}', 'ArticleController@show')->name('article.show');
route::get('/article/{article}/edit', 'ArticleController@edit')->name('article.edit');
route::put('/article/{article}', 'ArticleController@update')->name('article.update');
route::delete('/article/{article}/destroy', 'ArticleController@destroy')->name('article.destroy');
//ArticleController.php
    public function index(){
        $articles = Article::orderBy('created_at', 'desc')->paginate(15);
        return view('article.index', compact('articles'));
    }
    public function create()
    {
        return view('article.create');
    }
    public function store()
    {
        request()->validate([
            'title' => 'required',
            'content' => 'required'
        ]);
        $article = new Article();
        $article->title = request('title');
        $article->content= request('content');
        $article->save();
        return redirect()->route('article.index');
    }
    public function show(Article $article)
    {
        $article = Article::find(Article $article);
        return view('article.show', compact('article'));
    }
    public function edit(Article $article)
    {   
       $article = Article::find(Article $article); 
        return view('article.edit', compact('article'));
    }
    public function update(Article $article)
    {
       request()->validate([
            'title' => 'required',
            'content' => 'required'
        ]);
        $article = Article::find(Article $article);
        $article->title = request('title');
        $article->content= request('content');
        $article->save();
        return redirect()->route('article.show', $article->id);
    }
    public function destroy(Article $article)
    {
        $article = Article::find(Article $article);
        $article->delete();
        return redirect()->route('article.index');
    }
라우트에 모델을 바인딩하면 라라벨은 자동으로 테이블의 id칼럼을 조회하게 된다. id가 아닌 다른 칼럼을 조회하게 하려면 model에 getRouteKeyName메서드를 오버라이드 해준다.
class Article extends Model
{
  function getRouteKeyName(){
    return 'title';
  }
}

11. 중복제거

validate와 게시글 저장, 업데이트 하는부분이 store와 update에서 중복이 발생했다. validation 부분을 메서드로 작성하여 중복을 제거하자.

public function index(){
        $articles = Article::orderBy('created_at', 'desc')->paginate(15);
        return view('article.index', compact('articles'));
    }
    public function create()
    {
        return view('article.create');
    }
    public function store()
    {
        Article::create($this->validateArticle());
        return redirect()->route('article.index');
    }
    public function show(Article $article)
    {
        return view('article.show', compact('article'));
    }
    public function edit(Article $article)
    {   
        return view('article.edit', compact('article'));
    }
    public function update(Article $article)
    {
        $article->update($this->validateArticle());
        return redirect()->route('article.show', $article->id);
    }
    public function destroy(Article $article)
    {
        $article->delete();
        return redirect()->route('article.index');
    }
    protected function validateArticle(){
        return request()->validate([
            'title' => 'required',
            'content' => 'required'
        ]);
    }

중복을 제거 했지만 글을 작성하거나 업데이트 하면 아래와 같이 에러가 발생할 것이다.

Add [title] to fillable property to allow mass assignment on [App\Article]. 

기본적으로 모든 Eloquent 모델은 대량 할당-Mass Assignment 으로부터 보호되기 때문에, 모델의 ‘fillable’나 ‘guarded’속성을 지정해야 주어야 한다.
fillable은 화이트리스트방식이고, guarded는 블랙리스트 방식이라고 생각하면 된다.

class Article extends Model
{
  protected $guarded = [];
  또는
  protected $fillable = ['title', 'content'];
}