# 코드 속 이메일: 한눈에 보고, 옮기고, 개선하기

정식 URL: https://brew.new/ko/blog/emails-in-code-visibility-migration-mcp
글쓴이: Thomas Park
게시일: 2026-10-12
수정일: 2026-10-15

코드 속 이메일이란 코드베이스에 정의되어 다른 기능처럼 배포되는 제품 이메일을 말해요. Resend와 React Email을 쓰는 팀에 제품이 어떤 이메일을 보내는지 물어보면 잠시 정적이 흘러요. 웰컴 이메일은 `emails/` 폴더의 컴포넌트예요. 비밀번호 재설정 이메일은 인증 서비스의 대시보드에 있어요. 체험 종료 이메일은 크론 작업 안의 문자열이에요. 영수증은 Stripe가 보내요. “보고 싶었어요” 이메일은 무엇이 보내는지 아무도 기억하지 못해요.

이메일 하나하나는 배포하기 쉬웠던 탓에, 전체 그림을 책임지는 사람이 없어요. 저희라면 가시성 확보, 마이그레이션, 개선 순서로 바로잡겠어요. 대부분은 이미 쓰고 있는 코딩 에이전트를 MCP로 Brew에 연결해 처리할 수 있어요.

## 핵심 요약

- 새 도구가 아니라 목록 정리에서 시작하세요. 모든 이메일과 각각의 트리거, 파일, 변수를 정리하는 거예요.
- 코딩 에이전트는 발송 호출을 찾고, 템플릿마다 렌더링한 다음, `import_email` MCP 도구로 가져올 수 있어요. 가져오기는 정해진 규칙대로만 처리돼 결과가 항상 같고, AI를 쓰지 않으며, 무료예요.
- 가져온 이메일은 그룹마다 캔버스 하나에 모이므로, 팀이 데스크톱 화면과 모바일 화면에서 나란히 놓고 검토할 수 있어요.
- 옮길 때는 Resend 방식의 발송 호출이 트리거와 게시된 자동화로 바뀌고, REST 호출 한 번으로 실행돼요. 실서비스 코드는 MCP가 아니라 REST를 써요.
- 기능 플래그 뒤에서 이메일 유형을 하나씩 옮기고, 트랜잭션 이메일은 마지막에 옮기세요.

## 코드로 정의한 이메일이 보이지 않게 되는 이유

React Email과 Resend 덕분에 개발자에게 이메일은 훨씬 다루기 좋아졌어요. 타입이 지정된 컴포넌트로 만들고, 풀 리퀘스트에서 리뷰하죠. 그 대가는 나중에 드러나요.

**이메일이 흩어져 있어요.** 컴포넌트, 백그라운드 작업 안의 인라인 HTML, 서드파티 대시보드의 템플릿, 다른 서비스가 대신 보내는 이메일까지요. 이 모두를 담은 파일은 없어요.

**미리보기는 실제 발송본이 아니에요.** React Email 미리보기 서버는 미리보기용 props를 넣은 템플릿 하나를 보여 줄 뿐이에요. 고객이 실제로 받은 이메일은 보여 주지 않고, 다른 템플릿은 아예 보여 주지 않아요.

**엔지니어만 볼 수 있어요.** 제품, 디자인, 고객 지원 담당자가 보려면 엔지니어에게 프로젝트를 실행해 달라고 부탁해야 하니, 대부분은 그냥 보지 않아요.

**출시 후에는 아무도 검토하지 않아요.** 2년 된 이메일에는 여전히 예전 로고와 더 이상 팔지 않는 요금제가 들어 있어요. 이메일을 한데 모아 보는 사람이 없으니 아무도 눈치채지 못해요.

## 1단계: 가시성 확보하기

목표는 모든 이메일을 한곳에서 함께 보는 거예요. 이 단계에서는 실서비스를 전혀 건드리지 않아요.

![코드에서 이메일을 꺼내 옮기는 세 단계: import_email과 그룹별 캔버스로 가시성 확보, 도메인·연락처·트리거·자동화로 마이그레이션, 점검·렌더링 테스트·분석으로 최적화.](/images/blog/emails-in-code-three-phases.png)

*각 단계는 그 자체로 쓸모가 있고, 어느 단계에서든 멈춰도 돼요.*

### 코딩 에이전트를 Brew에 연결하기

Brew의 MCP 서버는 `https://brew.new/api/mcp` 주소에 있고, OAuth를 써요. Brew로 로그인한 뒤 에이전트가 작업할 브랜드를 고르면 돼요. Claude Code에서는 이렇게 추가해요.

```bash
claude mcp add --transport http brew "https://brew.new/api/mcp"
```

서버를 추가해도 이 프로젝트의 로컬 구성에만 저장돼요. 모든 프로젝트에서 쓰려면 `--scope user` 옵션을 붙이세요. Brew 도구를 쓰기 전에 `claude mcp login brew` 명령을 실행하거나, `/mcp` 메뉴를 열어 Brew 인증을 마치세요.

[클라이언트 설정 문서](https://docs.brew.new/api-reference/mcp/connect-your-client)에는 Cursor와 Codex 설정 방법도 있어요. OAuth를 지원하지 않는 클라이언트는 브랜드 전용 API 키를 쓸 수 있어요. 그다음 에이전트에게 `get_brew_capabilities` 도구를 호출하게 해서, 추측하지 말고 현재 도구 목록을 읽게 하세요.

### 이메일 목록 만들기

에이전트에게 목록을 요청하세요.

```text
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 유틸리티](https://react.email/docs/utilities/render)를 쓰면 그 문자열을 얻을 수 있어요.

```tsx
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` 값은 제품 영역마다 하나씩 정해요.

```text
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.
```

개발자라면 가져오기가 이메일을 다시 쓰는 건 아닌지 묻는 게 당연해요. 그렇지 않아요.

- AI 모델이 아니라 정해진 규칙대로 작동하는 컴파일러이고, 크레딧이 들지 않아요.
- 편집할 수 있는 Brew 디자인을 만들어 주고, 발송할 때는 가져온 행에 원본 소스 HTML을 그대로 써요.
- 공개 이미지는 다시 호스팅하고, 가져오지 못한 이미지는 `assetReport` 항목에 표시돼요.
- 스크립트, iframe, 폼, 이벤트 핸들러는 제거하고, 기존 오픈 추적 픽셀은 작동하지 않게 처리해요.
- 소스 하나는 최대 5 MB까지 가능해요. URL로는 가져올 수 없어요. 이 도구는 마크업 자체를 받고, `baseUrl` 값은 상대 이미지 경로를 해석하는 데만 쓰여요.

### 팀과 함께 이메일 검토하기

그룹마다 캔버스가 하나씩 생겨요. `brew.new/emails/onboarding` 주소를 열면 시퀀스 전체가 나란히 놓여 있고, 데스크톱·모바일 전환 버튼과 디자인별 댓글도 있어요. 캔버스 링크는 Brew 워크스페이스에 로그인한 사람만 열 수 있어요. 공개 공유 링크는 없으니 검토할 사람을 초대하세요.

문제가 드러나는 건 바로 이때예요. 로고가 두 개이고, 버튼 링크가 깨져 있고, 푸터가 세 종류예요. 무엇이든 고치기 전에 `audit_email` 도구를 실행하세요. 링크, 이미지, 규정 준수, 로드 용량, 클라이언트 지원, 접근성, 카피를 점검하고, 전체 점검 한 번에 크레딧 5개가 들어요. 저장된 `emailId` 값과 원본 HTML을 모두 받으므로, 아직 가져오지 않은 이메일도 점검할 수 있어요. 각 점검 항목과 기준값은 [발송 전 이메일 QA 체크리스트](/ko/blog/pre-send-email-qa-checklist)에서 설명해요.

이제 이메일 목록, 캔버스에 모인 모든 이메일, 문제 목록이 생겼어요. 그리고 코드는 여전히 예전처럼 모든 이메일을 보내고 있어요.

## 2단계: 마이그레이션하기

이제 일부 이메일은 코드 대신 Brew가 보내요. 모든 이메일을 옮길 필요는 없고, 어떤 이메일 유형이든 한 시점에는 한 시스템만 보내야 해요.

### 개념 대응시키기

| 지금 코드에서는 | Brew에서는 |
| --- | --- |
| `resend.emails.send({ react: <WelcomeEmail /> })` | 트리거 이벤트와, ‘이메일 보내기’ 단계 하나가 있는 게시된 자동화 |
| 컴포넌트에 넘기는 props | `{{ firstName \| there }}`, `{{ trigger.planName }}` 같은 개인화 태그로 쓰는 트리거 페이로드 필드 |
| `from` 도메인 | Brew에서 인증된 발신 도메인 |
| 사용자 테이블 | REST API나 연동으로 만드는 연락처 |
| 시퀀스를 보내는 크론 작업 | ‘대기’, ‘필터’, ‘이메일 보내기’ 단계가 있는 자동화 |

### 1. 발신 도메인 설정하기

에이전트가 `create_domain` 도구를 호출해 DNS 레코드를 받고, 레코드가 반영되면 `verify_domain` 도구를 호출해요. MCP나 API로 추가한 도메인은 마케팅 도메인이에요. 비밀번호 재설정과 영수증에는 Brew 앱에서 용도가 트랜잭션인 도메인을 설정하세요. 이 도메인은 수신 거부 푸터를 넣지 않고, 수신 거부한 연락처에게도 전달해요. 레코드는 [발신 도메인 인증하기](https://docs.brew.new/get-started/verify-your-sending-domain) 문서에서 다뤄요.

### 2. 연락처 가져오기

MCP로는 연락처를 업데이트할 수 있지만 만들 수는 없어요. 연락처는 자체 백엔드에서 `POST /v1/contacts` 엔드포인트로 만들거나, 앱에서 CSV를 업로드하세요. 이 엔드포인트는 연락처 하나 또는 최대 1,000개를 한 번에 생성하거나 업데이트하고, 사용자 지정 필드도 자동으로 만들어요. 트리거를 실행해도 페이로드에 담긴 연락처가 생성되거나 업데이트돼요. 연락을 받기로 동의한 사람만 가져오세요. Brew가 기대하는 데이터는 [연락처 추가하기](https://docs.brew.new/audience/add-contacts) 문서에서 설명해요.

### 3. 미리보기 값을 개인화 태그로 바꾸기

놓치기 쉬운 부분이 있어요. 가져온 웰컴 이메일에는 말 그대로 “Hi Ana”라고 적혀 있어요. 픽스처 값마다 `{{ firstName | there }}` 같은 개인화 태그로 바꾸세요. 세로 막대 뒤의 텍스트가 대체값이에요. Brew 에디터에서 직접 바꾸거나, 에이전트에게 이 값만 바꾸도록 범위를 정해 `edit_email` 도구를 실행하게 하세요. 크레딧이 드는 AI 수정이니 미리보기를 확인하세요. 문법은 [개인화 태그](https://docs.brew.new/create-emails/merge-tags) 문서에 정리되어 있어요.

### 4. 트리거와 자동화 만들기

에이전트가 `save_trigger` 도구로 트리거를 만들어요. 페이로드 스키마에는 수신자의 `email` 필드가 반드시 있어야 해요. 타입이 지정된 페이로드 정의도 만들어 두세요(`infer_payload_contract` 도구가 예시 페이로드로 초안을 만들고, `set_trigger_contract` 도구가 저장해요). 그다음 `get_trigger_contract` 도구로 `ts` 또는 `zod` 형식의 정의를 가져오면, 백엔드와 Brew가 같은 페이로드 구조를 쓰게 돼요.

이어서 `save_automation` 도구가 트리거와, 이메일·도메인·제목을 지정한 ‘이메일 보내기’ 단계를 만들어요. 게시하기 전까지는 초안으로 남아요. `dryRun` 옵션은 그래프를 검증하고, `test_automation` 도구는 내 주소로 실제 사본을 보내고, `check_trigger` 도구는 실서비스에서 이벤트를 보내기 전에 게시된 자동화가 트리거를 수신 대기 중인지 확인해 줘요.

### 5. 호출 지점 바꾸기

애플리케이션 코드에서 바뀌는 곳은 여기뿐이에요. 바꾸기 전, Resend를 쓸 때는 이래요.

```ts
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를 쓸 때는 이래요.

```ts
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 비교 가이드](/ko/blog/mcp-vs-rest-api-for-email-marketing-which-workflow-should-you-use)에서 다뤄요.

### 6. 이메일을 하나씩 전환하기

1. 새 호출을 기능 플래그 뒤에 두고 기존 호출 옆에 나란히 두세요. 그래야 설정만 바꿔 되돌릴 수 있어요.
2. 라이프사이클 넛지처럼 위험이 낮은 이메일부터 시작하고, 며칠 동안 전달과 클릭을 지켜보세요.
3. 트리거가 올바른 페이로드로 실행되는 것을 확인했다면, 그다음에 시퀀스를 옮기세요.
4. 트랜잭션 이메일은 마지막에 옮기세요. 사용자는 비밀번호 재설정과 매직 링크가 고장 나면 가장 먼저 알아차려요.
5. 일주일 동안 문제가 없으면 기존 경로를 지우고 `EMAILS.md` 파일을 업데이트하세요.

[Loops 마이그레이션 가이드](https://docs.brew.new/migrations/loops)도 같은 방식을 따라요.

### 코드에 남겨 둘 것

Supabase의 이메일 인증 메일과 매직 링크 이메일은 계속 Supabase에서 보내요. Brew의 Supabase 연동은 Supabase Auth의 이메일을 대신하는 게 아니라, 사용자 이벤트를 수신해 자동화를 시작해요. 발송 시점에 복잡한 데이터로 만드는 항목별 인보이스도 코드에 두는 편이 더 간단할 수 있으니, 개인화 태그로 무엇이 필요한지 먼저 확인하세요. 이런 이메일도 계속 보이도록 가져온 사본을 캔버스에 남겨 두세요.

## 3단계: 최적화하기

**전체를 점검하세요.** 그룹 전체에 `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` 파일에 추가한다.

## 자주 묻는 질문

### Brew로 React Email 템플릿을 가져올 수 있나요?

네. `import_email` 도구는 렌더링한 HTML, MJML, 저장된 `.eml` 메시지, React Email 모듈을 받아요. 가장 확실한 건 렌더링한 HTML이에요. 모듈은 정적으로 읽기 때문에 props는 기본값이나 미리보기 값이 되고, 다른 로컬 파일에서 가져온 컴포넌트는 빠지거든요.

### Brew가 내 Resend 계정과 연결되나요?

아니요. Resend 연동도, Resend에서 가져오기나 Resend로 내보내기도 없어요. 코드가 렌더링한 HTML을 가져오고, 옮긴다면 코드가 Resend를 호출하는 대신 Brew 트리거를 실행하게 바꾸면 돼요.

### 백엔드에서 MCP로 Brew를 호출해야 하나요?

아니요. MCP는 코딩 에이전트에서 설정, 가져오기, 점검, 테스트에 쓰세요. 실서비스 코드는 API 키로 REST API를 호출해야 해요. OAuth 연결에서는 MCP가 실제 발송과 트리거 실행 때마다 사람에게 확인을 요청하기 때문이에요.

### 이메일을 가져오면 모양이 바뀌나요?

가져오기는 이메일을 편집할 수 있는 Brew 디자인으로 바꾸고, 발송에는 원본 소스 HTML을 그대로 써요. 스크립트, iframe, 폼, 이벤트 핸들러를 제거하고, 기존 추적 픽셀은 작동하지 않게 처리하며, 공개 이미지는 다시 호스팅해요. Brew에서 보내기 전에 캔버스에서 데스크톱 화면과 모바일 화면으로 하나씩 검토하세요.
