프레임워크 & 라이브러리/라라벨

[Laravel] 라라벨 블레이드 템플릿 (상속, 컴포넌트, 슬릇)

이번에는 블레이드 템플릿에서 상속, 컴포넌트, 뷰 컴포저에 대해 간단히 알아보도록 하자. 블레이드에는 여러 지시어(Directive)가 존재하는데, 상속, 컴포넌트, 슬릇 기능 등은 많이 쓰이는 기능이다. 따라서 이를 대표적으로 알아보자. 지시어의 수가 다소 많은터라 나도 다 알지 못한다.

상속

상속과 관련된 지시어는 @extends, @yield, @section, @show, @stack 이 존재한다. 자바스크립트 템플릿을 사용하듯 블레이드를 사용해서도 템플릿을 상속하고, 다른 템플릿을 포함시킬 수 있는 기능을 가지고 있다. 즉, 템플릿을 기준에 따라 분리가 가능하다는 이야기다. 다만, 다소 헷갈리는 기능들이 많기 때문에 차이점을 알아두면 좋다.

@extends

@extends 는 자식 템플릿이 특정 템플릿에 대해 상속받을때 사용한다. 템플릿을 상속한다는 이야기는, 어떤 정해진 레이아웃이 있다면, 거기에 일부 내용만 다르게 데이터를 바인딩하거나 삽입할 수 있다는 이야기다. views/layouts/app.blade.php 라는 레이아웃 템플릿이 있을 때, 자식 템플릿에서는 아래와 같이 상속받을 수 있다. 자식 템플릿의 맨 위에 해당 지시어를 사용한다.

@extends('layouts.app')

@yield

일반적으로 yield 키워드는 제네레이터(Generator)에서 많이 사용하는 키워드다. 템플릿에서 사용하게 되면 자식 템플릿에서 내용을 대체(Replace)할 수 있게 된다. 예를 들어 부모 템플릿에서 다음과 같이 정의되어 있다고 가정하자. title 은 이름이고, Home page 는 기본 값이다. 따라서 자식 템플릿에서 별도로 정의되어 있지 않다면 Home page 가 나타난다.

@yield('title', 'Home page')

자, 여기서 자식 템플릿에서 이 부분에 데이터를 대체하기 위해서는 @section, @endsection 을 사용한다. 이렇게 사용하면 @yield 를 사용하여 나타낸 부분이 Hello, world 로 대체된다.

@section('title')
    Hello, world
@endsection

{{-- @section('title', 'Hello, world') --}}

@endsection 을 사용하지 않고 축약형태로도 사용할 수 있다.

@section/@show, @endsection

위에서 @section 키워드에 대해서 이야기 했지만, 사실 이 키워드는 부모 템플릿에서도 사용할 수 있는데, @show 지시어와 함께 사용할 수 있다. 부모 템플릿에서 @section 을 사용하여 표현하려면 @show 지시어를 함께 사용해야 하며 사용한 부분에 자식 템플식에 지정한 내용으로 치환된다. 부모 템플릿에 아래와 같이 정의되어 있다면,

@section('main')
    Hello, world
@show

이제, 자식 템플릿에서 사용할 텐데, @yield 에서 사용했던 것처럼 사용하면 Hello, world 가 나타나지 않을 것이다. 이는 부모 템플릿에 있는 기본값이기 때문이다. 만약, 자식 템플릿에서 Hello, world 에 더해, 내용을 추가하고 싶다면 @parent 지시어와 함께 사용하자.

@section('main')
    @parent
    Who are you?
@endsection

부모 템플릿에서 @section/@show 를 사용한 것과 @yield 를 사용한 것은 기능측면으로는 본질적으로 같으나 차이가 있다면 자식 템플릿에서 @parent 를 사용하여 부모 템플릿의 기본값에 접근할 수 있냐 없냐의 차이다. @yield 를 사용하게 되면 Home page 라는 기본 값에는 접근할 수 없다.

@include(-If, -When, -First)

@include 지시어를 이름 그대로 사용하면 다른 템플릿을 포함할 수 있으며, 게다가 템플릿에 데이터를 같이 전달할 수 있다. 아래와 같이 사용하면 views/main.blade.php 를 포함시킨다. $titleHello, world 라는 값을 넣어 포함시킬 수 있다.

// views/main.blade.php
@include('main', ['title' => 'Hello, world'])

다른 템플릿으로부터 전달받은 변수를 {{ $title }} 과 같이 사용하게 되면, Hello, world 가 나타난다.

@includeIf, @includeWhen, @includeFirst

@includeIf 를 사용하면 어떤 조건에 부합하면 템플릿을 포함시키고, @includeWhen 을 사용하면 주어진 조건에 부합하게 되면 포함시키게 되며, 마지막으로 @includeFirst 를 사용하게 되면 주어진 여러 개의 템플릿 중에서 존재하는 첫 번째 템플릿을 포함시키게 된다. 여기서 자주 사용하게 될 지시어는 @includeWhen 정도가 있겠다.

@each

@each 키워드는 다소 특이하다. 먼저, 반복문 안에서 하나의 요소에 접근하여 이를 템플릿에 전달할 때를 생각해보자. 예를 들면 우리는 아래와 같이 할 수 있다. $messages 에 요소가 있다면 views/main.blade.php 를, 만약 비어있다면 views/main-empty.blade.php 를 사용하도록 하는 코드다.

@if(count($messages) > 1)
    @foreach($messages as $message)
        @include('main', compact('message'))
    @endforeach
@else
    @include('main-empty')
@endif

이 코드를 단 하나로 축약한 것이 바로 @each 지시어다. 편하기는 한데 굳이 공식적으로 존재할 필요는 있을까 싶은 지시어이긴 하다. 위와 같은 코드를 다음과 같이 간단하게 사용할 수 있다.

@each('main', $messages, 'message', 'main-empty')

@stack

@stack 키워드를 사용하면 @section/@show, @parent 를 사용하여 부모에 추가하던 것을 단순하게 할 수 있으나, 스택이라는 이름처럼 그 용도는 @section 과 구분해야 한다. 만약 기본적으로 포함해야 할 app.css 가 존재하고, 페이지에 따라 별도의 css 파일을 추가해야 할 때 사용할 수 있다. 일단 스택을 사용하듯 @push/@endpush, @prepend/@endprepend 키워드를 사용할 수 있다.

<body>
    <script src="app.js"></script>
    @stack('scripts')
</body>

app.js 는 공통적으로 사용할 부분이며, 다른 템플릿에서 @push, @prepend 와 같은 키워드를 사용하여 페이지마다 다른 내용을 추가시킬 수 있다.

@push('scripts')
    <script src="vendor.js"></script>
@endpush

컴포넌트

이 기능은 Vue.js 의 컴포넌트, 슬릇 기능과 유사하다. @include 로 처리하기 애매한 부분들, 예를 들면 views/parials/modal.blade.php 에 본문을 전달한다고 가정해보자. 다음과 같이 해볼 수 있다. body 의 내용이 짧다면 다행이지만, 만약 길다면 이를 전달하기에는 다소 보기가 좋지 않다.

@include('partials.modal', ['body' => '<h1>Hello, world</h1>'])

이 코드를 컴포넌트를 사용하여 구성하면 아래와 같이 할 수 있다.

@component('partials.modal')
    <h1>Hello, world</h1>
@endcomponent

이렇게 사용한 코드를 views/partials/model.blade.php 에서 사용하려면 {{ $slot }} 을 사용하여 내용을 표현할 수 있게된다. 즉, $slot슬릇에 이름을 지정하지 않고 사용했을 때 선언되는 가장 기본적인 형태라는 의미다.

다중 슬릇

이름이 있는 슬릇(Named Slot)을 사용하면 여러 개의 슬릇을 사용하여 컴포넌트에 전달할 수 있게 된다. 예를 들면 title 이라는 이름을 가진 슬릇이 있을 때,

@component('partials.modal')
    @slot('title')
        <h1>Hello, world</h1>
    @endslot

    {{-- ... --}}
@endcomponent

자식 템플릿에서는 {{ $slot }} 이 아닌, {{ $title }} 을 사용하여 표현할 수 있으며, 기존의 $slot 은 이름 없이 사용된 슬릇에게 적용할 수 있다. 추가적으로 @include 에서 사용했던 것처럼 @component 지시어에 두번째 파라매터에 데이터를 전달할 수 있다. 물론 보기 매우 좋지 않다.

@component('partials.modal', ['title' => '<h1>Hello, world</h1>'])
    {{-- ... --}}
@endcomponent

이렇게 전달하고 출력하려고 하면 기본적인 보간에 의해 이프케이스 되므로 {!! $title !!} 로 표현해야 한다. 따라서 용도의 분리가 분명히 필요하고, 컴포넌트를 사용하겠다면 어지간하면 슬릇으로 전달하는 것이 좋을 듯하다.

컴포넌트 클래스

컨트롤러에서 뷰를 반환하듯이 컴포넌트를 별도의 클래스와 뷰로 분리하여 사용할 수도 있다. 아티즌 명령어를 다음과 같이 사용해보자.

php artisan make:component Modal

이 명령어를 실행하게 되면 views/components/modal.blade.php 파일과 app/View/Components/Modal.php 파일이 생성된다. 이렇게 생성한 컴포넌트를 사용하는 방법은 @component 지시어도 물론 동작하지만, 아래와 같이 묘한 방법으로 사용할 수 있다.

<x-modal title="Hello, world"></x-modal>

@slot 을 사용하여 슬릇을 사용할 수도 있지만, 위에서 보이는 것처럼 마치 프론트엔드 프레임워크에서 전달하는 컴포넌트 데이터 전달마냥 사용하려면 컴포넌트 클래스의 프로퍼티에 추가를 해주면 된다.

class Modal extends Component
{
    public $title;

    /**
     * Create a new component instance.
     *
     * @return void
     */
    public function __construct(string $title)
    {
        $this->title = $title;
    }

    /**
     * Get the view / contents that represent the component.
     *
     * @return \Illuminate\Contracts\View\View|\Closure|string
     */
    public function render()
    {
        return view('components.modal');
    }
}

템플릿의 경우 하나를 하기 위해 각기 다른 표현을 사용할 수 있기 때문에 헷갈리는 부분이 상당히 많다. 실무에서는 일관성있게 하나로 표현할 수 있도록 하는 것이 바람직할 것이다. 개인적으로 이렇게 다른 표현이 여럿 존재하는 것은 그다지 좋아하지 않는다.

더 읽을거리

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

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