메일
번역일: 2026년 7월 2일
메일
- 소개
- Mailable 생성
- Mailable 작성
- Markdown Mailable
- 메일 전송
- Mailable 렌더링
- Mailable 다국어 처리
- 테스트
- 로컬 개발 환경에서의 메일
- 이벤트
- 커스텀 트랜스포트
소개
이메일 전송은 거의 모든 웹 애플리케이션에서 필요한 기능입니다. Laravel은 Symfony Mailer 컴포넌트를 기반으로 하는 깔끔한 메일 API를 제공합니다. SMTP, Mailgun, Postmark, Resend, Amazon SES, sendmail 등 다양한 드라이버를 지원하므로, 로컬 환경이나 클라우드 서비스 어느 쪽에서든 빠르게 메일 전송을 시작할 수 있습니다.
설정
메일 관련 설정은 config/mail.php 파일에서 관리합니다. 각 메일러(mailer)마다 고유한 설정과 트랜스포트를 지정할 수 있어, 메일 유형에 따라 서로 다른 서비스를 사용하는 것도 가능합니다. 예를 들어 트랜잭션 메일은 Postmark로, 마케팅 메일은 Amazon SES로 보내는 식으로 구성할 수 있습니다.
config/mail.php 안에는 mailers 배열이 있으며, 각 항목에 드라이버와 접속 정보를 설정합니다. default 키는 메일러를 명시하지 않았을 때 기본으로 사용할 메일러를 지정합니다.
드라이버 사전 준비
Mailgun, Postmark, Resend, MailerSend와 같은 API 기반 드라이버는 SMTP보다 간단하고 빠른 경우가 많습니다. 가능하다면 이런 드라이버 중 하나를 사용하길 권장합니다.
Mailgun 드라이버
Mailgun을 사용하려면 Composer로 Symfony의 Mailgun Mailer 트랜스포트를 설치합니다.
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 트랜스포트를 설치합니다.
composer require symfony/postmark-mailer symfony/http-client그다음 config/mail.php의 default 옵션을 postmark로 설정합니다. mailers 배열에는 아래 내용을 추가합니다.
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],특정 메일러에 Postmark 메시지 스트림을 지정하고 싶다면 message_stream_id 설정을 추가하면 됩니다. 이를 활용하면 메일러 설정마다 다른 메시지 스트림을 사용할 수 있습니다.
이어서 config/services.php 파일에 Postmark API 토큰을 추가합니다.
'postmark' => [
'token' => env('POSTMARK_TOKEN'),
],Resend 드라이버
Resend를 사용하려면 Composer로 Resend PHP SDK를 설치합니다.
composer require resend/resend-php그다음 config/mail.php의 default 옵션을 resend로 설정하고, mailers 배열에 아래 내용을 추가합니다.
'resend' => [
'transport' => 'resend',
],이어서 config/services.php 파일에 Resend API 키를 추가합니다.
'resend' => [
'key' => env('RESEND_KEY'),
],SES 드라이버
Amazon SES를 사용하려면 먼저 AWS SDK for PHP를 설치해야 합니다.
composer require aws/aws-sdk-php그다음 config/mail.php의 default 옵션을 ses로 설정하고, mailers 배열에 아래 설정이 있는지 확인합니다.
'ses' => [
'transport' => 'ses',
],AWS 임시 자격 증명을 사용하기 위해 세션 토큰이 필요하다면, token 키를 함께 설정에 포함시킬 수 있습니다.
'ses' => [
'transport' => 'ses',
'token' => env('AWS_SESSION_TOKEN'),
],이어서 config/services.php 파일에 AWS 인증 정보를 추가합니다.
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],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용 자체 메일 드라이버를 제공합니다. 해당 패키지는 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 드라이버 공식 문서를 참고하세요.
장애 조치 설정
외부 메일 서비스가 일시적으로 장애 상황에 처할 수 있습니다. 이런 경우를 대비해 하나 이상의 백업 메일 전송 설정을 정의해 두는 것이 좋습니다.
이를 위해 config/mail.php에서 failover 트랜스포트를 사용하는 메일러를 정의합니다. mailers 배열에 나열된 드라이버 순서대로 시도하며, 앞선 드라이버가 실패하면 다음 드라이버로 자동 전환됩니다.
'mailers' => [
'failover' => [
'transport' => 'failover',
'mailers' => [
'postmark',
'mailgun',
'sendmail',
],
],
// ...
],장애 조치 메일러를 정의했다면, config/mail.php의 default 키 값을 해당 메일러 이름으로 설정합니다.
'default' => env('MAIL_MAILER', 'failover'),라운드 로빈 설정
roundrobin 트랜스포트는 여러 메일러에 메일 전송 부하를 분산시킵니다. failover와 달리 장애 발생 시 전환하는 게 아니라, 전송마다 순서를 돌아가며 메일러를 선택합니다.
'mailers' => [
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
],
// ...
],라운드 로빈 메일러를 정의한 후, config/mail.php의 default 키를 해당 이름으로 지정합니다.
'default' => env('MAIL_MAILER', 'roundrobin'),라운드 로빈은 등록된 메일러 목록에서 무작위로 시작점을 선택한 후, 이후 메일부터는 순서대로 다음 메일러를 사용합니다. failover가 고가용성(HA)을 위한 설정이라면, roundrobin은 부하 분산을 위한 설정입니다.
Mailable 생성
Laravel에서 각 메일 유형은 "Mailable" 클래스로 표현됩니다. 이 클래스들은 app/Mail 디렉터리에 저장됩니다. 해당 디렉터리가 없더라도 처음 Mailable 클래스를 생성할 때 자동으로 만들어집니다. make:mail Artisan 명령어로 새 Mailable 클래스를 생성할 수 있습니다.
php artisan make:mail OrderShippedMailable 작성
Mailable 클래스를 생성하고 나면, 내부 구조를 살펴보겠습니다. Mailable 클래스의 설정은 envelope, content, attachments 등 여러 메서드를 통해 이루어집니다.
envelope 메서드는 메시지 제목과 경우에 따라 수신자를 정의하는 Illuminate\Mail\Mailables\Envelope 객체를 반환합니다. content 메서드는 메시지 내용을 생성할 Blade 템플릿을 지정하는 Illuminate\Mail\Mailables\Content 객체를 반환합니다.
발신자 설정
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' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', '예시 앱'),
],전역 reply_to 주소도 마찬가지로 설정할 수 있습니다.
'reply_to' => ['address' => 'example@example.com', 'name' => '앱 이름'],뷰 설정
Mailable 클래스의 content 메서드에서 view를 지정하면, 해당 Blade 템플릿이 메일 본문을 렌더링하는 데 사용됩니다.
/**
* 메시지 콘텐츠 정의를 반환합니다.
*/
public function content(): Content
{
return new Content(
view: 'mail.orders.shipped',
);
}NOTE
메일 전용 Blade 템플릿을 모아 두기 위해 resources/views/mail 디렉터리를 따로 만들어 관리하는 것을 권장합니다. 물론 resources/views 아래 어디에나 자유롭게 위치시킬 수 있습니다.
일반 텍스트 메일
평문(plain text) 버전의 메일을 함께 제공하고 싶다면 Content 정의에 text 파라미터를 추가합니다. view와 text 파라미터는 같이 사용할 수 있습니다.
/**
* 메시지 콘텐츠 정의를 반환합니다.
*/
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 클래스에 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 프로퍼티로 설정된 데이터는 Blade 템플릿에서 일반 변수처럼 바로 접근할 수 있습니다.
<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로 전달된 데이터도 Blade 템플릿에서 변수로 사용할 수 있습니다.
<div>
주문 금액: {{ $orderPrice }}
</div>첨부 파일
메일에 파일을 첨부하려면 Mailable의 attachments 메서드에서 반환하는 배열에 첨부 파일을 추가합니다. 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'),
];
}원시 데이터 첨부
바이트 문자열로 된 데이터를 직접 첨부할 때는 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 변수는 일반 텍스트(plain text) 메일 템플릿에서는 사용할 수 없습니다. 일반 텍스트 메시지는 인라인 첨부를 지원하지 않기 때문입니다.
원시 데이터 인라인 삽입
이미지 파일 경로 대신 원시 이미지 데이터 문자열을 삽입하고 싶다면 embedData 메서드를 사용합니다.
<body>
원시 데이터로부터 이미지 삽입:
<img src="{{ $message->embedData($data, 'example-image.jpg') }}">
</body>Attachable 객체
단순히 파일 경로 문자열로 첨부하는 것이 애플리케이션 요구사항을 충족하지 못할 때가 있습니다. 예를 들어, 애플리케이션의 모델이 첨부 파일과 관련된 경우, 해당 모델 자체를 첨부 가능한 객체로 만들 수 있습니다. 이를 위해 모델에 Illuminate\Contracts\Mail\Attachable 인터페이스를 구현합니다.
이 인터페이스를 구현하는 클래스는 toMailAttachment 메서드를 정의해야 하며, 이 메서드는 Illuminate\Mail\Mailables\Attachment 인스턴스를 반환해야 합니다.
<?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 같은 원격 스토리지에 저장되어 있을 수도 있습니다. Laravel의 파일 스토리지 디스크에 저장된 데이터로 첨부 인스턴스를 생성할 수도 있습니다.
// 기본 디스크에서 첨부 생성
return Attachment::fromStorage($this->path);
// 특정 디스크에서 첨부 생성
return Attachment::fromStorageDisk('backblaze', $this->path);또한 메모리에 있는 데이터로 첨부를 생성할 때는 fromData 메서드에 클로저를 전달합니다.
return Attachment::fromData(fn () => $this->content, '사진.jpg');이 외에도 as, withMime 메서드로 첨부 파일 이름과 MIME 타입을 커스터마이징할 수 있습니다.
return Attachment::fromPath('/path/to/file')
->as('사진.jpg')
->withMime('image/jpeg');헤더
때로는 메시지에 추가 헤더를 붙여야 할 때가 있습니다. 예를 들어 커스텀 Message-Id나 임의의 텍스트 헤더가 필요할 수 있습니다.
이런 경우 Mailable에 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 정의에 tags와 metadata를 추가하면 됩니다.
use Illuminate\Mail\Mailables\Envelope;
/**
* 메시지 봉투를 반환합니다.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '주문이 발송되었습니다',
tags: ['shipment'],
metadata: [
'order_id' => $this->order->id,
],
);
}Mailgun 드라이버의 태그 및 메타데이터는 Mailgun 공식 문서를, Postmark의 경우는 Postmark 공식 문서를 참고하세요.
Amazon SES를 사용하는 경우 metadata 메서드를 통해 메시지에 SES "태그"를 첨부할 수 있습니다.
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을 사용하면 메일 알림에서 사용하는 미리 만들어진 템플릿과 컴포넌트를 메일에서도 그대로 활용할 수 있습니다. Markdown으로 메시지를 작성하면 Laravel이 반응형 HTML 템플릿을 자동으로 렌더링하고, 동시에 일반 텍스트 버전도 생성해 줍니다.
Markdown Mailable 생성
Markdown 템플릿을 포함한 Mailable 클래스를 생성하려면 make:mail 명령어에 --markdown 옵션을 사용합니다.
php artisan make:mail OrderShipped --markdown=mail.orders.shipped그다음 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>
<h1 id="button-component">주문 발송 완료</h1>
주문이 발송되었습니다!
<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 테이블 문법을 사용합니다. 컬럼 정렬 기능도 기본 Markdown 테이블 정렬 문법을 그대로 지원합니다.
<x-mail::table>
| 상품명 | 색상 | 수량 |
| --------- | ---------: | ---: |
| 노트북 | 실버 | 1 |
| 마우스 | 블랙 | 2 |
</x-mail::table>컴포넌트 커스터마이징
모든 Markdown 메일 컴포넌트를 내보내어(export) 원하는 대로 수정할 수 있습니다. 컴포넌트를 내보내려면 vendor:publish Artisan 명령어로 laravel-mail 에셋 태그를 퍼블리시합니다.
php artisan vendor:publish --tag=laravel-mail이 명령을 실행하면 resources/views/vendor/mail 디렉터리에 Markdown 메일 컴포넌트가 저장됩니다. mail 디렉터리 안에는 html과 text 두 가지 서브 디렉터리가 있으며, 각각 HTML 및 일반 텍스트 형태의 컴포넌트가 들어 있습니다. 원하는 파일을 자유롭게 수정하면 됩니다.
CSS 커스터마이징
컴포넌트를 내보내면 resources/views/vendor/mail/html/themes 디렉터리에 default.css 파일이 생성됩니다. 이 파일의 CSS를 수정하면 스타일이 자동으로 Markdown 메일의 HTML 표현에 인라인으로 적용됩니다.
Laravel의 Markdown 컴포넌트를 위한 완전히 새로운 테마를 만들고 싶다면 themes 디렉터리에 CSS 파일을 추가한 후, config/mail.php의 theme 옵션에 해당 파일 이름(확장자 제외)을 설정합니다.
특정 Mailable에만 다른 테마를 적용하려면 해당 Mailable 클래스의 $theme 프로퍼티에 테마 이름을 지정합니다.
/**
* 이 Mailable에서 사용할 테마 이름입니다.
*
* @var string
*/
public $theme = 'custom-theme-name';메일 전송
메일을 전송하려면 Mail 파사드의 to 메서드를 사용합니다. to 메서드에는 이메일 주소, 사용자 인스턴스, 또는 사용자 컬렉션을 전달할 수 있습니다. 객체나 객체 컬렉션을 전달하면 마일러가 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 인스턴스를 생성해야 합니다. 인스턴스를 재사용하면 이전 수신자 정보가 누적될 수 있기 때문입니다.
foreach (['taylor@example.com', 'dries@example.com'] as $recipient) {
Mail::to($recipient)->send(new OrderShipped($order));
}특정 메일러로 전송하기
기본적으로 Laravel은 config/mail.php의 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 메서드를 사용합니다. later는 첫 번째 인자로 DateTime 인스턴스를 받습니다.
Mail::to($request->user())
->cc($moreUsers)
->bcc($evenMoreUsers)
->later(now()->addMinutes(10), new OrderShipped($order));특정 큐에 추가하기
make:mail 명령어로 생성된 모든 Mailable 클래스는 Illuminate\Bus\Queueable 트레이트를 사용합니다. 이를 통해 onQueue와 onConnection 메서드를 호출하여 메시지가 사용할 큐 연결과 큐 이름을 지정할 수 있습니다.
$message = (new OrderShipped($order))
->onConnection('sqs')
->onQueue('emails');
Mail::to($request->user())->queue($message);기본적으로 큐잉하기
항상 큐를 통해 전송하고 싶은 Mailable 클래스가 있다면, ShouldQueue 인터페이스를 구현하면 됩니다. 이렇게 하면 send 메서드를 사용하더라도 해당 Mailable은 항상 큐잉됩니다.
use Illuminate\Contracts\Queue\ShouldQueue;
class OrderShipped extends Mailable implements ShouldQueue
{
// ...
}Mailable 암호화
큐에 넣기 전에 Mailable 데이터를 암호화하고 싶다면 ShouldBeEncrypted 인터페이스를 구현합니다. 자세한 내용은 큐 Job 암호화 문서를 참고하세요.
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
class OrderShipped extends Mailable implements ShouldQueue, ShouldBeEncrypted
{
// ...
}Mailable 렌더링
메일을 실제로 보내지 않고도 Mailable의 HTML 내용을 문자열로 받아올 수 있습니다. render 메서드를 호출하면 됩니다.
use App\Mail\InvoicePaid;
use App\Models\Invoice;
$invoice = Invoice::find(1);
return (new InvoicePaid($invoice))->render();브라우저에서 Mailable 미리보기
Mailable 템플릿을 디자인할 때 Blade 뷰처럼 브라우저에서 바로 확인하고 싶을 때가 있습니다. 이를 위해 라우트 클로저나 컨트롤러에서 Mailable 인스턴스를 직접 반환하면 됩니다. 반환된 Mailable은 자동으로 렌더링되어 브라우저에 표시됩니다.
Route::get('/mailable', function () {
$invoice = App\Models\Invoice::find(1);
return new App\Mail\InvoicePaid($invoice);
});Mailable 다국어 처리
Laravel은 현재 요청의 로케일과 다른 로케일로 Mailable을 전송할 수 있으며, 메일이 큐에 들어간 경우에도 지정한 로케일이 유지됩니다.
이를 위해 Mail 파사드의 locale 메서드로 원하는 언어를 지정합니다. Mailable의 템플릿이 평가될 때 해당 로케일로 전환되었다가, 평가가 완료되면 이전 로케일로 복원됩니다.
Mail::to($request->user())->locale('ko')->send(
new OrderShipped($order)
);사용자 선호 로케일
사용자마다 선호하는 언어를 저장해 두는 애플리케이션이 있습니다. 모델에 HasLocalePreference 인터페이스를 구현하면, 메일 전송 시 locale 메서드를 매번 호출하지 않아도 Laravel이 자동으로 해당 로케일을 사용합니다.
use Illuminate\Contracts\Translation\HasLocalePreference;
class User extends Model implements HasLocalePreference
{
/**
* 사용자가 선호하는 로케일을 반환합니다.
*/
public function preferredLocale(): string
{
return $this->locale;
}
}인터페이스를 구현하면 별도로 locale을 지정하지 않아도 됩니다. Mail 파사드가 모델의 preferredLocale 메서드를 자동으로 호출합니다.
Mail::to($request->user())->send(new OrderShipped($order));테스트
Mailable 콘텐츠 테스트
Laravel은 Mailable의 구조를 검사하는 다양한 메서드를 제공합니다. 이를 활용하면 Mailable에 기대하는 콘텐츠가 포함되어 있는지 편리하게 확인할 수 있습니다. 주요 어설션 메서드로는 assertSeeInHtml, assertDontSeeInHtml, assertSeeInOrderInHtml, assertSeeInText, assertDontSeeInText, assertSeeInOrderInText, assertHasAttachment, assertHasAttachedData, assertHasAttachmentFromStorage, assertHasAttachmentFromStorageDisk 등이 있습니다.
"Html" 어설션은 Mailable의 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('example-tag');
$mailable->assertHasMetadata('key', 'value');
$mailable->assertSeeInHtml($invoice->number);
$mailable->assertSeeInHtml('인보이스 결제 완료');
$mailable->assertSeeInOrderInHtml(['인보이스 결제 완료', '감사합니다']);
$mailable->assertSeeInText($invoice->number);
$mailable->assertSeeInOrderInText(['인보이스 결제 완료', '감사합니다']);
$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의 콘텐츠 테스트와 특정 사용자에게 메일이 "전송"되었는지를 검증하는 테스트는 분리해서 작성하는 것을 권장합니다. 실제 메일 전송 여부를 테스트할 때는 보통 Mailable의 내용 자체는 검사할 필요가 없습니다.
Mail::fake()을 사용하면 실제 메일 전송을 막고, 어떤 Mailable이 어떤 사용자에게 전송됐는지 어설션할 수 있습니다.
<?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이 두 번 전송됐는지 확인
Mail::assertSent(OrderShipped::class, 2);
// 여러 Mailable이 모두 전송됐는지 확인
Mail::assertSent([OrderShipped::class, AnotherMailable::class]);
// 특정 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이 하나라도 있으면 해당 어설션은 통과합니다.
Mail::assertSent(function (OrderShipped $mail) use ($order) {
return $mail->order->id === $order->id;
});Mail::assertSent 클로저 안에서 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('...');
<h1 id="mail-and-local-development">메일</h1>
<h2 id="log-driver">목차</h2>
- [소개](#introduction)
- [설정](#configuration)
- [드라이버 / 전송 수단 사전 준비](#driver-prerequisites)
- [Failover 설정](#failover-configuration)
- [Round Robin 설정](#round-robin-configuration)
- [Mailable 생성](#generating-mailables)
---
<h2 id="mailtrap">소개</h2>
Laravel은 [Symfony Mailer](https://symfony.com/doc/current/mailer.html) 컴포넌트를 기반으로 깔끔하고 직관적인 이메일 전송 API를 제공합니다. SMTP, Mailgun, Postmark, Resend, Amazon SES, `sendmail` 등 다양한 전송 수단을 지원하므로, 로컬 환경이나 클라우드 서비스를 통해 빠르게 메일 발송을 시작할 수 있습니다.
<h2 id="using-a-global-to-address">설정</h2>
이메일 관련 설정은 `config/mail.php` 파일에서 관리합니다. 이 파일 안에는 여러 개의 **mailer**를 정의할 수 있으며, 각 mailer는 독립적인 설정과 전송 수단(transport)을 가질 수 있습니다. 예를 들어, 트랜잭션 메일(주문 확인, 비밀번호 재설정 등)은 Postmark로, 대량 발송 메일은 Amazon SES로 처리하는 식으로 구분해서 운영할 수 있습니다.
`config/mail.php`의 `mailers` 배열에는 Laravel이 지원하는 주요 드라이버별 샘플 설정이 포함되어 있으며, `default` 값으로 기본적으로 사용할 mailer를 지정합니다.
<h3 id="events">드라이버 / 전송 수단 사전 준비</h3>
Mailgun, Postmark, Resend처럼 API 기반 드라이버는 SMTP보다 설정이 간단하고 전송 속도도 빠릅니다. 가능하다면 이러한 API 기반 드라이버 사용을 권장합니다.
<h4 id="custom-transports">Mailgun 드라이버</h4>
Mailgun 드라이버를 사용하려면 Composer로 다음 패키지를 설치합니다:
```shell
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',
],미국 리전이 아닌 다른 Mailgun 리전을 사용하는 경우(예: EU 리전), 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-clientconfig/mail.php의 default를 postmark로 설정한 뒤, config/services.php에 API 키를 추가합니다:
'postmark' => [
'key' => env('POSTMARK_API_KEY'),
],특정 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 드라이버를 사용하려면 Resend PHP SDK를 설치합니다:
composer require resend/resend-phpconfig/mail.php의 default를 resend로 설정한 뒤, config/services.php에 API 키를 추가합니다:
'resend' => [
'key' => env('RESEND_API_KEY'),
],SES 드라이버
Amazon SES 드라이버를 사용하려면 AWS PHP SDK를 설치합니다:
composer require aws/aws-sdk-phpconfig/mail.php의 default를 ses로 설정하고, config/services.php에 AWS 인증 정보를 추가합니다:
'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'],
],
],
],Failover 설정
외부 메일 서비스에 장애가 발생했을 때를 대비해 백업 전송 수단을 미리 설정해 둘 수 있습니다. failover 전송 수단을 사용하면 기본 mailer가 실패할 경우 정의된 순서대로 다음 mailer를 자동으로 시도합니다.
config/mail.php의 mailers 배열에 다음과 같이 failover mailer를 정의합니다:
'mailers' => [
'failover' => [
'transport' => 'failover',
'mailers' => [
'postmark',
'mailgun',
'sendmail',
],
'retry_after' => 60,
],
// ...
],설정 후 .env 파일에서 기본 mailer를 failover로 지정합니다:
MAIL_MAILER=failoverNOTE
failover는 **고가용성(high availability)**을 목적으로 합니다. 기본 mailer가 정상이면 항상 기본 mailer만 사용하고, 장애 시에만 다음 mailer로 전환됩니다.
Round Robin 설정
roundrobin 전송 수단을 사용하면 메일 발송 부하를 여러 mailer에 분산시킬 수 있습니다. config/mail.php에 다음과 같이 설정합니다:
'mailers' => [
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
'retry_after' => 60,
],
// ...
],그리고 config/mail.php의 default 값을 roundrobin으로 지정합니다:
'default' => env('MAIL_MAILER', 'roundrobin'),Round robin 전송 수단은 설정된 mailer 목록 중 무작위로 하나를 선택한 뒤, 이후 메일을 보낼 때마다 다음 mailer로 순환합니다.
failover와 roundrobin의 차이를 정리하면 다음과 같습니다:
| 전송 수단 | 목적 | 동작 방식 |
|---|---|---|
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메서드:Illuminate\Mail\Mailables\Content객체를 반환하며, 메일 본문 렌더링에 사용할 Blade 템플릿을 지정합니다.
발신자 설정
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('no-reply@myapp.kr', '내 서비스'),
subject: '주문이 발송되었습니다',
);
}replyTo 주소도 별도로 지정할 수 있습니다.
return new Envelope(
from: new Address('no-reply@myapp.kr', '내 서비스'),
replyTo: [
new Address('support@myapp.kr', '고객센터'),
],
subject: '주문이 발송되었습니다',
);전역 from 주소 사용하기
애플리케이션 전체에서 동일한 발신자 주소를 사용한다면, 각 Mailable 클래스마다 from을 반복 지정하는 것은 번거롭습니다. 이럴 때는 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' => '앱 이름',
],뷰(View) 설정
content 메서드 안에서 메일 본문을 렌더링할 Blade 템플릿을 지정합니다. Blade 템플릿 엔진의 모든 기능을 그대로 활용할 수 있으므로, 조건문·반복문·컴포넌트 등을 자유롭게 사용할 수 있습니다.
/**
* 메시지 콘텐츠 정의를 반환합니다.
*/
public function content(): Content
{
return new Content(
view: 'mail.orders.shipped',
);
}NOTE
이메일 템플릿 파일은 resources/views/mail 디렉터리에 모아두는 것을 권장하지만, resources/views 안이라면 원하는 위치 어디든 배치할 수 있습니다.
텍스트 전용 이메일
HTML 버전과 함께 텍스트(plain-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 프로퍼티는 자동으로 Blade 템플릿에서 사용할 수 있게 됩니다.
<?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 파라미터를 통한 전달
템플릿에 전달하기 전에 데이터를 가공하거나 변수명을 바꾸고 싶다면, Content의 with 파라미터를 사용하세요. 이 경우 생성자에서 받은 데이터를 protected 또는 private 프로퍼티로 선언하여 자동 노출을 방지하고, with를 통해 필요한 형태로만 뷰에 전달합니다.
<?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 클래스의 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 타입을 지정할 수 있습니다.
/**
* 메시지의 첨부 파일 목록을 반환합니다.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [
Attachment::fromPath('/path/to/file')
->as('report.pdf')
->withMime('application/pdf'),
];
}파일시스템 디스크에서 첨부
파일시스템 디스크에 저장된 파일을 첨부할 때는 fromStorage 메서드를 사용합니다.
/**
* 메시지의 첨부 파일 목록을 반환합니다.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [
Attachment::fromStorage('/path/to/file'),
];
}표시 이름과 MIME 타입 지정도 동일하게 할 수 있습니다.
/**
* 메시지의 첨부 파일 목록을 반환합니다.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [
Attachment::fromStorage('/path/to/file')
->as('report.pdf')
->withMime('application/pdf'),
];
}기본 디스크가 아닌 다른 디스크(예: S3)에서 가져오려면 fromStorageDisk 메서드를 사용합니다.
/**
* 메시지의 첨부 파일 목록을 반환합니다.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [
Attachment::fromStorageDisk('s3', '/path/to/file')
->as('report.pdf')
->withMime('application/pdf'),
];
}원시 데이터 첨부
파일을 디스크에 저장하지 않고 메모리 상의 바이트 데이터를 직접 첨부하려면 fromData 메서드를 사용합니다. PDF를 동적으로 생성한 후 바로 첨부하는 경우가 대표적인 예입니다. fromData는 원시 데이터를 반환하는 클로저와 첨부 파일 이름을 인자로 받습니다.
/**
* 메시지의 첨부 파일 목록을 반환합니다.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [
Attachment::fromData(fn () => $this->pdf, 'Report.pdf')
->withMime('application/pdf'),
];
}인라인 첨부(이미지 삽입)
메일 본문에 이미지를 직접 삽입(임베드)하는 것은 일반적으로 번거롭지만, Laravel은 이를 간편하게 처리할 수 있도록 지원합니다. Blade 템플릿 안에서 $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 객체
단순 파일 경로 문자열로 첨부하는 것으로 충분한 경우도 많지만, 실제 애플리케이션에서 첨부 대상은 종종 Eloquent 모델과 같은 클래스로 표현됩니다. 예를 들어 사진을 메일에 첨부해야 한다면, 애플리케이션에 이미 Photo 모델이 있을 것입니다. 이 경우 Photo 모델 자체를 attachments 배열에 넣을 수 있다면 더 편리하지 않을까요? Attachable 객체가 바로 이를 가능하게 해줍니다.
Illuminate\Contracts\Mail\Attachable 인터페이스를 구현하면, 해당 객체를 첨부 파일로 사용할 수 있습니다. 이 인터페이스는 Illuminate\Mail\Attachment 인스턴스를 반환하는 toMailAttachment 메서드를 정의하도록 요구합니다.
<?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 같은 원격 스토리지에 있는 경우에도 동일하게 처리할 수 있습니다.
// 기본 디스크의 파일로 첨부 인스턴스 생성
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');헤더 커스터마이징
발송 메시지에 커스텀 헤더를 추가해야 하는 경우(예: 커스텀 Message-Id 또는 임의의 텍스트 헤더), Mailable 클래스에 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를 기반으로 동작합니다. Envelope의 using 파라미터를 이용하면, 메일 발송 직전에 Symfony Email 인스턴스에 직접 접근하여 세밀한 커스터마이징을 할 수 있습니다.
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을 사용하면 메일 알림에서 제공하는 사전 구축된 템플릿과 컴포넌트를 Mailable에서도 그대로 활용할 수 있습니다. 메시지를 Markdown으로 작성하면, Laravel이 자동으로 보기 좋은 반응형 HTML 이메일을 렌더링하고, 동시에 일반 텍스트(plain-text) 버전도 자동으로 생성해 줍니다.
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 표를 컴포넌트의 콘텐츠로 전달하면 되며, Markdown 기본 문법의 열 정렬(가운데, 오른쪽 등)도 지원됩니다.
<x-mail::table>
| 상품명 | 수량 | 금액 |
| ------------- | :-----------: | ------------: |
| 티셔츠 | 2 | ₩30,000 |
| 모자 | 1 | ₩15,000 |
</x-mail::table>컴포넌트 커스터마이징
Markdown 메일 컴포넌트를 직접 수정하고 싶다면, 먼저 애플리케이션으로 내보내야 합니다. vendor:publish Artisan 명령으로 laravel-mail 애셋 태그를 퍼블리시합니다.
php artisan vendor:publish --tag=laravel-mail이 명령을 실행하면 resources/views/vendor/mail 디렉터리에 Markdown 메일 컴포넌트 파일들이 복사됩니다. mail 디렉터리 안에는 html과 text 두 개의 하위 디렉터리가 있으며, 각각 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 string $theme = 'invoice';메일 발송
기본 발송
메일을 발송하려면 Mail 파사드의 to 메서드를 사용합니다. to 메서드는 이메일 주소 문자열, 사용자 인스턴스, 또는 사용자 컬렉션을 인수로 받습니다. 객체나 컬렉션을 전달하면 메일러가 해당 객체의 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));수신자 목록 순회 시 주의사항
여러 수신자에게 메일을 한 명씩 발송해야 할 때는 반복문을 사용하게 됩니다. 그런데 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()->plus(minutes: 10), new OrderShipped($order));특정 큐 및 연결 지정
make:mail 명령으로 생성된 Mailable 클래스는 Illuminate\Bus\Queueable 트레이트를 사용하므로, onQueue와 onConnection 메서드를 통해 큐 이름과 연결(connection)을 지정할 수 있습니다.
$message = (new OrderShipped($order))
->onConnection('sqs')
->onQueue('emails');
Mail::to($request->user())
->cc($moreUsers)
->bcc($evenMoreUsers)
->queue($message);항상 큐로 처리되는 Mailable
특정 Mailable 클래스를 항상 큐로 처리하고 싶다면 ShouldQueue 인터페이스를 구현하세요. 이 인터페이스가 구현되어 있으면 send 메서드를 호출해도 자동으로 큐에 추가됩니다.
use Illuminate\Contracts\Queue\ShouldQueue;
class OrderShipped extends Mailable implements ShouldQueue
{
// ...
}큐 메일과 데이터베이스 트랜잭션
데이터베이스 트랜잭션 내에서 큐 Mailable을 디스패치하면, 트랜잭션이 커밋되기 전에 큐 워커가 해당 Job을 먼저 처리할 수 있습니다. 이 경우 트랜잭션 안에서 변경하거나 생성한 모델·레코드가 아직 데이터베이스에 반영되지 않은 상태일 수 있으며, Mailable이 이 데이터에 의존한다면 예기치 않은 오류가 발생할 수 있습니다.
큐 연결의 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 클래스에 failed 메서드가 정의되어 있으면 해당 메서드가 호출됩니다. 실패 원인이 된 Throwable 인스턴스가 인수로 전달됩니다.
<?php
namespace App\Mail;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Throwable;
class OrderDelayed extends Mailable implements ShouldQueue
{
use SerializesModels;
/**
* 큐 메일 발송 실패 시 처리합니다.
*/
public function failed(Throwable $exception): void
{
// 실패 처리 로직...
}
}Mailable 렌더링
메일을 실제로 발송하지 않고 HTML 콘텐츠만 확인하고 싶을 때는 Mailable 객체의 render 메서드를 사용합니다. 이 메서드는 메일의 HTML을 문자열로 반환합니다.
use App\Mail\InvoicePaid;
use App\Models\Invoice;
$invoice = Invoice::find(1);
return (new InvoicePaid($invoice))->render();브라우저에서 메일 미리보기
메일 템플릿을 디자인할 때마다 실제 이메일을 발송해서 확인하는 것은 번거롭습니다. Laravel에서는 라우트 클로저나 컨트롤러에서 Mailable 객체를 직접 반환하면, 브라우저에서 렌더링된 메일을 바로 확인할 수 있습니다. Blade 뷰를 개발하듯 빠르게 레이아웃과 디자인을 검토할 수 있어 편리합니다.
Route::get('/mailable', function () {
$invoice = App\Models\Invoice::find(1);
return new App\Mail\InvoicePaid($invoice);
});NOTE
이 방법은 로컬 개발 환경에서만 활용하세요. 프로덕션 환경에서는 해당 라우트를 반드시 제거하거나 미들웨어로 접근을 제한해야 합니다.
Mailable 로컬라이징
Laravel은 현재 요청의 로케일과 다른 언어로 Mailable을 전송할 수 있습니다. 메일이 큐에 등록된 경우에도 지정한 로케일이 유지됩니다.
Mail 파사드의 locale 메서드로 원하는 언어를 지정하면, Mailable 템플릿이 렌더링되는 동안 해당 로케일로 전환되었다가 렌더링이 완료되면 이전 로케일로 자동 복원됩니다.
Mail::to($request->user())->locale('ko')->send(
new OrderShipped($order)
);NOTE
예를 들어 한국어(ko), 영어(en), 일본어(ja) 등 사용자별로 다른 언어로 주문 확인 메일을 보내야 할 때 유용합니다.
사용자별 선호 로케일
애플리케이션에서 각 사용자의 선호 언어를 저장하는 경우, 모델에 HasLocalePreference 인터페이스를 구현하면 Laravel이 메일 전송 시 해당 로케일을 자동으로 사용합니다.
use Illuminate\Contracts\Translation\HasLocalePreference;
class User extends Model implements HasLocalePreference
{
/**
* 사용자의 선호 로케일을 반환합니다.
*/
public function preferredLocale(): string
{
return $this->locale;
}
}인터페이스를 구현하면, 해당 모델로 메일이나 알림을 보낼 때 Laravel이 자동으로 선호 로케일을 적용합니다. 따라서 locale 메서드를 별도로 호출할 필요가 없습니다.
Mail::to($request->user())->send(new OrderShipped($order));테스트
Mailable 콘텐츠 테스트
Laravel은 Mailable의 구조를 검사하고, 예상한 콘텐츠가 포함되어 있는지 확인할 수 있는 다양한 assertion 메서드를 제공합니다.
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->assertDontSeeInHtml('Invoice Not Paid');
$mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);
$mailable->assertSeeInText($user->email);
$mailable->assertDontSeeInText('Invoice Not Paid');
$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->assertDontSeeInHtml('Invoice Not Paid');
$mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);
$mailable->assertSeeInText($user->email);
$mailable->assertDontSeeInText('Invoice Not Paid');
$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']);
}assertSeeInHtml 계열 메서드는 메일의 HTML 버전에 해당 문자열이 포함되어 있는지 확인하고, assertSeeInText 계열 메서드는 일반 텍스트 버전을 기준으로 검사합니다.
Mailable 발송 테스트
Mailable의 콘텐츠 검증과 발송 여부 검증은 별도의 테스트로 분리하는 것을 권장합니다. 발송 여부를 확인하는 테스트에서는 메일 내용보다는 "해당 Mailable이 특정 사용자에게 발송 지시가 내려졌는가"에 집중하면 충분합니다.
Mail 파사드의 fake 메서드를 호출하면 실제 메일 발송을 차단하고, 이후 다양한 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);
// 특정 Mailable이 정확히 2회 발송되었는지 확인...
Mail::assertSentTimes(OrderShipped::class, 2);
// 총 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);
// 특정 Mailable이 정확히 2회 발송되었는지 확인...
Mail::assertSentTimes(OrderShipped::class, 2);
// 총 3개의 Mailable이 발송되었는지 확인...
Mail::assertSentCount(3);
}
}메일을 백그라운드 큐에 넣어 비동기로 발송하는 경우에는 assertSent 대신 assertQueued를 사용해야 합니다.
Mail::assertQueued(OrderShipped::class);
Mail::assertNotQueued(OrderShipped::class);
Mail::assertNothingQueued();
Mail::assertQueuedCount(3);발송 또는 큐에 등록된 메일의 총 건수를 한 번에 검증하려면 assertOutgoingCount를 사용하세요.
Mail::assertOutgoingCount(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('...') &&
$mail->hasMetadata('order_id', $mail->order->id);
$mail->usesMailer('ses');
});첨부 파일 검증도 Mailable 인스턴스의 메서드로 처리할 수 있습니다.
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 메일 드라이버는 메일을 실제로 전송하는 대신, 모든 메일 메시지를 로그 파일에 기록합니다. 이 드라이버는 주로 로컬 개발 환경에서만 사용합니다. 환경별 설정 방법에 대한 자세한 내용은 설정 문서를 참고하세요.
HELO / Mailtrap / Mailpit
또 다른 방법으로, HELO나 Mailtrap 같은 서비스를 활용하는 방법이 있습니다. smtp 드라이버를 통해 가상의 수신함으로 메일을 전송하면, 실제 메일 클라이언트와 유사한 환경에서 렌더링 결과를 직접 확인할 수 있습니다. 특히 메일의 HTML 레이아웃이나 스타일이 의도한 대로 표시되는지 검증할 때 유용합니다.
Laravel Sail을 사용하고 있다면 Mailpit으로 메일을 미리 확인할 수 있습니다. Sail이 실행 중인 상태에서 http://localhost:8025에 접속하면 Mailpit 인터페이스를 바로 사용할 수 있습니다.
전역 수신 주소(to) 지정
Mail 파사드의 alwaysTo 메서드를 사용하면 전역 수신 주소를 고정할 수 있습니다. 애플리케이션에서 발송되는 모든 메일이 지정한 주소 하나로만 전달되므로, 개발 중 의도치 않게 실제 사용자에게 메일이 전송되는 상황을 방지할 수 있습니다. 이 메서드는 보통 서비스 프로바이더의 boot 메서드에서 호출합니다.
use Illuminate\Support\Facades\Mail;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
if ($this->app->environment('local')) {
Mail::alwaysTo('taylor@example.com');
}
}NOTE
alwaysTo 메서드를 사용하면 메일에 지정된 cc 및 bcc 주소는 모두 제거됩니다. 전역 수신 주소만 남으니 주의하세요.
이벤트
Laravel은 메일을 발송할 때 두 가지 이벤트를 디스패치합니다. MessageSending 이벤트는 메시지가 발송되기 직전에, MessageSent 이벤트는 발송이 완료된 직후에 디스패치됩니다. 이 이벤트들은 메일이 실제로 발송될 때 디스패치되며, 큐에 등록될 때는 디스패치되지 않는다는 점에 주의하세요.
NOTE
메일을 큐에 등록(queue())한 경우, 이벤트는 큐 워커가 실제로 메일을 처리하는 시점에 디스패치됩니다. 큐에 넣는 순간이 아닙니다.
애플리케이션에서 이 이벤트에 대한 이벤트 리스너를 등록하여 활용할 수 있습니다. 예를 들어, 발송 직전에 메시지 내용을 로깅하는 리스너를 아래와 같이 작성할 수 있습니다:
use Illuminate\Mail\Events\MessageSending;
// use Illuminate\Mail\Events\MessageSent;
class LogMessage
{
/**
* 이벤트를 처리합니다.
*/
public function handle(MessageSending $event): void
{
// $event->message 를 통해 발송될 메시지 객체에 접근할 수 있습니다.
}
}메일
커스텀 트랜스포트
Laravel은 다양한 메일 트랜스포트를 기본으로 제공하지만, 기본 지원 목록에 없는 서비스를 통해 이메일을 전송하고 싶다면 직접 트랜스포트 클래스를 작성할 수 있습니다.
시작하려면 Symfony\Component\Mailer\Transport\AbstractTransport 클래스를 상속하는 클래스를 정의하고, doSend와 __toString 메서드를 구현하세요:
<?php
namespace App\Mail;
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 메서드를 통해 등록합니다. 일반적으로 AppServiceProvider의 boot 메서드 안에서 등록하는 것이 좋습니다. extend에 전달하는 클로저에는 $config 인자가 주입되는데, 이 값은 config/mail.php에서 해당 mailer에 대해 정의한 설정 배열입니다:
use App\Mail\MailchimpTransport;
use Illuminate\Support\Facades\Mail;
use MailchimpTransactional\ApiClient;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Mail::extend('mailchimp', function (array $config = []) {
$client = new ApiClient;
$client->setApiKey($config['key']);
return new MailchimpTransport($client);
});
}트랜스포트를 등록한 후에는 config/mail.php에 해당 트랜스포트를 사용하는 mailer 설정을 추가합니다:
'mailchimp' => [
'transport' => 'mailchimp',
'key' => env('MAILCHIMP_API_KEY'),
// ...
],Symfony 추가 트랜스포트
Laravel은 Mailgun, Postmark 등 Symfony에서 공식으로 관리하는 일부 트랜스포트를 기본 지원합니다. 하지만 그 외의 Symfony 공식 트랜스포트도 Composer로 패키지를 설치한 뒤 Laravel에 등록하는 방식으로 사용할 수 있습니다.
예를 들어, "Brevo"(구 "Sendinblue") Symfony mailer를 설치하고 등록하는 방법은 다음과 같습니다:
composer require symfony/brevo-mailer symfony/http-client패키지 설치 후, config/services.php에 Brevo API 키 설정을 추가합니다:
'brevo' => [
'key' => env('BREVO_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에 새 트랜스포트를 사용하는 mailer 설정을 추가합니다:
'brevo' => [
'transport' => 'brevo',
// ...
],NOTE
Symfony 공식 트랜스포트 목록과 각 패키지 이름은 Symfony Mailer 공식 문서에서 확인할 수 있습니다.