[Rails] Action Mailer: 이메일 발송
정의
Action Mailer는 Rails의 이메일 처리 프레임워크. 컨트롤러처럼 구조화된 mailer 클래스 + ERB 템플릿 + Active Job 통합. SMTP, Sendmail, SES, SendGrid 등 다양한 백엔드.
생성
bin/rails generate mailer UserMailer welcome reset_password
생성:
app/mailers/user_mailer.rbapp/mailers/application_mailer.rbapp/views/user_mailer/welcome.html.erb,.text.erbapp/views/layouts/mailer.html.erb,.text.erbtest/mailers/user_mailer_test.rb
Mailer 클래스
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: "noreply@example.com"
layout "mailer"
end
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def welcome(user)
@user = user
@url = root_url
mail(to: user.email, subject: "환영합니다")
end
def reset_password(user, token)
@user = user
@reset_url = edit_password_reset_url(token)
mail(
to: user.email,
subject: "비밀번호 재설정",
cc: ["audit@example.com"],
reply_to: "support@example.com",
)
end
end
템플릿
<%# app/views/user_mailer/welcome.html.erb %>
<!DOCTYPE html>
<html>
<body>
<h1><%= @user.name %>님 환영합니다!</h1>
<p>가입을 완료하려면 <%= link_to "여기", @url %>를 클릭하세요.</p>
</body>
</html>
<%# app/views/user_mailer/welcome.text.erb %>
<%= @user.name %>님 환영합니다!
가입 완료: <%= @url %>
HTML + text 둘 다 자동으로 multipart 메일에 포함. 클라이언트가 선택.
전송
# 즉시
UserMailer.welcome(user).deliver_now
# 비동기 (Active Job)
UserMailer.welcome(user).deliver_later
# 옵션
UserMailer.welcome(user).deliver_later(
wait: 5.minutes,
wait_until: Date.tomorrow.noon,
queue: "mailers",
priority: 10,
)
기본 큐 이름: mailers. Sidekiq/Solid Queue 워커가 처리.
설정
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV["SMTP_HOST"],
port: 587,
domain: "example.com",
user_name: ENV["SMTP_USER"],
password: ENV["SMTP_PASS"],
authentication: "plain",
enable_starttls_auto: true,
}
config.action_mailer.default_url_options = { host: "example.com" }
config.action_mailer.raise_delivery_errors = true
config.action_mailer.perform_deliveries = true
다른 백엔드
| Backend | 용도 |
|---|---|
:smtp | 표준 SMTP |
:sendmail | 시스템 sendmail |
:file | 파일에 저장 (개발) |
:test | 메모리 (ActionMailer::Base.deliveries) |
:async_job | 큐 |
# 개발
config.action_mailer.delivery_method = :letter_opener # gem
# 브라우저에서 자동 열림
URL helpers in mailer
Mailer는 별도 컨텍스트라 host 명시 필요.
# config/environments/production.rb
config.action_mailer.default_url_options = { host: "example.com", protocol: "https" }
# config/environments/development.rb
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
<%= link_to "프로필", profile_url(@user) %> <%# url 헬퍼 (path 아님) %>
*_url 사용 (*_path는 mailer에서 사용 X).
첨부
def invoice(invoice)
@invoice = invoice
attachments["invoice.pdf"] = File.read(invoice.pdf_path)
# 또는 ActiveStorage
attachments["invoice.pdf"] = invoice.pdf_blob.download
mail(to: invoice.user.email, subject: "청구서")
end
inline 이미지:
attachments.inline["logo.png"] = File.read("app/assets/images/logo.png")
<%= image_tag attachments["logo.png"].url %>
Preview
bin/rails generate mailer가 자동 생성.
# test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
def welcome
user = User.first
UserMailer.welcome(user)
end
def reset_password
user = User.first
UserMailer.reset_password(user, "test-token")
end
end
브라우저: http://localhost:3000/rails/mailers
각 메일을 HTML/text로 미리 보기. 디자인 작업에 유용.
테스트
class UserMailerTest < ActionMailer::TestCase
test "welcome" do
user = users(:alice)
mail = UserMailer.welcome(user)
assert_equal "환영합니다", mail.subject
assert_equal [user.email], mail.to
assert_equal ["noreply@example.com"], mail.from
assert_match user.name, mail.body.encoded
end
end
RSpec:
RSpec.describe UserMailer, type: :mailer do
let(:user) { create(:user) }
let(:mail) { UserMailer.welcome(user) }
it "renders" do
expect(mail.subject).to eq("환영합니다")
expect(mail.to).to eq([user.email])
expect(mail.body.encoded).to match(user.name)
end
end
전송 카운트:
assert_emails 1 do
UserMailer.welcome(user).deliver_now
end
Action Mailbox (수신)
이메일 수신 처리.
class InboundMailbox < ApplicationMailbox
def process
parent = User.find_by(email: mail.from)
return unless parent
Comment.create!(author: parent, body: mail.body.decoded)
end
end
라우팅:
# app/mailboxes/application_mailbox.rb
class ApplicationMailbox < ActionMailbox::Base
routing /^reply-/i => :inbound
routing "support@example.com" => :support
end
수신 게이트웨이: SendGrid Inbound, Mailgun, Postfix 등.
자주 보는 패턴
사용자 알림
class NotificationMailer < ApplicationMailer
def comment_added(comment)
@comment = comment
@post = comment.post
mail(to: @post.author.email, subject: "새 댓글")
end
end
# Comment 모델
after_create_commit -> { NotificationMailer.comment_added(self).deliver_later }
다국어
def welcome(user)
I18n.with_locale(user.locale) do
mail(to: user.email, subject: I18n.t("mailer.welcome.subject"))
end
end
<h1><%= t("mailer.welcome.title", name: @user.name) %></h1>
발송 실패 처리
class UserMailer < ApplicationMailer
rescue_from Net::SMTPAuthenticationError do |e|
Rails.logger.error("SMTP auth failed: #{e.message}")
SentryService.notify(e)
end
end
대량 발송 (rate limit)
users.find_each do |user|
UserMailer.newsletter(user).deliver_later(wait: rand(0..3600).seconds)
end
랜덤 분산으로 SMTP rate limit 회피.
함정
1. deliver_now in controller
def signup
user.save
UserMailer.welcome(user).deliver_now # 1-3초 응답 지연
redirect_to root_path
end
→ deliver_later.
2. 트랜잭션과 deliver_later
User.transaction do
user = User.create!(...)
UserMailer.welcome(user).deliver_later
end
# 트랜잭션 rollback 시 메일은 이미 enqueue → 워커가 user 못 찾음
해결: Solid Queue (같은 DB라 안전) 또는 after_commit.
3. 큰 첨부
이미지 5MB+ → SMTP 거부. 큰 파일은 다운로드 URL로.
4. SPF/DKIM 미설정
from: noreply@example.com 보내려면 도메인에 SPF/DKIM. 안 그러면 스팸. SES/SendGrid 같은 서비스는 자체 도메인 사용.
5. 개발에서 실제 발송
# config/environments/development.rb
config.action_mailer.delivery_method = :test # 또는 :letter_opener
:smtp로 두면 개발 중 실제 발송 위험.
외부 서비스 통합
| 서비스 | gem |
|---|---|
| SendGrid | sendgrid-ruby 또는 SMTP |
| Mailgun | mailgun-ruby 또는 SMTP |
| AWS SES | aws-sdk-rails |
| Postmark | postmark-rails |
SMTP가 가장 간단하나 API 사용이 더 강력 (tags, metadata, webhook).
💬 댓글