메일

번역일: 2026년 7월 2일

메일

소개

메일 발송은 거의 모든 웹 애플리케이션에서 필요한 기능입니다. Laravel은 Symfony Mailer 컴포넌트를 기반으로 깔끔하고 직관적인 메일 API를 제공합니다. SMTP, Mailgun, Postmark, Resend, Amazon SES 등 다양한 드라이버를 지원하므로, 원하는 로컬 또는 클라우드 기반 서비스를 자유롭게 선택할 수 있습니다.

설정

메일 관련 설정은 config/mail.php 파일에서 관리합니다. 각 메일러(mailer)마다 고유한 설정과 트랜스포트를 지정할 수 있어, 이메일 종류에 따라 서로 다른 발송 서비스를 사용하는 것도 가능합니다. 예를 들어 트랜잭션 메일은 Postmark으로, 대량 메일은 Amazon SES로 발송하는 식으로 운영할 수 있습니다.

mail.php 설정 파일 안에는 mailers 배열이 있으며, 여기에 각 메일러 설정 샘플이 포함되어 있습니다. default 값은 별도로 지정하지 않을 때 기본으로 사용할 메일러를 가리킵니다.

드라이버 사전 준비

Mailgun, Postmark, Resend, MailerSend 같은 API 기반 드라이버는 SMTP보다 빠르고 간편한 경우가 많습니다. 가능하다면 이런 API 드라이버 사용을 권장합니다.

Mailgun 드라이버

Mailgun을 사용하려면 Composer로 Symfony의 Mailgun 메일러 트랜스포트를 설치합니다.

composer require symfony/mailgun-mailer symfony/http-client

그런 다음 config/mail.php에서 default 옵션을 mailgun으로 설정하고, mailers 배열에 아래와 같이 추가합니다.

'mailgun' => [ 'transport' => 'mailgun', // 'client' => [ // 'timeout' => 5, // ], ],

기본 미국 리전이 아닌 경우(예: EU 리전), config/services.php에서 리전을 지정할 수 있습니다.

'mailgun' => [ 'domain' => env('MAILGUN_DOMAIN'), 'secret' => env('MAILGUN_SECRET'), 'endpoint' => env('MAILGUN_ENDPOINT', 'api.eu.mailgun.net'), 'scheme' => 'https', ],

Postmark 드라이버

Postmark 드라이버를 사용하려면 아래 패키지를 설치합니다.

composer require symfony/postmark-mailer symfony/http-client

이후 config/mail.phpdefaultpostmark로 설정하고, mailers 배열에 다음을 추가합니다.

'postmark' => [ 'transport' => 'postmark', // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), // 'client' => [ // 'timeout' => 5, // ], ],

특정 Mailable에 Postmark 메시지 스트림을 지정하려면 message_stream_id 옵션을 활용하거나, Mailable의 headers 메서드를 통해 직접 헤더를 추가할 수 있습니다.

config/services.php에 Postmark 토큰도 등록해야 합니다.

'postmark' => [ 'token' => env('POSTMARK_TOKEN'), ],

Resend 드라이버

Resend를 사용하려면 아래 패키지를 설치합니다.

composer require resend/resend-laravel

config/mail.phpdefaultresend로 설정하고, mailers 배열에 추가합니다.

'resend' => [ 'transport' => 'resend', ],

config/services.php에 API 키도 등록합니다.

'resend' => [ 'key' => env('RESEND_KEY'), ],

SES 드라이버

Amazon SES를 사용하려면 먼저 AWS SDK for PHP를 설치합니다.

composer require aws/aws-sdk-php

config/mail.phpdefaultses로 설정하고, config/services.php에 AWS 인증 정보를 추가합니다.

'ses' => [ 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'ap-northeast-2'), // 서울 리전 예시 ],

AWS 임시 자격증명(세션 토큰)을 사용한다면 token 키를 추가합니다.

'ses' => [ 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'ap-northeast-2'), 'token' => env('AWS_SESSION_TOKEN'), ],

SES의 구독 관리 기능을 사용하려면, Mailable의 headers 메서드에서 반환하는 배열에 X-Ses-List-Management-Options 헤더를 포함시키면 됩니다.

/** * 메시지 헤더를 반환합니다. */ public function headers(): Headers { return new Headers( text: [ 'X-Ses-List-Management-Options' => 'contactListName=MyContactList;topicName=MyTopic', ], ); }

메일 발송 시 SES API에 추가 옵션을 전달하고 싶다면 services.phpses 설정에 options 배열을 정의합니다.

'ses' => [ 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'ap-northeast-2'), 'options' => [ 'ConfigurationSetName' => 'MyConfigurationSet', 'EmailTags' => [ ['Name' => 'foo', 'Value' => 'bar'], ], ], ],

MailerSend 드라이버

MailerSend는 트랜잭션 이메일 및 SMS 서비스로, Laravel용 자체 드라이버를 제공합니다. Composer로 패키지를 설치합니다.

composer require mailersend/laravel-driver

설치 후 .env 파일에 MAILERSEND_API_KEY 환경 변수를 추가하고, MAIL_MAILER 환경 변수를 mailersend로 설정합니다.

MAIL_MAILER=mailersend MAIL_FROM_ADDRESS=hello@example.com MAIL_FROM_NAME="앱 이름" MAILERSEND_API_KEY=your-api-key

마지막으로 config/mail.phpmailers 배열에 MailerSend를 추가합니다.

'mailersend' => [ 'transport' => 'mailersend', ],

MailerSend의 호스팅 템플릿 사용을 포함한 더 자세한 내용은 MailerSend 드라이버 공식 문서를 참고하세요.

장애 복구 설정

외부 메일 서비스가 일시적으로 장애를 겪을 경우를 대비해, 하나 이상의 백업 발송 설정을 지정할 수 있습니다. 이를 위해 config/mail.php에서 failover 트랜스포트를 사용하는 메일러를 정의합니다. failover 메일러의 설정 배열에는 실제 발송에 사용할 메일러 목록을 순서대로 지정합니다.

'mailers' => [ 'failover' => [ 'transport' => 'failover', 'mailers' => [ 'postmark', 'mailgun', 'sendmail', ], ], // ... ],

failover 메일러를 정의했다면, config/mail.phpdefault 키 값을 failover로 설정하여 기본 메일러로 지정합니다.

'default' => env('MAIL_MAILER', 'failover'),

라운드 로빈 설정

roundrobin 트랜스포트는 여러 메일러에 발송 부하를 분산시킵니다. failover가 하나의 메일러가 실패할 때 다음 메일러로 전환하는 방식이라면, roundrobin은 정상 상태에서도 순서대로 메일러를 번갈아 사용합니다.

'mailers' => [ 'roundrobin' => [ 'transport' => 'roundrobin', 'mailers' => [ 'ses', 'postmark', ], ], // ... ],

정의 후 defaultroundrobin으로 설정합니다.

'default' => env('MAIL_MAILER', 'roundrobin'),

라운드 로빈 트랜스포트는 등록된 메일러 목록에서 무작위로 시작점을 선택한 뒤, 이후 메일 발송마다 순서대로 다음 메일러를 사용합니다. failover와 달리 고가용성이 아닌 부하 분산을 목적으로 합니다.

Mailable 생성

Laravel에서는 발송할 각 이메일 유형을 "Mailable" 클래스로 표현합니다. 이 클래스들은 app/Mail 디렉터리에 저장됩니다. 이 디렉터리는 처음에는 존재하지 않지만, 아래 Artisan 명령으로 Mailable을 생성하면 자동으로 만들어집니다.

php artisan make:mail OrderShipped

Mailable 작성

Mailable 클래스를 생성했다면, 내용을 살펴보겠습니다. Mailable 클래스의 설정은 주로 envelope, content, attachments 세 가지 메서드를 통해 이루어집니다.

  • envelope: 메시지의 제목(subject)과 수신자를 정의합니다.
  • content: 메시지 본문을 렌더링할 Blade 템플릿을 지정합니다.
  • attachments: 첨부 파일을 정의합니다.
<?php namespace App\Mail; use App\Models\Order; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; use Illuminate\Queue\SerializesModels; class OrderShipped extends Mailable { use Queueable, SerializesModels; /** * 새 Mailable 인스턴스를 생성합니다. */ public function __construct( public Order $order, ) {} /** * 메시지 봉투(Envelope)를 반환합니다. */ public function envelope(): Envelope { return new Envelope( subject: '주문이 발송되었습니다', ); } /** * 메시지 콘텐츠 정의를 반환합니다. */ public function content(): Content { return new Content( view: 'mail.orders.shipped', ); } /** * 메시지의 첨부 파일을 반환합니다. * * @return array<int, \Illuminate\Mail\Mailables\Attachment> */ public function attachments(): array { return []; } }

발신자 설정

Envelope로 발신자 지정

발신자(From)를 지정하는 방법은 두 가지입니다. 첫 번째는 envelope에서 직접 지정하는 방법입니다.

use Illuminate\Mail\Mailables\Address; use Illuminate\Mail\Mailables\Envelope; /** * 메시지 봉투를 반환합니다. */ public function envelope(): Envelope { return new Envelope( from: new Address('jeffrey@example.com', 'Jeffrey Way'), subject: '주문이 발송되었습니다', ); }

replyTo 주소도 함께 지정할 수 있습니다.

return new Envelope( from: new Address('jeffrey@example.com', 'Jeffrey Way'), replyTo: [ new Address('taylor@example.com', 'Taylor Otwell'), ], subject: '주문이 발송되었습니다', );

전역 발신자 주소 사용

모든 메일에 동일한 발신자 주소를 사용하는 경우, 각 Mailable마다 일일이 지정하는 것은 번거롭습니다. 대신 config/mail.php에 전역 발신자 주소를 설정하면 편리합니다. 개별 Mailable에서 from을 별도로 지정하지 않으면 이 전역 설정이 사용됩니다.

'from' => [ 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 'name' => env('MAIL_FROM_NAME', '서비스 이름'), ],

마찬가지로 전역 reply_to 주소도 설정할 수 있습니다.

'reply_to' => ['address' => 'example@example.com', 'name' => 'App Name'],

뷰 설정

content 메서드에서 view를 지정하면 해당 Blade 템플릿이 메일 본문으로 사용됩니다.

/** * 메시지 콘텐츠 정의를 반환합니다. */ public function content(): Content { return new Content( view: 'mail.orders.shipped', ); }

메일 전용 뷰는 resources/views/mail 디렉터리 안에 모아두는 것이 일반적입니다. 물론 resources/views 아래 어디에 두어도 무방합니다.

플레인 텍스트 이메일

HTML 본문 외에 플레인 텍스트 버전도 함께 제공하고 싶다면 text 파라미터를 사용합니다. HTML을 지원하지 않는 이메일 클라이언트를 위해 텍스트 버전을 함께 제공하는 것이 좋습니다.

/** * 메시지 콘텐츠 정의를 반환합니다. */ public function content(): Content { return new Content( view: 'mail.orders.shipped', text: 'mail.orders.shipped-text', ); }

명확성을 위해 html 파라미터를 view의 별칭으로 사용할 수도 있습니다.

return new Content( html: 'mail.orders.shipped', text: 'mail.orders.shipped-text', );

뷰 데이터

public 프로퍼티 활용

Mailable 클래스에서 Blade 템플릿으로 데이터를 전달하는 가장 간단한 방법은 public 프로퍼티를 사용하는 것입니다. public으로 선언된 프로퍼티는 자동으로 뷰에서 접근 가능합니다.

<?php namespace App\Mail; use App\Models\Order; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; use Illuminate\Queue\SerializesModels; class OrderShipped extends Mailable { use Queueable, SerializesModels; /** * 새 Mailable 인스턴스를 생성합니다. */ public function __construct( public Order $order, ) {} /** * 메시지 콘텐츠 정의를 반환합니다. */ public function content(): Content { return new Content( view: 'mail.orders.shipped', ); } }

public으로 설정된 데이터는 뷰 템플릿에서 일반 변수처럼 바로 사용할 수 있습니다.

<div> 주문 가격: {{ $order->price }} </div>

`with` 파라미터 활용

데이터를 뷰에 전달하기 전에 직접 가공하거나, 특정 데이터만 선택적으로 전달하고 싶다면 Contentwith 파라미터를 사용합니다. 이때 생성자 프로퍼티는 protectedprivate으로 선언하여 자동 노출을 막는 것이 좋습니다.

<?php namespace App\Mail; use App\Models\Order; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; use Illuminate\Queue\SerializesModels; class OrderShipped extends Mailable { use Queueable, SerializesModels; /** * 새 Mailable 인스턴스를 생성합니다. */ public function __construct( protected Order $order, ) {} /** * 메시지 콘텐츠 정의를 반환합니다. */ public function content(): Content { return new Content( view: 'mail.orders.shipped', with: [ 'orderName' => $this->order->name, 'orderPrice' => $this->order->price, ], ); } }

with로 전달된 데이터도 뷰에서 동일하게 사용할 수 있습니다.

<div> 주문 가격: {{ $orderPrice }} </div>

첨부 파일

이메일에 파일을 첨부하려면 attachments 메서드에서 Attachment 객체의 배열을 반환합니다. Attachment 클래스의 fromPath 정적 메서드에 파일 경로를 전달해 첨부 파일을 생성합니다.

use Illuminate\Mail\Mailables\Attachment; /** * 메시지의 첨부 파일을 반환합니다. * * @return array<int, \Illuminate\Mail\Mailables\Attachment> */ public function attachments(): array { return [ Attachment::fromPath('/path/to/file'), ]; }

파일을 첨부할 때 표시될 파일명이나 MIME 타입을 지정하려면 aswithMime 메서드를 체이닝합니다.

/** * 메시지의 첨부 파일을 반환합니다. * * @return array<int, \Illuminate\Mail\Mailables\Attachment> */ public function attachments(): array { return [ Attachment::fromPath('/path/to/file') ->as('invoice.pdf') ->withMime('application/pdf'), ]; }

스토리지 디스크에서 첨부

파일을 파일 스토리지 디스크에 저장해 두었다면, fromStorage 메서드로 첨부할 수 있습니다.

/** * 메시지의 첨부 파일을 반환합니다. * * @return array<int, \Illuminate\Mail\Mailables\Attachment> */ public function attachments(): array { return [ Attachment::fromStorage('/path/to/file'), ]; }

기본 디스크가 아닌 다른 디스크를 지정하려면 fromStorageDisk를 사용합니다.

/** * 메시지의 첨부 파일을 반환합니다. * * @return array<int, \Illuminate\Mail\Mailables\Attachment> */ public function attachments(): array { return [ Attachment::fromStorageDisk('s3', '/path/to/file') ->as('invoice.pdf') ->withMime('application/pdf'), ]; }

원시 데이터 첨부

파일이 디스크에 저장되어 있지 않고 메모리상의 바이트 문자열로 존재한다면, fromData 메서드로 첨부합니다. 예를 들어 PDF를 메모리에서 생성하여 파일로 저장하지 않고 바로 첨부하는 경우에 유용합니다.

/** * 메시지의 첨부 파일을 반환합니다. * * @return array<int, \Illuminate\Mail\Mailables\Attachment> */ public function attachments(): array { return [ Attachment::fromData(fn () => $this->pdf, '청구서.pdf') ->withMime('application/pdf'), ]; }

인라인 첨부

이메일 본문에 이미지를 직접 삽입하는 인라인 첨부는 번거로울 수 있지만, Laravel은 이를 간편하게 처리할 수 있는 방법을 제공합니다. Blade 템플릿 안에서 $message 변수의 embed 메서드를 사용하면 됩니다. Laravel은 모든 이메일 뷰에 $message 변수를 자동으로 주입하므로 별도로 전달할 필요가 없습니다.

<body> 이것은 이미지입니다: <img src="{{ $message->embed($pathToImage) }}"> </body>

WARNING

플레인 텍스트 이메일 뷰에서는 인라인 첨부를 사용할 수 없습니다. $message 변수가 텍스트 뷰에는 제공되지 않습니다.

원시 데이터 인라인 첨부

이미 이미지의 원시 바이트 문자열이 메모리에 있다면, embedData 메서드로 본문에 삽입할 수 있습니다.

<body> 원시 데이터로부터의 이미지: <img src="{{ $message->embedData($data, '이미지명.jpg') }}"> </body>

Attachable 객체

단순한 파일 경로 문자열로 첨부 파일을 처리하는 것이 대부분의 경우에 충분하지만, 애플리케이션의 첨부 가능한 엔티티가 클래스로 표현된다면 더 우아한 방법을 사용할 수 있습니다. Illuminate\Contracts\Mail\Attachable 인터페이스를 구현하고 toMailAttachment 메서드를 정의하면, 해당 객체를 직접 첨부 파일로 사용할 수 있습니다.

<?php namespace App\Models; use Illuminate\Contracts\Mail\Attachable; use Illuminate\Database\Eloquent\Model; use Illuminate\Mail\Mailables\Attachment; class Photo extends Model implements Attachable { /** * 모델의 첨부 파일 표현을 반환합니다. */ public function toMailAttachment(): Attachment { return Attachment::fromPath('/path/to/file'); } }

Attachable 객체를 정의했다면, attachments 메서드에서 해당 인스턴스를 반환하면 됩니다.

/** * 메시지의 첨부 파일을 반환합니다. * * @return array<int, \Illuminate\Mail\Mailables\Attachment> */ public function attachments(): array { return [$this->photo]; }

첨부 파일 데이터를 Amazon S3 같은 외부 스토리지에서 가져와야 하는 경우에도, toMailAttachment에서 유연하게 처리할 수 있습니다.

// 기본 디스크에서 첨부 public function toMailAttachment(): Attachment { return Attachment::fromStorage($this->path); } // 특정 디스크에서 첨부 public function toMailAttachment(): Attachment { return Attachment::fromStorageDisk('s3', $this->path); }

헤더

때로는 발송하는 메시지에 커스텀 헤더를 추가해야 할 수 있습니다. 예를 들어 Message-Id나 기타 텍스트 헤더를 직접 지정하는 경우입니다. 이를 위해 Mailable에 headers 메서드를 정의합니다.

headers 메서드는 Illuminate\Mail\Mailables\Headers 인스턴스를 반환해야 하며, messageId, references, text 파라미터를 지원합니다. 필요한 파라미터만 지정하면 됩니다.

use Illuminate\Mail\Mailables\Headers; /** * 메시지 헤더를 반환합니다. */ public function headers(): Headers { return new Headers( messageId: 'custom-message-id@example.com', references: ['previous-message@example.com'], text: [ 'X-Custom-Header' => 'Custom Value', ], ); }

태그 및 메타데이터

Mailgun, Postmark 같은 일부 서드파티 메일 서비스는 메시지 "태그"와 "메타데이터"를 지원하며, 이를 통해 애플리케이션이 발송한 이메일을 그룹화하거나 추적할 수 있습니다. Envelope 정의에서 태그와 메타데이터를 추가할 수 있습니다.

use Illuminate\Mail\Mailables\Envelope; /** * 메시지 봉투를 반환합니다. * * @return \Illuminate\Mail\Mailables\Envelope */ public function envelope(): Envelope { return new Envelope( subject: '주문이 발송되었습니다', tags: ['주문발송'], metadata: [ 'order_id' => $this->order->id, ], ); }

Mailgun 드라이버의 태그와 메타데이터에 대한 자세한 내용은 Mailgun 공식 문서를 참고하세요. Postmark의 경우 태그메타데이터 문서를 참고하십시오.

Amazon SES를 사용 중이라면 metadataSES "태그"를 첨부할 수 있습니다.

Symfony 메시지 커스터마이징

Laravel의 메일 기능은 Symfony Mailer를 기반으로 합니다. 메시지 발송 전에 Symfony Message 인스턴스에 직접 접근하여 추가 커스터마이징이 필요하다면, Envelope에서 using 파라미터를 사용합니다.

use Illuminate\Mail\Mailables\Envelope; use Symfony\Component\Mime\Email; /** * 메시지 봉투를 반환합니다. */ public function envelope(): Envelope { return new Envelope( subject: '주문이 발송되었습니다', using: [ function (Email $message) { // ... }, ] ); }

Markdown Mailable

Markdown Mailable을 사용하면 Laravel에서 제공하는 메일 알림의 사전 빌드된 템플릿과 컴포넌트를 활용할 수 있습니다. Markdown으로 메시지를 작성하면 Laravel이 반응형 HTML 템플릿과 플레인 텍스트 버전을 자동으로 생성해 줍니다.

Markdown Mailable 생성

Markdown 템플릿을 포함한 Mailable을 생성하려면 make:mail 명령에 --markdown 옵션을 사용합니다.

php artisan make:mail OrderShipped --markdown=mail.orders.shipped

content 메서드에서는 view 대신 markdown 파라미터를 사용합니다.

use Illuminate\Mail\Mailables\Content; /** * 메시지 콘텐츠 정의를 반환합니다. */ public function content(): Content { return new Content( markdown: 'mail.orders.shipped', with: [ 'url' => $this->orderUrl, ], ); }

Markdown 메시지 작성

Markdown Mailable은 Blade 컴포넌트와 Markdown 문법을 조합하여, Laravel에서 제공하는 이메일 UI 컴포넌트를 편리하게 활용할 수 있습니다.

<x-mail::message> # 주문 발송 완료 주문이 발송되었습니다! <x-mail::button :url="$url"> 주문 확인하기 </x-mail::button> 감사합니다,<br> {{ config('app.name') }} </x-mail::message>

NOTE

Markdown 이메일을 작성할 때 들여쓰기를 과도하게 사용하지 마세요. Markdown 파서는 들여쓰기된 내용을 코드 블록으로 처리합니다.

버튼 컴포넌트

버튼 컴포넌트는 가운데 정렬된 버튼 링크를 렌더링합니다. url과 선택적으로 color를 지정할 수 있으며, 지원 색상은 primary, success, error입니다. 하나의 메시지에 버튼을 여러 개 추가해도 됩니다.

<x-mail::button :url="$url" color="success"> 주문 확인하기 </x-mail::button>

패널 컴포넌트

패널 컴포넌트는 지정된 텍스트 블록을 나머지 본문과 시각적으로 구분되는 배경색 패널로 감싸 표시합니다. 특정 내용을 강조할 때 유용합니다.

<x-mail::panel> 여기에 패널 내용을 작성하세요. </x-mail::panel>

테이블 컴포넌트

테이블 컴포넌트는 Markdown 테이블을 HTML 테이블로 변환합니다. 열 정렬은 Markdown 표준 문법을 그대로 사용합니다.

<x-mail::table> | 상품명 | 색상 | 수량 | | :-------- | -------: | :--: | | 라라벨 머그 | 검정 | 1 | | 라라벨 티셔츠 | 회색 | 2 | </x-mail::table>

컴포넌트 커스터마이징

Markdown 메일 컴포넌트를 애플리케이션에 맞게 직접 수정하고 싶다면, 먼저 컴포넌트를 내보내야 합니다. 아래 명령을 실행하면 컴포넌트 파일들이 resources/views/vendor/mail 디렉터리로 복사됩니다.

php artisan vendor:publish --tag=laravel-mail

이 명령을 실행하면 resources/views/vendor/mail 디렉터리에 htmltext 두 폴더가 생성되며, 각 컴포넌트의 HTML 버전과 텍스트 버전이 포함됩니다. 이 파일들을 자유롭게 수정하면 됩니다.

CSS 커스터마이징

컴포넌트를 내보내면 resources/views/vendor/mail/html/themes/default.css 파일을 수정하여 스타일을 변경할 수 있습니다. 이 파일의 CSS는 Markdown 메일의 HTML 표현으로 자동 인라인 처리됩니다.

Laravel Markdown 컴포넌트를 위한 완전히 새로운 테마를 만들려면, html/themes 디렉터리에 CSS 파일을 추가하고 config/mail.phptheme 옵션을 해당 파일명으로 설정하면 됩니다.

특정 Mailable에만 다른 테마를 적용하려면, 해당 Mailable 클래스에서 $theme 프로퍼티를 지정합니다.

/** * 사용할 테마 이름입니다. * * @var string */ public $theme = 'invoice';

메일 발송

메일을 발송하려면 Mail 파사드to 메서드를 사용합니다. to 메서드에는 이메일 주소 문자열, 사용자 인스턴스, 또는 사용자 컬렉션을 전달할 수 있습니다. 객체나 컬렉션을 전달하면 Laravel이 자동으로 emailname 프로퍼티를 사용하여 수신자를 설정합니다. 수신자를 지정했다면, Mailable 인스턴스를 send 메서드에 전달합니다.

<?php namespace App\Http\Controllers; use App\Mail\OrderShipped; use App\Models\Order; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Mail; class OrderShipmentController extends Controller { /** * 주어진 주문을 발송 처리합니다. */ public function store(Request $request): RedirectResponse { $order = Order::findOrFail($request->order_id); // 주문 발송 처리 로직... Mail::to($request->user())->send(new OrderShipped($order)); return redirect('/orders'); } }

메일 발송 시 to 외에도 cc, bcc 메서드를 체이닝할 수 있습니다.

Mail::to($request->user()) ->cc($moreUsers) ->bcc($evenMoreUsers) ->send(new OrderShipped($order));

다수 수신자에게 반복 발송

여러 수신자에게 메일을 보낼 때, 같은 Mailable 인스턴스를 재사용하면 이전 수신자 정보가 누적될 수 있으므로, 수신자마다 새 Mailable 인스턴스를 생성해야 합니다.

foreach (['user1@example.com', 'user2@example.com'] as $recipient) { Mail::to($recipient)->send(new OrderShipped($order)); }

특정 메일러로 발송

기본적으로 Laravel은 mail 설정 파일의 default 메일러를 사용합니다. 특정 메일러로 발송하려면 mailer 메서드를 사용합니다.

Mail::mailer('postmark') ->to($request->user()) ->send(new OrderShipped($order));

메일 큐 처리

메일 메시지 큐에 넣기

메일 발송은 응답 시간에 영향을 줄 수 있으므로, 백그라운드 큐로 처리하는 것이 좋습니다. Laravel의 통합 큐 API를 활용하면 쉽게 구현할 수 있습니다. send 대신 queue 메서드를 사용하면 됩니다.

Mail::to($request->user()) ->cc($moreUsers) ->bcc($evenMoreUsers) ->queue(new OrderShipped($order));

이 메서드는 메시지를 큐에 푸시하여 백그라운드에서 발송하는 작업을 자동으로 처리합니다. 이 기능을 사용하기 전에 큐 설정을 완료해야 합니다.

지연 발송

큐에 넣은 메시지의 발송 시점을 지연시키고 싶다면 later 메서드를 사용합니다. 첫 번째 인수로 DateTime 인스턴스를 전달합니다.

Mail::to($request->user()) ->cc($moreUsers) ->bcc($evenMoreUsers) ->later(now()->addMinutes(10), new OrderShipped($order));

특정 큐와 커넥션으로 보내기

make:mail 명령으로 생성한 Mailable 클래스에는 Illuminate\Bus\Queueable 트레이트가 이미 포함되어 있습니다. 따라서 Mailable 인스턴스에서 onQueue, onConnection 메서드를 체이닝하여 큐 이름과 커넥션을 지정할 수 있습니다.

$message = (new OrderShipped($order)) ->onConnection('sqs') ->onQueue('emails'); Mail::to($request->user())->queue($message);

기본적으로 큐로 처리

항상 큐를 통해 발송하고 싶은 Mailable이 있다면, ShouldQueue 인터페이스를 구현하면 됩니다. 이렇게 하면 send로 발송해도 자동으로 큐로 처리됩니다.

use Illuminate\Contracts\Queue\ShouldQueue; class OrderShipped extends Mailable implements ShouldQueue { // ... }

Mailable과 데이터베이스 트랜잭션

큐에 등록된 Mailable이 아직 커밋되지 않은 데이터베이스 트랜잭션 안에서 디스패치될 경우, 큐 워커가 트랜잭션 커밋 전에 해당 Job을 처리할 수 있습니다. 이때 트랜잭션 중 변경된 모델이나 레코드가 데이터베이스에 반영되지 않았을 수 있으며, 트랜잭션이 롤백되었다면 해당 데이터가 아예 존재하지 않을 수도 있습니다.

큐 커넥션의 after_commit 옵션이 false로 설정되어 있더라도, 메일 발송 시 afterCommit 메서드를 체이닝하면 모든 열린 트랜잭션이 커밋된 후에 메일이 디스패치됩니다.

Mail::to($request->user())->send( (new OrderShipped($order))->afterCommit() );

또는 Mailable 클래스 생성자에서 afterCommit을 호출할 수도 있습니다.

<?php namespace App\Mail; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; class OrderShipped extends Mailable implements ShouldQueue { use Queueable, SerializesModels; /** * 새 Mailable 인스턴스를 생성합니다. */ public function __construct() { $this->afterCommit(); } }

NOTE

이 문제를 우회하는 방법에 대한 자세한 내용은 큐와 데이터베이스 트랜잭션 문서를 참고하세요.

Mailable 렌더링

실제로 메일을 발송하지 않고 Mailable의 HTML 내용만 확인하고 싶을 때는 render 메서드를 사용합니다. 이 메서드는 Mailable의 HTML을 문자열로 반환합니다.

use App\Mail\InvoicePaid; use App\Models\Invoice; $invoice = Invoice::find(1); return (new InvoicePaid($invoice))->render();

브라우저에서 Mailable 미리보기

Mailable 뷰를 개발 중에 빠르게 확인하려면, 라우트의 클로저에서 Mailable을 직접 반환하면 됩니다. 브라우저에서 바로 렌더링된 결과를 볼 수 있습니다.

Route::get('/mailable', function () { $invoice = App\Models\Invoice::find(1); return new App\Mail\InvoicePaid($invoice); });

WARNING

브라우저에서 Mailable을 렌더링할 때 인라인 첨부 파일은 표시되지 않습니다. 인라인 첨부를 미리보려면 MailHogHELO 같은 이메일 테스팅 도구를 사용하세요.

Mailable 현지화

Laravel은 현재 요청의 로케일과 다른 언어로 메일을 발송할 수 있도록 지원하며, 메일이 큐로 처리될 때도 지정된 로케일이 유지됩니다.

Mail 파사드의 locale 메서드로 원하는 언어를 지정할 수 있습니다. Mailable의 템플릿이 렌더링될 때 해당 로케일로 전환되며, 렌더링이 끝나면 이전 로케일로 복원됩니다.

Mail::to($request->user())->locale('ko')->send( new OrderShipped($order) );

사용자 선호 로케일

애플리케이션에서 각 사용자의 선호 언어를 저장하는 경우, 모델에 HasLocalePreference 인터페이스를 구현하면 Laravel이 메일 발송 시 자동으로 해당 로케일을 사용합니다.

use Illuminate\Contracts\Translation\HasLocalePreference; class User extends Model implements HasLocalePreference { /** * 사용자의 선호 로케일을 반환합니다. */ public function preferredLocale(): string { return $this->locale; } }

이 인터페이스를 구현하면, locale 메서드를 별도로 호출할 필요 없이 Laravel이 자동으로 선호 로케일을 사용합니다. 알림(Notification)에도 동일하게 적용됩니다.

Mail::to($request->user())->send(new OrderShipped($order));

테스트

Mailable 내용 테스트

Laravel은 Mailable의 구조를 검사하기 위한 다양한 메서드를 제공합니다. 또한 예상하는 내용이 Mailable에 포함되어 있는지 확인할 수 있는 편리한 메서드도 제공합니다. 주요 테스트 메서드는 다음과 같습니다: assertSeeInHtml, assertDontSeeInHtml, assertSeeInOrderInHtml, assertSeeInText, assertDontSeeInText, assertSeeInOrderInText, assertHasAttachment, assertHasAttachedData, assertHasAttachmentFromStorage, assertHasAttachmentFromStorageDisk.

Html 계열 메서드는 HTML 본문을, Text 계열 메서드는 플레인 텍스트 버전을 검사합니다.

<?php use App\Mail\InvoicePaid; use App\Models\Invoice; test('mailable content', function () { $invoice = Invoice::factory()->create(); $mailable = new InvoicePaid($invoice); $mailable->assertFrom('jeffrey@example.com'); $mailable->assertTo('taylor@example.com'); $mailable->assertHasCc('abigail@example.com'); $mailable->assertHasBcc('victoria@example.com'); $mailable->assertHasReplyTo('tyler@example.com'); $mailable->assertHasSubject('청구서 결제 완료'); $mailable->assertHasTag('invoice-paid'); $mailable->assertHasMetadata('invoice_id', $invoice->id); $mailable->assertSeeInHtml($invoice->number); $mailable->assertSeeInHtml('결제 완료'); $mailable->assertSeeInOrderInHtml(['결제 완료', $invoice->number]); $mailable->assertSeeInText($invoice->number); $mailable->assertSeeInOrderInText(['결제 완료', $invoice->number]); $mailable->assertHasAttachment('/path/to/file'); $mailable->assertHasAttachment(Attachment::fromPath('/path/to/file')); $mailable->assertHasAttachedData($pdfData, '청구서.pdf', ['mime' => 'application/pdf']); $mailable->assertHasAttachmentFromStorage('/path/to/file', '청구서.pdf', ['mime' => 'application/pdf']); $mailable->assertHasAttachmentFromStorageDisk('s3', '/path/to/file', '청구서.pdf', ['mime' => 'application/pdf']); });
<?php namespace Tests\Unit\Mail; use App\Mail\InvoicePaid; use App\Models\Invoice; use Tests\TestCase; class InvoicePaidTest extends TestCase { public function test_mailable_content(): void { $invoice = Invoice::factory()->create(); $mailable = new InvoicePaid($invoice); $mailable->assertFrom('jeffrey@example.com'); $mailable->assertTo('taylor@example.com'); $mailable->assertHasCc('abigail@example.com'); $mailable->assertHasBcc('victoria@example.com'); $mailable->assertHasReplyTo('tyler@example.com'); $mailable->assertHasSubject('청구서 결제 완료'); $mailable->assertHasTag('invoice-paid'); $mailable->assertHasMetadata('invoice_id', $invoice->id); $mailable->assertSeeInHtml($invoice->number); $mailable->assertSeeInHtml('결제 완료'); $mailable->assertSeeInOrderInHtml(['결제 완료', $invoice->number]); $mailable->assertSeeInText($invoice->number); $mailable->assertSeeInOrderInText(['결제 완료', $invoice->number]); $mailable->assertHasAttachment('/path/to/file'); $mailable->assertHasAttachment(Attachment::fromPath('/path/to/file')); $mailable->assertHasAttachedData($pdfData, '청구서.pdf', ['mime' => 'application/pdf']); $mailable->assertHasAttachmentFromStorage('/path/to/file', '청구서.pdf', ['mime' => 'application/pdf']); $mailable->assertHasAttachmentFromStorageDisk('s3', '/path/to/file', '청구서.pdf', ['mime' => 'application/pdf']); } }

Mailable 발송 테스트

Mailable의 내용 테스트와 특정 사용자에게 메일이 발송되었는지에 대한 테스트는 별도로 진행하는 것이 좋습니다. 실제 메일이 전송되었는지 테스트하는 방법은 Mail Fake 문서를 참고하세요.

로컬 개발 환경에서의 메일

이메일 발송 기능을 개발할 때 실제 수신자에게 메일이 전달되는 것은 바람직하지 않습니다. Laravel은 로컬 개발 환경에서 실제 발송을 차단하는 여러 방법을 제공합니다.

로그 드라이버

log 메일 드라이버는 메일을 실제로 발송하지 않고 로그 파일에 기록합니다. 로컬 개발 중에 빠르게 메일 내용을 확인할 수 있는 가장 간단한 방법입니다. 환경별 설정 방법은 설정 문서를 참고하세요.

HELO / Mailtrap / Mailpit

다른 방법으로는 HELOMailtrap

메일

소개

Laravel은 Symfony Mailer 컴포넌트를 기반으로 한 간결한 이메일 API를 제공합니다. SMTP, Mailgun, Postmark, Resend, Amazon SES, sendmail 등 다양한 전송 수단을 지원하므로, 로컬 환경이든 클라우드 서비스든 원하는 방식으로 빠르게 이메일을 발송할 수 있습니다.

설정

이메일 관련 설정은 config/mail.php 파일에서 관리합니다. 이 파일 안에는 mailers 배열이 있으며, 각 mailer마다 고유한 설정과 전송 방식(transport)을 지정할 수 있습니다. 예를 들어, 트랜잭션 메일은 Postmark로, 대량 발송 메일은 Amazon SES로 처리하는 식으로 용도에 따라 다른 서비스를 사용할 수 있습니다.

default 값은 애플리케이션이 기본으로 사용할 mailer를 지정합니다.

드라이버 / 전송 사전 준비

Mailgun, Postmark, Resend, MailerSend와 같이 API 기반 드라이버는 SMTP 방식보다 일반적으로 더 단순하고 빠릅니다. 가능하다면 이 중 하나를 선택하는 것을 권장합니다.

Mailgun 드라이버

Mailgun 드라이버를 사용하려면 Composer로 다음 패키지를 설치하세요:

composer require symfony/mailgun-mailer symfony/http-client

그런 다음 config/mail.php에서 두 가지를 수정합니다.

첫째, 기본 mailer를 mailgun으로 설정합니다:

'default' => env('MAIL_MAILER', 'mailgun'),

둘째, mailers 배열에 다음 설정을 추가합니다:

'mailgun' => [
    'transport' => 'mailgun',
    // 'client' => [
    //     'timeout' => 5,
    // ],
],

마지막으로 config/services.php에 Mailgun 인증 정보를 추가합니다:

'mailgun' => [
    'domain' => env('MAILGUN_DOMAIN'),
    'secret' => env('MAILGUN_SECRET'),
    'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
    'scheme' => 'https',
],

미국 외 지역(예: EU)의 Mailgun 리전을 사용하는 경우, endpoint를 해당 리전에 맞게 변경하세요:

'mailgun' => [
    'domain' => env('MAILGUN_DOMAIN'),
    'secret' => env('MAILGUN_SECRET'),
    'endpoint' => env('MAILGUN_ENDPOINT', 'api.eu.mailgun.net'),
    'scheme' => 'https',
],

Postmark 드라이버

Postmark 드라이버를 사용하려면 다음 패키지를 설치하세요:

composer require symfony/postmark-mailer symfony/http-client

config/mail.phpdefaultpostmark로 설정한 뒤, config/services.php에 아래 내용을 추가합니다:

'postmark' => [
    'token' => env('POSTMARK_TOKEN'),
],

특정 mailer에 Postmark 메시지 스트림을 지정하고 싶다면, config/mail.php의 해당 mailer 설정에 message_stream_id를 추가하세요:

'postmark' => [
    'transport' => 'postmark',
    'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
    // 'client' => [
    //     'timeout' => 5,
    // ],
],

이 방식을 활용하면 서로 다른 메시지 스트림을 가진 Postmark mailer를 여러 개 설정할 수도 있습니다.

Resend 드라이버

Resend 드라이버를 사용하려면 PHP SDK를 설치하세요:

composer require resend/resend-php

config/mail.phpdefaultresend로 설정한 뒤, config/services.php에 API 키를 추가합니다:

'resend' => [
    'key' => env('RESEND_KEY'),
],

SES 드라이버

Amazon SES 드라이버를 사용하려면 먼저 AWS SDK for PHP를 설치하세요:

composer require aws/aws-sdk-php

config/mail.phpdefaultses로 설정하고, config/services.php에 다음 내용을 추가합니다:

'ses' => [
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],

AWS 임시 자격 증명(세션 토큰)을 사용하는 경우 token 키를 추가하세요:

'ses' => [
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
    'token' => env('AWS_SESSION_TOKEN'),
],

SES의 구독 관리 기능을 사용하려면, 메일 클래스의 headers 메서드에서 X-Ses-List-Management-Options 헤더를 반환하면 됩니다:

/** * 메시지 헤더를 반환합니다. */ public function headers(): Headers { return new Headers( text: [ 'X-Ses-List-Management-Options' => 'contactListName=MyContactList;topicName=MyTopic', ], ); }

이메일 발송 시 AWS SDK의 SendEmail 메서드에 추가 옵션을 전달하고 싶다면, ses 설정에 options 배열을 정의하세요:

'ses' => [
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
    'options' => [
        'ConfigurationSetName' => 'MyConfigurationSet',
        'EmailTags' => [
            ['Name' => 'foo', 'Value' => 'bar'],
        ],
    ],
],

MailerSend 드라이버

MailerSend는 트랜잭션 이메일 및 SMS 서비스로, Laravel용 API 기반 메일 드라이버를 자체적으로 제공합니다. Composer로 패키지를 설치하세요:

composer require mailersend/laravel-driver

설치 후 .env 파일에 다음 환경 변수를 추가합니다:

MAIL_MAILER=mailersend MAIL_FROM_ADDRESS=app@yourdomain.com MAIL_FROM_NAME="App Name" MAILERSEND_API_KEY=your-api-key

그리고 config/mail.phpmailers 배열에 아래 설정을 추가합니다:

'mailersend' => [ 'transport' => 'mailersend', ],

호스팅 템플릿 사용 방법 등 자세한 내용은 MailerSend 드라이버 문서를 참고하세요.

페일오버 설정

기본 이메일 서비스가 일시적으로 장애 상태에 빠지는 경우를 대비해, 백업 mailer를 순서대로 지정해 두는 페일오버(failover) 설정을 활용할 수 있습니다.

config/mail.phpmailers 배열에 failover 전송 방식을 사용하는 mailer를 정의하고, 시도할 mailer 목록을 우선순위 순으로 나열하세요:

'mailers' => [
    'failover' => [
        'transport' => 'failover',
        'mailers' => [
            'postmark',
            'mailgun',
            'sendmail',
        ],
    ],

    // ...
],

페일오버 mailer를 기본값으로 사용하려면 default 값을 변경합니다:

'default' => env('MAIL_MAILER', 'failover'),

NOTE

페일오버는 고가용성(high availability) 을 목적으로 합니다. 기본 mailer가 실패할 때만 다음 mailer로 넘어가는 방식입니다.

라운드 로빈 설정

roundrobin 전송 방식은 여러 mailer에 발송 부하를 분산하고 싶을 때 사용합니다. config/mail.phpmailers 배열에 다음과 같이 정의하세요:

'mailers' => [
    'roundrobin' => [
        'transport' => 'roundrobin',
        'mailers' => [
            'ses',
            'postmark',
        ],
    ],

    // ...
],

라운드 로빈 mailer를 기본값으로 설정합니다:

'default' => env('MAIL_MAILER', 'roundrobin'),

라운드 로빈은 설정된 mailer 목록에서 무작위로 첫 번째 mailer를 선택한 뒤, 이후 이메일마다 다음 mailer로 순환합니다. failover고가용성을 위한 것이라면, roundrobin부하 분산(load balancing) 을 위한 방식입니다.

방식목적동작
failover고가용성앞 순서 mailer가 실패하면 다음으로 전환
roundrobin부하 분산메일마다 다음 mailer로 순환

메일

Mailable 클래스 생성

Laravel 애플리케이션에서 발송하는 각 이메일 유형은 하나의 "mailable" 클래스로 표현됩니다. 이 클래스들은 app/Mail 디렉터리에 저장됩니다. 처음에는 이 디렉터리가 존재하지 않을 수 있지만, 아래 Artisan 명령어로 첫 번째 mailable 클래스를 생성하면 자동으로 만들어집니다.

php artisan make:mail OrderShipped

메일

Mailable 클래스 작성하기

Mailable 클래스를 생성했다면 파일을 열어 내부 구조를 살펴보겠습니다. Mailable 클래스의 설정은 주로 envelope, content, attachments 세 가지 메서드를 통해 이루어집니다.

  • envelope 메서드: 메시지의 제목과 경우에 따라 수신자를 정의하는 Illuminate\Mail\Mailables\Envelope 객체를 반환합니다.
  • content 메서드: 메일 본문을 렌더링할 Blade 템플릿을 정의하는 Illuminate\Mail\Mailables\Content 객체를 반환합니다.

발신자 설정

Envelope로 발신자 지정하기

발신자, 즉 이메일의 "보내는 사람(from)"을 설정하는 방법은 두 가지입니다. 첫 번째는 Envelope에 직접 from 주소를 지정하는 방식입니다.

use Illuminate\Mail\Mailables\Address; use Illuminate\Mail\Mailables\Envelope; /** * 메시지 Envelope를 반환합니다. */ public function envelope(): Envelope { return new Envelope( from: new Address('info@myapp.kr', '내 서비스'), subject: '주문이 발송되었습니다', ); }

답장 받을 주소(replyTo)도 별도로 지정할 수 있습니다.

return new Envelope( from: new Address('info@myapp.kr', '내 서비스'), replyTo: [ new Address('support@myapp.kr', '고객센터'), ], subject: '주문이 발송되었습니다', );

전역 from 주소 사용하기

애플리케이션 전체에서 동일한 발신자 주소를 사용한다면, 매번 Mailable 클래스마다 설정하는 것은 번거롭습니다. 이런 경우 config/mail.php에 전역 발신자 주소를 지정해두면, Mailable 클래스에 별도의 from이 없을 때 이 값이 자동으로 사용됩니다.

'from' => [ 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 'name' => env('MAIL_FROM_NAME', 'Example'), ],

전역 reply_to 주소도 동일하게 설정할 수 있습니다.

'reply_to' => ['address' => 'support@example.com', 'name' => '앱 이름'],

뷰(템플릿) 설정

content 메서드 안에서 메일 본문을 렌더링할 Blade 템플릿을 지정합니다. Blade의 모든 기능을 그대로 활용할 수 있어 HTML 이메일을 유연하게 작성할 수 있습니다.

/** * 메시지 콘텐츠 정의를 반환합니다. */ public function content(): Content { return new Content( view: 'mail.orders.shipped', ); }

NOTE

이메일 템플릿은 resources/views/emails 디렉터리에 모아두는 것이 좋습니다. 다만 resources/views 하위라면 어디에 두어도 무방합니다.

일반 텍스트(Plain Text) 이메일

HTML과 함께 텍스트 전용 버전의 메일을 제공하려면 text 파라미터로 텍스트 템플릿을 추가로 지정합니다. 일부 이메일 클라이언트나 환경에서는 HTML 대신 텍스트 버전을 사용하므로, 두 버전을 모두 제공하면 호환성이 높아집니다.

/** * 메시지 콘텐츠 정의를 반환합니다. */ public function content(): Content { return new Content( view: 'mail.orders.shipped', text: 'mail.orders.shipped-text' ); }

가독성을 높이기 위해 view 대신 html을 별칭으로 사용할 수도 있습니다.

return new Content( html: 'mail.orders.shipped', text: 'mail.orders.shipped-text' );

뷰 데이터 전달

public 프로퍼티로 전달하기

뷰에서 사용할 데이터를 전달하는 가장 간단한 방법은 Mailable 클래스에 public 프로퍼티를 선언하는 것입니다. public으로 선언된 프로퍼티는 별도의 처리 없이 자동으로 뷰에서 접근할 수 있습니다.

<?php namespace App\Mail; use App\Models\Order; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; use Illuminate\Queue\SerializesModels; class OrderShipped extends Mailable { use Queueable, SerializesModels; /** * 새 메시지 인스턴스를 생성합니다. */ public function __construct( public Order $order, ) {} /** * 메시지 콘텐츠 정의를 반환합니다. */ public function content(): Content { return new Content( view: 'mail.orders.shipped', ); } }

public 프로퍼티로 설정된 데이터는 Blade 템플릿에서 바로 사용할 수 있습니다.

<div> 가격: {{ $order->price }} </div>

with 파라미터로 전달하기

뷰에 넘기기 전에 데이터를 가공하거나 변수명을 바꾸고 싶다면, Contentwith 파라미터를 사용합니다. 이 경우 생성자에서 받은 데이터를 protected 또는 private으로 선언해야 뷰에 자동 노출되지 않습니다.

<?php namespace App\Mail; use App\Models\Order; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; use Illuminate\Queue\SerializesModels; class OrderShipped extends Mailable { use Queueable, SerializesModels; /** * 새 메시지 인스턴스를 생성합니다. */ public function __construct( protected Order $order, ) {} /** * 메시지 콘텐츠 정의를 반환합니다. */ public function content(): Content { return new Content( view: 'mail.orders.shipped', with: [ 'orderName' => $this->order->name, 'orderPrice' => $this->order->price, ], ); } }

with로 전달한 데이터는 Blade 템플릿에서 지정한 키 이름으로 바로 사용할 수 있습니다.

<div> 가격: {{ $orderPrice }} </div>

첨부 파일

이메일에 파일을 첨부하려면 attachments 메서드에서 Attachment 객체 배열을 반환합니다. Attachment::fromPath()로 파일 경로를 직접 지정할 수 있습니다.

use Illuminate\Mail\Mailables\Attachment; /** * 메시지에 첨부할 파일 목록을 반환합니다. * * @return array<int, \Illuminate\Mail\Mailables\Attachment> */ public function attachments(): array { return [ Attachment::fromPath('/path/to/file'), ]; }

첨부 파일의 표시 이름과 MIME 타입은 as, withMime 메서드로 지정할 수 있습니다.

public function attachments(): array { return [ Attachment::fromPath('/path/to/file') ->as('견적서.pdf') ->withMime('application/pdf'), ]; }

파일시스템 디스크에서 첨부하기

파일시스템 디스크에 저장된 파일은 fromStorage 메서드로 첨부합니다.

public function attachments(): array { return [ Attachment::fromStorage('/path/to/file'), ]; }

표시 이름과 MIME 타입도 함께 지정할 수 있습니다.

public function attachments(): array { return [ Attachment::fromStorage('/path/to/file') ->as('견적서.pdf') ->withMime('application/pdf'), ]; }

기본 디스크가 아닌 다른 디스크(예: S3)를 사용하려면 fromStorageDisk 메서드를 사용합니다.

public function attachments(): array { return [ Attachment::fromStorageDisk('s3', '/path/to/file') ->as('견적서.pdf') ->withMime('application/pdf'), ]; }

메모리 데이터를 첨부하기

파일을 디스크에 저장하지 않고 메모리에서 바로 생성된 데이터(예: 동적으로 생성한 PDF)를 첨부할 때는 fromData 메서드를 사용합니다. 클로저로 원시 바이트 데이터를 반환하고, 두 번째 인자로 파일명을 지정합니다.

public function attachments(): array { return [ Attachment::fromData(fn () => $this->pdf, '보고서.pdf') ->withMime('application/pdf'), ]; }

인라인 첨부(이미지 삽입)

이메일 본문에 이미지를 직접 삽입하는 작업은 까다롭지만, Laravel은 $message 변수의 embed 메서드를 통해 간편하게 처리할 수 있습니다. $message 변수는 모든 이메일 템플릿에서 자동으로 사용 가능합니다.

<body> 아래는 첨부된 이미지입니다: <img src="{{ $message->embed($pathToImage) }}"> </body>

WARNING

$message 변수는 일반 텍스트(Plain Text) 템플릿에서는 사용할 수 없습니다. 텍스트 형식은 인라인 첨부를 지원하지 않습니다.

원시 데이터를 인라인 이미지로 삽입하기

이미지 데이터를 이미 메모리에 갖고 있다면 embedData 메서드로 인라인 삽입할 수 있습니다. 두 번째 인자로 파일명을 지정해야 합니다.

<body> 원시 데이터로부터 삽입된 이미지: <img src="{{ $message->embedData($data, 'example-image.jpg') }}"> </body>

Attachable 객체

단순 파일 경로 문자열 대신, 모델 같은 객체 자체를 첨부 대상으로 사용하면 코드가 훨씬 직관적이 됩니다. 예를 들어 Photo 모델이 있다면 Attachment 객체를 반환하는 toMailAttachment 메서드를 정의하면 됩니다.

Illuminate\Contracts\Mail\Attachable 인터페이스를 구현하고, toMailAttachment 메서드에서 Illuminate\Mail\Attachment 인스턴스를 반환하면 됩니다.

<?php namespace App\Models; use Illuminate\Contracts\Mail\Attachable; use Illuminate\Database\Eloquent\Model; use Illuminate\Mail\Attachment; class Photo extends Model implements Attachable { /** * 메일 첨부 형태로 변환합니다. */ public function toMailAttachment(): Attachment { return Attachment::fromPath('/path/to/file'); } }

이렇게 하면 attachments 메서드에서 모델 인스턴스를 그대로 반환할 수 있습니다.

public function attachments(): array { return [$this->photo]; }

원격 스토리지(예: Amazon S3, Backblaze)에 저장된 파일도 동일하게 활용할 수 있습니다.

// 기본 디스크에서 첨부 return Attachment::fromStorage($this->path); // 특정 디스크에서 첨부 return Attachment::fromStorageDisk('backblaze', $this->path);

메모리 데이터로도 첨부 인스턴스를 만들 수 있습니다.

return Attachment::fromData(fn () => $this->content, '사진');

as, withMime으로 파일명과 MIME 타입을 커스터마이징할 수 있습니다.

return Attachment::fromPath('/path/to/file') ->as('사진') ->withMime('image/jpeg');

헤더 커스터마이징

발송 메시지에 커스텀 헤더를 추가해야 할 때는 headers 메서드를 정의합니다. 이 메서드는 Illuminate\Mail\Mailables\Headers 인스턴스를 반환하며, messageId, references, text 파라미터를 지원합니다. 필요한 항목만 선택적으로 지정하면 됩니다.

use Illuminate\Mail\Mailables\Headers; /** * 메시지 헤더를 반환합니다. */ public function headers(): Headers { return new Headers( messageId: 'custom-message-id@example.com', references: ['previous-message@example.com'], text: [ 'X-Custom-Header' => 'Custom Value', ], ); }

태그와 메타데이터

Mailgun, Postmark 같은 이메일 서비스 제공업체는 메시지에 "태그"와 "메타데이터"를 붙여 이메일을 그룹화하거나 추적하는 기능을 제공합니다. Envelope 정의에서 이를 설정할 수 있습니다.

use Illuminate\Mail\Mailables\Envelope; /** * 메시지 Envelope를 반환합니다. * * @return \Illuminate\Mail\Mailables\Envelope */ public function envelope(): Envelope { return new Envelope( subject: '주문이 발송되었습니다', tags: ['shipment'], metadata: [ 'order_id' => $this->order->id, ], ); }

Symfony Message 커스터마이징

Laravel의 메일 기능은 내부적으로 Symfony Mailer를 기반으로 동작합니다. 메시지 발송 직전에 Symfony Message 인스턴스에 직접 접근해 세밀하게 제어하고 싶다면, Envelopeusing 파라미터에 콜백을 등록합니다.

use Illuminate\Mail\Mailables\Envelope; use Symfony\Component\Mime\Email; /** * 메시지 Envelope를 반환합니다. */ public function envelope(): Envelope { return new Envelope( subject: '주문이 발송되었습니다', using: [ function (Email $message) { // Symfony Message 인스턴스를 직접 조작합니다. }, ] ); }

Markdown Mailable

Markdown Mailable을 사용하면 메일 알림에서 제공하는 미리 만들어진 템플릿과 컴포넌트를 메일 클래스에서도 그대로 활용할 수 있습니다. 메시지를 Markdown으로 작성하면, Laravel이 반응형 HTML 템플릿을 자동으로 렌더링하고, 동시에 플레인 텍스트 버전도 자동으로 생성해 줍니다.

Markdown Mailable 생성

Markdown 템플릿이 함께 생성되도록 하려면 make:mail Artisan 명령에 --markdown 옵션을 추가합니다:

php artisan make:mail OrderShipped --markdown=mail.orders.shipped

그런 다음, Mailable 클래스의 content 메서드에서 Content 객체를 반환할 때 view 대신 markdown 파라미터를 사용합니다:

use Illuminate\Mail\Mailables\Content; /** * 메시지 콘텐츠 정의를 반환합니다. */ public function content(): Content { return new Content( markdown: 'mail.orders.shipped', with: [ 'url' => $this->orderUrl, ], ); }

Markdown 메시지 작성

Markdown Mailable은 Blade 컴포넌트와 Markdown 문법을 함께 사용합니다. Laravel이 기본 제공하는 이메일 UI 컴포넌트를 활용해 깔끔한 메일 본문을 손쉽게 작성할 수 있습니다:

<x-mail::message> # 주문이 발송되었습니다 주문하신 상품이 발송되었습니다! <x-mail::button :url="$url"> 주문 확인하기 </x-mail::button> 감사합니다,<br> {{ config('app.name') }} </x-mail::message>

NOTE

Markdown 이메일을 작성할 때 불필요한 들여쓰기를 사용하지 마세요. Markdown 표준에 따라 들여쓰기된 내용은 코드 블록으로 렌더링됩니다.

Button 컴포넌트

버튼 컴포넌트는 가운데 정렬된 링크 버튼을 렌더링합니다. url과 선택적으로 color를 인자로 받으며, 지원하는 색상은 primary, success, error입니다. 메시지 안에 버튼을 여러 개 추가해도 됩니다:

<x-mail::button :url="$url" color="success"> 주문 확인하기 </x-mail::button>

Panel 컴포넌트

패널 컴포넌트는 나머지 본문과 배경색이 살짝 다른 박스 안에 텍스트 블록을 표시합니다. 특정 내용을 강조하고 싶을 때 유용합니다:

<x-mail::panel> 이 내용이 패널 안에 표시됩니다. </x-mail::panel>

Table 컴포넌트

테이블 컴포넌트는 Markdown 테이블 문법을 HTML 테이블로 변환해 렌더링합니다. Markdown 기본 테이블 정렬 문법(:---:, ---: 등)을 그대로 사용할 수 있습니다:

<x-mail::table> | 상품명 | 수량 | 금액 | | ------------- | :-----------: | ------------: | | 티셔츠 | 2 |30,000 | | 모자 | 1 |15,000 | </x-mail::table>

컴포넌트 커스터마이징

Markdown 메일 컴포넌트를 직접 수정하고 싶다면, 먼저 vendor:publish 명령으로 컴포넌트 파일을 애플리케이션으로 복사합니다:

php artisan vendor:publish --tag=laravel-mail

이 명령을 실행하면 resources/views/vendor/mail 디렉터리에 컴포넌트 파일이 생성됩니다. 이 디렉터리 아래에는 htmltext 두 개의 하위 디렉터리가 있으며, 각각 HTML 버전과 플레인 텍스트 버전의 컴포넌트 파일을 포함합니다. 이 파일들을 자유롭게 수정하면 됩니다.

CSS 커스터마이징

컴포넌트를 내보내면 resources/views/vendor/mail/html/themes 디렉터리에 default.css 파일이 생성됩니다. 이 파일의 CSS를 수정하면, 변경된 스타일이 Markdown 메일의 HTML 표현 안에 인라인 CSS로 자동 변환되어 적용됩니다.

완전히 새로운 테마를 만들고 싶다면, html/themes 디렉터리에 새 CSS 파일을 추가하세요. 파일을 저장한 뒤, config/mail.php 설정 파일의 theme 옵션을 새 파일명(확장자 제외)으로 변경하면 됩니다.

특정 Mailable에만 별도 테마를 적용하고 싶다면, 해당 Mailable 클래스에 $theme 프로퍼티를 선언하고 사용할 테마 이름을 지정하세요:

/** * 이 Mailable에 사용할 테마 이름 * * @var string */ public $theme = 'my-custom-theme';

메일 발송

메일을 발송하려면 Mail 파사드to 메서드를 사용합니다. to 메서드는 이메일 주소 문자열, 사용자 인스턴스, 또는 사용자 컬렉션을 인수로 받습니다. 객체나 컬렉션을 전달하면 메일러가 해당 객체의 emailname 속성을 자동으로 수신자 정보로 사용하므로, 객체에 이 속성들이 정의되어 있어야 합니다. 수신자를 지정한 뒤 send 메서드에 Mailable 클래스 인스턴스를 전달하면 메일이 발송됩니다.

<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Mail\OrderShipped; use App\Models\Order; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Mail; class OrderShipmentController extends Controller { /** * 주문을 배송 처리합니다. */ public function store(Request $request): RedirectResponse { $order = Order::findOrFail($request->order_id); // 주문 배송 처리... Mail::to($request->user())->send(new OrderShipped($order)); return redirect('/orders'); } }

to 외에도 cc(참조), bcc(숨은 참조)를 메서드 체이닝으로 함께 지정할 수 있습니다.

Mail::to($request->user()) ->cc($moreUsers) ->bcc($evenMoreUsers) ->send(new OrderShipped($order));

여러 수신자에게 반복 발송

수신자 목록을 순회하며 각자에게 메일을 보내야 할 때는 주의가 필요합니다. to 메서드는 수신자를 기존 목록에 추가하는 방식으로 동작하기 때문에, 루프 안에서 동일한 Mailable 인스턴스를 재사용하면 매 반복마다 이전 수신자들에게도 메일이 중복 발송됩니다. 따라서 반복마다 Mailable 인스턴스를 새로 생성해야 합니다.

foreach (['kim@example.com', 'lee@example.com'] as $recipient) { Mail::to($recipient)->send(new OrderShipped($order)); }

특정 메일러로 발송

기본적으로 Laravel은 config/mail.phpdefault 설정에 지정된 메일러를 사용합니다. 특정 메일러를 명시적으로 사용하고 싶다면 mailer 메서드를 체이닝합니다.

Mail::mailer('postmark') ->to($request->user()) ->send(new OrderShipped($order));

메일 큐 처리

메일 큐에 추가하기

메일 발송은 외부 SMTP 서버와 통신하는 작업이므로 응답 시간에 영향을 줄 수 있습니다. 사용자 경험을 위해 메일을 백그라운드 큐로 처리하는 것이 일반적입니다. Laravel의 통합 큐 API를 사용하면 send 대신 queue 메서드만 호출하면 됩니다.

Mail::to($request->user()) ->cc($moreUsers) ->bcc($evenMoreUsers) ->queue(new OrderShipped($order));

이 메서드는 자동으로 Job을 큐에 추가하여 백그라운드에서 메일을 발송합니다. 이 기능을 사용하려면 먼저 큐 설정을 완료해야 합니다.

발송 시간 지연하기

메일 발송 시점을 늦추고 싶다면 later 메서드를 사용합니다. 첫 번째 인수로 발송할 시각을 나타내는 DateTime 인스턴스를 전달합니다.

Mail::to($request->user()) ->cc($moreUsers) ->bcc($evenMoreUsers) ->later(now()->addMinutes(10), new OrderShipped($order));

특정 큐와 커넥션 지정하기

make:mail 명령으로 생성된 Mailable 클래스는 Illuminate\Bus\Queueable 트레이트를 사용하므로, onConnectiononQueue 메서드로 큐 커넥션과 큐 이름을 직접 지정할 수 있습니다.

$message = (new OrderShipped($order)) ->onConnection('sqs') ->onQueue('emails'); Mail::to($request->user()) ->cc($moreUsers) ->bcc($evenMoreUsers) ->queue($message);

기본적으로 항상 큐 처리하기

특정 Mailable 클래스를 항상 큐로 처리하고 싶다면 ShouldQueue 인터페이스를 구현합니다. 이렇게 하면 send 메서드를 호출하더라도 해당 Mailable은 자동으로 큐에 추가됩니다.

use Illuminate\Contracts\Queue\ShouldQueue; class OrderShipped extends Mailable implements ShouldQueue { // ... }

큐 Mailable과 데이터베이스 트랜잭션

데이터베이스 트랜잭션 내에서 큐 Mailable을 디스패치하면, 트랜잭션이 커밋되기 전에 큐 워커가 해당 Job을 먼저 처리할 수 있습니다. 이 경우 트랜잭션 내에서 변경하거나 생성한 모델 및 레코드가 아직 데이터베이스에 반영되지 않은 상태일 수 있어 예기치 않은 오류가 발생할 수 있습니다.

큐 커넥션의 after_commit 설정이 false인 경우에도, afterCommit 메서드를 호출하면 열린 트랜잭션이 모두 커밋된 후에 Mailable이 디스패치되도록 할 수 있습니다.

Mail::to($request->user())->send( (new OrderShipped($order))->afterCommit() );

또는 Mailable 클래스의 생성자에서 afterCommit을 호출하는 방법도 있습니다.

<?php namespace App\Mail; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; class OrderShipped extends Mailable implements ShouldQueue { use Queueable, SerializesModels; /** * 새 메시지 인스턴스를 생성합니다. */ public function __construct() { $this->afterCommit(); } }

NOTE

이 문제를 우회하는 방법에 대한 자세한 내용은 큐 Job과 데이터베이스 트랜잭션 문서를 참고하세요.

Mailable 렌더링

메일을 실제로 전송하지 않고 HTML 콘텐츠만 추출해야 할 때가 있습니다. 이럴 때는 Mailable 객체의 render 메서드를 호출하면 됩니다. 이 메서드는 평가된 HTML을 문자열로 반환합니다.

use App\Mail\InvoicePaid; use App\Models\Invoice; $invoice = Invoice::find(1); return (new InvoicePaid($invoice))->render();

브라우저에서 Mailable 미리보기

메일 템플릿을 디자인할 때, 매번 실제 메일을 발송해 확인하는 것은 번거롭습니다. Laravel에서는 Mailable 객체를 라우트 클로저나 컨트롤러에서 직접 반환하면, 브라우저에서 렌더링 결과를 바로 확인할 수 있습니다. Blade 뷰를 브라우저로 확인하는 것과 동일한 방식입니다.

Route::get('/mailable', function () { $invoice = App\Models\Invoice::find(1); return new App\Mail\InvoicePaid($invoice); });

NOTE

이 방법은 로컬 개발 환경에서만 사용하세요. 프로덕션 환경에서 Mailable을 라우트로 노출하면 민감한 정보가 외부에 공개될 수 있습니다.

Mailable 현지화

Laravel은 현재 요청의 로케일과 다른 언어로 메일을 발송할 수 있으며, 메일이 큐에 등록된 경우에도 해당 로케일을 기억합니다.

Mail 파사드의 locale 메서드로 원하는 언어를 지정하면, Mailable 템플릿을 렌더링하는 동안 애플리케이션이 해당 로케일로 전환되고, 렌더링이 완료되면 이전 로케일로 되돌아옵니다.

Mail::to($request->user())->locale('ko')->send( new OrderShipped($order) );

사용자별 선호 로케일

사용자마다 선호하는 언어를 데이터베이스에 저장해두는 경우가 많습니다. 모델에 HasLocalePreference 컨트랙트를 구현하면, 메일 발송 시 Laravel이 해당 사용자의 저장된 로케일을 자동으로 사용합니다.

use Illuminate\Contracts\Translation\HasLocalePreference; class User extends Model implements HasLocalePreference { /** * 사용자의 선호 로케일을 반환합니다. */ public function preferredLocale(): string { return $this->locale; } }

인터페이스를 구현하면 메일 및 알림 발송 시 선호 로케일이 자동으로 적용됩니다. 따라서 locale 메서드를 별도로 호출할 필요가 없습니다.

Mail::to($request->user())->send(new OrderShipped($order));

NOTE

다국어 서비스를 운영하는 경우, User 모델 외에도 로케일 정보를 가지는 다른 모델(예: Tenant, Organization)에도 동일하게 HasLocalePreference를 구현할 수 있습니다.

테스트

Mailable 콘텐츠 테스트

Laravel은 Mailable의 구조를 검사하고 콘텐츠를 검증하는 다양한 메서드를 제공합니다. 주요 assertion 메서드는 다음과 같습니다: assertSeeInHtml, assertDontSeeInHtml, assertSeeInOrderInHtml, assertSeeInText, assertDontSeeInText, assertSeeInOrderInText, assertHasAttachment, assertHasAttachedData, assertHasAttachmentFromStorage, assertHasAttachmentFromStorageDisk.

이름에서 알 수 있듯이, Html 계열 메서드는 Mailable의 HTML 버전에 특정 문자열이 포함되어 있는지를, Text 계열 메서드는 일반 텍스트 버전에 특정 문자열이 포함되어 있는지를 확인합니다.

Pest

use App\Mail\InvoicePaid; use App\Models\User; test('mailable content', function () { $user = User::factory()->create(); $mailable = new InvoicePaid($user); $mailable->assertFrom('jeffrey@example.com'); $mailable->assertTo('taylor@example.com'); $mailable->assertHasCc('abigail@example.com'); $mailable->assertHasBcc('victoria@example.com'); $mailable->assertHasReplyTo('tyler@example.com'); $mailable->assertHasSubject('Invoice Paid'); $mailable->assertHasTag('example-tag'); $mailable->assertHasMetadata('key', 'value'); $mailable->assertSeeInHtml($user->email); $mailable->assertSeeInHtml('Invoice Paid'); $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']); $mailable->assertSeeInText($user->email); $mailable->assertSeeInOrderInText(['Invoice Paid', 'Thanks']); $mailable->assertHasAttachment('/path/to/file'); $mailable->assertHasAttachment(Attachment::fromPath('/path/to/file')); $mailable->assertHasAttachedData($pdfData, 'name.pdf', ['mime' => 'application/pdf']); $mailable->assertHasAttachmentFromStorage('/path/to/file', 'name.pdf', ['mime' => 'application/pdf']); $mailable->assertHasAttachmentFromStorageDisk('s3', '/path/to/file', 'name.pdf', ['mime' => 'application/pdf']); });

PHPUnit

use App\Mail\InvoicePaid; use App\Models\User; public function test_mailable_content(): void { $user = User::factory()->create(); $mailable = new InvoicePaid($user); $mailable->assertFrom('jeffrey@example.com'); $mailable->assertTo('taylor@example.com'); $mailable->assertHasCc('abigail@example.com'); $mailable->assertHasBcc('victoria@example.com'); $mailable->assertHasReplyTo('tyler@example.com'); $mailable->assertHasSubject('Invoice Paid'); $mailable->assertHasTag('example-tag'); $mailable->assertHasMetadata('key', 'value'); $mailable->assertSeeInHtml($user->email); $mailable->assertSeeInHtml('Invoice Paid'); $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']); $mailable->assertSeeInText($user->email); $mailable->assertSeeInOrderInText(['Invoice Paid', 'Thanks']); $mailable->assertHasAttachment('/path/to/file'); $mailable->assertHasAttachment(Attachment::fromPath('/path/to/file')); $mailable->assertHasAttachedData($pdfData, 'name.pdf', ['mime' => 'application/pdf']); $mailable->assertHasAttachmentFromStorage('/path/to/file', 'name.pdf', ['mime' => 'application/pdf']); $mailable->assertHasAttachmentFromStorageDisk('s3', '/path/to/file', 'name.pdf', ['mime' => 'application/pdf']); }

Mailable 발송 테스트

Mailable의 콘텐츠 테스트발송 여부 테스트는 분리해서 작성하는 것을 권장합니다. 발송 여부를 테스트할 때는 Mailable의 내용 자체보다, 특정 상황에서 Laravel이 해당 Mailable을 발송하도록 지시받았는지를 확인하는 것으로 충분합니다.

Mail 파사드의 fake 메서드를 호출하면 실제 메일 발송을 막을 수 있습니다. 이후 Mailable이 올바르게 발송 지시되었는지, 전달된 데이터가 올바른지를 assertion으로 검증할 수 있습니다.

Pest

<?php use App\Mail\OrderShipped; use Illuminate\Support\Facades\Mail; test('orders can be shipped', function () { Mail::fake(); // 주문 배송 처리 로직 실행... // 발송된 메일이 없는지 확인... Mail::assertNothingSent(); // 특정 Mailable이 발송되었는지 확인... Mail::assertSent(OrderShipped::class); // 특정 Mailable이 2번 발송되었는지 확인... Mail::assertSent(OrderShipped::class, 2); // 특정 이메일 주소로 발송되었는지 확인... Mail::assertSent(OrderShipped::class, 'example@laravel.com'); // 여러 이메일 주소로 발송되었는지 확인... Mail::assertSent(OrderShipped::class, ['example@laravel.com', '...']); // 특정 Mailable이 발송되지 않았는지 확인... Mail::assertNotSent(AnotherMailable::class); // 총 3개의 Mailable이 발송되었는지 확인... Mail::assertSentCount(3); });

PHPUnit

<?php namespace Tests\Feature; use App\Mail\OrderShipped; use Illuminate\Support\Facades\Mail; use Tests\TestCase; class ExampleTest extends TestCase { public function test_orders_can_be_shipped(): void { Mail::fake(); // 주문 배송 처리 로직 실행... // 발송된 메일이 없는지 확인... Mail::assertNothingSent(); // 특정 Mailable이 발송되었는지 확인... Mail::assertSent(OrderShipped::class); // 특정 Mailable이 2번 발송되었는지 확인... Mail::assertSent(OrderShipped::class, 2); // 특정 이메일 주소로 발송되었는지 확인... Mail::assertSent(OrderShipped::class, 'example@laravel.com'); // 여러 이메일 주소로 발송되었는지 확인... Mail::assertSent(OrderShipped::class, ['example@laravel.com', '...']); // 특정 Mailable이 발송되지 않았는지 확인... Mail::assertNotSent(AnotherMailable::class); // 총 3개의 Mailable이 발송되었는지 확인... Mail::assertSentCount(3); } }

백그라운드 큐로 메일을 발송하는 경우에는 assertSent 대신 assertQueued 계열 메서드를 사용해야 합니다.

Mail::assertQueued(OrderShipped::class); Mail::assertNotQueued(OrderShipped::class); Mail::assertNothingQueued(); Mail::assertQueuedCount(3);

assertSent, assertNotSent, assertQueued, assertNotQueued 메서드에는 클로저를 전달할 수 있습니다. 클로저가 true를 반환하는 Mailable이 하나라도 존재하면 해당 assertion은 통과합니다.

Mail::assertSent(function (OrderShipped $mail) use ($order) { return $mail->order->id === $order->id; });

클로저 내부에서 Mailable 인스턴스의 다양한 검사 메서드를 활용할 수 있습니다.

Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($user) { return $mail->hasTo($user->email) && $mail->hasCc('...') && $mail->hasBcc('...') && $mail->hasReplyTo('...') && $mail->hasFrom('...') && $mail->hasSubject('...'); });

첨부 파일에 대한 검사도 마찬가지로 클로저 안에서 수행할 수 있습니다.

use Illuminate\Mail\Mailables\Attachment; Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) { return $mail->hasAttachment( Attachment::fromPath('/path/to/file') ->as('name.pdf') ->withMime('application/pdf') ); }); Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) { return $mail->hasAttachment( Attachment::fromStorageDisk('s3', '/path/to/file') ); }); Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($pdfData) { return $mail->hasAttachment( Attachment::fromData(fn () => $pdfData, 'name.pdf') ); });

메일이 발송되지 않았음을 확인하는 메서드로는 assertNotSentassertNotQueued 두 가지가 있습니다. 발송도, 큐 등록도 모두 이루어지지 않았음을 한 번에 검증하고 싶다면 assertNothingOutgoingassertNotOutgoing 메서드를 사용하세요.

Mail::assertNothingOutgoing(); Mail::assertNotOutgoing(function (OrderShipped $mail) use ($order) { return $mail->order->id === $order->id; });

메일

로컬 개발 환경에서의 메일 처리

실제 이메일 주소로 메일이 발송되지 않도록 하면서 메일 기능을 개발하고 싶을 때, Laravel은 몇 가지 편리한 방법을 제공합니다.

Log 드라이버

log 메일 드라이버를 사용하면 실제로 메일을 발송하는 대신, 모든 이메일 내용을 로그 파일에 기록합니다. 로그 파일은 storage/logs 디렉터리에서 확인할 수 있으며, 로컬 개발 환경에서 가장 간단하게 사용할 수 있는 방법입니다. 환경별 설정 방법에 대한 자세한 내용은 설정 문서를 참고하세요.

HELO / Mailtrap / Mailpit

또 다른 방법으로, HELOMailtrap 같은 서비스를 smtp 드라이버와 함께 사용할 수 있습니다. 이 서비스들은 실제 수신함처럼 메일을 확인할 수 있는 "가상 받은편지함"을 제공합니다. 실제 이메일 클라이언트와 유사한 환경에서 최종 렌더링 결과를 직접 눈으로 확인할 수 있다는 장점이 있습니다.

Laravel Sail을 사용하고 있다면, Mailpit으로 메일을 미리 볼 수 있습니다. Sail이 실행 중인 상태에서 http://localhost:8025에 접속하면 Mailpit 인터페이스를 확인할 수 있습니다.

NOTE

Mailtrap, HELO, Mailpit 모두 개발 단계에서 실수로 실제 사용자에게 메일이 발송되는 상황을 방지하는 데 유용합니다. 팀 단위로 개발할 경우 Mailtrap의 공유 받은편지함 기능이 특히 편리합니다.

전역 수신 주소 지정

마지막으로, Mail 파사드의 alwaysTo 메서드를 사용하면 모든 메일이 항상 특정 주소로만 발송되도록 고정할 수 있습니다. 이 메서드는 보통 서비스 프로바이더의 boot 메서드에서 호출합니다.

use Illuminate\Support\Facades\Mail; /** * 애플리케이션 서비스를 부트스트랩합니다. */ public function boot(): void { if ($this->app->environment('local')) { Mail::alwaysTo('taylor@example.com'); } }

이렇게 설정하면 로컬 환경에서는 수신자 주소에 관계없이 모든 메일이 지정된 주소로 전달됩니다. 실제 사용자 주소로 테스트 메일이 발송되는 사고를 예방할 수 있어, 팀 내 공용 개발 계정으로 지정해두면 유용합니다.

메일

이벤트

Laravel은 메일을 전송할 때 두 가지 이벤트를 디스패치합니다. MessageSending 이벤트는 메일이 실제로 전송되기 직전에, MessageSent 이벤트는 전송이 완료된 직후에 디스패치됩니다.

NOTE

이 두 이벤트는 메일이 실제로 전송될 때 디스패치됩니다. 큐에 등록되는 시점이 아니라는 점에 주의하세요. 큐로 처리하는 메일이라면, 큐 워커가 메일을 실제로 발송할 때 이벤트가 발생합니다.

애플리케이션에서 이 이벤트에 대한 이벤트 리스너를 아래와 같이 등록할 수 있습니다:

use Illuminate\Mail\Events\MessageSending; // use Illuminate\Mail\Events\MessageSent; class LogMessage { /** * 이벤트를 처리합니다. */ public function handle(MessageSending $event): void { // ... } }

예를 들어, 발송 전 메일 내용을 로그에 기록하거나, 발송 후 전송 이력을 데이터베이스에 저장하는 용도로 활용할 수 있습니다.

메일

커스텀 트랜스포트

Laravel은 다양한 메일 트랜스포트를 기본으로 제공하지만, 기본 지원 목록에 없는 외부 서비스를 통해 메일을 발송하고 싶다면 직접 트랜스포트를 구현할 수 있습니다. 커스텀 트랜스포트를 만들려면 Symfony\Component\Mailer\Transport\AbstractTransport 클래스를 상속한 뒤, doSend__toString() 메서드를 구현하면 됩니다.

use MailchimpTransactional\ApiClient; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\AbstractTransport; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\MessageConverter; class MailchimpTransport extends AbstractTransport { /** * 새 Mailchimp 트랜스포트 인스턴스를 생성합니다. */ public function __construct( protected ApiClient $client, ) { parent::__construct(); } /** * {@inheritDoc} */ protected function doSend(SentMessage $message): void { $email = MessageConverter::toEmail($message->getOriginalMessage()); $this->client->messages->send(['message' => [ 'from_email' => $email->getFrom(), 'to' => collect($email->getTo())->map(function (Address $email) { return ['email' => $email->getAddress(), 'type' => 'to']; })->all(), 'subject' => $email->getSubject(), 'text' => $email->getTextBody(), ]]); } /** * 트랜스포트의 문자열 표현을 반환합니다. */ public function __toString(): string { return 'mailchimp'; } }

커스텀 트랜스포트 클래스를 정의했다면, Mail 파사드의 extend 메서드를 사용해 Laravel에 등록합니다. 이 작업은 보통 AppServiceProviderboot 메서드 안에서 수행합니다. extend에 전달하는 클로저는 $config 인수를 받는데, 이 값은 config/mail.php에서 해당 메일러에 대해 정의한 설정 배열입니다.

use App\Mail\MailchimpTransport; use Illuminate\Support\Facades\Mail; /** * 애플리케이션 서비스를 부트스트랩합니다. */ public function boot(): void { Mail::extend('mailchimp', function (array $config = []) { return new MailchimpTransport(/* ... */); }); }

트랜스포트를 등록했으면, config/mail.php에 해당 트랜스포트를 사용하는 메일러 항목을 추가합니다.

'mailchimp' => [ 'transport' => 'mailchimp', // ... ],

Symfony 추가 트랜스포트

Laravel은 Mailgun, Postmark 등 일부 Symfony 공식 메일 트랜스포트를 기본으로 지원합니다. 이 외에도 Symfony가 관리하는 다른 트랜스포트를 Laravel에 추가할 수 있습니다. Composer로 필요한 패키지를 설치한 뒤 트랜스포트를 등록하면 됩니다.

예를 들어, "Brevo"(구 "Sendinblue") 트랜스포트를 사용하려면 아래와 같이 패키지를 설치합니다.

composer require symfony/brevo-mailer symfony/http-client

패키지 설치 후, 애플리케이션의 config/services.php에 Brevo API 키를 추가합니다.

'brevo' => [ 'key' => 'your-api-key', ],

다음으로, 서비스 프로바이더의 boot 메서드에서 Mail 파사드의 extend 메서드를 사용해 트랜스포트를 등록합니다.

use Illuminate\Support\Facades\Mail; use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; use Symfony\Component\Mailer\Transport\Dsn; /** * 애플리케이션 서비스를 부트스트랩합니다. */ public function boot(): void { Mail::extend('brevo', function () { return (new BrevoTransportFactory)->create( new Dsn( 'brevo+api', 'default', config('services.brevo.key') ) ); }); }

트랜스포트 등록이 완료되면, config/mail.php에 해당 트랜스포트를 사용하는 메일러 항목을 추가합니다.

'brevo' => [ 'transport' => 'brevo', // ... ],

NOTE

커스텀 트랜스포트를 여러 개 등록할 때는 각 트랜스포트 이름(extend의 첫 번째 인수)과 config/mail.phptransport 값이 정확히 일치해야 합니다. 이름이 다르면 올바른 트랜스포트를 찾지 못해 메일 발송에 실패합니다.

이 문서는 Laravel 공식 문서(MIT)를 한국 개발자를 위해 번역·재구성한 것입니다.

번역일: 2026년 7월 2일