블레이드 템플릿

블레이드 템플릿 (Blade Templates)

Laravel에 내장된 블레이드(Blade)는 간단하면서도 강력한 템플릿 엔진이에요. 다른 PHP 템플릿 엔진과 달리 템플릿에서 평범한 PHP 코드를 쓰는 걸 막지 않아요. 실제로 모든 블레이드 템플릿은 순수 PHP로 컴파일되고 변경 전까지 캐시되므로, 애플리케이션에 거의 오버헤드가 없죠. 블레이드 파일은 .blade.php 확장자를 쓰고 보통 resources/views 디렉터리에 둬요. 이 문서에서는 데이터 출력부터 지시어, 컴포넌트, 레이아웃, 확장까지 Laravel 12 기준으로 익혀볼게요.

출처: Laravel 공식 문서 — Blade Templates

시작하기

블레이드 뷰는 전역 view 헬퍼로 라우트나 컨트롤러에서 반환할 수 있어요. 두 번째 인자로 뷰에 전달할 데이터를 넘기죠.

Route::get('/', function () {
    return view('greeting', ['name' => 'Finn']);
});

블레이드를 한 단계 더 끌어올려 동적인 인터페이스를 쉽게 만들고 싶다면 Laravel Livewire를 살펴보세요. 라이브와이어는 보통 React·Svelte·Vue 같은 프런트엔드 프레임워크에서만 가능한 동적 기능을 블레이드 컴포넌트에 얹어줘서, 클라이언트 렌더링이나 빌드 단계 없이 현대적이고 반응형인 프런트를 만들 수 있게 해줘요.

데이터 출력하기 (Displaying Data)

뷰에 전달된 데이터는 변수를 중괄호로 감싸 출력해요. {{ }} 에코 구문은 XSS 공격을 막기 위해 PHP의 htmlspecialchars를 자동으로 통과해요.

Hello, {{ $name }}.

PHP 함수의 결과도 그대로 에코할 수 있어요. 사실 블레이드 에코 구문 안에는 어떤 PHP 코드든 넣을 수 있어요.

The current UNIX timestamp is {{ time() }}.

이스케이프 되지 않은 데이터

데이터를 이스케이프하지 않고 출력하고 싶다면 {!! !!} 구문을 써요. 단, 사용자 입력을 이렇게 출력하면 XSS 위험이 있으니, 사용자 제공 콘텐츠를 보여줄 때는 반드시 이스케이프되는 이중 중괄호 구문을 쓰는 게 원칙이에요.

Hello, {!! $name !!}.

블레이드와 자바스크립트 프레임워크

많은 자바스크립트 프레임워크도 중괄호로 표현식을 표시하므로, @ 기호를 붙이면 블레이드가 그 표현식을 건드리지 않아요. 예를 들어 @{{ name }}은 블레이드가 @만 제거하고 {{ name }}은 그대로 남겨 자바스크립트 프레임워크가 렌더링하게 둬요.

<h1>Laravel</h1>

Hello, @{{ name }}.

@는 블레이드 지시어를 이스케이프할 때도 쓸 수 있어요. @@if()는 출력 결과에서 @if()가 되죠.

배열을 뷰에 넘겨 JSON으로 렌더링해 자바스크립트 변수를 초기화해야 한다면 Js::from 메서드가 유용해요. 이 메서드는 HTML 따옴표 안에 안전하게 들어가도록 JSON을 적절히 이스케이프해 JSON.parse 자바스크립트 구문을 돌려줘요.

<script>
    var app = {{ Js::from($array) }};
</script>

@verbatim 지시어

템플릿의 넓은 영역에서 자바스크립트 변수를 출력한다면 그 부분을 @verbatim으로 감싸, 매 에코 구문마다 @를 붙이지 않게 할 수 있어요.

@verbatim
    <div class="container">
        Hello, {{ name }}.
    </div>
@endverbatim

블레이드 지시어 (Blade Directives)

블레이드는 조건문과 반복문 같은 일반적인 PHP 제어 구조를 위한 짧은 지시어들을 제공해요. PHP 대응 문법과 똑같이 동작하면서 훨씬 깔끔하게 쓸 수 있죠.

조건문

@if, @elseif, @else, @endif로 if 문을 만들고, 여분으로 @unless도 있어요. @isset@empty 지시어도 각각에 해당하는 PHP 함수의 단축 형태예요.

@if (count($records) === 1)
    I have one record!
@elseif (count($records) > 1)
    I have multiple records!
@else
    I don't have any records!
@endif

@unless (Auth::check())
    You are not signed in.
@endunless

@auth·@guest 지시어로 현재 사용자가 인증됐는지 빠르게 판별할 수 있고, 인증 가드를 지정할 수도 있어요. @production@env로 실행 환경을, @session으로 세션 값 존재 여부를 확인할 수 있죠.

@auth('admin')
    // 인증된 사용자...
@endauth

@production
    // 프로덕션 전용 콘텐츠...
@endproduction

@env(['staging', 'production'])
    // staging 또는 production 환경에서 실행 중...
@endenv

switch와 반복문

switch 문은 @switch, @case, @break, @default, @endswitch로 구성해요. 반복문에는 PHP와 동일하게 @for, @foreach, @forelse, @while 지시어를 써요. @continue@break로 현재 순회를 건너뛰거나 루프를 끝낼 수 있고, 조건을 지시어 선언에 함께 담을 수도 있어요.

@for ($i = 0; $i < 10; $i++)
    The current value is {{ $i }}
@endfor

@foreach ($users as $user)
    @continue($user->type == 1)

    <li>{{ $user->name }}</li>

    @break($user->number == 5)
@endforeach

@forelse ($users as $user)
    <li>{{ $user->name }}</li>
@empty
    <p>No users</p>
@endforelse

루프 변수

foreach를 순회하는 동안 $loop 변수가 안에서 사용 가능해요. 첫·마지막 순회인지, 현재 인덱스가 몇인지 같은 정보를 제공하죠. 중첩 루프에서는 $loop->parent로 부모 루프의 $loop에 접근할 수 있어요.

@foreach ($users as $user)
    @if ($loop->first)
        This is the first iteration.
    @endif

    @if ($loop->last)
        This is the last iteration.
    @endif

    <p>This is user {{ $user->id }}</p>
@endforeach

유용한 $loop 속성으로는 index(0부터), iteration(1부터), remaining, count, first, last, even, odd, depth, parent가 있어요.

조건부 클래스·속성

@class 지시어는 CSS 클래스 문자열을 조건부로 컴파일해요. 배열 키가 클래스, 값이 불리언 표현식인 배열을 받고, 숫자 키인 요소는 항상 포함돼요. @style 지시어는 인라인 스타일을, 그리고 @checked·@selected·@disabled·@readonly·@required 지시어는 각각 해당 HTML 속성을 조건에 따라 출력해줘요.

@php
    $isActive = false;
    $hasError = true;
@endphp

<span @class([
    'p-4',
    'font-bold' => $isActive,
    'text-gray-500' => ! $isActive,
    'bg-red' => $hasError,
])></span>

<span class="p-4 text-gray-500 bg-red"></span>
<input
    type="checkbox"
    name="active"
    value="active"
    @checked(old('active', $user->active))
/>

서브뷰 포함하기

@include 지시어로 다른 블레이드 뷰를 포함해요. 부모 뷰의 모든 변수가 포함된 뷰에도 전달되고, 추가 데이터 배열을 두 번째 인자로 넘길 수 있어요. 존재할 수도 있고 없을 수도 있는 뷰라면 @includeIf를, 조건에 따라 포함하려면 @includeWhen·@includeUnless를 써요.

<div>
    @include('shared.errors')

    <form>
        <!-- Form Contents -->
    </form>
</div>

컬렉션을 순회하며 뷰를 렌더링하는 @each도 있지만, @each로 렌더링된 뷰는 부모 변수를 상속받지 못해요. 부모 변수가 필요하면 @foreach@include를 조합하는 게 좋아요.

@once와 원시 PHP·주석

@once 지시어는 렌더링 주기당 한 번만 평가되는 부분을 정의해요. 스택을 이용해 자바스크립트를 페이지 헤더에 한 번만 push하는 용도로 유용하죠.

@once
    @push('scripts')
        <script>
            // Your custom JavaScript...
        </script>
    @endpush
@endonce

템플릿 안에 순수 PHP를 실행해야 할 땐 @php 지시어를, 클래스를 임포트할 땐 @use 지시어를 써요. 그리고 {{-- --}} 주석은 HTML 주석과 달리 최종 HTML에 포함되지 않아요.

@php
    $counter = 1;
@endphp

@use('App\Models\Flight')

{{-- This comment will not be present in the rendered HTML --}}

컴포넌트 (Components)

컴포넌트와 슬롯은 섹션·레이아웃·인클루드와 비슷한 혜택을 주는데, 정신적 모델이 더 이해하기 쉬워요. 컴포넌트는 클래스 기반 컴포넌트와 익명(anonymous) 컴포넌트 두 방식이 있어요. make:component 커맨드로 클래스 기반 컴포넌트를 만들면 app/View/Components 디렉터리에 클래스가, resources/views/components에 뷰 템플릿이 생겨요.

php artisan make:component Alert

컴포넌트 렌더링과 데이터 전달

컴포넌트는 x- 접두사 뒤에 케밥 케이스 이름을 붙인 태그로 렌더링해요. 디렉터리 중첩은 점(.)으로, 예를 들어 app/View/Components/Inputs/Button.php<x-inputs.button/>로 렌더링해요.

<x-alert/>
<x-user-profile/>

데이터 속성은 HTML 속성으로 전달해요. 하드코딩된 원시 값은 일반 속성 문자열로, PHP 표현식이나 변수는 : 접두사 속성으로 전달해요. 컴포넌트의 생성자에 데이터 속성을 모두 정의하고, 퍼블릭 프로퍼티는 자동으로 컴포넌트 뷰에서 사용할 수 있어요.

<x-alert type="error" :message="$message"/>
class Alert extends Component
{
    public function __construct(
        public string $type,
        public string $message,
    ) {}

    public function render(): View
    {
        return view('components.alert');
    }
}

생성자 인자는 camelCase로, HTML 속성에서는 kebab-case로 참조해요. 변수 이름이 속성 이름과 자주 일치하므로 :$userId 같은 짧은 속성 구문(short attribute syntax)도 편리하게 쓸 수 있어요.

<x-profile :$userId :$name />

{{-- 아래와 동일 --}}
<x-profile :user-id="$userId" :name="$name" />

슬롯 (Slots)

컴포넌트에 추가 콘텐츠를 넘겨야 할 때는 슬롯을 써요. 기본 슬롯은 $slot 변수로 출력하고, 여러 개의 이름 있는 슬롯은 x-slot 태그로 정의해요. isEmpty()·hasActualContent() 메서드로 슬롯에 콘텐츠가 있는지 확인할 수 있어요.

<!-- /resources/views/components/alert.blade.php -->
<div class="alert alert-danger">
    {{ $slot }}
</div>
<x-alert>
    <x-slot:title>
        Server Error
    </x-slot>

    <strong>Whoops!</strong> Something went wrong!
</x-alert>

컴포넌트 속성 (Attributes)

생성자에 없는 추가 HTML 속성들은 자동으로 "attribute bag"에 담기고 $attributes 변수로 접근할 수 있어요. merge 메서드로 기본값을 지정하거나 값을 합칠 수 있는데, 특히 항상 적용할 기본 CSS 클래스를 정의할 때 유용해요. class 메서드는 조건에 따라 클래스를 합쳐줘요.

<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
    {{ $message }}
</div>

<div {{ $attributes->class(['p-4', 'bg-red' => $hasError]) }}>
    {{ $message }}
</div>

익명 컴포넌트

익명 컴포넌트는 클래스 없이 뷰 파일 하나로 컴포넌트를 관리해요. resources/views/components 디렉터리에 블레이드 템플릿만 두면 <x-alert/>처럼 렌더링할 수 있고, 어떤 속성을 데이터 변수로 취급할지는 @props 지시어로 정해요. 부모 컴포넌트의 데이터를 자식에서 쓰고 싶다면 @aware 지시어를 써요.

<!-- /resources/views/components/menu/item.blade.php -->
@props(['color' => 'gray'])

<li {{ $attributes->merge(['class' => 'text-'.$color.'-800']) }}>
    {{ $slot }}
</li>

레이아웃 만들기 (Building Layouts)

대부분의 웹 애플리케이션은 여러 페이지에서 같은 레이아웃을 유지해요. 레이아웃을 단일 컴포넌트로 정의해두면 모든 뷰에 HTML을 반복하지 않아도 돼요. 컴포넌트 기반 레이아웃은 x-layout 태그로 감싸고, 기본 슬롯이나 이름 있는 슬롯으로 페이지 콘텐츠를 채우는 방식이에요.

<!-- resources/views/components/layout.blade.php -->
<html>
    <head>
        <title>{{ $title ?? 'Todo Manager' }}</title>
    </head>
    <body>
        <h1>Todos</h1>
        <hr/>
        {{ $slot }}
    </body>
</html>
<!-- resources/views/tasks.blade.php -->
<x-layout>
    @foreach ($tasks as $task)
        <div>{{ $task }}</div>
    @endforeach
</x-layout>

템플릿 상속 방식도 있어요. @extends로 레이아웃을 상속하고 @section으로 섹션에 콘텐츠를 주입하며, 레이아웃에서는 @yield로 그 섹션을 표시해요. 컴포넌트 도입 전의 주된 레이아웃 방식이에요.

<!-- resources/views/layouts/app.blade.php -->
<html>
    <head>
        <title>App Name - @yield('title')</title>
    </head>
    <body>
        @section('sidebar')
            This is the master sidebar.
        @show

        <div class="container">
            @yield('content')
        </div>
    </body>
</html>

폼 (Forms)

폼에는 CSRF 보호 미들웨어가 요청을 검증할 수 있도록 숨김 CSRF 토큰 필드가 필요해요. @csrf 지시어가 토큰 필드를 만들어주고, HTML 폼이 PUT·PATCH·DELETE를 할 수 없으므로 @method 지시어로 _method 필드를 만들어 스푸핑해요. @error 지시어는 특정 속성의 검증 오류가 있는지 확인하고, 안에서 $message 변수로 오류 메시지를 출력할 수 있어요.

<form method="POST" action="/profile">
    @csrf
    ...
</form>

<form action="/foo/bar" method="POST">
    @method('PUT')
    ...
</form>
<label for="title">Post Title</label>

<input
    id="title"
    type="text"
    class="@error('title') is-invalid @enderror"
/>

@error('title')
    <div class="alert alert-danger">{{ $message }}</div>
@enderror

스택 (Stacks)

블레이드의 @push 지시어로 이름 있는 스택에 콘텐츠를 밀어 넣고, 다른 뷰나 레이아웃에서 @stack으로 그 스택을 렌더링할 수 있어요. 자식 뷰가 필요로 하는 자바스크립트 라이브러리를 헤더에 지정하는 데 특히 유용하죠. 앞쪽에 붙이려면 @prepend를 써요.

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

<head>
    <!-- Head Contents -->
    @stack('scripts')
</head>

서비스 주입과 블레이드 확장

@inject 지시어로 서비스 컨테이너에서 서비스를 가져와 변수에 담을 수 있어요. 그리고 Blade::directive로 사용자 정의 지시어를, Blade::if로 사용자 정의 조건문 지시어를 만들 수 있어요. 지시어 로직을 바꿨다면 캐시된 블레이드 뷰를 view:clear 커맨드로 지워야 해요.

@inject('metrics', 'App\Services\MetricsService')

<div>
    Monthly Revenue: {{ $metrics->monthlyRevenue() }}.
</div>
public function boot(): void
{
    Blade::directive('datetime', function (string $expression) {
        return "<?php echo ($expression)->format('m/d/Y H:i'); ?>";
    });

    Blade::if('disk', function (string $value) {
        return config('filesystems.default') === $value;
    });
}

Turbo·htmx 같은 프런트엔드 프레임워크를 쓸 때는 뷰의 일부만 HTTP 응답으로 돌려주고 싶을 수 있는데, @fragment 지시어로 그 부분을 감싸고 뷰의 fragment 메서드로 응답에 포함할 프래그먼트를 지정하면 돼요.

@fragment('user-list')
    <ul>
        @foreach ($users as $user)
            <li>{{ $user->name }}</li>
        @endforeach
    </ul>
@endfragment
return view('dashboard', ['users' => $users])->fragment('user-list');

더 알아보기 (Learn more)

  • Laravel 공식 문서의 Blade Templates 페이지
  • Eloquent ORM 문서에서 데이터를 뷰로 전달하는 법 익히기
  • Validation 문서에서 @error 지시어와 연계된 오류 처리 이해하기
  • Views 문서에서 뷰 전반의 개념 살펴보기