본문 바로가기

메일

업데이트됨

번역일: 2026년 9월 10일

이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.

원문 수정
2026년 9월 9일
번역 갱신
2026년 9월 10일

메일

메일

소개

이메일 발송, 어렵게 생각할 필요 없습니다. Laravel은 유명한 Symfony Mailer 컴포넌트를 기반으로 깔끔하고 단순한 이메일 API를 제공합니다. Laravel과 Symfony Mailer는 SMTP, Cloudflare, Mailgun, Postmark, Resend, Amazon SES, sendmail을 통한 이메일 발송 드라이버를 제공하므로, 로컬 환경이든 클라우드 서비스든 원하는 방식으로 손쉽게 이메일 발송을 시작할 수 있습니다.

설정

Laravel의 메일 서비스는 애플리케이션의 config/mail.php 설정 파일을 통해 구성합니다. 이 파일에 설정된 각 메일러(mailer)는 저마다 고유한 설정과 "트랜스포트(transport)"를 가질 수 있으며, 덕분에 애플리케이션은 이메일 종류에 따라 서로 다른 발송 서비스를 사용할 수 있습니다. 예를 들어 거래(transactional) 메일은 Postmark로, 대량 메일은 Amazon SES로 발송하는 식의 구성이 가능합니다.

mail 설정 파일 안에는 mailers 설정 배열이 있습니다. 이 배열에는 Laravel이 지원하는 주요 메일 드라이버 / 트랜스포트별로 샘플 설정 항목이 들어 있으며, default 설정값은 애플리케이션이 이메일을 발송할 때 기본으로 사용할 메일러를 지정합니다.

드라이버 / 트랜스포트 사전 준비 사항

Mailgun, Postmark, Resend와 같은 API 기반 드라이버는 SMTP 서버를 통한 발송보다 대체로 더 간단하고 빠릅니다. 가능하다면 이러한 드라이버 중 하나를 사용하는 것을 권장합니다.

Cloudflare 드라이버

Cloudflare 드라이버를 사용하려면 Composer로 Symfony의 HTTP Client를 설치합니다.

composer require symfony/http-client

그다음 애플리케이션의 config/mail.php 설정 파일에서 두 가지를 변경해야 합니다. 먼저 기본 메일러를 cloudflare로 설정합니다.

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

두 번째로, mailers 배열에 다음 설정을 추가합니다.

'cloudflare' => [ 'transport' => 'cloudflare', ],

기본 메일러 설정을 마쳤다면, config/services.php 설정 파일에 아래 옵션을 추가하세요.

'cloudflare' => [ 'account_id' => env('CLOUDFLARE_ACCOUNT_ID'), 'key' => env('CLOUDFLARE_KEY'), ],

Mailgun 드라이버

Mailgun 드라이버를 사용하려면 Composer로 Symfony의 Mailgun Mailer 트랜스포트를 설치합니다.

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

그다음 애플리케이션의 config/mail.php 설정 파일에서 두 가지를 변경해야 합니다. 먼저 기본 메일러를 mailgun으로 설정합니다.

'default' => env('MAIL_MAILER', '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 리전을 사용하고 있다면, services 설정 파일에 해당 리전의 엔드포인트를 지정해야 합니다.

'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로 지정합니다. 기본 메일러 설정을 마쳤다면, config/services.php 설정 파일에 다음 옵션이 포함되어 있는지 확인하세요.

'postmark' => [ 'key' => env('POSTMARK_API_KEY'), ],

특정 메일러가 사용할 Postmark 메시지 스트림을 지정하고 싶다면, config/mail.php 파일에 있는 해당 메일러 설정 배열에 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-php

그다음 애플리케이션의 config/mail.php 설정 파일에서 default 옵션 값을 resend로 지정합니다. 기본 메일러 설정을 마쳤다면, config/services.php 설정 파일에 다음 옵션이 포함되어 있는지 확인하세요.

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

SES 드라이버

Amazon SES 드라이버를 사용하려면 먼저 Amazon AWS SDK for PHP를 설치해야 합니다. Composer 패키지 매니저를 통해 이 라이브러리를 설치할 수 있습니다.

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)을 사용하려면, 애플리케이션의 SES 설정에 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의 구독 관리 기능(subscription management features)을 사용하려면, 메일 메시지의 headers 메서드가 반환하는 배열에 X-Ses-List-Management-Options 헤더를 추가하면 됩니다.

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

SES 테넌트(tenant)를 통해 이메일을 발송하려면, headers 메서드에서 X-Ses-Tenant-Name 헤더를 반환하면 됩니다. Laravel은 메시지를 전송할 때 이 헤더 값을 SES의 TenantName 옵션으로 전달합니다.

public function headers(): Headers { return new Headers( text: [ 'X-Ses-Tenant-Name' => 'tenant-id', ], ); }

이메일을 발송할 때 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'], ], ], ],

장애 조치(Failover) 설정

애플리케이션의 메일 발송에 사용 중인 외부 서비스가 장애로 다운되는 경우가 있을 수 있습니다. 이런 상황에 대비해, 기본 발송 드라이버가 다운되었을 때 사용할 백업 메일 발송 설정을 하나 이상 정의해두면 유용합니다.

이를 위해서는 mail 설정 파일에서 failover 트랜스포트를 사용하는 메일러를 정의하면 됩니다. failover 메일러의 설정 배열에는 발송 시 어떤 순서로 메일러를 선택할지를 지정하는 mailers 배열을 포함해야 합니다.

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

failover 트랜스포트를 사용하는 메일러를 정의했다면, 이 장애 조치 기능이 실제로 동작하도록 애플리케이션의 .env 파일에서 해당 메일러를 기본 메일러로 지정해야 합니다.

MAIL_MAILER=failover

라운드 로빈(Round Robin) 설정

roundrobin 트랜스포트를 사용하면 메일 발송 부하를 여러 메일러에 분산시킬 수 있습니다. 먼저 mail 설정 파일에서 roundrobin 트랜스포트를 사용하는 메일러를 정의합니다. roundrobin 메일러의 설정 배열에는 발송에 사용할 메일러 목록을 담은 mailers 배열을 포함해야 합니다.

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

라운드 로빈 메일러를 정의했다면, mail 설정 파일의 default 설정 키 값으로 이 메일러의 이름을 지정하여 애플리케이션의 기본 메일러로 사용하도록 설정합니다.

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

roundrobin 트랜스포트는 설정된 메일러 목록 중 하나를 무작위로 선택한 뒤, 이후 메일을 발송할 때마다 순서대로 다음 메일러로 전환합니다. 고가용성(high availability) 확보에 도움을 주는 failover 트랜스포트와 달리, roundrobin 트랜스포트는 *부하 분산(load balancing)*을 제공한다는 차이가 있습니다.

NOTE

실무에서는 두 방식을 혼동하기 쉬운데, failover는 "장애 발생 시 대체 수단으로 전환"하는 것이 목적이고, roundrobin은 "장애 여부와 관계없이 발송량을 여러 서비스에 고르게 분산"하는 것이 목적이라는 점을 기억하면 됩니다.

메일

메일러블 클래스 생성하기

라라벨 애플리케이션에서 발송하는 모든 이메일 유형은 "메일러블(mailable)" 클래스로 표현됩니다. 이 클래스들은 app/Mail 디렉터리에 저장됩니다. 애플리케이션에 이 디렉터리가 아직 보이지 않더라도 걱정할 필요는 없습니다. make:mail Artisan 명령어로 첫 번째 메일러블 클래스를 생성하면 자동으로 만들어집니다.

php artisan make:mail OrderShipped

Mailable 클래스 작성하기

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

envelope 메서드는 메일의 제목과 (필요하다면) 수신자 정보를 정의하는 Illuminate\Mail\Mailables\Envelope 객체를 반환합니다. content 메서드는 메일 내용을 렌더링할 때 사용할 Blade 템플릿을 정의하는 Illuminate\Mail\Mailables\Content 객체를 반환합니다.

발신자 설정하기

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: 'Order Shipped', ); }

필요하다면 replyTo 주소도 함께 지정할 수 있습니다.

return new Envelope( from: new Address('jeffrey@example.com', 'Jeffrey Way'), replyTo: [ new Address('taylor@example.com', 'Taylor Otwell'), ], subject: 'Order Shipped', );

전역 `from` 주소 사용하기

애플리케이션에서 보내는 모든 이메일에 동일한 "from" 주소를 사용한다면, 생성하는 Mailable 클래스마다 매번 이를 지정하는 것은 번거로운 일입니다. 이런 경우에는 config/mail.php 설정 파일에 전역 "from" 주소를 지정해두면 됩니다. Mailable 클래스 내에서 별도로 "from" 주소를 지정하지 않으면 이 전역 설정이 사용됩니다.

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

또한 config/mail.php 설정 파일에 전역 "reply_to" 주소도 정의할 수 있습니다.

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

뷰 설정하기

Mailable 클래스의 content 메서드에서는 이메일 내용을 렌더링할 때 사용할 view(템플릿)을 정의할 수 있습니다. 일반적으로 이메일은 Blade 템플릿을 사용해 렌더링되므로, HTML 이메일을 작성할 때 Blade 템플릿 엔진의 강력한 기능을 그대로 활용할 수 있습니다.

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

NOTE

이메일 템플릿을 모아두기 위해 resources/views/mail 디렉터리를 만드는 것을 추천하지만, resources/views 디렉터리 내 원하는 위치에 자유롭게 배치해도 됩니다.

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

이메일의 일반 텍스트 버전을 함께 정의하고 싶다면, Content 정의에서 일반 텍스트용 템플릿을 지정하면 됩니다. view 매개변수와 마찬가지로 text 매개변수 역시 이메일 내용을 렌더링할 템플릿 이름을 지정합니다. HTML 버전과 일반 텍스트 버전을 동시에 정의하는 것도 가능합니다.

/** * 메시지 내용(content) 정의를 반환합니다. */ 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 속성을 통한 전달

이메일 HTML을 렌더링할 때 사용할 데이터를 뷰에 전달해야 하는 경우가 많습니다. 뷰에 데이터를 전달하는 방법은 두 가지입니다. 첫 번째는 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, ) {} /** * 메시지 내용(content) 정의를 반환합니다. */ public function content(): Content { return new Content( view: 'mail.orders.shipped', ); } }

데이터가 public 속성에 저장되면 뷰에서 자동으로 접근할 수 있으므로, 다른 Blade 데이터를 다루듯 그대로 사용하면 됩니다.

<div> Price: {{ $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; /** * 새 메시지 인스턴스를 생성합니다. */ public function __construct( protected Order $order, ) {} /** * 메시지 내용(content) 정의를 반환합니다. */ public function content(): Content { return new Content( view: 'mail.orders.shipped', with: [ 'orderName' => $this->order->name, 'orderPrice' => $this->order->price, ], ); } }

with 매개변수를 통해 전달한 데이터 역시 뷰에서 자동으로 사용할 수 있으므로, 다른 Blade 데이터와 동일한 방식으로 접근하면 됩니다.

<div> Price: {{ $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('name.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('name.pdf') ->withMime('application/pdf'), ]; }

기본 디스크가 아닌 다른 저장소 디스크를 지정하고 싶다면 fromStorageDisk 메서드를 사용하면 됩니다.

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

Raw 데이터 첨부하기

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'), ]; }

인라인 첨부(Inline Attachments)

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

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

WARNING

일반 텍스트 메시지 템플릿에서는 인라인 첨부를 사용하지 않으므로 $message 변수를 사용할 수 없습니다.

Raw 데이터로 인라인 첨부하기

이미 이미지의 원시 데이터 문자열을 가지고 있고 이를 이메일 템플릿에 삽입하고 싶다면, $message 변수의 embedData 메서드를 호출하면 됩니다. embedData 메서드를 호출할 때는 삽입할 이미지에 지정할 파일 이름도 함께 전달해야 합니다.

<body> 다음은 원시 데이터로부터 생성한 이미지입니다: <img src="{{ $message->embedData($data, 'example-image.jpg') }}"> </body>

Attachable 객체

간단한 문자열 경로로 파일을 첨부하는 것만으로도 충분한 경우가 많지만, 애플리케이션 내에서 첨부 대상 엔티티가 별도의 클래스로 표현되는 경우도 흔합니다. 예를 들어 애플리케이션이 사진을 메시지에 첨부한다면, 그 사진을 표현하는 Photo 모델이 이미 존재할 수 있습니다. 이런 경우 Photo 모델을 그대로 attach 메서드에 전달할 수 있다면 훨씬 편리하겠죠. 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와 같은 원격 파일 저장소에 저장되어 있을 수도 있습니다. Laravel은 애플리케이션의 파일시스템 디스크에 저장된 데이터로부터 첨부 파일 인스턴스를 생성하는 것도 지원합니다.

// 기본 디스크에 저장된 파일로부터 첨부 파일 생성... return Attachment::fromStorage($this->path); // 특정 디스크에 저장된 파일로부터 첨부 파일 생성... return Attachment::fromStorageDisk('backblaze', $this->path);

또한, 메모리에 있는 데이터로부터 첨부 파일 인스턴스를 만들 수도 있습니다. 이를 위해서는 fromData 메서드에 클로저를 전달하면 되며, 이 클로저는 첨부 파일을 구성할 원시 데이터를 반환해야 합니다.

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

이 외에도 Laravel은 첨부 파일을 커스터마이징할 수 있는 여러 메서드를 제공합니다. 예를 들어 as, withMime 메서드를 사용해 파일 이름과 MIME 타입을 지정할 수 있습니다.

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

헤더(Headers)

경우에 따라 발신 메시지에 추가 헤더를 첨부해야 할 수 있습니다. 예를 들어 커스텀 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', ], ); }

태그(Tags)와 메타데이터(Metadata)

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

use Illuminate\Mail\Mailables\Envelope; /** * 메시지 envelope을 반환합니다. * * @return \Illuminate\Mail\Mailables\Envelope */ public function envelope(): Envelope { return new Envelope( subject: 'Order Shipped', tags: ['shipment'], metadata: [ 'order_id' => $this->order->id, ], ); }

애플리케이션이 Mailgun 드라이버를 사용하는 경우, Mailgun 공식 문서에서 태그메타데이터에 대한 자세한 내용을 확인할 수 있습니다. 마찬가지로 Postmark를 사용한다면 Postmark 문서에서 태그메타데이터 지원에 대한 내용을 참고하세요.

Amazon SES를 사용해 이메일을 발송하는 경우에는, metadata 메서드를 사용해 SES "태그"를 메시지에 첨부하면 됩니다.

Symfony 메시지 커스터마이징하기

Laravel의 메일 발송 기능은 내부적으로 Symfony Mailer를 기반으로 동작합니다. Laravel은 메시지가 실제로 발송되기 전에 Symfony Message 인스턴스와 함께 호출되는 커스텀 콜백을 등록할 수 있도록 지원합니다. 이를 활용하면 메시지 발송 전에 원하는 만큼 깊이 있게 메시지를 커스터마이징할 수 있습니다. 이를 위해서는 Envelope 정의에 using 매개변수를 지정하면 됩니다.

use Illuminate\Mail\Mailables\Envelope; use Symfony\Component\Mime\Email; /** * 메시지 envelope을 반환합니다. */ public function envelope(): Envelope { return new Envelope( subject: 'Order Shipped', using: [ function (Email $message) { // ... }, ] ); }

Markdown Mailable

Markdown mailable를 사용하면 메일 알림에서 제공하는 사전 제작된 템플릿과 컴포넌트를 mailable에서도 그대로 활용할 수 있습니다. 메시지를 Markdown으로 작성하면, Laravel이 이를 반응형 HTML 템플릿으로 아름답게 렌더링해줄 뿐만 아니라 일반 텍스트 버전도 자동으로 생성해줍니다.

NOTE

HTML 이메일 클라이언트마다 렌더링 방식이 제각각이라 직접 스타일링하기 까다로운데, Markdown mailable을 쓰면 이런 고민 없이도 깔끔하고 반응형인 이메일을 빠르게 만들 수 있습니다.

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> <h1 id="panel-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> | Laravel | Table | Example | | ------------- | :-----------: | ------------: | | Col 2 is | Centered | $10 | | Col 3 is | Right-Aligned | $20 | </x-mail::table>

컴포넌트 커스터마이징

Markdown 메일 컴포넌트 전체를 애플리케이션으로 내보내 자유롭게 커스터마이징할 수 있습니다. 컴포넌트를 내보내려면 vendor:publish Artisan 명령어로 laravel-mail 에셋 태그를 퍼블리시하면 됩니다.

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

이 명령어를 실행하면 Markdown 메일 컴포넌트가 resources/views/vendor/mail 디렉터리에 퍼블리시됩니다. mail 디렉터리 안에는 htmltext 디렉터리가 있으며, 각 디렉터리에는 사용 가능한 모든 컴포넌트의 HTML 버전과 텍스트 버전이 각각 들어 있습니다. 이 컴포넌트들은 원하는 대로 자유롭게 수정할 수 있습니다.

CSS 커스터마이징

컴포넌트를 내보내고 나면 resources/views/vendor/mail/html/themes 디렉터리에 default.css 파일이 생성됩니다. 이 파일의 CSS를 수정하면, Markdown 메일 메시지의 HTML 버전을 렌더링할 때 해당 스타일이 자동으로 인라인 CSS로 변환되어 적용됩니다.

Laravel의 Markdown 컴포넌트를 위한 완전히 새로운 테마를 만들고 싶다면, html/themes 디렉터리에 새 CSS 파일을 추가하면 됩니다. CSS 파일을 원하는 이름으로 저장한 뒤, 애플리케이션의 config/mail.php 설정 파일에서 theme 옵션 값을 새로 만든 테마 이름으로 변경하세요.

특정 mailable 하나에만 테마를 다르게 적용하고 싶다면, 해당 mailable 클래스의 $theme 프로퍼티에 사용할 테마 이름을 지정하면 됩니다.

NOTE

예를 들어 서비스 브랜드에 맞춰 파란색 계열 테마를 하나 만들고, 프로모션 메일에는 별도로 강조 색상의 테마를 적용하는 식으로 mailable별 테마 분리가 가능합니다.

메일 발송

메일을 발송하려면 Mail 파사드to 메서드를 사용합니다. to 메서드는 이메일 주소, 사용자 인스턴스, 또는 사용자 컬렉션을 인자로 받을 수 있습니다. 객체나 객체 컬렉션을 전달하면 메일러가 자동으로 해당 객체의 emailname 속성을 사용해 수신자를 결정하므로, 전달하는 객체에 이 속성들이 존재하는지 미리 확인해두어야 합니다. 수신자를 지정한 다음에는 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 { /** * Ship the given order. */ public function store(Request $request): RedirectResponse { $order = Order::findOrFail($request->order_id); // Ship the order... Mail::to($request->user())->send(new OrderShipped($order)); return redirect('/orders'); } }

메일 발송 시 "to" 수신자만 지정할 수 있는 것은 아닙니다. to, cc, bcc 메서드를 체이닝하여 각각의 수신자를 자유롭게 지정할 수 있습니다.

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

여러 수신자에게 반복 발송하기

수신자 배열이나 이메일 주소 목록을 순회하면서 Mailable을 발송해야 하는 경우가 있습니다. 이때 주의할 점이 있는데, to 메서드는 Mailable의 수신자 목록에 이메일 주소를 계속 "추가"하는 방식으로 동작하기 때문에, 반복문을 돌 때마다 이전에 추가했던 모든 수신자에게 메일이 또다시 발송되는 문제가 생길 수 있습니다. 따라서 각 수신자마다 Mailable 인스턴스를 매번 새로 생성해야 합니다.

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를 통해 이 작업을 손쉽게 처리할 수 있도록 지원합니다. 메일 메시지를 큐에 담으려면, 수신자를 지정한 뒤 Mail 파사드의 queue 메서드를 호출하면 됩니다.

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

이 메서드는 메시지를 백그라운드에서 발송할 수 있도록 자동으로 Job을 큐에 등록해줍니다. 이 기능을 사용하려면 먼저 큐 설정을 완료해야 합니다.

지연 발송을 위한 큐 처리

큐에 등록한 이메일 메시지의 발송 시점을 지연시키고 싶다면 later 메서드를 사용하면 됩니다. later 메서드의 첫 번째 인자로는 메시지가 발송되어야 할 시점을 나타내는 DateTime 인스턴스를 전달합니다.

Mail::to($request->user()) ->cc($moreUsers) ->bcc($evenMoreUsers) ->later(now()->plus(minutes: 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()) ->cc($moreUsers) ->bcc($evenMoreUsers) ->queue($message);

또는 Mailable 클래스에 Connection, Queue 어트리뷰트를 지정하는 방식으로도 커넥션과 큐를 설정할 수 있습니다.

use Illuminate\Queue\Attributes\Connection; use Illuminate\Queue\Attributes\Queue; #[Connection('sqs')] #[Queue('emails')] class OrderShipped extends Mailable { // ... }

기본적으로 큐에 담아 발송하기

항상 큐를 통해 발송되어야 하는 Mailable 클래스가 있다면, 해당 클래스에 ShouldQueue 계약을 구현하면 됩니다. 이렇게 하면 메일 발송 시 send 메서드를 호출하더라도, 해당 클래스가 ShouldQueue를 구현하고 있으므로 자동으로 큐에 등록되어 처리됩니다.

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

큐에 등록된 Mailable과 데이터베이스 트랜잭션

큐에 등록된 Mailable을 데이터베이스 트랜잭션 내부에서 디스패치하면, 트랜잭션이 커밋되기도 전에 큐 워커가 먼저 Job을 처리해버릴 수 있습니다. 이 경우 트랜잭션 내에서 변경한 모델이나 데이터베이스 레코드의 내용이 아직 데이터베이스에 반영되지 않은 상태일 수 있으며, 트랜잭션 내에서 새로 생성한 모델이나 레코드는 아예 존재하지 않는 상태일 수도 있습니다. 만약 Mailable이 이런 모델들에 의존하고 있다면, 큐에 등록된 메일 발송 Job이 처리될 때 예상치 못한 오류가 발생할 수 있습니다.

사용 중인 큐 커넥션의 after_commit 설정 값이 false로 지정되어 있더라도, 특정 Mailable에 대해서만은 모든 열린 데이터베이스 트랜잭션이 커밋된 이후에 디스패치되도록 지정할 수 있습니다. 메일 발송 시 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; /** * Create a new message instance. */ public function __construct() { $this->afterCommit(); } }

NOTE

이와 관련된 문제를 해결하는 방법에 대해 더 자세히 알아보려면 큐 작업과 데이터베이스 트랜잭션 문서를 참고하세요.

큐에 등록된 이메일 발송 실패 처리

큐에 등록된 이메일 발송이 실패하면, 해당 Mailable 클래스에 failed 메서드가 정의되어 있는 경우 이 메서드가 호출됩니다. 발송 실패의 원인이 된 Throwable 인스턴스가 failed 메서드에 전달됩니다.

<?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; /** * Handle a queued email's failure. */ public function failed(Throwable $exception): void { // ... } }

메일링

메일러블 렌더링하기

메일러블을 실제로 발송하지 않고 HTML 콘텐츠만 확인하고 싶을 때가 있습니다. 이럴 때는 메일러블의 render 메서드를 호출하면 됩니다. 이 메서드는 메일러블을 렌더링한 HTML 콘텐츠를 문자열로 반환합니다:

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

브라우저에서 메일러블 미리보기

메일러블 템플릿을 디자인하다 보면 일반 Blade 템플릿처럼 브라우저에서 렌더링 결과를 빠르게 확인하고 싶을 때가 많습니다. 이를 위해 라라벨은 라우트 클로저나 컨트롤러에서 메일러블 인스턴스를 그대로 반환할 수 있도록 지원합니다. 메일러블을 반환하면 실제 이메일 주소로 발송하지 않고도 브라우저에 렌더링되어 표시되므로, 디자인을 빠르게 확인할 수 있습니다:

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

NOTE

이 방법은 로컬 개발 환경에서 메일 템플릿의 레이아웃과 스타일을 빠르게 검증할 때 특히 유용합니다. 실제 메일 발송 로직(큐 처리, 첨부 파일 등)까지 확인하려면 메일 테스트 기능이나 Mailtrap, Mailpit 같은 로컬 메일 캐치 도구를 함께 사용하는 것을 권장합니다.

메일 로케일 지정하기

Laravel에서는 애플리케이션의 현재 로케일과 다른 언어로 메일러블을 발송할 수 있으며, 메일이 큐에 등록되더라도 지정한 로케일을 그대로 기억합니다.

이 기능은 Mail 파사드가 제공하는 locale 메서드를 통해 사용할 수 있습니다. 메일러블의 템플릿이 렌더링되는 동안에는 지정한 로케일로 전환되며, 렌더링이 끝나면 원래 로케일로 되돌아갑니다.

Mail::to($request->user())->locale('es')->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; } }

이 인터페이스를 구현하고 나면, 해당 모델에 메일이나 알림을 보낼 때 Laravel이 자동으로 선호 로케일을 적용합니다. 따라서 이 인터페이스를 사용하는 경우에는 별도로 locale 메서드를 호출할 필요가 없습니다.

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

NOTE

사용자마다 선호 언어가 다른 서비스(예: 다국어를 지원하는 이커머스 플랫폼)를 운영한다면, HasLocalePreference 계약을 활용하는 방식이 매번 locale을 호출하는 것보다 코드 관리 측면에서 훨씬 편리합니다.

테스트

Mailable 콘텐츠 테스트하기

라라벨은 Mailable의 구조를 확인할 수 있는 다양한 방법을 제공합니다. 이와 더불어, 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']); }

이름에서 짐작할 수 있듯이, "HTML" 관련 어서션은 Mailable의 HTML 버전 안에 특정 문자열이 포함되어 있는지 검사하고, "text" 관련 어서션은 일반 텍스트(plain-text) 버전 안에 특정 문자열이 포함되어 있는지 검사합니다.

Mailable 발송 테스트하기

Mailable의 콘텐츠를 검증하는 테스트와, 특정 Mailable이 특정 사용자에게 "발송되었는지"를 검증하는 테스트는 서로 분리해서 작성하는 것을 권장합니다. 대부분의 경우 테스트하려는 코드 로직에서는 Mailable의 실제 내용까지는 중요하지 않으며, 단지 라라벨이 해당 Mailable을 발송하도록 지시받았는지만 확인해도 충분합니다.

Mail 파사드의 fake 메서드를 사용하면 실제로 메일이 발송되는 것을 막을 수 있습니다. Mail::fake()를 호출한 뒤에는, 특정 사용자에게 Mailable이 발송 지시되었는지 검증할 수 있을 뿐 아니라, Mailable이 전달받은 데이터까지 살펴볼 수 있습니다.

Pest

<?php use App\Mail\OrderShipped; use Illuminate\Support\Facades\Mail; test('orders can be shipped', function () { Mail::fake(); // 주문 배송 처리 로직 실행... // 아무 메일도 발송되지 않았는지 검증... Mail::assertNothingSent(); // 특정 메일이 발송되었는지 검증... Mail::assertSent(OrderShipped::class); // 특정 메일이 두 번 발송되었는지 검증... Mail::assertSent(OrderShipped::class, 2); // 특정 메일이 특정 이메일 주소로 발송되었는지 검증... Mail::assertSent(OrderShipped::class, 'example@laravel.com'); // 특정 메일이 여러 이메일 주소로 발송되었는지 검증... Mail::assertSent(OrderShipped::class, ['example@laravel.com', '...']); // 특정 메일이 발송되지 않았는지 검증... Mail::assertNotSent(AnotherMailable::class); // 특정 메일이 두 번 발송되었는지 검증... Mail::assertSentTimes(OrderShipped::class, 2); // 특정 메일이 정확히 한 번만 발송되었는지 검증... Mail::assertSentOnce(OrderShipped::class); // 총 3개의 메일이 발송되었는지 검증... 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(); // 특정 메일이 발송되었는지 검증... Mail::assertSent(OrderShipped::class); // 특정 메일이 두 번 발송되었는지 검증... Mail::assertSent(OrderShipped::class, 2); // 특정 메일이 특정 이메일 주소로 발송되었는지 검증... Mail::assertSent(OrderShipped::class, 'example@laravel.com'); // 특정 메일이 여러 이메일 주소로 발송되었는지 검증... Mail::assertSent(OrderShipped::class, ['example@laravel.com', '...']); // 특정 메일이 발송되지 않았는지 검증... Mail::assertNotSent(AnotherMailable::class); // 특정 메일이 두 번 발송되었는지 검증... Mail::assertSentTimes(OrderShipped::class, 2); // 특정 메일이 정확히 한 번만 발송되었는지 검증... Mail::assertSentOnce(OrderShipped::class); // 총 3개의 메일이 발송되었는지 검증... Mail::assertSentCount(3); } }

메일을 큐를 통해 백그라운드로 발송하도록 설정했다면, assertSent 대신 assertQueued 메서드를 사용해야 합니다.

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

assertOutgoingCount 메서드를 사용하면 발송되었거나 큐에 등록된 Mailable의 총 개수를 검증할 수도 있습니다.

Mail::assertOutgoingCount(3);

assertSent, assertNotSent, assertQueued, assertNotQueued 메서드에는 클로저를 전달할 수도 있습니다. 이 클로저는 일종의 "조건 검사기"로 동작하는데, 발송(혹은 큐 등록)된 Mailable 중 클로저가 전달한 조건을 만족하는 것이 하나라도 있으면 어서션이 통과됩니다.

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

Mail 파사드의 어서션 메서드를 호출할 때, 클로저에 전달되는 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') ); });

메일이 발송되지 않았음을 검증하는 메서드는 assertNotSentassertNotQueued 두 가지가 있다는 점을 눈치채셨을 겁니다. 때로는 메일이 즉시 발송되지도, 큐에 등록되지도 않았음을 한 번에 검증하고 싶을 수도 있습니다. 이럴 때는 assertNothingOutgoingassertNotOutgoing 메서드를 사용하면 됩니다.

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

NOTE

테스트에서 Mail::fake()를 호출하면 실제 메일 발송 로직 자체는 실행되지 않으므로, 메일 서버 설정이나 외부 API 키가 없어도 테스트를 안전하게 실행할 수 있습니다. 실제 메일이 잘 발송되는지 최종 확인이 필요하다면, 스테이징 환경에서 Mailtrap이나 Mailhog 같은 메일 캐치 도구를 사용하는 것이 좋습니다.

메일

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

이메일을 발송하는 애플리케이션을 개발할 때, 로컬 개발 단계에서까지 실제 이메일 주소로 메일이 발송되는 것은 원치 않을 것입니다. Laravel은 로컬 개발 중에 실제 메일 발송을 "비활성화"할 수 있는 몇 가지 방법을 제공합니다.

Log 드라이버

log 메일 드라이버를 사용하면 실제로 메일을 발송하는 대신, 모든 메일 메시지를 로그 파일에 기록합니다. 기록된 내용을 확인하며 개발할 수 있는 것이죠. 이 드라이버는 보통 로컬 개발 환경에서만 사용합니다. 환경별로 애플리케이션 설정을 다르게 적용하는 방법은 설정 문서를 참고하세요.

HELO / Mailtrap / Mailpit

또 다른 방법으로는 HELOMailtrap 같은 서비스와 smtp 드라이버를 함께 사용하여, 실제 수신자가 아닌 "더미" 메일함으로 메일을 보내고 이를 실제 이메일 클라이언트에서 확인하는 방법이 있습니다. 이 방식의 장점은 Mailtrap의 메시지 뷰어를 통해 최종적으로 발송될 이메일의 실제 모습을 그대로 확인할 수 있다는 점입니다.

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" 주소는 모두 제거되고, 오직 지정한 전역 주소로만 메일이 발송됩니다. 로컬 환경에서 실수로 실제 사용자에게 메일이 발송되는 사고를 막는 데 유용합니다.

이벤트

라라벨은 메일 메시지를 전송하는 과정에서 두 가지 이벤트를 발생시킵니다. MessageSending 이벤트는 메시지가 전송되기 직전에 발생하고, MessageSent 이벤트는 메시지가 전송된 직후에 발생합니다. 여기서 주의할 점은, 이 이벤트들은 메일이 큐에 등록될 때가 아니라 실제로 전송될 때 발생한다는 것입니다.

애플리케이션에서 이 이벤트들에 대한 이벤트 리스너를 만들어 활용할 수 있습니다:

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

커스텀 전송 방식(Custom Transports)

Laravel는 다양한 메일 전송 방식(mail transport)을 기본으로 제공합니다. 하지만 Laravel이 기본 지원하지 않는 다른 서비스로 메일을 보내야 한다면, 직접 전송 방식을 만들 수도 있습니다. 시작하려면 Symfony\Component\Mailer\Transport\AbstractTransport 클래스를 상속받는 클래스를 정의하세요. 그런 다음 해당 클래스에 doSend, __toString 메서드를 구현하면 됩니다.

다음은 Mailchimp Transactional(구 Mandrill)을 사용하는 커스텀 전송 방식의 예시입니다:

<?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 메서드를 통해 이를 등록할 수 있습니다. 보통 이 작업은 애플리케이션의 AppServiceProviderboot 메서드에서 수행합니다. extend 메서드에 전달하는 클로저는 $config 인자를 받게 되는데, 이 인자에는 애플리케이션의 config/mail.php 설정 파일에 정의된 해당 메일러의 설정 배열이 담겨 있습니다:

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 설정 파일에 새 전송 방식을 사용하는 메일러 설정을 추가할 수 있습니다:

'mailchimp' => [ 'transport' => 'mailchimp', 'key' => env('MAILCHIMP_API_KEY'), // ... ],

NOTE

위 예시처럼 실제 서비스 API를 호출하는 로직은 각 서비스의 공식 SDK 문서를 참고해 작성해야 합니다. doSend 메서드 안에서 반드시 SentMessage에서 실제 이메일 데이터를 추출한 뒤, 각 서비스의 API 규격에 맞게 변환해서 전송해야 한다는 점을 기억하세요.

추가 Symfony 전송 방식

Laravel는 Mailgun, Postmark처럼 Symfony에서 관리하는 일부 메일 전송 방식을 기본으로 지원합니다. 하지만 Symfony가 관리하는 다른 전송 방식을 Laravel에 추가로 연동하고 싶을 수도 있습니다. 이 경우 Composer로 필요한 Symfony 메일러 패키지를 설치한 뒤, 해당 전송 방식을 Laravel에 등록하면 됩니다. 예를 들어 "Brevo"(구 "Sendinblue") Symfony 메일러를 설치하고 등록하는 방법은 다음과 같습니다:

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

Brevo 메일러 패키지 설치가 끝났다면, 애플리케이션의 services 설정 파일에 Brevo API 인증 정보를 추가합니다:

'brevo' => [ 'key' => env('BREVO_API_KEY'), ],

그다음 Mail 파사드의 extend 메서드를 사용해 이 전송 방식을 Laravel에 등록합니다. 보통 이 작업은 서비스 프로바이더의 boot 메서드 안에서 수행합니다:

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', // ... ],

이처럼 커스텀 전송 방식이나 추가 Symfony 전송 방식을 활용하면, Laravel가 공식적으로 지원하지 않는 국내외 이메일 발송 서비스(예: 네이버 클라우드 플랫폼의 Cloud Outbound Mailer 등)와도 손쉽게 연동할 수 있습니다. 핵심은 항상 동일합니다 — 전송 로직을 캡슐화한 Transport 클래스를 만들고, Mail::extend로 등록한 뒤, config/mail.php에서 메일러로 연결하는 것입니다.

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

번역일: 2026년 9월 10일