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