본문 바로가기

Dev/PHP

[PHP] Laravel에서의 MVC 패턴 구현

 

 

이번 포스팅에서는 Laravel에서 어떻게 MVC 패턴을 구현하는지 알아보겠습니다.

아마도 java spring을 접해 보셨다면 구조 파악에 어렵지 않을 듯싶습니다.

본 포스팅은 MVC 패턴 이용한 메모 웹을 만들어 보겠습니다. 메모 웹은 내용 조회, 추가, 삭제 기능을 포함합니다. 

디자인은 전혀 고려하지 않은 포스팅이므로 이점 참고 바랍니다. 

*이 포스팅은 이 전 포스팅에서 생성한 memos 테이블 예시로 합니다. 

 

먼저 아래의 사진을 참고해 주세요.

Laravel에서의 MVC 개괄적인 흐름

View는 브라우저에서 보이는 화면입니다.

View에서의 모든 요청은 일단 routes 폴더에 있는 web.php에서 받습니다.

web.php에서는 요청에 따라 어떤 컨트롤러로 전달할지 결정합니다.

컨트롤러에서는 요청받은 내용을 수행하고(만약 DB 처리 작업이 필요할 경우 Model을 통해 작업을 수행합니다.), 전달할 데이터와 뷰를 맵핑해서 브라우저에 띄워줍니다.

물론 깊이 들어가면 더 다양한 처리 방식이 있을 듯싶습니다만, 포스팅을 남기는 이 시점에는 필자가 아직 파악하지 못 한 관계로, 훗날 추가 내용을 남기도록 하겠습니다.

 

그럼, 본격적으로 프로젝트에서 위 MVC 패턴을 구현해 보도록 하겠습니다.

 

1. Model, View, Controller 스크립트 생성

먼저 터미널을 열고 프로젝트 폴더로 이동한 후, 아래의 명령어를 통해 Model과 Controller 스크립트를 생성해 줍니다.

$ php artisan make:controller MainController
$ php artisan make:model Memo

위 명령어를 입력하면 각각 아래의 경로에 자동으로 생성됩니다.

 

[Controller 생성 위치]

app
└─Http
    └─Controllers
	├─Controller.php
	└─MainController.php

php artisan make:controller 명령어를 통해 생성한 컨트롤러는 Controllers 폴더에 생성되며, 같은 폴더에 있는 Controller.php를 상속합니다.

Controller.php는 프로젝트 초기 생성 시, 기본적으로 생성되는 스크립트입니다.

본 포스팅에서의 예제는 모든 요청 처리를 이 MainController를 통해 진행할 것입니다.

 

[Model 생성 위치]

app
└─Memo.php

Model 스크립트는 app 폴더에 만들어집니다.

이제부터 이 Memo 모델 스크립트는 mysql의 memos테이블과 상호 작용합니다. 

 

2. Controller 스크립트 작성

MainController.php 파일을 열어 class 내부에 아래의 내용을 입력합니다.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Memo; //Memo 모델을 사용한다.

class MainController extends Controller
{
    //웹 최초 진입시 처리.
    public function index(){
    	// memos 테이블에서 메모 생성 날짜 기준 내림차순으로 정렬해 가져온다.
        $memos = Memo::orderBy('created_at', 'desc')->get();
        //index view와 가져온 메모 데이터를 렌더링해 브라우저에 출력.
        return view('index',['memos' => $memos]);
    }
	
    //create 요청을 받는다.
    public function create(){
        return view('create'); //view를 렌더링해 브라우저에 출력.
    }
	
    //create view에서 메모 삽입 요청시 처리.
    public function store(Request $request){
    
    	//request 객체를 통해 메모 내용을 가져온다.
        //validate 메서드를 이용해 메모 길이가 500을 넘는지 검사한다.
        //500이 넘어가면 create view에 에러를 반환하고 데이터는 삽입되지 않는다.
        $content = $request->validate(['content' => 'required:max:500']);
        
        //memos테이블에 데이터 삽입.
        Memo::create($content);
        
        //삽입이 끝나면 index 메서드로 리다이렉트.
        return redirect()->route('index');
    }
	
    //메모 수정 요청
    public function edit(Request $request){
        //request 객체를 통해 수정하고자 하는 메모의 id값을 받는다.
        $memo = Memo::find($request->id);
        
        //edit view와 해당 메모를 렌더링, 브라우저 출력
        return view('edit',['memo' => $memo]);
    }

    //edit view에서 수정된 메모를 적용하는 요청.
    public function update(Request $request){

        //memo테이블에서 요청 받은 id값의 데이터를 호출.
        $memo = Memo::find($request->id);
        
        //메모 내용이 500자가 넘는지 검사.
        $content = $request->validate(['content' => 'required:max:500']);

        //수정된 메모를 테이블에 적용하고 save한다.
        $memo->fill($content)->save();
        
        //index 메서드로 리다이렉트.
        return redirect()->route('index');
    }
	
    //메모 삭제 요청
    public function delete(Request $request){
        //id를 통해 해당하는 row를 찾는다.
        $memo = Memo::find($request->id);
        
        $memo->delete(); // row 삭제.
         //index 메서드로 리다이렉트.
        return redirect()->route('index');
    }
}

 

위 스크립트에서 사용된 메서드는 정리하자면 다음과 같습니다.

index

메인 페이지 요청

create

메모 작성 페이지 요청

store

메모 삽입 요청

edit

메모 수정 페이지 요청

update

메모 수정 적용 요청

delete

메모 삭제 요청

 

3. Model 내용 추가

<?php
use App\Memo;

namespace App;

use Illuminate\Database\Eloquent\Model;

class Memo extends Model
{
    protected $table ='memos';
    protected $fillable =['content'];
}

Memo.php 파일을 열어 class 내부에 테이블을 지정합니다.

그리고 fillable을 선언해 content 칼럼의 대량 할당을 허용합니다.

fillable 선언의 의미는, 테이블에 메모 내용(content 칼럼)은 크기가 커도 삽입을 허용한다라는 의미입니다.

laravel에서는 보안상의 이유로 데이터 대량 할당을 기본적으로는 허용하지 않습니다.

해당 내용에 대해서는 3-1 항목의 첨부 링크를 참조해 주세요.

 

3-1. Eloquent ORM에 대한 부연 설명

 $memo = Memo::find($request->id);

위 구문은 Controller에서 Memo 클래스에 접근해 find($request->id)로 id가 일치하는 row를 찾아내는 구문입니다.

laravel에서는 Eloquent ORM을 사용해 쿼리문을 사용하지 않고 코드 형태로 mysql에 접근해 통신할 수 있습니다. 자세한 내용은 아래의 내용을 참고해주세요.

 

https://laravel.kr/docs/6.x/eloquent

 

라라벨 6.x - 시작하기

라라벨 한글 메뉴얼 6.x - 시작하기

laravel.kr

 

4. View 스크립트 생성

컨트롤러 클래스에서 랜더링 하는 view는 /resources/view 폴더에서 관리합니다.

view 스크립트는 .blade.php 확장자를 사용하며, 이를 블레이드 템플릿이라 합니다.

view 스크립트는 공식적으로는 artisan make 명령어를 통한 자동 생성을 별도로 지원하지 않기 때문에 본 예제에서는 직접 수동으로 생성해 줍니다.

MainController에서 index(메인화면), create(메모 작성 화면), edit(메모 수정 화면) 총 3개의 화면에 대한 요청을 정의하고 있으므로, 페이지 또한 3개를 각각 생성해야 합니다.

생성하기에 앞서, 본 예제에서는 레이아웃을 아래와 같이 정의하겠습니다.

 

공통 레이아웃과 개별 페이지

 

 

먼저, 모든 페이지에서 위 사진의 layout이란 이름으로 각 페이지에서 공통으로 사용할 레이아웃 파일을 생성합니다.

 

layout.blade.php

<html>
    <head>
    	<meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-salce=1">
        <title>laravel</title>
    </head>
    <body>
    	<h2>common layout</h>
        <hr>
    	@yield('content')
    </body>
</html>

위 코드에서의 핵심은 지시어 @yield('content')입니다.

@yield('content')는 @section('content')로 지시된 데이터를 보여주겠다는 선언입니다.

따라서, layout 외에 만드는 페이지에는 모두 @section('content') 지시어를 선언해 주어야 합니다.

 

이어서 나머지 view 페이지도 생성해 줍니다.

 

index.blade.php

@extends('layout')
@section('content')
    <input type="button" value="Create Memo" onclick="location.href='{{ route('create') }}'">
    @foreach($memos as $memo)
    <div>
    	<span>{{ $memo->content}}</span>
        <input type="button" onclick="location.href='{{route('edit', ['id' => $memo->id]) }}'" value="edit">
        <input type="button" onclick="location.href='{{route('delete', ['id' => $memo->id]) }}'" value="delete">
	</div>
    @endforeach
@endsection
   

첫 줄에 @extends('layout') 지시어가 선언되어 있는데, 이는 layout.blade.php를 상속받아서 사용하겠다는 의미입니다.

그리고 @section('content') ~ @endsection 범위 까지를 layout.blade.php에서의 @yield('content') 영역에 출력한다는 의미입니다.

 

MainController의 index 메서드에서는 index 뷰와 변수 $memos를 렌더링을 해 주었는데,

변수 $memos는 DB 테이블에 존재하는 모든 메모를 최근 생성 날짜 순으로 정렬해서 가져와 할당했습니다.

이를 @foreach문을 통해서 페이지에 뿌려줍니다.

 

페이지에 뿌려주는 <input> 내용에서 route('edit', ['id'=>$memo->id])는 컨트롤러로 보내는 요청과 파라미터입니다.

 

route('edit', ['id'=>$memo->id]) : edit이라는 이름으로 요청을 받는 컨트롤러에 파라미터 id를 전달한다.

(delete도 이와 마찬가지입니다.)

 

create.blade.php

@extends('layout')
@section('content')
    <form method="POST" action="{{ route('store') }}">
    @crsf
        <textarea name="content" rows="4"></textarea>
        @if($errors->any()
            @foreach($errors->all() as $error)
            <p>{{ $error }}</p>
            @endforeach
        @endif
        <input type="submit" value="create">
        <input type="button" value="back" onclick="location.href='{{ route('index')}}'">
    </form>
@endsection

index.blade.php와 동일하게 extends와 section 지시어를 사용했습니다.

@if($errors->any())는, MainController의 store 메서드에서 $request->validate()를 통해 데이터의 길이가 500을 넘는지 검사하는 구문이 있었는데, 유효성이 어긋날 경우 validate()에서 반환해 주는 값이 표기됩니다.

만약, 컨트롤러 측에서 반환하는 에러 값이 없다면 if 구문 안쪽 내용은 출력되지 않습니다.

<input type="submit" value="create"/>를 누를 경우, form의 method와 action 속성에 의해 store로 Post 요청을 보냅니다.

back 버튼을 추가로 두어 route('index') 요청 즉, 메인 화면으로 보내주는 코드를 선언해 줍니다.

 

*중요

laravel에서 Post 요청을 보낼 때는 @crsf 지시어를 선언해주어야  491 에러가 발생하지 않습니다.

이는 악의적인 요청으로부터 웹을 보호하기 위한 laravel의 보호 정책으로, @csrf를 선언하면 자동으로 사용자가 이 웹에서 정당하게 request를 요청할 수 있는 사람임을 증명하는 토큰을 발급해 줍니다.

@csrf 지시어는 반드시 form 안에 선언해 주어야 합니다.

자세한 사항은 아래의 링크를 참고해 주세요.

 

https://laravel.kr/docs/6.x/csrf

 

라라벨 6.x - CSRF 보호하기

라라벨 한글 메뉴얼 6.x - CSRF 보호하기

laravel.kr

 

edit.blade.php

@extends('layout')
@section('content')
    <form method="POST" action="{{ route('update',['id' => $memo->id]) }}">
    @crsf
        <textarea name="content" rows="4">{{ $memo->content}}</textarea>
        @if($errors->any())
            @foreach($errors->all() as $error)
                <p>{{$error}}</p>
            @endforeach
        @endif
        <input type="submit" value="update"/>
        <input type="button" value="back" onclick="location.href='{{rout('index')}}'"/>
    </form>
@endsection

create.blade.php와 구성은 거의 동일합니다. 다만, submit 버튼을 통해 update로 Post 요청을 보낼 때, 메모 id 파라미터를 같이 전달합니다.

 

5. Route 설정

*Laravel Framework의 버전이 8로 바뀌면서 라우트 선언 방식에 약간의 변화가 생겼습니다.

라우트를 선언하기 전에 아래의 글을 먼저 참고 해 주세요.

 

2020/09/20 - [Dev/PHP] - [PHP] Laravel - Target class [] does not exist. 문제 해결

 

[PHP] Laravel - Target class [] does not exist. 문제 해결

본 포스팅에서는 Laravel 프레임 워크를 8 버전으로 작업했을 때 발생한 문제점을 기록합니다. Laravel 8 버전에서만 발생하는 문제점으로 보이며, web.php에서 정의하는 Route 객체에서 Controller 네임스�

dev-overload.tistory.com

 

본 포스팅의 초반에 제가 view에서 무언가 요청을 보낼 때 그것을 받아주는 routes/web.php 에 의해 적절한 컨트롤러로 보내준다고 언급했습니다.

이제 페이지에서 route 메서드를 통해 보낸 요청을 받는 구문을 routes/web.php에 작성해줍니다.

 

routes/web.php

Route::get('/','MainController@index')->name('index');
Route::get('/create','MainController@create')->name('create');
Route::post('/store','MainController@store')->name('store');
Route::get('/edit','MainController@edit')->name('edit');
Route::post('/update','MainController@update')->name('update');
Route::get('/delete','MainController@delete')->name('delete');

create.blade.php와 edit.blade.php에서 각각 store, update 요청을 보낼때 method를 post로 했기 때문에,  /store, /update 요청을 get이 아닌 post로 해 주어야 합니다.

 

Route::get('/','MainController@index')->name('index');

 

위 구문의 의미는 url '/'를 get 방식으로 요청받았을 때, MainController의 index 메서드에서 요청 사항을 처리하며, 이 라우트의 이름은 index로 한다.입니다.

name() 이 설정되면 간편하게 view 페이지나 컨트롤러에서 route를 호출할 수 있습니다. ex) route('index')

 

이제 php artisan serve 명령어로 테스트용 서버를 구동한 다음, 위 내용들이 제대로 작성이 되었는지 테스트를 진행해 봅니다.

 

메모 웹 테스트

 

이렇게 기본적인 DB 연동과 MVC 패턴 구현이 완료되었습니다.