목적
유튜브 영상을 쌓아두는 속도가 볼 수 있는 속도보다 항상 빠르다. 리서치 목적으로 저장해둔 영상, 나중에 보려고 찜해둔 영상, 레퍼런스로 쓸 것 같아서 링크만 복사해둔 영상들이 점점 쌓인다. 문제는 시간이다.
기존에 유튜브 요약을 지원하는 툴들은 대부분 영상에 내장된 자막, 즉 이용자가 직접 올린 transcript를 기반으로 작동한다. transcript가 없는 영상이면 요약 자체가 불가능하다. 한국어 자막이 없는 영상, 자동 생성 자막 품질이 낮은 영상, 아예 자막이 없는 영상은 그냥 직접 봐야 했다.
이 워크플로우는 그 문제를 우회한다. transcript에 의존하지 않고, oEmbed와 HTML 파싱을 통해 영상 정보를 가져온 뒤 Gemini 모델이 직접 요약하는 구조다. 자동화 도구인 n8n 위에서 돌아가며, 요약 결과는 Markdown 파일로 저장된다. 이 글은 그 환경을 처음부터 구성하는 과정을 기록한 것이다..
방법론
https://n8n.io/workflows/2679-ai-powered-youtube-video-summarization-and-analysis/
이 워크플로우의 구조를 따라가되, telegram 연결은 하지 않았고, openai api 연결 대신 gemini api 연결로 대신했다.
그리고 응답을 md로 받아 로컬에 저장되게 만들었다.
Docker Desktop 실행
Docker Desktop 실행 화면
Docker 메뉴바 상태 확인
Docker는 애플리케이션을 격리된 컨테이너 환경에서 실행할 수 있게 해주는 도구다. n8n은 이 컨테이너 위에서 돌아가기 때문에, 가장 먼저 Docker 데몬이 실행 중인지 확인해야 한다. Applications에서 Docker를 실행하고, 메뉴바의 고래 아이콘이 running 상태인지 확인한다.
터미널에서 아래 명령어로 정상 실행 여부를 확인할 수 있다.
docker --version
docker compose version
docker ps
에러 없이 버전과 컨테이너 목록이 출력되면 정상이다.
프로젝트 폴더로 이동
docker compose 명령어와 워크플로우 파일이 모두 이 폴더 안에 있기 때문에, 작업 전에 반드시 해당 경로로 먼저 이동해야 한다.
cd ~/Desktop/n8n
이동 후 docker-compose.yml 파일이 현재 폴더에 존재하는지 확인한다.
.env 설정
.env는 API 키처럼 코드 밖에서 관리해야 하는 민감한 값들을 모아두는 환경 변수 파일이다. 이 값들이 없으면 컨테이너 실행 자체가 되지 않거나, 요약 단계에서 실패한다. 필수 항목은 다음과 같다.
GOOGLE_GEMINI_API_KEY=...실제키...
GEMINI_MODEL=gemini-2.5-flash
N8N_ENCRYPTION_KEY=충분히_긴_임의_문자열
N8N_HOST=localhost
GENERIC_TIMEZONE=Asia/Seoul
아래 항목은 선택이다. 사용하지 않는다면 비워두어도 무방하다.
OPENAI_API_KEY=
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
저장할 때 값에 공백이나 따옴표가 섞이지 않도록 주의한다. 이 부분에서 예상치 못한 오류가 발생하는 경우가 많다.
n8n 컨테이너 실행
n8n은 워크플로우 자동화 도구로, 코드 없이 여러 서비스를 연결해 자동화 흐름을 만들 수 있다. 아래 명령어 하나로 컨테이너를 백그라운드에서 띄울 수 있다.
docker compose up -d
up은 컨테이너와 네트워크, 볼륨을 생성하거나 기존 것을 재사용해 시작하는 명령이고, -d는 터미널을 점유하지 않고 백그라운드에서 실행한다는 옵션이다. 실행 후 아래 명령으로 상태를 확인한다.
docker compose ps
docker compose logs --tail=100 n8n
n8n 서비스가 Up 상태이고, 로그에 에디터 URL이 표시되면 정상적으로 올라온 것이다.
n8n 컨테이너 실행 상태 확인
워크플로우 Import
워크플로우 Import 화면
n8n은 로컬에 JSON 파일이 있어도 자동으로 읽어오지 않는다. 워크플로우는 직접 가져와야 한다.
브라우저에서 http://localhost:5678에 접속한 뒤, Workflows에서 Import from File을 선택하고 youtube-summarization-workflow.json을 불러온다. Save까지 완료한 후 캔버스에 빨간 에러 노드가 없으면 정상이다.
다음은 각 노드가 어떤 일을 하는지에 대한 간단한 설명이다.
Webhook
외부에서 POST 요청을 받는 진입점이다. /ytube 경로로 요청이 들어오면 워크플로우가 시작된다. responseMode가 responseNode로 설정되어 있어, 응답은 뒤에 있는 Respond to Webhook 노드가 직접 처리한다.
Get YouTube URL
Webhook으로 들어온 요청의 body에서 youtubeUrl 값을 꺼내 다음 노드로 넘긴다. Set 노드를 사용해 $json.body.youtubeUrl을 명시적으로 추출하는 단계다. 이후 노드들이 참조할 수 있도록 데이터를 정리하는 역할이다.
YouTube Video ID
URL 문자열에서 11자리 YouTube 영상 ID를 정규식으로 추출한다. 동시에 URL에 포함된 이스케이프 문자(\?, \=)와 공백을 제거하는 sanitization도 여기서 처리한다. curl로 요청을 보낼 때 특수문자가 섞여 들어오는 경우를 대비한 처리다.
Get YouTube Video Info
oEmbed API를 호출해 영상의 기본 정보를 가져온다. https://www.youtube.com/oembed?url=... 엔드포인트에 GET 요청을 보내며, 응답에는 영상 제목(title), 채널명(author_name), 썸네일 URL(thumbnail_url)이 포함된다. YouTube Data API 키 없이 공개 정보를 가져올 수 있는 방법이다.
Get Video Page HTML
영상 페이지 전체 HTML을 가져온다. oEmbed로는 얻을 수 없는 영상 설명(description)과 키워드(keywords)를 추출하기 위해 페이지 소스를 직접 fetch한다.
Extract Video Data
앞의 두 노드에서 가져온 데이터를 하나로 합친다. HTML에서 meta 태그와 JSON-LD 구조를 파싱해 description과 keywords를 추출하고, oEmbed 결과(title, author_name, thumbnail_url)와 함께 하나의 객체로 정리한다. 이 노드의 출력이 Gemini에 넘어가는 실제 입력 데이터다.
Gemini Summarize & Analyze
Gemini API에 POST 요청을 보내 요약을 생성한다. 모델명은 $env.GEMINI_MODEL에서 동적으로 읽으며, 값이 없을 경우 gemma-3-1b-it을 기본값으로 사용한다. 프롬프트에는 영상 제목, 채널명, URL, 설명, 키워드가 모두 포함되며 출력 형식은 Key topics, Topic shift map, Detailed timeline summary, Next steps의 4단계로 고정되어 있다. temperature는 0.55, maxOutputTokens는 3200으로 설정되어 있다.
Response Object
Gemini의 응답에서 요약 텍스트를 꺼내고, 영상 정보와 함께 이후 노드들이 쓸 수 있는 형태로 재구성한다. 이 노드에서 ** 볼드 마크다운이 제거된다(.replace(/\*\*/g, '')). markdown 필드는 영상 제목, 채널, URL, 요약을 합쳐 최종 저장 및 응답에 사용할 완성된 문서 형태로 조합한다.
Respond to Webhook
Response Object에서 만들어진 markdown 필드를 HTTP 응답 body로 반환한다. curl로 요청을 보낸 경우 터미널에 마크다운 텍스트가 출력되는 것이 이 노드의 역할이다.
Save Markdown File
Execute Command 노드로, save-summary-md.sh 스크립트를 호출해 요약 결과를 .md 파일로 저장한다. Response Object의 JSON 데이터를 stdin으로 넘기고(--from-json 플래그), 저장 경로는 /workspace/summaries다. 파일명은 스크립트 내부에서 타임스탬프와 영상 제목을 기반으로 자동 생성된다.
Telegram
요약 결과를 Telegram 봇으로 전송하는 노드다. 현재는 disabled: true로 비활성화되어 있다. 활성화하면 $env.TELEGRAM_CHAT_ID로 지정된 채팅방에 영상 제목, 채널명, URL, 요약을 포함한 메시지가 전송된다.
모델 바꾸기 (Gemini / Gemma)
모델 설정 변경 화면
기본 모델은 gemini-2.5-flash (돈이 없어서 무료 모델을 사용했다) 으로 설정되어 있으며, .env의 GEMINI_MODEL 값만 바꿔도 모델을 전환할 수 있다. 모델 별 limit은 https://aistudio.google.com/app/api-keys 에서 볼 수 있다.
GEMINI_MODEL=gemini-2.0-flash
# 또는
GEMINI_MODEL=gemini-2.0-flash-lite
변경 후에는 docker compose up -d를 다시 실행해 적용한다.
UI에서 직접 바꾸고 싶다면 Gemini Summarize & Analyze 노드를 클릭하고, URL 안의 /models/<모델명>:generateContent 부분만 교체한 뒤 저장하면 된다. JSON 파일을 직접 수정하는 방법도 있는데, 해당 노드의 url 값에서 같은 부분을 바꾸고 재-import하면 된다.
모델 관련 오류가 발생할 경우, 404나 400은 모델명 오타 또는 미지원 모델일 가능성이 높고, 429는 현재 키의 쿼터가 부족한 상태다.
Test / Prod 모드 선택
URL이 비슷해 보여도 동작 조건이 다르다. Test URL은 http://localhost:5678/we bhook-test/ytube이며, 호출 전에 반드시 Execute workflow를 눌러 Waiting for trigger event 상태로 만들어야 한다. Prod URL은 http://localhost:5678/webhook/ytube이며, 워크플로우가 Active 상태여야 작동한다. 현재 어떤 모드를 쓰고 있는지 확인하고 URL을 맞춰 쓰는 것이 중요하다.
오늘 내 목적 하에서는 prod를 쓸 일은 없었다. 아래 Execute workflow를 누른 다음 요청을 보내면 된다.
Execute workflow 버튼
curl로 요청 보내기
curl 요청 예시
curl은 터미널에서 HTTP 요청을 직접 보낼 수 있는 명령줄 도구다. webhook 입력을 빠르게 검증할 때 가장 유용하다. Test 모드와 Prod 모드에 따라 URL만 다르고 나머지는 동일하다.
Test 모드:
curl -X POST "http://localhost:5678/webhook-test/ytube" \
-H "Content-Type: application/json" \
-d '{"youtubeUrl":"https://www.youtube.com/watch?v=6zXcw8lf_zo"}'
Prod 모드:
curl -X POST "http://localhost:5678/webhook/ytube" \
-H "Content-Type: application/json" \
-d '{"youtubeUrl":"https://www.youtube.com/watch?v=6zXcw8lf_zo"}'
X POST는 webhook 메서드를 지정하고,H는 JSON body임을 명시하며,d에 실제 payload를 넣는다. payload의 키는 반드시youtubeUrl이어야 한다.
md 파일 생성 확인
생성된 md 파일 목록
Save Markdown File 노드가 실제로 저장까지 성공했는지 확인하려면 아래 명령어를 사용한다.
ls -lt ~/Desktop/n8n/summaries | head
방금 실행한 시간의 .md 파일이 생성되어 있으면 워크플로우 전체가 정상적으로 작동한 것이다.
나는 요즘 자주 보고있는 머니그라피를 예시로 넣어 출력해봤다.
자주 실패하는 지점
막히는 지점은 대체로 정해져 있다.
404 webhook not registered는 Test 모드에서 Execute workflow를 누르지 않았거나, Prod 모드에서 Active를 켜지 않은 경우다. Failed to parse request body는 URL에 \?나 \= 같은 이스케이프 문자가 섞인 경우다. access to env vars denied가 뜨면 N8N_BLOCK_ENV_ACCESS_IN_NODE=false 설정을 확인한다. Unrecognized node type: n8n-nodes-base.executeCommand는 NODES_EXCLUDE=["n8n-nodes-base.localFileTrigger"] 설정이 유지되고 있는지 확인해야 한다.
현재 워크플로우 구조에 대해
기존의 복잡한 transcript 기반 흐름과 langchain 의존 구조를 단순화했다. 현재는 oEmbed + HTML 파싱 + HTTP 모델 호출 방식으로 작동하며, 모델은 gemma-3-1b-it을 사용한다. 응답은 Markdown으로 반환되고, .md 파일로 자동 저장된다. transcript가 없는 영상에서도 요약이 가능하다는 것이 이 구조의 핵심이다.
Customization Points
기본 설치만으로도 작동은 하지만, 실제로 써보면 손봐야 할 지점이 생긴다. 출력이 너무 간략하거나, 모델을 바꿀 때마다 JSON을 직접 열어야 하거나, 결과물이 어디에도 저장되지 않는 문제들이다. 아래 세 가지는 그 지점들을 해결하기 위해 변경한 내용이다.
출력 포맷 구조화
기본 프롬프트로 생성된 요약은 너무 간략했다. 주제가 어떻게 전환되는지 포착되지 않았고, 타임라인 정보도 없었다. 이를 해결하기 위해 Gemini Summarize & Analyze 노드의 프롬프트를 4단계 구조로 다시 설계했다.
출력은 다음 순서로 고정된다.
- Key topics: 핵심 주제와 그 근거
- Topic shift map: 주제 전환 시점과 전환 신호
- Detailed timeline summary: 구간별 상세 분석
- Next steps: 후속 검토 항목
프롬프트 끝에는 규칙을 명시했다. 근거 없는 추측 금지, 불확실하면 불확실하다고 명시, 불필요한 서론과 면책 문구 금지가 핵심이다. 생성 설정은 temperature를 0.55로, maxOutputTokens를 3200으로 조정했다. 창의성과 일관성의 균형을 맞추면서 출력 길이를 충분히 확보하기 위해서다.
모델 동적 선택
처음에는 모델명을 워크플로우 JSON에 직접 하드코딩했다. 모델을 바꾸려면 파일을 열고 URL 문자열을 수동으로 수정한 뒤 재-import해야 했다. 쿼터 문제로 모델을 자주 전환해야 하는 상황에서는 번거로운 방식이었다.
해결 방법은 단순하다. URL에서 모델명 부분을 환경 변수로 교체했다.
"url": "=https://generativelanguage.googleapis.com/v1beta/models/{{ $env.GEMINI_MODEL || 'gemma-3-1b-it' }}:generateContent?key={{ $env.GOOGLE_GEMINI_API_KEY }}"
이제 .env의 GEMINI_MODEL 값만 바꾸고 docker compose up -d를 실행하면 된다. 워크플로우 JSON은 건드리지 않아도 된다. .env에는 현재 사용 가능한 모델 목록을 주석으로 달아두었다. 쿼터 상태에 따라 골라 쓰는 용도다.
Markdown 파일 자동 저장
워크플로우가 실행되어도 결과가 어디에도 남지 않으면 의미가 없다. Response Object 노드 뒤에 Save Markdown File 노드를 추가했다. Execute Command 노드로 구성되어 있으며, save-summary-md.sh 스크립트를 호출해 결과를 파일로 저장한다.
초기에는 Python으로 작성했다가 실패했다. n8n Docker 이미지에 Python이 설치되어 있지 않기 때문이다. Node.js는 이미지에 기본으로 포함되어 있어서 언어를 전환했다. 저장 경로는 /workspace/summaries이며, 파일명은 실행 시각과 영상 제목을 기반으로 자동 생성된다. 형식은 20260216-123039-slugified-title.md다.
ls -lt ~/Desktop/n8n/summaries | head
실행 후 이 명령어로 파일이 생성되었는지 확인하면 된다.
오답 노트 / 공부 노트
- Q. Dockerfile, docker-compose.yml을 만든 이유
- A.
- Dockerfile
- 언제 생김: “기본 이미지에 커스텀 노드까지 포함한 내 n8n 이미지 만들고 싶다” 할 때
- 왜 씀: 컨테이너를 다시 만들어도 같은 환경/노드가 유지되게
- docker-compose.yml
- 언제 생김: 컨테이너 실행 옵션(포트, env, 볼륨) 관리하고 싶을 때
- 왜 씀: docker compose up -d 한 줄로 실행/재시작/설정관리
-
curl 구조
curl은 터미널에서 HTTP 요청을 직접 보낼 수 있는 도구다. 브라우저가 하는 일을 명령어로 대신한다고 보면 된다. 브라우저 주소창에 URL을 입력하면 서버에 요청이 가고 응답이 돌아오는데, curl은 그 과정을 터미널에서 텍스트로 처리한다.
이 워크플로우에서 curl을 쓰는 이유는 webhook을 직접 테스트하기 위해서다. n8n은 외부에서 POST 요청이 들어와야 워크플로우가 시작되는데, 브라우저는 기본적으로 GET 요청만 보낼 수 있다. curl은 요청 방식을 직접 지정할 수 있어서 이 상황에 적합하다.
curl -X POST "http://localhost:5678/webhook-test/ytube" \ -H "Content-Type: application/json" \ -d '{"youtubeUrl":"https://www.youtube.com/watch?v=6zXcw8lf_zo"}'이 명령어는 세 가지 정보를 서버에 전달한다.
첫째,
-X POST는 요청 방식을 지정한다. HTTP 요청에는 여러 종류가 있다. GET은 데이터를 읽을 때, POST는 데이터를 보낼 때 사용한다. n8n webhook은 데이터를 받아야 작동하므로 POST를 명시해야 한다. 이 옵션을 생략하면 curl은 기본값인 GET 요청을 보내고, webhook은 반응하지 않는다.둘째,
-H "Content-Type: application/json"은 서버에게 “내가 보내는 데이터는 JSON 형식"이라고 알려주는 헤더다. 헤더는 요청 본문과 별개로, 이 요청이 어떤 성격인지를 설명하는 메타 정보다. 이 헤더가 없으면 서버는 데이터 형식을 모른 채 받게 되어 파싱에 실패할 수 있다.셋째,
-d는 실제로 보낼 데이터를 지정한다. 여기서는youtubeUrl이라는 키에 영상 URL을 값으로 담은 JSON 객체를 넣는다. 데이터를 작은따옴표로 감싸는 이유는 JSON 내부에 큰따옴표가 이미 사용되고 있기 때문이다. 큰따옴표로 감싸면 충돌이 생긴다.세 옵션의 관계를 정리하면 이렇다.
-X POST는 어떤 방식으로 보낼지,-H는 데이터가 어떤 형식인지,-d는 실제 내용이 무엇인지를 각각 담당한다. 셋 중 하나라도 빠지거나 잘못되면 워크플로우가 트리거되지 않거나 요청 파싱에 실패한다.
이렇게 생성한 md는 개인적인 공부 또는 시간 아끼기 목적으로만 사용하자! 저작권은 소중하다.