본문으로 건너뛰기

n8n 워크플로우 ID 하나만 넣으면 9:16 설명 영상이 만들어집니다

· 약 10분
Datapopcorn CEO / AI automation educator

n8n 워크플로우를 만든 뒤 그 결과를 블로그·유튜브·강의에 붙이는 일은 남아 있는데, 화면 녹화나 슬라이드 캡처를 다시 만드는 데 손이 많이 갑니다. 이 글은 n8n 워크플로우 ID 하나만 넘기면 세로 설명 영상을 만들어주는 방법을 공유합니다. 실행 기록, 노드 아이콘, 실제 결과 화면을 n8n에서 직접 긁어모은 뒤 spec.json이라는 중간 파일 하나로 영상을 렌더링합니다. 코드를 워크플로우마다 다시 쓰지 않는 게 핵심입니다.

n8n 워크플로우 하나를 9:16 설명 영상으로 바꾸는 방법

[AI비서] 모닝 브리핑 워크플로우(실행 기록 #68974)를 자동으로 만든 9:16 영상. 슬랙에 실제 도착한 메시지 화면과, 그 메시지가 만들어지는 0.24초의 내부 동작을 보여줍니다.

배경​

자동화를 몇 개 돌리다 보면 꼭 받는 질문이 있습니다. "이거 지금 어떻게 돌아가고 있어요?"

대부분의 답은 스크린샷입니다. n8n 캔버스를 캡처해서 붙이고, 실행 화면을 몇 장 잘라 붙입니다. 그러면 설명은 되지만 결과물이 계속 정적인 이미지입니다. 영상으로 보여 달라는 요청이 오면 다시 녹화를 해야 합니다.

그래서 항상 부딪히는 게 세 가지입니다.

첫째, 로고를 다시 그려야 합니다. OpenWeatherMap이나 Slack 로고를 텍스트로 대충 그렸다가 나오면 설명 영상보다 "억지로 만든 발표자료"처럼 보입니다.

둘째, 결과물 설명을 지어내야 합니다. 실제로 나가는 문구를 모르면 "다음 단계로 전달합니다" 같은 뻔한 자막만 붙게 됩니다.

셋째, 같은 작업을 워크플로우마다 반복합니다. 노드 3개인 워크플로우를 만들었다면, 7개인 워크플로우에서는 스크립트를 다시 짜야 합니다.

목표​

이 셋을 한 번에 없애는 게 목표입니다. 목표는 한 문장입니다.

워크플로우가 이미 실행된 상태라면, 워크플로우 ID만으로 그 실행을 설명하는 9:16 영상을 만든다.

"이미 실행된 상태"가 조건인 게 중요합니다. 이 도구는 실행을 새로 돌리지 않습니다. 슬랙 메시지를 보내는 워크플로우라면 실제로 메시지가 또 발송되기 때문입니다. 대신 n8n에 이미 쌓여 있는 실행 기록만 읽습니다.

구현 흐름​

전체 구조는 세 층입니다. 수집해서 spec.json으로 만들고, 그 파일을 읽어서 렌더링합니다.

이걸 두 파일로 나눈 게 핵심적인 결정입니다. 수집기는 n8n만 알고, 렌더러는 spec.json만 압니다. 그래서 워크플로우가 아무리 많아져도 렌더러 코드는 한 번만 고치면 됩니다.

1. 워크플로우는 문서가 아니라 API 응답이다​

n8n은 로그인한 브라우저 세션에서 자기 REST API를 그대로 제공합니다. 그래서 워크플로우를 열어서 눈으로 읽을 필요가 없습니다.

// 워크플로우 정의: 노드 좌표·연결선·노드 타입
const wf = await fetch(`/rest/workflows/${workflowId}`).then(r => r.json()).then(r => r.data);

// 실행 목록에서 성공한 것만
const runs = await fetch(`/rest/executions?limit=25&filter=${encodeURIComponent(
JSON.stringify({ workflowId })
)}`).then(r => r.json());

// 실행 하나를 펼치면 노드별 실행 시간이 나옴
const detail = await fetch(`/rest/executions/${id}`).then(r => r.json());
// detail.data.runData = { '노드이름': [{ executionTime, startTime, data: { main: [[...]] } }] }

한 번만 주의하면 됩니다. n8n은 실행 데이터를 평탄화(flatted) 형식으로 보냅니다. 값들이 하나의 배열에 들어 있고 서로를 문자열 인덱스로 가리키는 구조라, 그대로 읽으면 노드 출력 대신 "12" 같은 숫자 문자열만 보입니다. 인덱스를 재귀적으로 풀어야 실제 JSON이 나옵니다. 이거 안 풀면 "노드 출력값이 없다"는 잘못된 진단을 하게 됩니다.

2. 여러 실행 중 무엇을 설명할지 고르는 기준​

워크플로우를 켜 놨으면 실행 기록이 쌓입니다. 이 중 무엇을 영상으로 만들지는 아무거나 고르면 안 됩니다. 기준은 "실제로 돈 노드 수가 가장 많은 실행"입니다.

이유는 그 실행이 진짜 흐름을 다 담고 있기 때문입니다. 에러가 났거나 조건 분기가 막힌 실행을 고르면 그래프가 비어 있습니다. 모닝 브리핑은 2026-09-29 06:00 자동 실행(#68974)을 골랐고, 이 실행은 4개 노드 중 3개를 돌고 1개(에러 알림)는 돌지 않은 상태였습니다.

이걸 그대로 노출하는 게 낫다고 봤습니다. 에러 분기가 있는 워크플로우에서 "이번엔 에러가 안 났고, 이 경로는 안 탔습니다"까지 보여주면 워크플로우를 처음 보는 사람도 구조를 이해할 수 있습니다.

3. 로고는 다시 그리지 않고 캔버스에서 가져온다​

첫 버전은 노드 아이콘을 캔버스에 직접 그렸습니다. 비슷해 보여도 실제 앱 로고와는 달라서, 보는 사람이 어떤 서비스인지 바로 알아보기 어려웠습니다.

n8n 화면을 DOM으로 열어보면 각 노드에 이미 SVG 아이콘이 그려져 있습니다. [data-test-id="canvas-node"]를 순회하면서 그 SVG를 통째로 가져옵니다. 색이 currentColor로 들어와 있으면 실제로 렌더된 색을 계산해서 문자로 바꿔서 넣습니다.

// canvas-node 안의 .n8n-node-icon > svg
let svg = icon.outerHTML.replaceAll('currentColor', actualRenderedColor);

이렇게 가져온 결과는 n8n 화면과 픽셀 단위로 같습니다. 더 이상 "비슷한 로고"가 아니라 진짜 그 아이콘입니다. 화면에서 안 잡히는 노드는 /types/nodes.json의 iconUrl로 폴백합니다.

4. 입력과 출력은 실행 기록 안에 있다​

가장 중요한 부분입니다. 설명 영상이 설득력을 갖는 건 예시 화면이 아니라 그 실행에서 실제로 나간 것을 보여줄 수 있기 때문입니다.

입력은 runData의 binary에서 나옵니다. 파일이 흐른 워크플로우라면 mimeType·크기·파일명을 그대로 읽을 수 있습니다. 출력은 마지막 노드가 Slack이라면 메시지 안의 channel과 ts가 들어 있습니다. 이 두 값으로 슬랙 메시지 URL을 만들고, 실제 브라우저에서 그 메시지를 찾아 스크린샷합니다.

여기서 다듬은 게 있습니다. 슬랙 메시지는 세로로 길 수 있습니다. 화면보다 긴 메시지를 그냥 캡처하면 Slack 헤더(검색창, 채널명)가 잘려 들어갔습니다. 그래서 세 가지를 넣었습니다.

  • 패널 위쪽에 맞춘 스크롤로 헤더가 잘리지 않게 합니다
  • 메시지가 화면보다 길면 페이지 배율을 줄여 통째로 들어오게 합니다
  • 오른쪽에 메시지 없는 여백이 남으면 픽셀 단위로 잘라냅니다

캡처를 못 했을 때는 정직하게 "예상 결과 (재구성)"이라고 화면에 표시합니다. 재구성한 화면을 실제 결과처럼 내보내면 그게 검증 불가능한 마케팅 자료가 됩니다.

5. spec.json 하나로 합친다​

수집이 끝나면 워크플로우에 대한 지식이 전부 spec.json으로 빠져나옵니다.

{
"meta": {
"workflowId": "gdtZQi48u2rUx0c3",
"workflowName": "[AI비서] 모닝 브리핑 (날씨·뉴스·환율 AI 요약 → Slack)",
"executionId": "68974",
"executedAt": "2026-09-28T21:00:58.066Z"
},
"nodes": {
"n1": {
"name": "Fetch Seoul Weather",
"type": "n8n-nodes-base.openWeatherMap",
"typeLabel": "OpenWeatherMap",
"x": 544, "y": 336, "w": 96, "h": 96,
"outputs": 2,
"outLabels": ["success", "error"],
"ran": true,
"icon": "icons/n1.svg"
}
},
"result": {
"label": "실제 결과 · Slack 캡처",
"src": "media/output-slack.png"
}
}

렌더러는 OpenWeatherMap도 Slack도 모릅니다. 이 파일만 읽습니다. 노드 위치는 캔버스 좌표를 그대로 쓰고, outputs와 outLabels로 분기를 그리고, ran: false인 노드는 흐리게 처리합니다. 나중에 다른 워크플로우를 설명하게 되면 이 파일의 숫자만 달라집니다.

이 구조 덕분에 출력 크기도 쉽게 바꿀 수 있었습니다. 같은 설정으로 처음 렌더한 영상은 19.9MB였습니다. 저장소에 넣기엔 크길래 엔진에 출력 크기 옵션을 넣어서 720×1280 / 1.15Mbps로 다시 렌더링했습니다. 화면 레이아웃은 1080×1920 로직 좌표로 그대로 두고 출력만 줄입니다. 결과는 3.0MB입니다.

6. 브라우저에서 그대로 인코딩한다​

렌더러는 별도 빌드 도구를 쓰지 않습니다. 브라우저 캔버스에 프레임마다 그릴 수 있으면 됩니다. 인코딩은 WebCodecs, 컨테이너는 mp4 muxer를 씁니다.

const enc = new VideoEncoder({ output: (c, m) => mux.addVideoChunk(c, m) });
enc.configure({ codec: 'avc1.640033', width: 720, height: 1280, bitrate: 1150000, framerate: 30 });

for (let i = 0; i < frameCount; i++) {
draw(i / fps);
const px = ctx.getImageData(0, 0, 720, 1280);
const frame = new VideoFrame(px.data, { format: 'RGBA', codedWidth: 720, codedHeight: 1280 });
enc.encode(frame, { keyFrame: i % 60 === 0 });
frame.close();
}

이렇게 한 이유도 있습니다. Remotion CLI로 렌더하려 했는데 이 환경에서 Chromium 실행이 막혀 있었습니다. 브라우저 탭에서 렌더하면 이 문제가 아예 사라지고, 프레임 미리보기와 검증을 같은 코드에서 같이 할 수 있습니다.

7. 렌더가 성공했다고 믿지 않는다​

이 글에서 제일 강조하고 싶은 부분입니다. "렌더링 완료"는 검증이 아닙니다. 코드가 에러 없이 끝나도 애니메이션 타이밍이 어긋나면 화면이 몇 초씩 멈춰 있거나 검게 나올 수 있습니다.

그래서 렌더 직후 자동으로 두 가지를 봅니다.

  • 0.5초 간격으로 프레임을 뽑아 연속한 두 표본이 거의 같으면 정지, 전체 밝기가 임계값 이하면 암전
  • 표본 시트(여러 프레임을 한 장으로 이어 붙인 이미지)를 눈으로 직접 확인

이 단계가 없으면 "코드 실행 에러 0개"를 성공으로 보고합니다. 그건 검증이 아니라 로그 확인입니다.

결과​

모닝 브리핑 워크플로우로 손대지 않고 돌렸을 때 나온 결과입니다.

  • 20초 / 720×1280 / 3.0MB의 9:16 MP4
  • 4개 노드 중 3개 실행, 1개는 에러 분기라 미실행으로 표시
  • 0.240초를 3단계로 나눠 각 단계에 실제 노드 시간과 데이터 수 표시
  • 결과 화면은 실제 슬랙 캡처
  • 정지 프레임 0개, 암전 프레임 0개

이 도구는 처음에 Calvin and Hobbes 만화를 번역해서 Slack에 올리는 워크플로우(노드 7개)로 만들었습니다. 모닝 브리핑은 그다음에 워크플로우 ID만 넣어서 돌려 본 두 번째 사례입니다. 워크플로우에 맞춰 수집 코드를 따로 고치지 않았고, Slack 캡처의 스크롤과 여백 처리만 두 워크플로우에 공통으로 다듬었습니다. "ID만 있으면 된다"는 목표가 두 번째 워크플로우에서도 통했다는 뜻입니다.

배운 점​

실행되지 않은 경로도 보여줘야 구조가 보인다​

모닝 브리핑에는 날씨 조회가 실패하면 알림을 보내는 에러 경로가 있습니다. 이번 실행에서는 그 경로를 타지 않았습니다. 처음에는 실행된 노드만 보여줄까 했지만, 실행 안 된 노드를 흐리게 남겨 두는 편이 나았습니다. 그래야 보는 사람이 "실패하면 이쪽으로 간다"는 구조까지 이해할 수 있습니다.

실행 시간은 어느 실행 기준인지 밝혀야 한다​

Calvin and Hobbes 영상에 표시한 총 7.6초는 한 번의 실행에서 나온 값이 아니었습니다. 수동 실행이라 앞쪽 노드들이 이전 실행 결과를 재사용했고, 서로 다른 실행의 시간이 섞여 있었습니다. 화면만 봐서는 알 수 없는 문제입니다. 이제는 시간 표시 밑에 "실행 기록 #번호 기준"을 항상 붙이고, 같은 실행에서 안 나온 값은 그렇게 표시합니다.

자동 자막은 사람이 한 번 읽어야 한다​

기본 자막을 결과를 ${서비스}(으)로 보냅니다로 만들어 뒀더니 "결과를 Slack(으)로 보냅니다"처럼 어색한 문장이 나왔습니다. 서비스 이름이 영어라 코드가 받침 유무를 판단하지 못했기 때문입니다. 조사를 "에"로 바꿔서 해결했습니다. 이런 건 사람이 한 번 읽어야 잡히는 종류입니다. 자동 생성 자막은 방향만 잡아 주는 용도로 생각해야 합니다.

공개할 영상은 재료의 권리부터 확인한다​

Calvin and Hobbes 영상에는 실제 만화가 그대로 나옵니다. 내부 샘플로는 괜찮지만 공개 블로그에 올리기에는 저작권 문제가 있어서, 이 글의 예시는 날씨 알림 워크플로우로 바꿨습니다. 설명 영상은 자주 공유되는 자산이라, 새로 만들 때 재료가 누구 것인지부터 보는 게 낫습니다.

파일 크기와 해상도도 결과물로 확인한다​

웹용으로 다시 렌더한 뒤에는 파일 크기만 보지 않고, 브라우저에서 영상을 특정 시점으로 옮겨 실제 해상도(720×1280)와 글자 선명도를 확인했습니다. 용량을 줄이다 글자가 뭉개지면 설명 영상으로는 쓸 수 없기 때문입니다.

이렇게 써볼 수 있습니다​

세 단으로 나누어 봤으니, 자기 환경에서 조립하는 방법도 보입니다.

1단계 — 수집기. 워크플로우 ID로 다음 네 가지를 긁어옵니다. 워크플로우 정의, 성공한 실행 목록, 실제 실행 데이터, 캔버스에서 뽑은 노드 아이콘. 슬랙으로 끝나는 워크플로우라면 마지막 노드 출력의 channel과 ts를 같이 뽑습니다.

2단계 — spec.json. 위에서 모은 내용을 JSON으로 씁니다. 그림의 뼈대는 nodes, edges, steps, result 네 개 키이고, meta·intro·outro에는 출처 표기와 화면 문구가 들어갑니다. 노드 좌표는 n8n 캔버스 좌표를 그대로 씁니다.

3단계 — 렌더러. 9:16 캔버스를 두고 프레임마다 그리는 JS를 짜고, WebCodecs + mp4 muxer로 인코딩합니다. 마지막에 정지·암전 프레임 검사를 붙입니다.

필요한 전제조건은 셋입니다. n8n 인스턴스에 로그인한 브라우저 세션, 브라우저를 띄워서 작업할 수 있는 에이전트, 로컬에 Node를 돌릴 환경입니다. 서버는 Node 표준 라이브러리만 쓰는 짧은 정적 서버로 충분합니다.

저는 이 3단을 브라우저 에이전트(Aside)의 스킬로 묶어 두었습니다. 그래서 요청은 워크플로우 링크 한 줄이면 됩니다. 링크에서 n8n 주소와 워크플로우 ID를 읽기 때문에 n8n Cloud든 직접 설치한 n8n이든, 브라우저에서 로그인만 되어 있으면 같은 방식으로 동작합니다.

https://내-n8n-주소/workflow/워크플로우ID 이 워크플로우로 설명 영상 만들어줘

아직 이 스킬을 설치형으로 배포하지는 않았습니다. 지금은 위의 구조와 코드 조각을 참고해 자기 환경에 맞게 다시 조립하는 방식입니다.

정리하면, 이 방식의 가치는 영상이 아니라 구조에 있습니다. 수집·정리·렌더를 spec.json 하나로 끊어 놓으면 "설명 영상 한 편 더 만들기"가 코드를 고치는 일이 아니라 JSON을 다시 만드는 일이 됩니다. 설명할 대상이 늘어날수록 그 차이는 커집니다.

다음 액션​

  • 슬랙 캡처를 Gmail, Notion 같은 다른 대상으로 넓혀서 "채널에 올린 결과"만 자동으로 붙일 수 있게 하겠습니다
  • 자동 자막을 실행 데이터에서 뽑는 규칙으로 바꾸겠습니다. 지금은 "Slack에 이렇게 도착합니다"처럼 평범한 문장이 대부분입니다
  • 영상에 음성 내레이션과 자막 타이밍을 얹겠습니다. 지금은 효과음만 있습니다
  • 16:9 버전도 같은 spec.json에서 뽑아내도록 엔진에 비율만 추가하겠습니다