본문으로 건너뛰기
취준생을 위한 n8n 실전 워크플로우 레시피 · 43/44시리즈 보기 →

n8n으로 정적 웹서버 운영하기 — 수료증 발급부터 진위 확인까지, 멱등 처리로 안전하게

· 약 9분
Datapopcorn CEO / AI automation educator
이 글과 연결된 글 보기
35편 · 185개 연결
100%
급식 API로 배우는 n8n 페이지네이션 — 마지막 페이지까지 다 받는 루프 만들기구내식당 식단표 사진 대신 API로 — 매일 메뉴를 AI가 분석해 알림까지 보내는 n8n 워크플로우고객 문의 메일, AI가 분류하고 답장 초안까지 — CS 이메일 자동응답 워크플로우매일 아침 대통령 일정을 긁어 구글시트에 쌓는 n8n 워크플로우 — 최초 적재와 매일 증분을 나누는 이유매일 아침 7시 날씨를 디스코드로 받는 n8n 날씨 알림 — 시트 대신 외부 API를 읽는 첫 워크플로우수집·정제·분석 한 바퀴 — 포켓몬 API로 배우는 n8n 데이터 분석n8n Data Table 처음 써보기 — 트렌드 3개를 합쳐 매일 아침 분석 주제 뽑는 워크플로우경제지표 API로 매일 투자 리포트 받기 — AI에게 cURL부터 만들게 하는 n8n 워크플로우n8n 워크플로우가 조용히 죽지 않게 — Error Trigger로 실패를 슬랙으로 받는 에러 알림 만들기페이스북 광고 효율, AI가 매일 분석해서 알려주는 n8n 워크플로우 — 시리즈의 마지막 레시피제주 농지 데이터 수집기 — 문서 없는 공공 사이트를 n8n으로 내 데이터셋으로 옮기는 법받은 메일을 AI가 자동 분류해 DB에 쌓기 — n8n Gmail 자동 분류 워크플로우구글 뉴스 RSS 링크는 왜 안 열리나 — n8n 서브워크플로우로 원문 URL 디코드해서 노션에 쌓기구글시트 매출 데이터, AI가 매일 인사이트 리포트로 — 수작업 보고서에서 벗어나는 n8n 워크플로우호텔 가격 추이를 AI가 요약해주는 n8n 워크플로우 — OAuth 토큰이 필요한 API 다루기코드 없이 베스트셀러 긁기 — n8n HTML 노드로 배우는 웹 스크래핑n8n으로 정적 웹서버 운영하기 — 수료증 발급부터 진위 확인까지, 멱등 처리로 안전하게인스타그램 카드뉴스 자동 발행 — n8n으로 HTML을 이미지로, 이미지를 피드로벡터DB 없이 실시간으로 사이트를 읽는 챗봇 — 경복대학교 안내 챗봇 해부재고 10개 미만이면 슬랙으로 알려주는 n8n 워크플로우 — 첫 자동화로 이걸 추천하는 이유n8n으로 MCP 서버 만들기 — AI Agent와 무엇이 다른가녹음 파일 하나로 회의록·노션까지 — n8n으로 만드는 회의 요약 파이프라인명함 사진을 드라이브에 올리면 구글시트에 자동 정리 — n8n + Mistral OCR 명함 입력 자동화NFC 태그를 찍으면 AI 명언 페이지가 뜨는 n8n 웹훅 — n8n을 나만의 API 서버로 쓰기물컵 사진을 찍으면 습관 시트에 체크 — n8n 웹훅으로 이미지 받아 OpenAI로 분류하기n8n으로 반복업무 자동화 포트폴리오 만들기403이 뜨는 사이트 가격 추적하기 — 스크래핑이 막혔을 때 n8n으로 우회하고 어제 가격과 비교하기제품 사진 한 장으로 광고 영상 만들기 — n8n + Firecrawl + Veo 3.1 영상 생성 자동화공공데이터 API를 10분마다 구글시트에 쌓기 — 응급실 가용 병상 데이터로 배우는 수집 자동화공공데이터를 대시보드까지 — 중대재해 사고 현황을 n8n으로 모아 Tableau에 연결하기내 문서로 답하는 RAG 챗봇 — n8n으로 임베딩부터 검색까지레딧 인기글을 3시간마다 슬랙으로 요약해 받기 — n8n 커뮤니티 모니터링 자동화학교코드만 바꾸면 재사용 — n8n 워크플로우를 일회성 스크립트에서 템플릿으로 만드는 법구글시트에 쌓인 데이터를 인스타그램 카드뉴스로 — n8n 시트 인스타그램 자동발행서브워크플로우를 Tool로 감싸 AI Agent에게 서버 점검시키기 — 그리고 시스템 프롬프트만으로는 부족한 이유

드래그로 이동 · 휠/두 손가락으로 확대 · 방향키로 글 선택 · 0으로 맞춤. 실선: 본문 링크, 점선: 시리즈·주제의 대표 연결.

표시된 글 35편 목록
전체 글 연결 탐색

수료증을 발급하는 스케줄 워크플로우 하나, 수료증 열람과 진위 확인을 처리하는 웹훅 워크플로우 하나, 랜딩 페이지를 서빙하는 웹훅 워크플로우 하나. 이 셋이 Data Table 하나를 같이 보며 작은 웹서비스를 이룹니다. 별도 서버 없이 n8n만으로 페이지를 서빙하고, 메일 발송이 실패해도 다음 날 자동 재시도되는 멱등 구조까지 갖춘 사례입니다.

이 워크플로우 하나로 보여줄 수 있는 것​

지금까지 이 시리즈에서 다룬 워크플로우는 전부 "무언가 일어나면 흐름을 태워 처리한다"는 구조였습니다. 이번은 다릅니다. n8n이 흐름을 처리하는 도구가 아니라, 요청이 오면 페이지를 돌려주는 백엔드 그 자체로 씁니다. 브라우저가 특정 주소로 GET 요청을 보내면 웹훅이 받아 HTML을 만들어 돌려줍니다. 별도 서버나 호스팅 서비스가 필요 없습니다.

뜯어보면 세 가지 역량이 드러납니다.

  • 웹훅으로 페이지 서빙 — Webhook과 Respond to Webhook만으로 정적·동적 HTML을 돌려주는 경험
  • 멱등한 배치 설계 — 같은 스케줄이 여러 번 돌아도 중복·누락 없이 안전하게 재시도되는 구조를 설계한 경험
  • 접근 수준 분리 — 같은 데이터를 조회 목적에 따라 다르게 노출하는 설계 경험

세 번째까지 갖추면 "자동화를 만들 줄 안다"에서 "운영까지 고려해 설계한다"로 한 단계 올라간 인상을 줍니다.

배경 — 별도 호스팅 없이 굴러가는 작은 웹서비스​

datapopcorn의 n8n 실무 과정은 수료증을 자동으로 발급합니다. 수료 요건을 채운 수강생에게 매일 아침 수료증 메일을 보내고, 그 수료증에는 링크 두 개가 담깁니다. 인쇄용 수료증을 여는 링크, 진짜인지 제3자가 확인할 수 있는 링크입니다. 이 두 링크와 랜딩 페이지까지, 별도 서버 없이 n8n의 웹훅만으로 서빙합니다. 수료증 자동 발급 자체가 이 과정의 교보재이자 마케팅 포인트가 되도록 설계한 셈입니다.

설치나 기본 노드 설명은 n8n으로 반복업무 자동화 포트폴리오 만들기에 정리돼 있으니, 이 글에서는 시스템 자체만 다룹니다. 직전 편이 벡터DB 없이 실시간으로 웹페이지를 읽는 챗봇이었다면, 이번 편은 반대편입니다. 외부 페이지를 읽는 대신 n8n 스스로 페이지를 만들어 내보냅니다. 지난 Data Table 편에서 처음 다룬 이 리소스는, 여기서는 발급 기록과 열람 조회 양쪽의 공유 저장소로 재사용됩니다.

구조 — Trigger / Task / Target​

세 워크플로우는 각각 독립적으로 활성화돼 있지만, cert_students Data Table 하나를 같이 봅니다.

  • Trigger — 워크플로우 1은 매일 09:00 스케줄, 워크플로우 2는 GET /cert-view·/cert-verify 두 웹훅, 워크플로우 3은 GET /n8n-course 웹훅
  • Task — 발급 배치는 대상 조회 → 필터링 → ID 생성 → 메일 발송 → 기록. 페이지 서빙은 Data Table 조회 → 상태별 HTML 조립
  • Target — 발급 배치는 메일함·Data Table, 페이지 서빙은 요청을 보낸 브라우저

만들기​

1. Data Table 준비 — cert_students​

name, email, eligible(Boolean), cert_id(String), issued_at(String), is_public(Boolean) 컬럼을 만듭니다. eligible은 수료 요건을 채운 수강생에게 true로 표시하고, cert_id는 처음엔 비워둡니다. 이 값이 채워지는 순간이 곧 "발급됨"의 기준입니다.

2. 워크플로우 1 — 발급 대상 조회와 필터링​

Schedule Trigger를 매일 09:00으로 두고, Data Table 노드(Get Row, Return All)에서 eligible이 true인 행을 전부 가져옵니다. 다음 Filter 노드가 가장 중요한 지점입니다.

  • 왼쪽 값: {{ $json.cert_id }}
  • 연산자: String → is empty

이미 발급된 수강생은 cert_id가 채워져 있어 여기서 걸러집니다. 이 필터 하나가 "같은 사람에게 두 번 발급하지 않는다"를 보장합니다.

3. Certificate ID 생성 — Code 노드​

Filter를 통과한 행마다 32자리 무작위 hex 문자열을 Certificate ID로 만들고, 열람·진위 확인 링크를 함께 조립합니다.

let certId = '';
for (let i = 0; i < 32; i++) certId += Math.floor(Math.random() * 16).toString(16);
const issuedAt = $now.toFormat('yyyy-LL-dd');
const base = '<N8N_WEBHOOK_BASE_URL>/webhook';
return { json: { ...$json, certId, issuedAt, certUrl: base + '/cert-view?id=' + certId, verifyUrl: base + '/cert-verify?id=' + certId } };

base 자리에는 자신의 n8n 웹훅 기본 주소를 넣습니다. Webhook 노드 화면의 Production URL에서 확인됩니다. 코드에 직접 적기보다 $vars로 빼두면 도메인이 바뀌어도 이 노드만 손대면 됩니다.

4. 멱등 처리 — 메일 발송 성공 후에만 기록한다​

Gmail 노드로 수료증 메일을 보냅니다. 제목은 [datapopcorn] {{ $json.name }}님, n8n 업무자동화 실무 과정 수료증이 발급되었습니다이고, 본문 HTML에는 확인 버튼과 Certificate ID, 진위 확인 링크, "n8n GmbH 공식 인증 아님" 면책 문구가 들어 있습니다. retryOnFail: true, maxTries: 3, waitBetweenTries: 5000이 설정돼 있어 일시적 전송 실패는 5초 간격으로 세 번까지 자체 재시도합니다.

여기서 멱등성(idempotency)이라는 개념이 등장합니다. 같은 작업을 여러 번 실행해도 결과가 한 번 실행한 것과 같아야 한다는 원칙이고, 이 워크플로우는 두 축으로 지킵니다.

  1. 중복 방지 — 앞서 본 Filter 노드가 cert_id가 이미 있는 행을 걸러내므로, 같은 수강생을 두 번 처리하지 않습니다.
  2. 누락 방지 — 발급 기록 노드는 메일 발송 노드 뒤에 연결돼 있습니다. 메일이 세 번의 재시도 끝에도 실패하면 흐름은 여기서 끊기고 cert_id는 채워지지 않습니다. 다음 날 09:00, 이 수강생은 여전히 eligible=true이면서 cert_id가 비어 있으니 Filter를 다시 통과해 재시도됩니다.

이 두 축을 합치면 "메일이 최소 한 번은 전달된다(at-least-once)"를 보장하면서도 중복 걱정이 없습니다. 실패 건은 다음 날 다시 시도되고, 성공 건은 재시도되지 않습니다. 별도 큐 없이 스케줄 자체가 재시도 메커니즘을 겸합니다.

5. 발급 완료 기록​

Data Table 노드(Update Row)에서 Code 노드가 만든 행의 id와 일치하는 행을 찾아 cert_id·issued_at을 채웁니다.

필터: id equals {{ $('수료 정보 생성').item.json.id }}
컬럼: cert_id = {{ $('수료 정보 생성').item.json.certId }}
issued_at = {{ $('수료 정보 생성').item.json.issuedAt }}

이 업데이트가 끝나야 해당 수강생이 다음 스케줄의 Filter를 통과하지 않게 됩니다.

6. 워크플로우 2 — 수료증 페이지 서빙 (/cert-view)​

Webhook 노드를 GET, path cert-view, Response Mode를 Respond to Webhook Node로 둡니다. 뒤의 Data Table 노드는 쿼리 파라미터의 id와 cert_id가 일치하는 행을 하나만(limit: 1) 가져오는데, alwaysOutputData: true와 onError: continueErrorOutput이 함께 설정돼 있어 조회 결과가 없거나 조회 자체가 에러여도 흐름이 멈추지 않고 다음 Code 노드로 넘어갑니다.

Code 노드는 요청받은 id와 조회된 행의 cert_id가 정확히 일치할 때만 이름·과정명·Certificate ID·발급일·진위 확인 링크가 들어간 인쇄용 HTML을 만듭니다. 일치하는 행이 없으면 404 HTML을, 조회가 에러였으면 500 HTML을 반환합니다. 마지막 Respond to Webhook 노드는 {{ $json.html }}을 본문으로, {{ $json.statusCode }}를 응답 코드로, Content-Type: text/html; charset=utf-8을 헤더로 붙여 그대로 돌려줍니다.

7. 워크플로우 2 — 진위 확인 페이지 (/cert-verify)​

같은 워크플로우 안에 웹훅이 하나 더 있습니다. GET cert-verify도 같은 방식으로 cert_id를 조회하지만 목적이 다릅니다. 수료증을 받은 사람이 보는 페이지가 아니라, 수료증 하단 링크로 제3자(채용 담당자 등)가 "이 수료증이 진짜인지" 확인하러 오는 페이지입니다.

그래서 결과 HTML 로직도 다릅니다. 일치하는 행이 있으면 이름·과정명·수료일·Certificate ID가 담긴 "VALID" 카드를 보여주되, 그 행의 is_public이 false면 이름 자리에 "비공개 설정 수료자"라고만 표시합니다. 같은 Data Table, 같은 cert_id 조회지만 cert-view는 본인 확인용 전체 정보를, cert-verify는 불특정 다수가 오는 공개 페이지라 is_public 플래그로 노출 수준을 낮춰 두는 구조입니다. 일치하는 행이 없으면 마찬가지로 404입니다.

8. 워크플로우 3 — 랜딩 페이지 (/n8n-course)​

세 번째 워크플로우는 훨씬 단순합니다. GET n8n-course 웹훅 하나가 Respond to Webhook 노드로 바로 이어지고, 응답 본문에 과정 커리큘럼과 수료 요건, 모집 안내가 담긴 정적 HTML이 그대로 들어 있습니다. Content-Type을 text/html로 지정해두어 이 주소에 접속하면 브라우저가 진짜 웹페이지로 렌더링합니다. 조건 분기도, 데이터 조회도 없이 노드 두 개로 정적 사이트를 서빙하는 가장 단순한 형태입니다.

원본 캔버스에 남아 있는 흔적​

세 워크플로우 각각에 스티키 노트가 남아 있고, 위에서 설명한 설계 의도와 정확히 겹칩니다. 발급 워크플로우의 메모는 이렇습니다. "cert_students에서 eligible=true & cert_id 미발급 행을 찾아 Certificate ID 생성 → 수료증 메일 발송 → 발급 기록. 메일 실패 시 기록하지 않으므로 다음 날 자동 재시도(멱등)." 멱등 구조가 만든 사람의 의도였음을 확인할 수 있는 대목입니다.

페이지 서빙 워크플로우 메모는 두 개입니다. cert-view 쪽은 "미발급/불일치 ID는 404, 조회 실패는 500", cert-verify 쪽은 "is_public=false면 이름 비공개 처리 후 유효성만 표시"라고 적혀 있습니다. 랜딩 페이지 워크플로우의 메모는 유지보수 방법을 남겨뒀습니다. "정적 HTML 서빙. 내용 수정은 저장소 landing/index.html 수정 후 이 노드의 responseBody 교체." 페이지 원본은 저장소에 두고, n8n 노드는 그걸 반영한 사본을 서빙한다는 뜻입니다.

한계​

  • 인증 없는 열람 링크 — cert-view·cert-verify 모두 요청 쿼리의 id와 cert_id가 일치하는지만 확인합니다. 로그인·만료 로직이 없어, 32자리 무작위 문자열인 cert_id 자체가 사실상 유일한 접근 통제입니다.
  • 랜딩 페이지는 두 곳을 손대야 함 — 스티키 노트대로 내용을 바꾸려면 저장소의 landing/index.html과 Respond to Webhook 노드의 responseBody를 각각 고쳐야 합니다. 자동 동기화는 없습니다.
  • 발급 취소 로직 없음 — eligible이 나중에 false로 바뀌어도 이미 발급된 cert_id·issued_at은 그대로 남습니다. 취소·무효화 흐름은 이 3개 워크플로우 안에 없습니다.
  • 트래픽 관측 없음 — 요청량이나 404·500 비율을 보여주는 노드가 따로 없습니다. n8n 실행 로그를 직접 열어야 확인됩니다.

포트폴리오·면접 포인트​

포트폴리오에는 워크플로우 캔버스 세 개를 정리한 다이어그램과, 실제로 열어본 수료증 열람·진위 확인 페이지 스크린샷을 같이 넣으면 좋습니다. "자동화 흐름을 만들었다"가 아니라 "n8n 웹훅만으로 정적 사이트를 운영해봤다"는 흔치 않은 경험입니다.

면접에서는 "왜 발급 기록을 메일 발송 전이 아니라 후에 남겼나요?"가 나올 수 있습니다. 발송 전에 기록하면 실패해도 "발급됨"으로 남아 영영 수료증을 못 받습니다. 발송 후에 기록해야 실패 건이 다음 스케줄에서 다시 시도됩니다. "cert-view와 cert-verify는 왜 응답이 다른가요?"에는 조회 주체가 달라 is_public 플래그로 노출 수준을 나눴다고 답하면 됩니다.

다음 액션​

이 시스템은 조회와 응답까지만 다룹니다. 다음 편에서는 서브워크플로우를 Tool로 감싸 AI Agent에게 SSH로 서버를 직접 점검시키는 사례로 넘어갑니다. Chat Trigger로 받은 요청을 Agent가 판단해 서브워크플로우를 호출하고, 그 서브워크플로우가 서버에 SSH 명령을 실행하는 구조인데, "조회만 허용한다"는 안전장치를 시스템 프롬프트에 못 박아두는 방법이 핵심입니다. 이번 배치의 마지막 편입니다.