[둥지] Supabase와 n8n 연결해서 메일로 보내봅시다
n8n 워크플로우를 통해 Supabase 데이터를 조회하고, HTML 리포트를 생성하여 Gmail로 발송하는 자동화 로직을 구현합니다.
Gemini 3 출시 기념, 나의 멘토 Gemini와 함께 n8n + Supabase + 프론트엔드 (React) 연결까지 테스트해봤다.
사용한 프롬프트는 다음과 같다. 프롬프트와 함께 우리의 기획서와 프론트엔드 코드를 함께 제공했다.
집 계약을 처음하는 사회초년생을 위한 임대차계약 체크리스트 및 챗봇 서비스, ‘둥지: 집 찾는 아기새’를 기획했어. 해당 서비스는 AI 노코드, MCP 해커톤에서 개발할거야. n8n, RAGFlow를 백엔드 메인으로 해서 개발할 거야. 데이터베이스가 필요하다면 Supabase를 활용할 계획이야. 기획서와 프론트코드를 참고해서, n8n 워크 플로우를 어떻게 설계해야 할 지 조언해줘. 모든 기능을 구현하기에 앞서, 다음 기능을 구현해보면서 n8n의 노드 설계와 supabase, 프론트 연결 등을 테스트해보려고 해. 테스트해볼 기능은 DB(SUPABASE)의 체크리스트 테이블에서 해당 사용자의 체크 여부를 받아오고, 체크리스트 내용을 PDF로 내보내기 및 해당 PDF를 메일로 보내는 기능이야. n8n을 중심으로 어떻게 설계해야 하는지 단계별로 상세히 알려줘
“체크리스트 데이터 조회 -> PDF 생성 -> 이메일 발송”
Step 1. Supabase 데이터베이스 설계
1-1 테이블 만들기
체크리스트 진행 상황(checklist_progress) 테이블을 만든다.
Supabase 프로젝트 대시보드에 접속, 사이드바 메뉴 중 SQL Editor 아이콘(터미널 모양 >_)을 클릭한다.
사용자의 이메일과 체크리스트 항목 ID, 체크 여부를 저장하는 테이블 checklist_progress을 만들기 위해, SQL 코드를 작성하고 우측 하단의 RUN 버튼을 클릭한다.
1-2 RLS 설정
외부(프론트엔드, n8n)에서 접근할 수 있도록 RLS(Row Level Security, 행 수준 보안)를 설정
해커톤 초기 단계이고 n8n과 프론트엔드 연결 테스트가 목적이므로, 복잡한 인증 정책보다는 “일단 모든 요청을 허용”하는 정책을 설정하여 통신 오류를 방지하는 것이 좋습니다.
⚠️ 주의: 이 설정은 누구나 데이터를 읽고 쓸 수 있게 하므로, 실제 배포 시에는 반드시 Supabase Auth(auth.uid())를 연동한 보안 정책으로 수정해야 합니다.
새 쿼리 창에 RLS 활성화 코드를 입력하고 RUN을 클릭한다.
Step 2. n8n 워크플로우 설계
2-1 Webhook Node (시작점)
프론트엔드에서 “이메일 보내기” 버튼을 눌렀을 때 신호를 받는 문입니다.
- HTTP Method:
POST - Path:
send-checklist-pdf(원하는 이름으로 설정) - Authentication: 테스트 단계이므로
None으로 설정 (보안이 필요하면 나중에 Header Auth 추가) - Respond:
Using 'Respond to Webhook' Node(마지막에 응답을 직접 제어하기 위함) - Test: 노드를 활성화하고,
Test URL을 복사해둡니다.
2-2 Supabase Node (데이터 조회)
사용자가 어떤 항목을 체크했는지 DB에서 가져옵니다. Supabase의 Get many rows 노드를 선택했다.
- Credential: 앞서 확인한
Project URL과service_role키로 새 자격 증명(Credential)을 생성하여 연결합니다.
Supabase에서 설정 > Data API에서 Project URL를, API Keys의 Legacy API Keys 탭에서 service_role 키를 확인할 수 있다. service_role키가 막혀있는 경우, RLS 무시 설정(Step 1-2)을 했는지 확인하고, Reveal 버튼을 눌러 복사한다.
Supabase Settings > API Keys > Legacy API Keys
두 값을 Create New Credential 페이지에 붙여넣고 Save한다. 아래 사진처럼 초록색 “Connection tested successfully”가 뜨면 잘 연결된 거다.
나머지 파라미터들을 설정한다.
- Resource:
Database - Operation:
Get Many(여러 행 가져오기) - Table:
checklist_progress - Return All:
True(또는 Limit을 100 정도로 넉넉히 설정) - Filters:
user_emailEqual ``- (Webhook으로 들어오는 Body에
userEmail이 있다고 가정합니다)
2-3 Code Node (데이터 병합 및 HTML 생성)
DB에는 단순히 item_id와 is_checked 상태만 저장되어 있습니다. 이를 사람이 읽을 수 있는 제목(Title)과 설명(Description)으로 바꾸려면, 프론트엔드 코드(ChecklistSection.tsx)에 있는 데이터를 n8n 안에도 가지고 있어야 합니다.
아래 코드를 복사해서 Code 노드의 JavaScript(또는 TypeScript) 창에 붙여넣으세요. ChecklistSection.tsx의 데이터를 기반으로 작성했습니다.
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 1. Webhook에서 받은 사용자 이메일
const userEmail = $node["Webhook"].json.body.userEmail;
// 2. Supabase에서 가져온 체크된 항목들 (checkedItems)
// Supabase 노드에서 데이터가 없으면 빈 배열 처리
const dbItems = items.length > 0 ? items.map(item => item.json) : [];
const checkedIds = new Set(dbItems.filter(i => i.is_checked).map(i => i.item_id));
// 3. 체크리스트 정적 데이터 (ChecklistSection.tsx 내용 복사)
// 실제로는 더 많은 항목이 있지만, 테스트를 위해 일부만 예시로 넣었습니다.
// 필요하면 전체 데이터를 여기에 붙여넣으세요.
const checklistData = {
before: [
{ id: 'b1', title: '매매가격 확인하기', desc: '국토교통부 실거래가 조회로 깡통전세 예방' },
{ id: 'b2', title: '보증보험 가입 가능 여부 확인하기', desc: 'HUG/SGI 가입 가능 여부 확인' },
{ id: 'b3', title: '선순위 권리관계 확인하기', desc: '등기부등본 갑구/을구 확인' }
],
during: [
{ id: 'd1', title: '임대인 확인하기', desc: '신분증 진위 여부 및 소유자 일치 확인' },
{ id: 'd4', title: '계약 내용 꼼꼼히 확인 및 작성하기', desc: '표준 임대차 계약서 사용 및 필수 항목 확인' }
],
after: [
{ id: 'a3', title: '전입신고하여 대항력 확보하기', desc: '잔금 지급 즉시 주민센터 방문 또는 정부24 신고' },
{ id: 'a8', title: '임대차 신고하기', desc: '보증금 6천만원 초과 또는 월세 30만원 초과 시 의무' }
]
};
// 4. HTML 생성 (CSS 스타일 포함)
const date = new Date().toLocaleDateString('ko-KR');
let html = `
<html>
<head>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap');
body { font-family: 'Noto Sans KR', sans-serif; padding: 40px; color: #333; }
h1 { color: #83AF3B; text-align: center; margin-bottom: 10px; }
.subtitle { text-align: center; color: #666; margin-bottom: 40px; }
.section-title { color: #22909D; border-bottom: 2px solid #22909D; padding-bottom: 10px; margin-top: 30px; }
.item { padding: 12px 0; border-bottom: 1px solid #eee; }
.checkbox { display: inline-block; width: 20px; font-size: 18px; color: #ccc; }
.checkbox.checked { color: #83AF3B; font-weight: bold; }
.title { font-size: 16px; font-weight: bold; }
.desc { font-size: 12px; color: #888; display: block; margin-top: 4px; margin-left: 25px; }
.footer { margin-top: 50px; text-align: center; font-size: 12px; color: #aaa; }
</style>
</head>
<body>
<h1>둥지: 집 찾는 아기새</h1>
<p class="subtitle">임대차 계약 안전 점검 리포트 (${date})</p>
<p><strong>사용자:</strong> ${userEmail}</p>
`;
const phaseNames = { before: '계약 전', during: '계약 중', after: '계약 후' };
for (const [phase, list] of Object.entries(checklistData)) {
html += `<h2 class="section-title">${phaseNames[phase]} 단계</h2>`;
list.forEach(item => {
const isChecked = checkedIds.has(item.id);
const checkMark = isChecked ? "☑" : "☐";
const checkClass = isChecked ? "checked" : "";
html += `
<div class="item">
<span class="checkbox ${checkClass}">${checkMark}</span>
<span class="title">${item.title}</span>
<span class="desc">${item.desc}</span>
</div>
`;
});
}
html += `
<div class="footer">
본 리포트는 '둥지' 서비스를 통해 생성되었습니다.<br>
안전한 계약 되세요!
</div>
</body>
</html>
`;
// 5. 다음 노드로 HTML 전달
return [{ json: { html: html, email: userEmail } }];
2-4 HTML to PDF Node (또는 대체재)
생성된 HTML을 PDF 파일로 변환합니다. HTML to PDF 노드를 연결하고 Content 속성에 ``을 매핑하세요.
Create New Credential 에서 API Key를 추가해야 한다. API Docs 링크를 타고 들어가 로그인/회원가입 후 API Key 발급을 받으면 확인할 수 있다.
- HTML Content: ``
💡 추천: 해커톤 환경에서는 서버 세팅 문제로 기본 HTML to PDF 노드가 실패할 확률이 높습니다. 가장 빠르고 확실한 방법은 [Gmail 노드]에서 이메일 본문(Body) 타입을 ‘HTML’로 설정하고, PDF 첨부 대신 HTML을 메일 본문에 바로 넣어서 보내는 것입니다. 이 방법은 Credential 고민도 없고 한글 폰트 깨짐 문제도 피할 수 있어 가장 추천합니다.
💡 PDF 한글 폰트: n8n의 HTML to PDF 노드 사용 시 한글이 깨질 수 있습니다. HTML <head> 태그 안에 Google Fonts(예: Noto Sans KR) CDN 링크를 넣거나, 시스템 폰트 설정이 필요할 수 있습니다. 만약 한글이 계속 깨진다면, 해커톤에서는 PDF 대신 이메일 본문(HTML Body)에 체크리스트 표를 예쁘게 그려서 보내는 것으로 우회하는 것도 전략입니다.
2-5 Gmail Node (이메일 발송)
- Credential: Google OAuth2 연결 (미리 Google Cloud Console에서 설정 필요).
Create New Credential 버튼을 누르고, Sign in with Google에서 동의하면 바로 연결된다.
- Resource:
Message - Operation:
Send - To: `` (Code 노드에서 넘겨준 이메일)
- Subject:
[둥지] ${new Date().toLocaleDateString()} 체크리스트 리포트 - HTML / Body:
- PDF를 만들었다면: “첨부파일을 확인해주세요.”
- PDF가 없다면: `` (HTML 본문 직접 삽입)
- Attachments: PDF 생성 노드의 Output Binary Property 이름 (보통
data)을 입력.
2-6 Respond to Webhook Node (응답)
프론트엔드가 무한 대기하지 않도록 성공 신호를 보냅니다.
- Respond With:
JSON - Response Body:
{ "success": true, "message": "이메일이 성공적으로 발송되었습니다." }
n8n 워크플로우 테스트
이제 실제로 데이터가 흐르는지 확인하기 위해 Supabase에 가짜 데이터(Mock Data)를 넣고, n8n을 작동시켜 이메일을 받아보는 테스트를 진행해 보겠습니다.
만약 Gmail 노드 설정 시 받는 사람(To)을 `` 변수로 설정했다면, 실제 테스트할 때는 1단계 SQL에서 test@example.com 대신 본인이 확인 가능한 실제 이메일 주소로 데이터를 넣어야 메일을 받을 수 있습니다.
1
2
3
4
-- 본인 이메일로 테스트 데이터를 다시 넣고 싶다면:
UPDATE checklist_progress
SET user_email = 'my_real_email@gmail.com'
WHERE user_email = 'test@example.com';
화면 하단의 Test Workflow (또는 Execute Workflow) 버튼을 클릭하여 ‘Waiting for Webhook call’ 상태(대기 상태)로 만듭니다. Webhook Node를 더블 클릭하여 엽니다. 노드 설정 창 상단(또는 왼쪽)의 Test 탭을 찾거나, 단순히 이 상태에서 cURL 요청을 보냅니다.
1
curl -X POST https://ajjoona.app.n8n.cloud/webhook-test/send-checklist-pdf -H "Content-Type: application/json" -d "{\"userEmail\": \"ajjoona@gmail.com\"}"
Code Node 에서 ‘Error: Referenced node doesn’t exist’ 에러 발생했다.
코드에서는 Webhook이라는 이름의 노드를 찾고 있는데, 실제 워크플로우 상의 노드 이름은 "이메일 보내기" 버튼 클릭 시 로 변경되어 있어서 발생하는 문제입니다.
n8n의 Code 노드에서 $node["노드이름"]을 사용할 때는 노드의 이름이 정확히 일치해야 합니다.
Error: Referenced node doesn’t exist
Gmail Node에서 이메일 주소 변수가 잘못 입력되어 에러가 발생했다. ``로 변경해 올바른 이메일 주소를 받아오도록 변경했다.
Error: *Cannot read properties of undefined (reading ‘split’)
이번에는 data가 없다는 에러가 발생했다.
Error: This operation expects the node’s input data to contain a binary file ‘data’, but none was found
INPUT 데이터를 보면 HTML to PDF 노드가 파일 자체(Binary Data)가 아닌 다운로드 링크(pdf_url)를 반환하고 있기 때문입니다.
Gmail 노드는 “첨부파일을 보내줘”라고 설정되어 있어 data라는 이름의 파일을 찾고 있는데, 현재 데이터에는 링크 주소(텍스트)만 있고 실제 파일이 없어서 에러가 난 것입니다.
해결 방법: 중간에 파일을 다운로드하는 HTTP Request 노드를 하나 추가하면 됩니다.
- 연결 순서:
[HTML to PDF]➔[HTTP Request]➔[Gmail]

























