서론
“뉴스레터 디자인이 깨졌습니다.” 마케팅 팀의 절규 섞인 메시지를 받아본 적이 있는가? Gmail에서는 완벽했던 이메일이 Outlook에서는 레이아웃이 산산조각 나고, Apple Mail에서는 폰트가 전부 깨져 보인다. 이것은 단순한 불평이 아니라, 이메일 HTML 개발자들이 매일 마주하는 지옥이다.
2024년 현재에도 이메일 클라이언트는 1990년대의 웹 브라우저 수준으로 돌아간 것 같은 경험을 선사한다. Gmail은 <style> 태그의 일부를 무시하고, Outlook은 Microsoft Word 렌더링 엔진을 사용해 CSS를 파괴하며, 각 클라이언트마다 지원하는 HTML/CSS 속성이 제각각이다.
이런 상황에서 Notion을 콘텐츠 저작 도구로 활용하려는 시도는 자연스럽다. Notion은 직관적인 WYSIWYG 에디터, 협업 기능, 그리고 체계적인 콘텐츠 관리를 제공한다. 하지만 Notion에서 작성한 콘텐츠를 이메일로 발송하려면? HTML로 내보내고, CSS를 인라인화하고, 각 클라이언트 호환성을 테스트하는 지루한 과정을 거쳐야 한다.
notion-to-email은 바로 이 지점에서 등장했다. Notion 페이지 ID 하나만 넘기면, Gmail, Outlook, Apple Mail에서 정상 렌더링되는 HTML을 반환하는 TypeScript 라이브러리다. 이 글에서는 이 오픈소스 도구의 기술적 원리, 사용법, 그리고 실무 적용 방안을 깊이 있게 탐구해본다.
본론
이메일 HTML 호환성: 왜 이렇게 복잡한가?
이메일 HTML 작성이 어려운 근본 원인은 표준의 부재가 아니다. HTML5와 CSS3 표준은 존재하지만, 이메일 클라이언트들이 이를 제각각 구현했기 때문이다.
| 이메일 클라이언트 | 렌더링 엔진 | 주요 제약사항 | | :— | :— | :— | | Gmail | WebKit 기반 | <style> 태그 지원 제한, position: fixed 미지원 | | Outlook (Windows) | Microsoft Word | Flexbox/Grid 미지원, margin 버그, background-image 제한 | | Outlook (Mac) | WebKit | 상대적으로 현대적이나 여전히 제약 존재 | | Apple Mail | WebKit | 가장 표준 준수적, 그러나 다크모드 대응 필요 | | Yahoo Mail | WebKit | <style> 태그 내 @media 쿼리 지원 불안정 |
이런 파편화 때문에 **“Works in all email clients”**라는 문구는 개발자에게 있어 일종의 Holy Grail이다. notion-to-email은 이 문제를 Notion API → 중간 표현 → 클라이언트별 최적화된 HTML 파이프라인으로 접근한다.
notion-to-email 아키텍처
라이브러리의 핵심 동작 원리를 다이어그램으로 살펴보자:
1
2
3
4
5
6
7
| graph LR
A[Notion Page ID] --> B[Notion API]
B --> C[Block Parser]
C --> D[Intermediate AST]
D --> E[HTML Generator]
E --> F[CSS Inliner]
F --> G[Email Compatible HTML]
|
핵심 컴포넌트 분석:
- Notion API 클라이언트:
@notionhq/client를 래핑하여 페이지와 하위 블록을 순회 2. Block Parser: Notion의 50+ 블록 타입을 중간 추상 구문 트리(AST)로 변환 3. HTML Generator: AST를 테이블 기반 레이아웃의 레거시 HTML로 변환 4. CSS Inliner: <style> 태그의 스타일을 인라인 style 속성으로 변환
설치 및 기본 사용법
TypeScript/Node.js 환경에서 설치부터 시작한다:
1
2
3
4
5
| npm install notion-to-email
# 또는
yarn add notion-to-email
# 또는
pnpm add notion-to-email
|
기본 사용 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| import { renderFromNotion } from 'notion-to-email';
// Notion Integration Token (내부 통합 시크릿)
const notionToken = process.env.NOTION_INTEGRATION_TOKEN;
// 변환할 Notion 페이지 ID
const pageId = '1234567890abcdef1234567890abcdef';
async function main() {
try {
const html = await renderFromNotion({
notionToken,
pageId,
});
console.log('Generated HTML:', html);
// 이메일 발송 라이브러리와 연동
// await sendEmail({
// to: 'subscriber@example.com',
// html: html,
// });
} catch (error) {
console.error('Conversion failed:', error);
}
}
main();
|
지원하는 Notion 블록 타입
notion-to-email은 Notion의 주요 블록 타입을 이메일 호환 HTML로 변환한다:
| Notion 블록 | HTML 변환 결과 | 호환성 고려사항 | | :— | :— | :— | | Paragraph | <p> + 인라인 스타일 | line-height, margin 명시적 설정 | | Heading 1-3 | <h1>~<h3> | 폰트 크기 px 단위 고정 | | Bulleted List | <table> 기반 리스트 | <ul> 대신 테이블로 Outlook 대응 | | Numbered List | <table> 기반 번호 매기기 | 카운터 CSS 대신 텍스트로 번호 삽입 | | Toggle | 펼쳐진 상태의 <div> | 인터랙션 제거 (이메일에서 JS 불가) | | Callout | <table> 박스 | 아이콘 + 텍스트 수평 정렬 | | Code | <pre><code> | 구문 강조 미지원, 모노스페이스 폰트 | | Image | <img> with width | 절대 URL 필요, Base64 권장 | | Divider | <hr> | border 스타일 인라인화 | | Quote | <blockquote> | 좌측 테두리 border-left 사용 |
고급 설정 및 커스터마이징
실무에서는 브랜드 가이드라인에 맞춘 스타일 커스터마이징이 필수다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| import { renderFromNotion, RenderOptions } from 'notion-to-email';
const options: RenderOptions = {
notionToken: process.env.NOTION_INTEGRATION_TOKEN!,
pageId: 'your-page-id',
// 커스텀 스타일 적용
styles: {
body: {
fontFamily: "'Pretendard', -apple-system, sans-serif",
fontSize: '16px',
lineHeight: '1.6',
color: '#333333',
},
h1: {
fontSize: '28px',
fontWeight: '700',
marginBottom: '16px',
color: '#1a1a1a',
},
h2: {
fontSize: '24px',
fontWeight: '600',
marginTop: '24px',
marginBottom: '12px',
},
link: {
color: '#0066cc',
textDecoration: 'underline',
},
callout: {
backgroundColor: '#f0f7ff',
borderLeft: '4px solid #0066cc',
padding: '16px',
},
},
// 이미지 처리 옵션
imageHandling: {
// 이미지를 Base64로 인라인화 (일부 클라이언트 호환성 향상)
embedAsBase64: false,
// 이미지 최대 너비
maxWidth: 600,
},
// 다크모드 대응
darkModeSupport: true,
};
const html = await renderFromNotion(options);
|
Step-by-Step: Notion 뉴스레터 자동화 파이프라인 구축
실제 프로덕션 환경에서의 통합 가이드를 단계별로 살펴본다:
Step 1: Notion Integration 생성
Notion 설정에서 내부 통합(Internal Integration)을 생성하고, API 시크릿을 발급받는다. 대상 페이지에 이 통합을 초대(Invite)하여 읽기 권한을 부여한다.
1
2
3
| // .env
NOTION_INTEGRATION_SECRET=secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NOTION_NEWSLETTER_PAGE_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
Step 2: 이메일 서비스 연동
SendGrid, Resend, AWS SES 등의 이메일 서비스와 통합한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| import { renderFromNotion } from 'notion-to-email';
import Resend from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
interface NewsletterPayload {
pageId: string;
subject: string;
recipients: string[];
}
async function sendNewsletter(payload: NewsletterPayload) {
// 1. Notion 페이지를 HTML로 변환
const htmlContent = await renderFromNotion({
notionToken: process.env.NOTION_INTEGRATION_SECRET!,
pageId: payload.pageId,
styles: {
body: {
fontFamily: 'system-ui, -apple-system, sans-serif',
maxWidth: '600px',
margin: '0 auto',
},
},
});
// 2. 이메일 발송
const { data, error } = await resend.emails.send({
from: 'newsletter@yourcompany.com',
to: payload.recipients,
subject: payload.subject,
html: htmlContent,
});
if (error) {
throw new Error(`Email send failed: ${error.message}`);
}
return data;
}
// 실행
sendNewsletter({
pageId: process.env.NOTION_NEWSLETTER_PAGE_ID!,
subject: '주간 개발 뉴스 - 2024년 1월 셋째 주',
recipients: ['subscribers@yourcompany.com'],
});
|
Step 3: 자동화 스케줄링
GitHub Actions, cron, 혹은 Vercel Cron Jobs을 활용해 주간 뉴스레터를 자동화한다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # .github/workflows/newsletter.yml
name: Weekly Newsletter
on:
schedule:
# 매주 월요일 오전 9시 (UTC)
- cron: '0 0 * * 1'
workflow_dispatch:
jobs:
send-newsletter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Send newsletter
env:
NOTION_INTEGRATION_SECRET: ${{ secrets.NOTION_INTEGRATION_SECRET }}
NOTION_NEWSLETTER_PAGE_ID: ${{ secrets.NOTION_NEWSLETTER_PAGE_ID }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
run: npx tsx scripts/send-newsletter.ts
|
성능 고려사항
Notion API는 속도 제한(Rate Limit)이 존재한다. 대용량 페이지나 잦은 변환 작업 시 캐싱 전략이 필요하다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import { renderFromNotion } from 'notion-to-email';
import NodeCache from 'node-cache';
// TTL 1시간 캐시
const cache = new NodeCache({ stdTTL: 3600 });
async function getCachedHtml(pageId: string, notionToken: string) {
const cacheKey = `notion-html:${pageId}`;
const cached = cache.get<string>(cacheKey);
if (cached) {
console.log('Cache hit for page:', pageId);
return cached;
}
console.log('Cache miss, fetching from Notion...');
const html = await renderFromNotion({ notionToken, pageId });
cache.set(cacheKey, html);
return html;
}
|
결론
핵심 요약
notion-to-email은 Notion 콘텐츠를 이메일 호환 HTML로 변환하는 실용적인 오픈소스 도구다. 핵심 가치는 다음과 같다:
- 호환성 보장: Gmail, Outlook, Apple Mail 등 주요 이메일 클라이언트에서 정상 렌더링 2. 간단한 API: 페이지 ID 하나로 완전한 HTML 반환 3. TypeScript 지원: 타입 안전성과 IDE 자동완성
출처: https://news.hada.io/topic?id=28228