이메일이 코드 속에 있는 팀을 위한 가이드예요. 코딩 에이전트로 목록을 만들고, MCP로 Brew 캔버스 하나에 모은 다음, 안전하게 옮기고 개선하세요.

코드 속 이메일이란 코드베이스에 정의되어 다른 기능처럼 배포되는 제품 이메일을 말해요. Resend와 React Email을 쓰는 팀에 제품이 어떤 이메일을 보내는지 물어보면 잠시 정적이 흘러요. 웰컴 이메일은 emails/ 폴더의 컴포넌트예요. 비밀번호 재설정 이메일은 인증 서비스의 대시보드에 있어요. 체험 종료 이메일은 크론 작업 안의 문자열이에요. 영수증은 Stripe가 보내요. “보고 싶었어요” 이메일은 무엇이 보내는지 아무도 기억하지 못해요.
이메일 하나하나는 배포하기 쉬웠던 탓에, 전체 그림을 책임지는 사람이 없어요. 저희라면 가시성 확보, 마이그레이션, 개선 순서로 바로잡겠어요. 대부분은 이미 쓰고 있는 코딩 에이전트를 MCP로 Brew에 연결해 처리할 수 있어요.
import_email MCP 도구로 가져올 수 있어요. 가져오기는 정해진 규칙대로만 처리돼 결과가 항상 같고, AI를 쓰지 않으며, 무료예요.React Email과 Resend 덕분에 개발자에게 이메일은 훨씬 다루기 좋아졌어요. 타입이 지정된 컴포넌트로 만들고, 풀 리퀘스트에서 리뷰하죠. 그 대가는 나중에 드러나요.
이메일이 흩어져 있어요. 컴포넌트, 백그라운드 작업 안의 인라인 HTML, 서드파티 대시보드의 템플릿, 다른 서비스가 대신 보내는 이메일까지요. 이 모두를 담은 파일은 없어요.
미리보기는 실제 발송본이 아니에요. React Email 미리보기 서버는 미리보기용 props를 넣은 템플릿 하나를 보여 줄 뿐이에요. 고객이 실제로 받은 이메일은 보여 주지 않고, 다른 템플릿은 아예 보여 주지 않아요.
엔지니어만 볼 수 있어요. 제품, 디자인, 고객 지원 담당자가 보려면 엔지니어에게 프로젝트를 실행해 달라고 부탁해야 하니, 대부분은 그냥 보지 않아요.
출시 후에는 아무도 검토하지 않아요. 2년 된 이메일에는 여전히 예전 로고와 더 이상 팔지 않는 요금제가 들어 있어요. 이메일을 한데 모아 보는 사람이 없으니 아무도 눈치채지 못해요.
목표는 모든 이메일을 한곳에서 함께 보는 거예요. 이 단계에서는 실서비스를 전혀 건드리지 않아요.

각 단계는 그 자체로 쓸모가 있고, 어느 단계에서든 멈춰도 돼요.
Brew의 MCP 서버는 https://brew.new/api/mcp 주소에 있고, OAuth를 써요. Brew로 로그인한 뒤 에이전트가 작업할 브랜드를 고르면 돼요. Claude Code에서는 이렇게 추가해요.
claude mcp add --transport http brew "https://brew.new/api/mcp"서버를 추가해도 이 프로젝트의 로컬 구성에만 저장돼요. 모든 프로젝트에서 쓰려면 --scope user 옵션을 붙이세요. Brew 도구를 쓰기 전에 claude mcp login brew 명령을 실행하거나, /mcp 메뉴를 열어 Brew 인증을 마치세요.
클라이언트 설정 문서에는 Cursor와 Codex 설정 방법도 있어요. OAuth를 지원하지 않는 클라이언트는 브랜드 전용 API 키를 쓸 수 있어요. 그다음 에이전트에게 get_brew_capabilities 도구를 호출하게 해서, 추측하지 말고 현재 도구 목록을 읽게 하세요.
에이전트에게 목록을 요청하세요.
Find every email this codebase sends. Search for provider calls
(resend.emails.send, postmark, sendgrid, nodemailer, SES), React Email
components and render() calls, and any email templates referenced from
config or a third-party dashboard.
For each email, record: a name, what triggers it, the file and line that
sends it, the template file, the provider, and the variables it uses.
Write the result as a table in EMAILS.md. List anything you could not
resolve under "Unknown" instead of guessing.“Unknown” 섹션은 지우지 마세요. 인증 확인 이메일과 Stripe 영수증은 코드 안의 발송 호출이 아니지만, 목록에는 들어가야 해요. EMAILS.md 파일을 커밋하세요. 여기서 멈추더라도 어제는 없던 목록이 생긴 거예요.
가져오기에 가장 좋은 입력은 코드가 이미 만들어 내는 HTML이에요. React Email의 render 유틸리티를 쓰면 그 문자열을 얻을 수 있어요.
import { render } from 'react-email'
import WelcomeEmail from './emails/welcome'
const html = await render(
<WelcomeEmail firstName="Ana" planName="Pro" trialDays={14} />
)예전 프로젝트에서는 @react-email/render 패키지에서 render 함수를 가져와요. 팀이 직접 검토할 결과물이니 "test" 같은 값 말고 실제와 비슷한 픽스처를 쓰세요. 모든 템플릿을 파일로 렌더링하는 스크립트는 에이전트에게 맡기세요.
Brew는 format: "jsx" 옵션으로 React Email 모듈을 바로 가져올 수도 있어요. 이때는 모듈을 실행하지 않고 정적으로 읽어요. props는 기본값이나 PreviewProps 값으로 채워지고, 다른 로컬 파일에서 가져온 컴포넌트는 빠지거나 거부되며, await 같은 구문은 받아들이지 않아요. 파일 하나로 완결되지 않는 템플릿이라면 렌더링한 HTML을 쓰세요.
그다음 렌더링 결과를 import_email 도구로 하나씩 가져오세요. groupName 값은 제품 영역마다 하나씩 정해요.
For each rendered email in ./email-renders, call import_email with
format "html", the title and subject line from EMAILS.md, and a
groupName by area: "Auth", "Onboarding", "Billing" or "Lifecycle".
Report the emailId for each one and any warnings in its assetReport.개발자라면 가져오기가 이메일을 다시 쓰는 건 아닌지 묻는 게 당연해요. 그렇지 않아요.
assetReport 항목에 표시돼요.baseUrl 값은 상대 이미지 경로를 해석하는 데만 쓰여요.그룹마다 캔버스가 하나씩 생겨요. brew.new/emails/onboarding 주소를 열면 시퀀스 전체가 나란히 놓여 있고, 데스크톱·모바일 전환 버튼과 디자인별 댓글도 있어요. 캔버스 링크는 Brew 워크스페이스에 로그인한 사람만 열 수 있어요. 공개 공유 링크는 없으니 검토할 사람을 초대하세요.
문제가 드러나는 건 바로 이때예요. 로고가 두 개이고, 버튼 링크가 깨져 있고, 푸터가 세 종류예요. 무엇이든 고치기 전에 audit_email 도구를 실행하세요. 링크, 이미지, 규정 준수, 로드 용량, 클라이언트 지원, 접근성, 카피를 점검하고, 전체 점검 한 번에 크레딧 5개가 들어요. 저장된 emailId 값과 원본 HTML을 모두 받으므로, 아직 가져오지 않은 이메일도 점검할 수 있어요. 각 점검 항목과 기준값은 발송 전 이메일 QA 체크리스트에서 설명해요.
이제 이메일 목록, 캔버스에 모인 모든 이메일, 문제 목록이 생겼어요. 그리고 코드는 여전히 예전처럼 모든 이메일을 보내고 있어요.
이제 일부 이메일은 코드 대신 Brew가 보내요. 모든 이메일을 옮길 필요는 없고, 어떤 이메일 유형이든 한 시점에는 한 시스템만 보내야 해요.
| 지금 코드에서는 | Brew에서는 |
|---|---|
resend.emails.send({ react: <WelcomeEmail /> }) |
트리거 이벤트와, ‘이메일 보내기’ 단계 하나가 있는 게시된 자동화 |
| 컴포넌트에 넘기는 props | {{ firstName | there }}, {{ trigger.planName }} 같은 개인화 태그로 쓰는 트리거 페이로드 필드 |
from 도메인 |
Brew에서 인증된 발신 도메인 |
| 사용자 테이블 | REST API나 연동으로 만드는 연락처 |
| 시퀀스를 보내는 크론 작업 | ‘대기’, ‘필터’, ‘이메일 보내기’ 단계가 있는 자동화 |
에이전트가 create_domain 도구를 호출해 DNS 레코드를 받고, 레코드가 반영되면 verify_domain 도구를 호출해요. MCP나 API로 추가한 도메인은 마케팅 도메인이에요. 비밀번호 재설정과 영수증에는 Brew 앱에서 용도가 트랜잭션인 도메인을 설정하세요. 이 도메인은 수신 거부 푸터를 넣지 않고, 수신 거부한 연락처에게도 전달해요. 레코드는 발신 도메인 인증하기 문서에서 다뤄요.
MCP로는 연락처를 업데이트할 수 있지만 만들 수는 없어요. 연락처는 자체 백엔드에서 POST /v1/contacts 엔드포인트로 만들거나, 앱에서 CSV를 업로드하세요. 이 엔드포인트는 연락처 하나 또는 최대 1,000개를 한 번에 생성하거나 업데이트하고, 사용자 지정 필드도 자동으로 만들어요. 트리거를 실행해도 페이로드에 담긴 연락처가 생성되거나 업데이트돼요. 연락을 받기로 동의한 사람만 가져오세요. Brew가 기대하는 데이터는 연락처 추가하기 문서에서 설명해요.
놓치기 쉬운 부분이 있어요. 가져온 웰컴 이메일에는 말 그대로 “Hi Ana”라고 적혀 있어요. 픽스처 값마다 {{ firstName | there }} 같은 개인화 태그로 바꾸세요. 세로 막대 뒤의 텍스트가 대체값이에요. Brew 에디터에서 직접 바꾸거나, 에이전트에게 이 값만 바꾸도록 범위를 정해 edit_email 도구를 실행하게 하세요. 크레딧이 드는 AI 수정이니 미리보기를 확인하세요. 문법은 개인화 태그 문서에 정리되어 있어요.
에이전트가 save_trigger 도구로 트리거를 만들어요. 페이로드 스키마에는 수신자의 email 필드가 반드시 있어야 해요. 타입이 지정된 페이로드 정의도 만들어 두세요(infer_payload_contract 도구가 예시 페이로드로 초안을 만들고, set_trigger_contract 도구가 저장해요). 그다음 get_trigger_contract 도구로 ts 또는 zod 형식의 정의를 가져오면, 백엔드와 Brew가 같은 페이로드 구조를 쓰게 돼요.
이어서 save_automation 도구가 트리거와, 이메일·도메인·제목을 지정한 ‘이메일 보내기’ 단계를 만들어요. 게시하기 전까지는 초안으로 남아요. dryRun 옵션은 그래프를 검증하고, test_automation 도구는 내 주소로 실제 사본을 보내고, check_trigger 도구는 실서비스에서 이벤트를 보내기 전에 게시된 자동화가 트리거를 수신 대기 중인지 확인해 줘요.
애플리케이션 코드에서 바뀌는 곳은 여기뿐이에요. 바꾸기 전, Resend를 쓸 때는 이래요.
await resend.emails.send({
from: 'Acme <hello@acme.com>',
to: user.email,
subject: 'Welcome to Acme',
react: <WelcomeEmail firstName={user.firstName} planName={plan.name} />,
})바꾼 뒤, Brew를 쓸 때는 이래요.
const res = await fetch(
`https://brew.new/api/v1/automations/triggers/${process.env.BREW_WELCOME_TRIGGER_ID}/fire`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.BREW_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `welcome-${user.id}`,
},
body: JSON.stringify({
payload: { email: user.email, firstName: user.firstName, planName: plan.name },
}),
}
)
if (!res.ok) {
// 422 means the automation is still a draft; 400 means the payload failed the schema.
throw new Error(`Brew trigger failed: ${res.status} ${await res.text()}`)
}fetch 함수는 서버가 요청을 거부해도 그대로 resolve되므로, res.ok 값을 확인하고 작업 실행기가 재시도하거나 알림을 보내게 하세요. 이 엔드포인트는 이벤트를 받아들이면 202 코드를, 그 트리거를 기다리는 게시된 자동화가 없으면 422 코드를 돌려줘요.
Idempotency-Key 헤더 값은 사용자나 주문처럼 바뀌지 않는 ID로 만드세요. 같은 키가 다시 오면 두 번 보내는 대신 원래 실행 결과를 돌려주고, 같은 키를 다른 본문과 함께 다시 쓰면 409 IDEMPOTENCY_CONFLICT 오류가 나요. Brew는 트리거 실행에 쓰인 키를 트리거 수신 기록과 같은 기간, 즉 90일 동안 보관해요. 많은 API가 쓰는 24시간이 아니에요. 키는 동작 하나에 하나씩 쓰는 영구적인 값으로 다루세요. 타임스탬프로 만든 키는 재시도할 때마다 바뀌고, 몇 주 뒤 다른 동작에 같은 키를 다시 쓰면 발송되는 대신 이전 요청이 재실행된 것으로 처리될 수 있어요.
실서비스 코드는 MCP가 아니라 API 키로 이 엔드포인트를 호출해요. OAuth로 연결한 MCP는 실제 발송, 트리거 실행, 게시를 할 때 사람에게 확인을 요청해요. 채팅에는 맞지만 요청 핸들러에는 맞지 않는 방식이에요. 둘을 어떻게 나눠 쓸지는 MCP와 REST 비교 가이드에서 다뤄요.
EMAILS.md 파일을 업데이트하세요.Loops 마이그레이션 가이드도 같은 방식을 따라요.
Supabase의 이메일 인증 메일과 매직 링크 이메일은 계속 Supabase에서 보내요. Brew의 Supabase 연동은 Supabase Auth의 이메일을 대신하는 게 아니라, 사용자 이벤트를 수신해 자동화를 시작해요. 발송 시점에 복잡한 데이터로 만드는 항목별 인보이스도 코드에 두는 편이 더 간단할 수 있으니, 개인화 태그로 무엇이 필요한지 먼저 확인하세요. 이런 이메일도 계속 보이도록 가져온 사본을 캔버스에 남겨 두세요.
전체를 점검하세요. 그룹 전체에 audit_email 도구를 실행하고, 고친 다음, 대량 발송 전에 다시 실행하세요.
실제 클라이언트에서 확인하세요. test_email_rendering 도구는 다크 모드를 포함해 특정 이메일 클라이언트와 기기의 스크린샷을 돌려주고, 실행 한 번에 크레딧 10개가 들어요. create_inbox_placement_test 도구는 시드 메일함으로 실제 사본을 보내 어디에 도착했는지 알려 주고, 역시 크레딧 10개가 들어요.
일관성을 맞추세요. 일관되지 않은 부분은 에디터에서 고치거나, 에이전트에게 edit_email 도구로 새 푸터 같은 변경 하나를 그룹 전체에 적용하게 하세요.
결과를 읽으세요. get_email_analytics 도구는 발송과 참여 현황을 알려 줘요. list_insights 도구는 Brew가 계정에서 발견한 인사이트를 근거 지표와 함께 돌려줘요.
시안은 계획적으로 테스트하세요. 캔버스에서 시안을 만들고, 클릭 여부 같은 행동을 기준으로 ‘분할’ 단계에서 연락처를 나누세요. Brew가 무작위 분할의 승자를 대신 골라 주지는 않으니 시안은 직접 비교하세요.
새 이메일도 계속 보이게 하세요. AGENTS.md 같은 파일에 이런 규칙을 한 줄 추가하세요. 새 제품 이메일은 Brew에서 만들고, 트리거로 실행하고, 같은 풀 리퀘스트에서 EMAILS.md 파일에 추가한다.
네. import_email 도구는 렌더링한 HTML, MJML, 저장된 .eml 메시지, React Email 모듈을 받아요. 가장 확실한 건 렌더링한 HTML이에요. 모듈은 정적으로 읽기 때문에 props는 기본값이나 미리보기 값이 되고, 다른 로컬 파일에서 가져온 컴포넌트는 빠지거든요.
아니요. Resend 연동도, Resend에서 가져오기나 Resend로 내보내기도 없어요. 코드가 렌더링한 HTML을 가져오고, 옮긴다면 코드가 Resend를 호출하는 대신 Brew 트리거를 실행하게 바꾸면 돼요.
아니요. MCP는 코딩 에이전트에서 설정, 가져오기, 점검, 테스트에 쓰세요. 실서비스 코드는 API 키로 REST API를 호출해야 해요. OAuth 연결에서는 MCP가 실제 발송과 트리거 실행 때마다 사람에게 확인을 요청하기 때문이에요.
가져오기는 이메일을 편집할 수 있는 Brew 디자인으로 바꾸고, 발송에는 원본 소스 HTML을 그대로 써요. 스크립트, iframe, 폼, 이벤트 핸들러를 제거하고, 기존 추적 픽셀은 작동하지 않게 처리하며, 공개 이미지는 다시 호스팅해요. Brew에서 보내기 전에 캔버스에서 데스크톱 화면과 모바일 화면으로 하나씩 검토하세요.
