메일
번역일: 2026년 7월 2일
메일
- 소개
- Mailable 생성
- Mailable 작성
- Markdown Mailable
- 메일 발송
- Mailable 렌더링
- Mailable 현지화
- 테스트
- 로컬 개발 환경에서의 메일
- 이벤트
- 커스텀 Transport
소개
메일 발송은 대부분의 웹 애플리케이션에서 빠질 수 없는 기능입니다. Laravel은 Symfony Mailer 컴포넌트를 기반으로 하는 깔끔하고 간결한 메일 API를 제공합니다. SMTP, Mailgun, Postmark, Resend, Amazon SES 등 다양한 드라이버를 통해 원하는 서비스로 손쉽게 메일을 발송할 수 있습니다.
설정
메일 관련 설정은 config/mail.php 파일에서 관리합니다. 이 파일에는 각 메일러(mailer)별 설정을 독립적으로 정의할 수 있으며, 서로 다른 드라이버와 옵션을 조합할 수 있습니다. 즉, 애플리케이션 내에서 상황에 따라 서로 다른 메일 서비스를 사용하는 것이 가능합니다.
mailers 항목 안에 사용할 메일러들을 정의하고, default 항목으로 기본 메일러를 지정합니다. 대부분의 설정값은 .env 환경 변수로 관리하는 것이 권장됩니다.
드라이버 사전 요구사항
Mailgun, Postmark, Resend, MailerSend 등 API 기반 드라이버는 SMTP보다 간단하고 빠른 경우가 많습니다. 가능하다면 이러한 API 드라이버를 사용하는 것을 권장합니다.
Mailgun 드라이버
Mailgun을 사용하려면 먼저 Composer로 Symfony의 Mailgun Mailer transport를 설치합니다.
composer require symfony/mailgun-mailer symfony/http-client그런 다음 config/mail.php에서 default 값을 mailgun으로 지정하고, mailers 배열에 아래 설정을 추가합니다.
'mailgun' => [
'transport' => 'mailgun',
// 'client' => [
// 'timeout' => 5,
// ],
],기본 메일러를 설정한 뒤, config/services.php에 다음 항목을 추가합니다.
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
'scheme' => 'https',
],미국 외 지역의 Mailgun 리전을 사용하는 경우 endpoint 값을 해당 리전에 맞게 변경합니다.
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
'endpoint' => env('MAILGUN_ENDPOINT', 'api.eu.mailgun.net'),
'scheme' => 'https',
],Postmark 드라이버
Postmark를 사용하려면 Composer로 Symfony의 Postmark Mailer transport를 설치합니다.
composer require symfony/postmark-mailer symfony/http-client그런 다음 config/mail.php의 default 값을 postmark로 지정합니다. 기본 메일러를 설정한 뒤, config/services.php에 다음 항목을 추가합니다.
'postmark' => [
'token' => env('POSTMARK_TOKEN'),
],특정 메일러에서 사용할 Postmark 메시지 스트림을 지정하려면 메일러 설정 배열에 message_stream_id 항목을 추가합니다.
'postmark' => [
'transport' => 'postmark',
'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],이렇게 하면 메시지 스트림이 다른 여러 Postmark 메일러를 구성할 수도 있습니다.
Resend 드라이버
Resend를 사용하려면 Composer로 Resend PHP SDK를 설치합니다.
composer require resend/resend-laravel그런 다음 config/mail.php의 default 값을 resend로 지정합니다. 기본 메일러를 설정한 뒤, config/services.php에 다음 항목을 추가합니다.
'resend' => [
'key' => env('RESEND_KEY'),
],SES 드라이버
Amazon SES를 사용하려면 먼저 Amazon AWS SDK for PHP를 설치합니다.
composer require aws/aws-sdk-php그런 다음 config/mail.php의 default 값을 ses로 지정하고, 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',
],
);
}메일 발송 시 Laravel이 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을 위한 자체 메일 드라이버를 제공합니다. 해당 패키지는 Composer로 설치합니다.
composer require mailersend/laravel-driver패키지 설치 후 .env에 MAILERSEND_API_KEY 환경 변수를 추가합니다. 또한 MAIL_MAILER 환경 변수를 mailersend로 설정합니다.
MAIL_MAILER=mailersend
MAIL_FROM_ADDRESS=app@yourdomain.com
MAIL_FROM_NAME="앱 이름"
MAILERSEND_API_KEY=your-api-key마지막으로 config/services.php에 MailerSend 설정을 추가합니다.
'mailersend' => [
'api_key' => env('MAILERSEND_API_KEY'),
],MailerSend의 호스팅 템플릿 사용 방법 등 상세 내용은 MailerSend 드라이버 공식 문서를 참고하세요.
장애 조치(Failover) 설정
외부 메일 서비스가 일시적으로 장애를 겪는 상황에 대비해, 하나의 메일러가 실패했을 때 자동으로 다음 메일러로 전환하는 백업 전송 설정을 구성할 수 있습니다.
config/mail.php에서 failover transport를 사용하는 메일러를 정의하고, 시도할 메일러 목록을 mailers 배열로 지정합니다.
'mailers' => [
'failover' => [
'transport' => 'failover',
'mailers' => [
'postmark',
'mailgun',
'sendmail',
],
],
// ...
],정의한 뒤, default 키 값을 failover로 설정합니다.
'default' => env('MAIL_MAILER', 'failover'),라운드 로빈(Round Robin) 설정
roundrobin transport를 사용하면 여러 메일러에 발송 부하를 분산시킬 수 있습니다. 설정 방법은 failover와 동일하나 transport 값을 roundrobin으로 지정합니다.
'mailers' => [
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
],
// ...
],정의한 뒤, default 키 값을 roundrobin으로 설정합니다.
'default' => env('MAIL_MAILER', 'roundrobin'),라운드 로빈 transport는 설정된 메일러 목록에서 순서대로 메일러를 선택하여 발송합니다. failover가 *고가용성(HA)*을 위한 설정이라면, roundrobin은 *부하 분산(load balancing)*을 위한 설정입니다.
Mailable 생성
Laravel에서 각각의 메일 유형은 "Mailable" 클래스로 표현됩니다. 이 클래스들은 app/Mail 디렉터리에 위치하며, Artisan 명령어로 생성할 수 있습니다.
php artisan make:mail OrderShippedMailable 작성
Mailable 클래스를 생성했다면, 파일을 열어 내용을 살펴보세요. Mailable 클래스의 설정은 주로 envelope, content, attachments 메서드를 통해 이루어집니다.
envelope메서드: 발신자, 수신자, 제목 등 메시지 봉투 정보를 반환합니다.content메서드: 메일 본문을 렌더링할 Blade 템플릿 등 콘텐츠 정보를 반환합니다.attachments메서드: 첨부 파일 목록을 반환합니다.
발신자 설정
Envelope 사용
먼저 메일 발신자, 즉 "보내는 사람"을 설정하는 방법을 살펴보겠습니다. 발신자는 두 가지 방법으로 설정할 수 있습니다.
첫 번째는 envelope 메서드에서 from 주소를 직접 지정하는 방법입니다.
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Envelope;
/**
* 메시지 봉투(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 클래스마다 from을 지정하는 것은 번거롭습니다. 대신 config/mail.php에서 전역 발신자 주소를 지정할 수 있습니다.
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],또한 전역 reply_to 주소도 설정할 수 있습니다.
'reply_to' => ['address' => 'example@example.com', 'name' => 'App Name'],뷰 설정
content 메서드에서 view를 지정하여 메일 본문을 렌더링할 템플릿을 설정합니다. 일반적으로 Blade 템플릿을 사용하여 메일 본문의 HTML을 구성합니다.
/**
* 메시지 콘텐츠 정의를 반환합니다.
*/
public function content(): Content
{
return new Content(
view: 'mail.orders.shipped',
);
}NOTE
메일 전용 Blade 템플릿을 resources/views/mail 디렉터리 하위에 별도로 관리하면 뷰 파일을 체계적으로 정리할 수 있습니다.
플레인 텍스트 메일
HTML 외에 플레인 텍스트 버전도 함께 제공하려면 text 파라미터에 템플릿을 지정합니다. view와 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 프로퍼티 사용
Blade 템플릿에서 사용할 데이터를 전달하는 가장 간단한 방법은 Mailable 클래스의 public 프로퍼티로 선언하는 것입니다. public 프로퍼티로 선언된 값은 자동으로 뷰에서 사용 가능합니다.
예를 들어, 생성자에서 Order 모델을 받아 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',
);
}
}이렇게 설정하면 Blade 템플릿에서 $order로 바로 접근할 수 있습니다.
<div>
가격: {{ $order->price }}
</div>`with` 파라미터 사용
뷰에 전달되는 데이터의 형식을 직접 제어하고 싶다면, Content 생성자의 with 파라미터를 통해 데이터를 전달할 수 있습니다. 이 경우에는 생성자 프로퍼티를 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;
/**
* 새 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 타입을 지정하려면 as와 withMime 메서드를 체이닝합니다.
/**
* 메시지 첨부 파일을 반환합니다.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [
Attachment::fromPath('/path/to/file')
->as('주문명세서.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('주문명세서.pdf')
->withMime('application/pdf'),
];
}원시 데이터(Raw Data) 첨부
파일 경로 없이 메모리상의 바이너리 데이터를 직접 첨부하려면 fromData 메서드를 사용합니다. 예를 들어, PDF를 동적으로 생성하여 디스크에 저장하지 않고 바로 첨부할 때 유용합니다.
/**
* 메시지 첨부 파일을 반환합니다.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [
Attachment::fromData(fn () => $this->pdf, '주문명세서.pdf')
->withMime('application/pdf'),
];
}인라인 첨부
메일 본문 HTML에 이미지를 직접 삽입하려면(인라인 이미지) Blade 템플릿 내에서 $message 변수를 사용합니다. $message 변수는 Laravel이 모든 메일 템플릿에 자동으로 주입하므로 별도로 선언할 필요가 없습니다.
<body>
안녕하세요!
<img src="{{ $message->embed($pathToImage) }}">
</body>WARNING
$message 변수는 플레인 텍스트 템플릿에서는 사용할 수 없습니다. 플레인 텍스트 메시지는 인라인 첨부를 지원하지 않습니다.
원시 이미지 데이터 삽입
이미 Base64 인코딩된 이미지 문자열 등 원시 이미지 데이터를 삽입하려면 embedData 메서드를 사용합니다.
<body>
인라인 이미지 예시입니다.
<img src="{{ $message->embedData($data, 'example-image.jpg') }}">
</body>Attachable 객체
단순한 파일 경로나 데이터를 첨부하는 것 외에도, 애플리케이션의 특정 모델을 직접 첨부 가능한 객체로 만들 수 있습니다. 예를 들어, Photo 모델을 메일에 첨부해야 하는 경우를 생각해보세요. 이런 상황에서는 Attachable 인터페이스를 구현하면 됩니다.
Attachable 인터페이스를 구현하는 클래스는 toMailAttachment 메서드를 정의해야 하며, 이 메서드는 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');
}
}Attachable 객체를 구현했다면, attachments 메서드에서 해당 객체 인스턴스를 그대로 반환할 수 있습니다.
/**
* 메시지 첨부 파일을 반환합니다.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [$this->photo];
}첨부 데이터는 Amazon S3와 같은 원격 저장소에 있을 수도 있습니다. Laravel의 파일시스템 디스크에 저장된 데이터로 Attachment 인스턴스를 생성하는 것도 가능합니다.
// 기본 디스크에서 Attachment 생성
return Attachment::fromStorage($this->path);
// 특정 디스크에서 Attachment 생성
return Attachment::fromStorageDisk('backblaze', $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 같은 서드파티 메일 서비스는 메시지에 "태그"와 "메타데이터"를 추가하는 기능을 제공합니다. 이를 통해 애플리케이션에서 발송한 메일을 분류하고 추적할 수 있습니다.
Mailable의 Envelope에 tags와 metadata를 지정하세요.
use Illuminate\Mail\Mailables\Envelope;
/**
* 메시지 봉투(Envelope)를 반환합니다.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '주문이 발송되었습니다',
tags: ['shipment'],
metadata: [
'order_id' => $this->order->id,
],
);
}Mailgun 드라이버를 사용하는 경우, 태그와 메타데이터에 대한 자세한 내용은 Mailgun 공식 문서를 참고하세요. Postmark의 경우 태그와 메타데이터 문서를 참고하세요.
Amazon SES로 메일을 발송한다면 metadata 메서드를 이용해 SES 태그를 첨부해야 합니다.
Symfony 메시지 커스터마이징
Laravel의 메일 기능은 Symfony Mailer를 기반으로 동작합니다. 따라서 메시지 발송 전에 Symfony Message 인스턴스에 직접 접근하여 세부 설정을 변경할 수 있습니다. Envelope의 using 파라미터에 콜백을 등록하면 됩니다.
use Illuminate\Mail\Mailables\Envelope;
use Symfony\Component\Mime\Email;
/**
* 메시지 봉투(Envelope)를 반환합니다.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '주문이 발송되었습니다',
using: [
function (Email $message) {
// ...
},
],
);
}Markdown Mailable
Markdown Mailable을 사용하면 Laravel의 사전 빌드된 메일 UI 컴포넌트를 활용하면서 동시에 맞춤형 긴 메시지도 손쉽게 작성할 수 있습니다. Markdown으로 작성된 메시지는 자동으로 아름다운 반응형 HTML 이메일로 변환되며, 플레인 텍스트 버전도 함께 생성됩니다.
Markdown Mailable 생성
Markdown 템플릿과 함께 Mailable을 생성하려면 make:mail Artisan 명령어에 --markdown 옵션을 사용합니다.
php artisan make:mail OrderShipped --markdown=mail.orders.shipped생성된 Mailable의 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>
<h1 id="button-component">주문이 발송되었습니다</h1>
주문이 발송되었습니다!
<x-mail::button :url="$url">
주문 확인하기
</x-mail::button>
감사합니다,<br>
{{ config('app.name') }}
</x-mail::message>NOTE
Markdown 이메일 작성 시 들여쓰기를 과도하게 사용하지 마세요. Markdown 표준에 따라 4개 이상의 공백으로 들여쓴 내용은 코드 블록으로 처리됩니다.
버튼 컴포넌트
버튼 컴포넌트는 중앙 정렬된 버튼 링크를 렌더링합니다. 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 메일 컴포넌트를 직접 커스터마이징하고 싶다면, 먼저 vendor:publish Artisan 명령어로 컴포넌트 파일들을 프로젝트로 내보냅니다.
php artisan vendor:publish --tag=laravel-mail이 명령어를 실행하면 resources/views/vendor/mail 디렉터리에 컴포넌트 파일들이 생성됩니다. mail 디렉터리 안에는 html과 text 두 개의 하위 디렉터리가 있으며, 각각 HTML과 플레인 텍스트 버전 컴포넌트가 포함되어 있습니다. 이 파일들을 원하는 대로 수정하세요.
CSS 커스터마이징
컴포넌트 내보내기 후 resources/views/vendor/mail/html/themes 디렉터리에 default.css 파일이 생성됩니다. 이 CSS를 수정하면 Markdown 이메일의 스타일이 자동으로 인라인 CSS로 변환되어 적용됩니다.
완전히 새로운 테마를 만들려면 themes 디렉터리에 CSS 파일을 새로 추가하고, config/mail.php의 theme 옵션에 해당 파일 이름을 지정합니다.
특정 Mailable에만 다른 테마를 적용하려면 해당 Mailable 클래스의 $theme 프로퍼티에 테마 이름을 설정합니다.
/**
* 사용할 테마 이름
*
* @var string
*/
public $theme = 'invoice';메일 발송
메일을 발송하려면 Mail 파사드의 to 메서드를 사용합니다. to 메서드는 이메일 주소 문자열, 사용자 인스턴스, 또는 사용자 컬렉션을 인수로 받습니다. 문자열이 아닌 객체나 컬렉션을 전달하면 Mailer가 해당 객체의 email과 name 프로퍼티를 자동으로 사용합니다. 따라서 해당 속성들이 모델에 있는지 확인하세요. 수신자를 지정한 뒤 send 메서드에 Mailable 클래스 인스턴스를 전달합니다.
<?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 인스턴스를 생성해야 한다는 것입니다. to 메서드는 수신자를 기존 목록에 추가하기 때문에, 같은 인스턴스를 재사용하면 이전 수신자들에게도 메일이 중복 발송됩니다.
foreach (['taylor@example.com', 'dries@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));이 메서드는 자동으로 큐에 Job을 등록하여 백그라운드에서 메일을 발송합니다. 이 기능을 사용하기 전에 큐 설정을 완료해야 합니다.
지연 발송
큐에 등록된 메일을 일정 시간 이후에 발송하려면 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 계약(contract)을 구현하면 됩니다. 이렇게 하면 send 메서드를 사용하더라도 자동으로 큐에 등록됩니다.
use Illuminate\Contracts\Queue\ShouldQueue;
class OrderShipped extends Mailable implements ShouldQueue
{
// ...
}Mailable 미들웨어
Mailable 클래스에도 미들웨어를 정의할 수 있습니다. middleware 메서드를 Mailable 클래스에 추가하면 됩니다.
use Illuminate\Queue\Middleware\WithoutOverlapping;
/**
* Mailable이 통과할 미들웨어를 반환합니다.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new WithoutOverlapping('key')];
}Mailable 렌더링
실제로 메일을 발송하지 않고 Mailable의 HTML 내용만 얻고 싶을 때가 있습니다. 이런 경우 Mailable 인스턴스의 render 메서드를 호출합니다. 이 메서드는 렌더링된 HTML 문자열을 반환합니다.
use App\Mail\InvoicePaid;
use App\Models\Invoice;
$invoice = Invoice::find(1);
return (new InvoicePaid($invoice))->render();브라우저에서 Mailable 미리보기
개발 중에 이메일 템플릿 디자인을 실제 메일 클라이언트 없이도 브라우저에서 바로 확인하면 훨씬 편리합니다. 이를 위해 라우트 클로저나 컨트롤러에서 Mailable을 직접 반환할 수 있습니다.
Route::get('/mailable', function () {
$invoice = App\Models\Invoice::find(1);
return new App\Mail\InvoicePaid($invoice);
});Mailable 현지화
Laravel은 현재 요청의 로케일이 아닌 다른 로케일로 메일을 발송하는 기능을 제공합니다. 큐에 등록된 메일에도 설정한 로케일이 유지됩니다.
Mail 파사드의 locale 메서드로 원하는 언어를 지정합니다.
Mail::to($request->user())->locale('ko')->send(
new OrderShipped($order)
);사용자 선호 로케일
애플리케이션에서 사용자별 선호 언어를 저장하는 경우가 있습니다. 모델에 HasLocalePreference 계약을 구현하면 메일 발송 시 자동으로 해당 로케일을 사용합니다.
use Illuminate\Contracts\Translation\HasLocalePreference;
class User extends Model implements HasLocalePreference
{
/**
* 사용자의 선호 로케일을 반환합니다.
*/
public function preferredLocale(): string
{
return $this->locale;
}
}인터페이스를 구현한 뒤에는 locale 메서드를 별도로 호출하지 않아도 됩니다. Laravel이 자동으로 사용자의 선호 로케일을 적용합니다.
Mail::to($request->user())->send(new OrderShipped($order));테스트
Mailable 내용 테스트
Laravel은 Mailable의 구조를 검사하는 다양한 메서드를 제공합니다. 또한 Mailable에 기대하는 내용이 포함되어 있는지 확인하는 편리한 메서드들도 제공합니다. 제공되는 주요 assertion 메서드는 다음과 같습니다: assertSeeInHtml, assertDontSeeInHtml, assertSeeInOrderInHtml, assertSeeInText, assertDontSeeInText, assertSeeInOrderInText, assertHasAttachment, assertHasAttachedData, assertHasAttachmentFromStorage, assertHasAttachmentFromStorageDisk.
이름에서 알 수 있듯, Html 메서드는 HTML 버전의 Mailable을, Text 메서드는 플레인 텍스트 버전을 기준으로 검사합니다.
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']);
});use App\Mail\InvoicePaid;
use App\Models\Invoice;
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 내용 테스트와 분리하는 것을 권장합니다. 메일이 실제로 발송되었는지 확인하는 방법은 Mail 파사드를 가짜(fake)로 교체하는 것입니다. Mail::fake()를 호출한 뒤 비즈니스 로직을 수행하고, Mailable이 발송되었는지, 수신자가 올바른지, 전달된 데이터가 맞는지 등을 assertion으로 검증합니다.
<?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);
// 두 번 발송되었는지 확인
Mail::assertSent(OrderShipped::class, 2);
// 특정 이메일 주소로 발송되었는지 확인
Mail::assertSent(OrderShipped::class, 'example@laravel.com');
// 특정 이메일 주소 목록으로 발송되었는지 확인
Mail::assertSent(OrderShipped::class, ['example@laravel.com', 'example2@laravel.com']);
// 특정 Mailable이 발송되지 않았는지 확인
Mail::assertNotSent(AnotherMailable::class);
// 총 3개의 Mailable이 발송되었는지 확인
Mail::assertSentCount(3);
});<?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);
// 두 번 발송되었는지 확인
Mail::assertSent(OrderShipped::class, 2);
// 특정 이메일 주소로 발송되었는지 확인
Mail::assertSent(OrderShipped::class, 'example@laravel.com');
// 특정 이메일 주소 목록으로 발송되었는지 확인
Mail::assertSent(OrderShipped::class, ['example@laravel.com', 'example2@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 메서드에는 클로저
메일
목차
소개
이메일 발송은 생각보다 훨씬 간단합니다. Laravel은 널리 사용되는 Symfony Mailer 컴포넌트를 기반으로 깔끔하고 직관적인 이메일 API를 제공합니다. SMTP, Mailgun, Postmark, Amazon SES, sendmail 등 다양한 드라이버를 지원하므로, 로컬 환경이나 클라우드 서비스 중 원하는 방식으로 빠르게 이메일을 발송할 수 있습니다.
설정
이메일 관련 설정은 config/mail.php 파일에서 관리합니다. 이 파일에는 여러 개의 mailer를 정의할 수 있으며, 각 mailer는 고유한 설정과 트랜스포트를 가질 수 있습니다. 예를 들어, 트랜잭션 이메일(회원가입 확인, 비밀번호 재설정 등)은 Postmark로, 대량 발송 이메일은 Amazon SES로 처리하는 식으로 상황에 맞게 구분하여 사용할 수 있습니다.
config/mail.php 내의 mailers 배열에는 Laravel이 지원하는 주요 드라이버별 샘플 설정이 포함되어 있습니다. default 값은 별도로 지정하지 않았을 때 기본으로 사용할 mailer를 결정합니다.
드라이버 / 트랜스포트 사전 요구사항
Mailgun, Postmark, MailerSend 같은 API 기반 드라이버는 SMTP 방식보다 설정이 간단하고 속도도 빠른 경우가 많습니다. 가능하다면 이러한 API 기반 드라이버 사용을 권장합니다.
Mailgun 드라이버
Mailgun 드라이버를 사용하려면 Composer로 다음 패키지를 설치합니다:
composer require symfony/mailgun-mailer symfony/http-client설치 후, config/mail.php의 default 옵션을 mailgun으로 변경합니다. 그리고 config/services.php에 아래 설정을 추가합니다:
'mailgun' => [
'transport' => 'mailgun',
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
],미국 리전이 아닌 다른 리전을 사용하는 경우, endpoint 옵션으로 해당 리전의 엔드포인트를 지정할 수 있습니다:
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
'endpoint' => env('MAILGUN_ENDPOINT', 'api.eu.mailgun.net'),
],Postmark 드라이버
Postmark 드라이버를 사용하려면 Composer로 다음 패키지를 설치합니다:
composer require symfony/postmark-mailer symfony/http-client설치 후, config/mail.php의 default 옵션을 postmark으로 변경합니다. 그리고 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'),
],이 방식을 활용하면 서로 다른 메시지 스트림을 사용하는 Postmark mailer를 여러 개 구성할 수도 있습니다.
SES 드라이버
Amazon SES 드라이버를 사용하려면 먼저 AWS PHP SDK를 설치해야 합니다:
composer require aws/aws-sdk-php설치 후, config/mail.php의 default 옵션을 ses로 변경하고, 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 임시 자격증명(Temporary Credentials)을 세션 토큰과 함께 사용하는 경우, 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'),
],이메일 발송 시 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=mailersendMAIL_FROM_ADDRESS=app@yourdomain.comMAIL_FROM_NAME="App Name"MAILERSEND_API_KEY=your-api-key호스팅 템플릿 사용 방법 등 MailerSend에 대한 자세한 내용은 MailerSend 드라이버 공식 문서를 참고하세요.
페일오버 설정
외부 이메일 서비스가 장애 상황일 때를 대비해 백업 mailer를 미리 구성해 둘 수 있습니다. 이를 페일오버(Failover) 설정이라고 합니다.
config/mail.php에서 failover 트랜스포트를 사용하는 mailer를 정의하고, mailers 배열에 우선순위 순서대로 사용할 mailer 목록을 지정합니다:
'mailers' => [
'failover' => [
'transport' => 'failover',
'mailers' => [
'postmark',
'mailgun',
'sendmail',
],
],
// ...
],NOTE
페일오버는 목록 순서대로 동작합니다. postmark가 실패하면 mailgun을 시도하고, 그것도 실패하면 sendmail을 사용합니다.
페일오버 mailer를 기본값으로 설정하려면 config/mail.php의 default 값을 변경합니다:
'default' => env('MAIL_MAILER', 'failover'),라운드 로빈 설정
roundrobin 트랜스포트를 사용하면 이메일 발송 부하를 여러 mailer에 분산시킬 수 있습니다. config/mail.php에서 roundrobin 트랜스포트를 사용하는 mailer를 정의합니다:
'mailers' => [
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
],
// ...
],라운드 로빈 mailer를 기본값으로 설정하려면 config/mail.php의 default 값을 변경합니다:
'default' => env('MAIL_MAILER', 'roundrobin'),라운드 로빈 트랜스포트는 설정된 mailer 목록에서 무작위로 시작한 뒤, 이후 이메일마다 순서대로 다음 mailer로 전환합니다.
NOTE
페일오버 vs 라운드 로빈: 페일오버는 장애 대비를 위한 고가용성(High Availability) 목적이고, 라운드 로빈은 발송 부하를 균등하게 나누는 로드 밸런싱(Load Balancing) 목적입니다. 두 방식의 목적이 다르므로, 상황에 맞게 선택하세요.
메일
Mailable 클래스 생성하기
Laravel 애플리케이션에서 발송하는 각각의 이메일 유형은 "mailable" 클래스로 표현됩니다. 이 클래스들은 app/Mail 디렉터리에 저장됩니다. 처음에는 이 디렉터리가 보이지 않을 수 있지만, 아래 Artisan 명령어로 첫 번째 mailable 클래스를 생성하면 자동으로 만들어집니다.
php artisan make:mail OrderShipped메일
Mailable 클래스 작성하기
Mailable 클래스를 생성했다면, 열어서 내부 구조를 살펴봅시다. Mailable 클래스의 설정은 주로 envelope, content, attachments 세 가지 메서드를 통해 이루어집니다.
envelope메서드:Illuminate\Mail\Mailables\Envelope객체를 반환하며, 메일의 제목과 수신자를 정의합니다.content메서드:Illuminate\Mail\Mailables\Content객체를 반환하며, 메일 본문을 렌더링할 Blade 템플릿을 지정합니다.
발신자 설정
Envelope으로 발신자 지정하기
메일을 누가 보내는지, 즉 "from" 주소를 설정하는 방법은 두 가지입니다. 첫 번째는 envelope 메서드에서 직접 지정하는 방식입니다.
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', '서비스 이름'),
],전역 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 버전과 함께 텍스트 버전의 메일도 제공하고 싶다면, Content 정의에 text 파라미터를 추가하세요. 두 버전을 동시에 정의할 수 있습니다.
/**
* 메시지 콘텐츠 정의를 반환합니다.
*/
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 파라미터로 전달하기
템플릿에 전달하기 전에 데이터를 가공하거나 변수명을 바꾸고 싶다면 with 파라미터를 사용하세요. 이 경우 생성자에서 받은 데이터를 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로 전달한 데이터는 템플릿에서 지정한 키 이름으로 사용할 수 있습니다.
<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'),
];
}as()와 withMime()을 체인으로 사용하면 수신자에게 보여질 파일명과 MIME 타입을 지정할 수 있습니다.
public function attachments(): array
{
return [
Attachment::fromPath('/path/to/file')
->as('report.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('report.pdf')
->withMime('application/pdf'),
];
}기본 디스크가 아닌 특정 디스크(예: S3)를 사용하려면 fromStorageDisk()를 사용하세요.
public function attachments(): array
{
return [
Attachment::fromStorageDisk('s3', '/path/to/file')
->as('report.pdf')
->withMime('application/pdf'),
];
}원시 데이터(Raw Data) 첨부
메모리상의 바이너리 데이터(예: 동적으로 생성한 PDF)를 디스크에 저장하지 않고 바로 첨부하려면 fromData()를 사용하세요. 클로저가 원시 데이터를 반환하도록 작성합니다.
public function attachments(): array
{
return [
Attachment::fromData(fn () => $this->pdf, 'Report.pdf')
->withMime('application/pdf'),
];
}인라인 이미지
메일 본문에 이미지를 인라인으로 삽입하려면 Blade 템플릿 안에서 $message->embed()를 사용하세요. Laravel은 모든 메일 템플릿에 $message 변수를 자동으로 제공합니다.
<body>
주문 확인 이미지입니다:
<img src="{{ $message->embed($pathToImage) }}">
</body>WARNING
$message 변수는 일반 텍스트 메일 템플릿에서는 사용할 수 없습니다. 텍스트 전용 메일은 인라인 첨부를 지원하지 않기 때문입니다.
원시 이미지 데이터 인라인 삽입
이미 메모리에 이미지 데이터가 있다면 $message->embedData()로 직접 삽입할 수 있습니다. 두 번째 인자로 파일명을 지정해야 합니다.
<body>
원시 데이터로 생성한 이미지입니다:
<img src="{{ $message->embedData($data, 'example-image.jpg') }}">
</body>Attachable 객체
단순 파일 경로 대신, 첨부 파일을 모델 객체로 표현하고 싶은 경우가 있습니다. 예를 들어 Photo 모델을 그대로 attachments()에서 반환할 수 있다면 더 직관적일 것입니다. Laravel은 Attachable 인터페이스를 통해 이를 지원합니다.
Illuminate\Contracts\Mail\Attachable 인터페이스를 구현하고, toMailAttachment() 메서드에서 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 같은 원격 스토리지에 있는 경우에도 동일하게 활용할 수 있습니다.
// 기본 디스크의 파일로 첨부 생성
return Attachment::fromStorage($this->path);
// 특정 디스크의 파일로 첨부 생성
return Attachment::fromStorageDisk('s3', $this->path);메모리상의 데이터로 첨부를 생성하려면 fromData()에 클로저를 전달하세요.
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,
],
);
}- Mailgun 사용 시: 태그 및 메타데이터 공식 문서를 참고하세요.
- Postmark 사용 시: 태그 및 메타데이터 공식 문서를 참고하세요.
- Amazon SES 사용 시:
metadata메서드를 통해 SES "태그"를 붙이세요.
Symfony Message 커스터마이징
Laravel의 메일 기능은 내부적으로 Symfony Mailer를 사용합니다. 메일 발송 직전에 Symfony의 Email 객체에 직접 접근해 세밀하게 커스터마이징하고 싶다면, Envelope의 using 파라미터에 콜백을 등록하세요.
use Illuminate\Mail\Mailables\Envelope;
use Symfony\Component\Mime\Email;
/**
* 메시지 Envelope을 반환합니다.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '주문이 발송되었습니다',
using: [
function (Email $message) {
// Symfony Email 객체를 직접 조작
},
]
);
}메일
Markdown Mailable
Markdown Mailable을 사용하면 메일 알림에서 제공하는 사전 제작된 템플릿과 컴포넌트를 메일 클래스에서도 그대로 활용할 수 있습니다. 메시지를 Markdown으로 작성하면, Laravel이 반응형 HTML 이메일 템플릿을 자동으로 렌더링하는 동시에 플레인 텍스트 버전도 함께 생성해 줍니다.
Markdown Mailable 생성하기
Markdown 템플릿이 포함된 Mailable 클래스를 생성하려면 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 컴포넌트
Button 컴포넌트는 가운데 정렬된 버튼 링크를 렌더링합니다. url과 선택적 color 두 가지 인수를 받으며, 지원하는 색상은 primary, success, error입니다. 메시지 안에 버튼을 여러 개 추가할 수도 있습니다:
<x-mail::button :url="$url" color="success">
주문 확인하기
</x-mail::button>Panel 컴포넌트
Panel 컴포넌트는 지정한 텍스트 블록을 주변 내용과 살짝 다른 배경색의 패널로 감싸 렌더링합니다. 특정 내용을 강조하고 싶을 때 유용합니다:
<x-mail::panel>
이곳에 강조하고 싶은 내용을 입력하세요.
</x-mail::panel>Table 컴포넌트
Table 컴포넌트는 Markdown 표를 HTML 표로 변환해 줍니다. Markdown의 기본 표 정렬 문법을 그대로 사용해 열 정렬을 지정할 수 있습니다:
<x-mail::table>
| 상품명 | 수량 | 금액 |
| ------------- |:-------------:| --------:|
| 상품 A | 1 | 10,000원 |
| 상품 B | 2 | 20,000원 |
</x-mail::table>컴포넌트 커스터마이징
Markdown 메일 컴포넌트를 직접 수정하려면 먼저 애플리케이션으로 내보내야 합니다. vendor:publish Artisan 명령에 laravel-mail 에셋 태그를 지정해 실행합니다:
php artisan vendor:publish --tag=laravel-mail이 명령을 실행하면 Markdown 메일 컴포넌트가 resources/views/vendor/mail 디렉터리에 복사됩니다. 이 디렉터리 안에는 html과 text 두 개의 하위 디렉터리가 생성되며, 각각 HTML 버전과 플레인 텍스트 버전의 컴포넌트 파일이 담겨 있습니다. 이 파일들을 자유롭게 수정하면 됩니다.
CSS 커스터마이징
컴포넌트를 내보내면 resources/views/vendor/mail/html/themes 디렉터리에 default.css 파일이 생성됩니다. 이 파일의 CSS를 수정하면 스타일이 HTML 이메일의 인라인 CSS로 자동 변환되어 적용됩니다.
완전히 새로운 테마를 만들고 싶다면 html/themes 디렉터리에 새 CSS 파일을 추가하세요. 파일을 저장한 후, config/mail.php 설정 파일의 theme 옵션을 새 테마 파일 이름(확장자 제외)으로 변경하면 됩니다.
특정 Mailable 클래스에만 별도의 테마를 적용하려면, 해당 Mailable 클래스의 $theme 프로퍼티를 사용할 테마 이름으로 설정하면 됩니다.
메일 발송
메일을 보내려면 Mail 파사드의 to 메서드를 사용합니다. to 메서드는 이메일 주소 문자열, 사용자 인스턴스, 또는 사용자 컬렉션을 인수로 받습니다. 객체나 컬렉션을 전달하면 메일러가 해당 객체의 email과 name 속성을 자동으로 사용해 수신자를 결정하므로, 객체에 이 속성이 반드시 존재해야 합니다. 수신자를 지정한 뒤에는 Mailable 클래스 인스턴스를 send 메서드에 전달해 메일을 발송합니다.
<?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은 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));이 메서드는 자동으로 Job을 큐에 푸시하여 메일을 백그라운드에서 발송합니다. 이 기능을 사용하려면 먼저 큐 설정을 완료해야 합니다.
메일 발송 지연 처리
큐에 등록된 메일을 일정 시간 후에 발송하고 싶다면 later 메서드를 사용합니다. 첫 번째 인수로 발송 시각을 나타내는 DateTime 인스턴스를 전달합니다.
Mail::to($request->user())
->cc($moreUsers)
->bcc($evenMoreUsers)
->later(now()->addMinutes(10), new OrderShipped($order));특정 큐에 푸시하기
make:mail 명령으로 생성된 모든 Mailable 클래스는 Illuminate\Bus\Queueable 트레이트를 사용하므로, onConnection과 onQueue 메서드를 호출해 사용할 커넥션과 큐 이름을 직접 지정할 수 있습니다.
$message = (new OrderShipped($order))
->onConnection('sqs')
->onQueue('emails');
Mail::to($request->user())
->cc($moreUsers)
->bcc($evenMoreUsers)
->queue($message);기본적으로 항상 큐 처리하기
특정 Mailable 클래스를 항상 큐로 처리하고 싶다면 ShouldQueue 인터페이스를 구현하면 됩니다. 이 인터페이스를 구현하면 send 메서드로 발송하더라도 자동으로 큐를 통해 처리됩니다.
use Illuminate\Contracts\Queue\ShouldQueue;
class OrderShipped extends Mailable implements ShouldQueue
{
// ...
}큐 메일과 데이터베이스 트랜잭션
데이터베이스 트랜잭션 내에서 큐 메일을 디스패치하면, 트랜잭션이 커밋되기 전에 큐 워커가 해당 Job을 먼저 처리할 수 있습니다. 이 경우 트랜잭션 내에서 변경하거나 생성한 모델 및 데이터베이스 레코드가 아직 DB에 반영되지 않은 상태일 수 있습니다. Mailable이 이런 데이터에 의존하고 있다면 예기치 않은 오류가 발생할 수 있습니다.
큐 커넥션의 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;
/**
* 새 메시지 인스턴스 생성
*/
public function __construct()
{
$this->afterCommit();
}
}NOTE
이 문제를 우회하는 방법에 대한 자세한 내용은 큐 Job과 데이터베이스 트랜잭션 문서를 참고하세요.
Mailable 렌더링
메일을 실제로 전송하지 않고 HTML 콘텐츠만 추출하고 싶을 때는 Mailable의 render 메서드를 사용합니다. 이 메서드는 Mailable의 HTML 결과물을 문자열로 반환합니다.
use App\Mail\InvoicePaid;
use App\Models\Invoice;
$invoice = Invoice::find(1);
return (new InvoicePaid($invoice))->render();브라우저에서 Mailable 미리보기
메일 템플릿을 디자인할 때, Blade 뷰를 개발하듯 브라우저에서 바로 렌더링 결과를 확인할 수 있으면 편리합니다. Laravel은 라우트 클로저나 컨트롤러에서 Mailable 객체를 직접 반환하는 방식으로 이를 지원합니다. Mailable이 반환되면 HTML로 렌더링되어 브라우저에 표시되므로, 실제 이메일 주소로 발송하지 않고도 레이아웃과 디자인을 즉시 확인할 수 있습니다.
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 계약(Contract)을 구현하면, Laravel이 메일 발송 시 해당 사용자의 저장된 로케일을 자동으로 사용합니다.
use Illuminate\Contracts\Translation\HasLocalePreference;
class User extends Model implements HasLocalePreference
{
/**
* 사용자의 선호 로케일을 반환합니다.
*/
public function preferredLocale(): string
{
return $this->locale;
}
}인터페이스를 구현하면 메일 및 알림 발송 시 locale 메서드를 별도로 호출하지 않아도 Laravel이 자동으로 선호 로케일을 적용합니다.
Mail::to($request->user())->send(new OrderShipped($order));NOTE
한국어 서비스라면 preferredLocale()에서 'ko'를 반환하도록 구현하고, 리소스 디렉터리에 lang/ko/ 번역 파일을 준비해 두면 됩니다.
테스트
Mailable 콘텐츠 테스트
Laravel은 Mailable의 구조를 검사하고, 예상한 콘텐츠가 포함되어 있는지 확인하기 위한 다양한 assertion 메서드를 제공합니다. 주요 메서드는 다음과 같습니다: assertSeeInHtml, assertDontSeeInHtml, assertSeeInOrderInHtml, assertSeeInText, assertDontSeeInText, assertSeeInOrderInText, assertHasAttachment, assertHasAttachedData, assertHasAttachmentFromStorage, assertHasAttachmentFromStorageDisk.
이름에서 알 수 있듯이 Html 계열 메서드는 메일의 HTML 버전에, Text 계열 메서드는 일반 텍스트 버전에 특정 문자열이 포함되어 있는지 검사합니다.
use App\Mail\InvoicePaid;
use App\Models\User;
public function test_mailable_content(): void
{
$user = User::factory()->create();
$mailable = new InvoicePaid($user);
$mailable->assertFrom('sender@example.com');
$mailable->assertTo('receiver@example.com');
$mailable->assertHasCc('cc@example.com');
$mailable->assertHasBcc('bcc@example.com');
$mailable->assertHasReplyTo('replyto@example.com');
$mailable->assertHasSubject('청구서 결제 완료');
$mailable->assertHasTag('example-tag');
$mailable->assertHasMetadata('key', 'value');
$mailable->assertSeeInHtml($user->email);
$mailable->assertSeeInHtml('청구서 결제 완료');
$mailable->assertSeeInOrderInHtml(['청구서 결제 완료', '감사합니다']);
$mailable->assertSeeInText($user->email);
$mailable->assertSeeInOrderInText(['청구서 결제 완료', '감사합니다']);
$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이 올바른 사용자에게 발송 지시되었는가"에 집중하면 충분합니다.
Mail 파사드의 fake 메서드를 호출하면 실제 메일 발송을 막을 수 있습니다. 이후 assertion 메서드로 어떤 Mailable이 누구에게 발송 지시되었는지, 어떤 데이터를 받았는지 검사할 수 있습니다.
<?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);
// 특정 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 메서드에 클로저를 전달하면, 특정 조건을 만족하는 Mailable이 발송되었는지 세밀하게 검사할 수 있습니다. 조건을 통과하는 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')
);
});메일이 발송되지 않았음을 확인하는 메서드로는 assertNotSent와 assertNotQueued 두 가지가 있습니다. 발송도 큐 등록도 모두 이루어지지 않았음을 한 번에 검증하려면 assertNothingOutgoing과 assertNotOutgoing을 사용하세요.
Mail::assertNothingOutgoing();
Mail::assertNotOutgoing(function (OrderShipped $mail) use ($order) {
return $mail->order->id === $order->id;
});메일과 로컬 개발 환경
이메일을 발송하는 기능을 개발할 때, 실제 이메일 주소로 메일이 전송되는 것은 바람직하지 않습니다. Laravel은 로컬 개발 환경에서 실제 메일 발송을 "차단"할 수 있는 몇 가지 방법을 제공합니다.
Log 드라이버
log 메일 드라이버를 사용하면 실제로 메일을 발송하는 대신, 모든 이메일 내용을 로그 파일에 기록합니다. 로그 파일은 storage/logs 디렉터리에서 확인할 수 있습니다. 이 드라이버는 일반적으로 로컬 개발 환경에서만 사용합니다. 환경별 설정에 대한 자세한 내용은 설정 문서를 참고하세요.
HELO / Mailtrap / Mailpit
또 다른 방법으로, HELO나 Mailtrap 같은 외부 서비스를 smtp 드라이버와 함께 사용할 수 있습니다. 이 서비스들은 "가상 받은 편지함"을 제공하여, 실제 메일 클라이언트와 유사한 환경에서 최종 이메일이 어떻게 렌더링되는지 직접 확인할 수 있다는 장점이 있습니다.
Laravel Sail을 사용하고 있다면, Mailpit으로 메일을 미리 볼 수 있습니다. Sail이 실행 중인 상태에서 브라우저로 http://localhost:8025에 접속하면 Mailpit 인터페이스를 사용할 수 있습니다.
전역 수신 주소 지정
마지막으로, Mail 파사드의 alwaysTo 메서드를 사용해 모든 메일이 특정 주소로만 전송되도록 강제할 수 있습니다. 이 메서드는 일반적으로 서비스 프로바이더의 boot 메서드 안에서 호출합니다.
use Illuminate\Support\Facades\Mail;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
if ($this->app->environment('local')) {
Mail::alwaysTo('taylor@example.com');
}
}NOTE
alwaysTo를 설정하면 수신자를 누구로 지정하든 실제 발송은 지정한 주소로만 이루어집니다. 로컬 환경에서만 적용되도록 환경 조건을 반드시 확인하세요.
이벤트
Laravel은 메일 메시지를 발송하는 과정에서 두 가지 이벤트를 발생시킵니다. MessageSending 이벤트는 메시지가 발송되기 직전에, MessageSent 이벤트는 메시지가 발송된 직후에 발생합니다. 주의할 점은, 이 이벤트들은 메일이 실제로 전송될 때 발생하며, 큐에 등록될 때는 발생하지 않는다는 것입니다.
App\Providers\EventServiceProvider 서비스 프로바이더에서 다음과 같이 이벤트 리스너를 등록할 수 있습니다.
use App\Listeners\LogSendingMessage;
use App\Listeners\LogSentMessage;
use Illuminate\Mail\Events\MessageSending;
use Illuminate\Mail\Events\MessageSent;
/**
* 애플리케이션의 이벤트 리스너 매핑
*
* @var array
*/
protected $listen = [
MessageSending::class => [
LogSendingMessage::class,
],
MessageSent::class => [
LogSentMessage::class,
],
];메일
커스텀 트랜스포트
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에 등록합니다. 이 작업은 보통 AppServiceProvider의 boot 메서드 안에서 처리합니다. 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가 관리하는 다른 트랜스포트를 추가로 사용하고 싶다면, Composer로 해당 패키지를 설치한 뒤 Laravel에 등록하면 됩니다.
예를 들어, "Brevo"(구 "Sendinblue") Symfony 메일러를 설치하고 등록하는 방법은 다음과 같습니다.
composer require symfony/brevo-mailer symfony/http-client패키지 설치 후, config/services.php에 Brevo API 키를 추가합니다.
'brevo' => [
'key' => 'your-api-key',
],다음으로, AppServiceProvider의 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',
// ...
],