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

유튜브 영상 URL만 넣으면 자막 요약까지 — 커뮤니티 노드 대신 서브워크플로우를 쓴 이유

· 약 9분
Datapopcorn CEO / AI automation educator
이 글과 연결된 글 보기
7편 · 20개 연결
100%
n8n Data Table 처음 써보기 — 트렌드 3개를 합쳐 매일 아침 분석 주제 뽑는 워크플로우n8n으로 정적 웹서버 운영하기 — 수료증 발급부터 진위 확인까지, 멱등 처리로 안전하게벡터DB 없이 실시간으로 사이트를 읽는 챗봇 — 경복대학교 안내 챗봇 해부n8n으로 반복업무 자동화 포트폴리오 만들기구글시트에 쌓인 데이터를 인스타그램 카드뉴스로 — n8n 시트 인스타그램 자동발행서브워크플로우를 Tool로 감싸 AI Agent에게 서버 점검시키기 — 그리고 시스템 프롬프트만으로는 부족한 이유유튜브 영상 URL만 넣으면 자막 요약까지 — 커뮤니티 노드 대신 서브워크플로우를 쓴 이유

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

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

n8n Form에 유튜브 영상 URL과 언어 코드를 입력하면, 커뮤니티 노드 설치 없이 HTTP 요청만으로 자막을 뽑고 AI가 제목·개요·섹션으로 구조화한 요약을 돌려주는 워크플로우를 만들 수 있습니다. 원래는 커뮤니티가 만든 전용 노드를 썼지만, 그 노드가 n8n Cloud에서는 설치조차 안 된다는 걸 확인하고 같은 기능을 서브워크플로우로 직접 구현한 캔버스가 그대로 남아 있어, 이식 가능한 자동화를 설계하는 법을 그 흔적에서 배울 수 있습니다.

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

면접에서 "커뮤니티 노드를 써봤나요?"라는 질문을 받으면 대부분 설치 경험을 답합니다. 이 워크플로우는 한 걸음 더 나간 답을 할 수 있게 해줍니다. "커뮤니티 노드가 배포 환경에서 막혀서, 같은 기능을 HTTP 요청과 Code 노드로 직접 구현하고 서브워크플로우로 분리했습니다."

여기에는 세 가지 역량이 들어 있습니다.

  • 플랫폼 제약 위에서 대안 설계 — 특정 노드가 안 되는 환경을 만났을 때, 포기하지 않고 같은 결과를 다른 방식으로 만들어낸 경험
  • 서브워크플로우 분리 — 복잡한 추출 로직을 별도 워크플로우로 떼어, 메인 흐름을 단순하게 유지하고 재사용 가능하게 만든 경험
  • AI 구조화 출력 — 자막이라는 긴 텍스트를 그냥 요약이 아니라 제목·개요·섹션으로 나뉜 JSON으로 강제해본 경험

이 시리즈는 구글 뉴스 RSS 링크 디코딩 편과 구내식당 식단표 분석 편에서도 각각 파싱과 이미지 처리를 다뤘습니다. 이번 편은 그중 서브워크플로우 분리 패턴을 유튜브 자막이라는 소스에 적용한 사례입니다.

배경 — 커뮤니티 노드에서 서브워크플로우로​

원본 캔버스를 열어보면 스티키 노트 세 개가 워크플로우를 세 구역으로 나눠 놓았습니다. "Original - Community Node", "Renew - Sub-Workflow", "(Sub-workflow) Get Transcript"입니다. 이름 그대로, 처음엔 커뮤니티가 만든 자막 추출 노드(n8n-nodes-youtube-transcription.youtubeTranscripter)를 썼다가 같은 기능을 서브워크플로우로 새로 만들어 바꾼 흔적입니다. 원래 구역은 Manual Trigger로 그 노드를 실행해 Split Out·Aggregate로 정리한 뒤 OpenAI로 100자 요약을 받는 다섯 노드짜리 흐름이었습니다.

왜 바꿨는지는 스티키 노트에 따로 적혀 있지 않지만, 이 워크플로우가 있는 폴더 이름(cloud/workflows)과 n8n 공식 문서를 같이 보면 이유가 분명해집니다. n8n 공식 문서는 검증되지 않은 커뮤니티 노드는 n8n Cloud에서 쓸 수 없고, npm을 통한 설치는 self-hosted 인스턴스에서만 가능하다고 안내합니다(Installation and management). 데이터팝콘이 이 워크플로우를 만든 n8n Cloud 인스턴스에서는 커뮤니티 노드 자체를 설치할 수 없었던 셈이고, 노드 하나 때문에 플랫폼을 옮길 수는 없으니 같은 기능을 HTTP 요청과 Code 노드로 직접 구현하는 쪽을 택한 것으로 보입니다.

이렇게 만든 결과물이 이 글에서 다룰 "Renew - Sub-Workflow" 구역입니다. n8n 설치나 기본 노드 사용법은 n8n으로 반복업무 자동화 포트폴리오 만들기에 정리돼 있으니, 여기서는 이 워크플로우 자체만 다룹니다.

구조 — Trigger / Task / Target​

  • Trigger — Form에 유튜브 URL과 언어 코드를 입력하고 제출
  • Task — 서브워크플로우 호출(유튜브 페이지 HTML 요청 → 자막 URL 추출 → 자막 XML 요청·파싱) → 자막 조각을 하나로 합치기 → AI Agent가 구조화된 요약 생성
  • Target — 없습니다. AI Agent의 응답이 이 캔버스의 마지막 노드이고, 슬랙이나 이메일·시트로 결과를 보내는 노드는 붙어 있지 않습니다. 포트폴리오로 가져간다면 여기에 전송 노드를 하나 추가하는 게 첫 번째 개선 지점입니다.

만들기​

1. Form Trigger — URL과 언어 두 개만 받기​

On form submission 노드에 필드 두 개를 둡니다. 하나는 url(예시 placeholder로 공개 유튜브 링크 하나), 다른 하나는 language (ko, en)이라는 라벨에 ko placeholder를 넣은 필드입니다. 두 필드 모두 필수로 켜져 있습니다.

여기서 짚어둘 점이 하나 있습니다. n8n Form Trigger는 필드 라벨을 그대로 JSON 키로 씁니다. 라벨에 language (ko, en)처럼 괄호가 들어가면, 이후 노드에서는 $json['language (ko, en)'] 같은 대괄호 표기로 꺼내야 하고 점 표기($json.language)는 다른 필드를 가리키게 됩니다. 이 문제가 실제로 나타나는 지점은 아래 "원본 캔버스에 남아 있는 흔적"에서 다룹니다.

2. Execute Workflow — 자막 추출 로직 떼어내기​

Form 다음에 바로 Execute Workflow 노드가 옵니다. Workflow ID로 별도 서브워크플로우를 지정하고, Options의 Wait For Sub-Workflow Completion을 켰습니다. 이 옵션이 꺼져 있으면 메인 흐름은 서브워크플로우 완료를 기다리지 않고 바로 다음 노드로 넘어가버리므로, 자막 결과를 받아야 하는 이 워크플로우에는 필수입니다.

입력 매핑은 비워뒀지만, 서브워크플로우 쪽 트리거가 "Passthrough" 모드라 Form에서 받은 url과 language (ko, en)이 매핑 없이 그대로 전달됩니다. 별도 워크플로우로 떼는 이유는 이 시리즈의 구글 뉴스 RSS 서브워크플로우 편과 같습니다. 복잡한 파싱 로직을 메인 흐름에서 분리하면 따로 테스트할 수 있고, 다른 워크플로우에서도 재사용할 수 있습니다.

3. 서브워크플로우 안 — HTML에서 자막 URL 뽑아내기​

여기부터가 커뮤니티 노드가 하던 일을 대신하는 부분입니다. When Executed by Another Workflow 트리거가 입력을 그대로 넘겨받으면, HTTP Request 노드가 {{ $json.url }}로 유튜브 시청 페이지 자체를 요청합니다. User-Agent 헤더를 실제 브라우저 값으로 넣어야 정상적인 페이지가 돌아옵니다.

받은 HTML에는 자막 트랙 목록이 captionTracks라는 이름으로 페이지 스크립트 안에 박혀 있습니다. 다음 Code 노드가 정규식으로 이 부분만 잘라내 언어에 맞는 트랙을 고릅니다.

for (const item of items) {
const html = item.json.data;
const preferredLang = item.json.language; // optional 사용
const match = html.match(/"captionTracks":\[(.*?)\]/);

if (!match) {
item.json = { error: 'Transcript not found (no captionTracks)' };
continue;
}

const tracks = JSON.parse(`[${match[1]}]`);

let track = null;
if (preferredLang) {
track = tracks.find(t =>
(t.languageCode === preferredLang || t.vssId?.includes(`.${preferredLang}`))
);
}

// 지정 언어가 없거나 찾지 못했으면: 수동 자막 → 자동 자막 순서로 아무거나
if (!track) {
track = tracks.find(t => !t.kind) || tracks.find(t => t.kind === 'asr');
}

item.json.transcriptUrl = track?.baseUrl || null;
item.json.transcriptLang = track?.languageCode || null;
}

return items;

언어를 못 찾으면 수동 자막(kind 값이 없는 트랙)을 먼저 찾고, 그것도 없으면 자동 생성 자막(kind: 'asr')으로 넘어갑니다. captionTracks가 아예 없는 영상은 에러 객체로 표시해 다음 단계에서 걸러지도록 열어뒀습니다.

이렇게 찾은 transcriptUrl로 다시 HTTP Request 노드가 실제 자막 파일(XML)을 요청합니다. 마지막 Code 노드가 이 XML을 파싱해 자막 조각 배열로 바꿉니다.

const xmlString = $input.first().json.data;
const youtubeUrl = $input.first().json.youtubeUrl;

const lang = 'ko';
const matches = [...xmlString.matchAll(/<text start="([^"]+)" dur="([^"]+)">([\s\S]*?)<\/text>/g)];

const transcript = matches.map(match => ({
text: match[3].replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>'),
duration: parseFloat(match[2]),
offset: parseFloat(match[1]),
lang: lang
}));

return [{ json: { youtubeId: youtubeUrl, transcript: transcript } }];

공식 자막 API가 아니라 XML을 직접 정규식으로 읽는 방식이라, 커뮤니티 노드 없이도 자막을 뽑아낼 수 있지만 유튜브가 이 XML 구조를 바꾸면 정규식도 같이 깨집니다.

4. Split Out → Aggregate — 자막 조각을 한 덩어리로​

서브워크플로우가 돌려준 transcript 배열은 문장 단위 조각들입니다. Split Out으로 개별 아이템으로 풀고, Aggregate에서 text 필드만 골라 다시 하나의 배열로 합칩니다(mergeLists 옵션). 이 두 노드는 원래 커뮤니티 노드 구역의 Split Out·Aggregate와 설정이 같습니다. 서브워크플로우 출력 형태(transcript: [{ text, duration, offset, lang }])를 커뮤니티 노드와 똑같이 맞춰, 뒤쪽 요약 부분은 손대지 않고 이어붙일 수 있었던 것으로 보입니다.

5. AI Agent + Structured Output Parser — 요약을 JSON으로​

AI Agent 노드의 Text에 {{ $json.text }}(합쳐진 자막)를 넣고, Chat Model로 Google Gemini(gemini-2.5-pro-preview-03-25)를 연결합니다. Require Specific Output Format을 켜고 Structured Output Parser를 붙이면, 답이 자유 문장이 아니라 정해진 JSON 스키마로 나옵니다.

{
"title": "요약의 제목 또는 주제",
"summary": "전체 내용을 압축한 핵심 개요",
"sections": [
{ "heading": "소제목 1", "content": "해당 섹션의 요약 내용" }
]
}

System Message에는 "주제·주장·논리 전개 순서를 구조화된 형태로 요약하라", "답변은 반드시 한글로"라는 지시가 들어 있습니다. 원래 커뮤니티 노드 구역은 OpenAI에 "100자 이내로 요약해줘"라고만 시키는 한 줄짜리 프롬프트였으니, 자막 소스를 바꾸는 김에 요약 방식도 구조화 출력으로 함께 업그레이드한 셈입니다.

원본 캔버스에 남아 있는 흔적 — 필드 이름이 서로 다릅니다​

이 서브워크플로우에는 통일되지 않은 흔적이 하나 남아 있습니다. Fetch Transcript 노드는 {{ $('When Executed by Another Workflow').item.json['language (ko, en)'] }}처럼 정확한 대괄호 표기를 씁니다. 하지만 앞선 두 곳 — HTTP Request 노드의 Accept-Language 헤더({{ $json.language }})와 자막 URL을 고르는 Code 노드의 item.json.language(주석은 "optional 사용") — 는 Form이 실제로 넘기는 키인 language (ko, en)이 아니라 language를 참조합니다. Form 필드 라벨이 그대로 키가 되므로, 이 두 값은 실행 시점에 비어 있을 가능성이 큽니다.

결과적으로 언어 선택은 "수동 자막 우선, 없으면 자동 자막" 순서에 기댈 가능성이 큽니다. Format 노드도 lang: 'ko'를 문자열로 고정해, en을 입력해도 자막 조각의 lang 메타데이터는 항상 ko로 찍힙니다. 요약 결과(text)에는 영향이 없지만, 언어 선택 UI를 만들어놓고 절반만 반영되는 지점이라 눈여겨볼 부분입니다.

한계​

  • 필드 이름 불일치 — 위에서 설명한 대로, language (ko, en) 대신 language를 참조하는 두 곳을 고치지 않으면 언어 지정이 절반만 동작합니다. 대괄호 표기로 통일하는 게 첫 번째 수정 지점입니다.
  • 결과가 어디로도 안 감 — AI Agent 요약이 마지막 노드입니다. 이메일이나 슬랙, 노션으로 보내는 노드를 붙여야 실제로 쓸 수 있는 결과물이 됩니다.
  • 스크래핑 방식의 취약함 — 공식 API가 아니라 유튜브 페이지 HTML과 자막 XML을 직접 파싱하는 방식이라, 유튜브가 페이지 구조나 captionTracks 마크업을 바꾸면 정규식이 그대로 깨집니다. 공식 API처럼 안정적인 계약이 없다는 점을 감안해야 합니다.
  • 자막 없는 영상 처리 미완성 — Extract Transcript URL 코드는 자막이 없으면 error 필드를 채워두기만 하고, 그 뒤로 이어지는 분기 처리는 캔버스에 없습니다. 에러 상황을 실제로 걸러내려면 IF 노드를 하나 추가해야 합니다.

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

포트폴리오에는 위에서 짚은 필드 이름 불일치를 직접 고친 버전을 올리는 걸 권합니다. "원본에서 언어 지정이 절반만 동작하는 걸 발견하고 대괄호 표기로 통일했다"는 한 문단은, 워크플로우를 그대로 베낀 사람과 코드를 읽고 이해한 사람을 가르는 지점입니다. 여기에 Slack이나 이메일로 요약을 보내는 노드를 하나 추가하면 "Target이 없다"는 한계도 같이 채워집니다.

면접에서는 "왜 커뮤니티 노드 대신 HTTP 요청으로 직접 구현했나요?"라는 질문이 나올 수 있습니다. 검증되지 않은 커뮤니티 노드는 n8n Cloud에서 설치할 수 없고, self-hosted로 옮기지 않는 이상 같은 기능을 다른 방식으로 만들어야 합니다. HTTP Request와 Code 노드로 같은 결과를 만들어봤다면, "공식 노드가 없어서 못 한다" 대신 "직접 만들면 된다"고 답할 수 있습니다.

다음 액션​

이번 편은 영상 하나를 입력받아 자막을 요약하는 흐름이었습니다. 다음 편에서는 방향을 조금 바꿔, 유튜브에 새로 올라온 영상을 키워드로 걸러 알림을 받는 워크플로우를 다룹니다. 자막을 읽는 대신 새 영상 목록 자체를 감시하는 패턴이라, 이번 편의 서브워크플로우 구조와 함께 쓰면 "관심 키워드의 새 영상이 올라오면 자막까지 자동으로 요약해서 받아보기"로 이어붙일 수 있습니다.