무색
기술블로그
에세이
연구
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

contact@museck.com

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관·연락처
INCHEON, KR
n8n 동영상 자막 아키텍처 키 비주얼
n8n 자동화편
2026. 1. 21.

15개 노드에서 2개로: n8n 동영상 자막 서비스의 아키텍처 결정

n8narchitecturetraefikkubernetesASR

이전 글에서 n8n으로 PDF 번역 서비스를 만들면서 OOM과 씨름했던 이야기를 했다. 이번에는 동영상 자막 생성 서비스를 만들 차례였는데, 같은 실수를 또 하게 될 줄은 몰랐다.

15개 노드로 시작한 자막 서비스

동영상 자막 생성 서비스의 백엔드는 이미 있었다. GPU 서버에서 돌아가는 ASR 파이프라인이 오디오 추출부터 화자분리, 텍스트 교정, SRT 생성까지 전부 처리한다. 필요한 건 사용자가 동영상을 올리고 결과를 받을 수 있는 웹 인터페이스뿐이었다.

PDF 번역 서비스를 n8n으로 만들었으니 이것도 같은 패턴으로 가면 되겠다 싶었다. 파일 업로드 받고, 비밀번호 검증하고, GPU 서버에 자막 요청 보내고, 상태 폴링하고, 결과 다운로드까지. 노드 15개짜리 워크플로가 완성됐다.

OOM, 다시 만나다

테스트로 300MB짜리 동영상을 올리자마자 n8n 파드가 죽었다. PDF 때와 같은 문제다. n8n이 K8s에서 1Gi 메모리로 돌아가고 있는데 동영상 파일을 Base64로 인코딩하면 크기가 2.3배로 불어난다. multipart로 포워딩해도 마찬가지. 수백MB 파일을 1Gi 메모리 안에서 중계하는 건 애초에 불가능한 구조였다.

PDF 때는 파일 크기가 수십MB 수준이라 메모리 최적화로 버텼지만 동영상은 그 선을 넘어섰다. n8n이 파일을 만지게 두면 안 된다는 걸 깨달았다.

2개 노드로의 피벗

발상을 뒤집었다. n8n이 파일을 중계할 필요가 없다면? 브라우저가 GPU 서버와 직접 통신하면 된다. n8n은 그 브라우저 페이지만 내려주면 끝이다.

15개 노드가 2개로 줄었다. Webhook GET 요청을 받으면 HTML 페이지를 응답하는 게 전부다. 파일 업로드, 자막 요청, 상태 폴링, 결과 다운로드는 전부 HTML 안의 JavaScript가 GPU 서버 API를 직접 호출한다.

Traefik으로 경로 분리

한 가지 문제가 남았다. 브라우저는 asr.museck.com 하나만 알고 있는데 n8n과 GPU 서버라는 두 개의 백엔드가 있다. Traefik IngressRoute에서 PathPrefix로 해결했다.

# /webhook/* -> n8n (HTML 페이지만)
# /api/*     -> GPU 서버 (strip-api-prefix Middleware)
# /          -> /webhook/video-transcribe-public (리다이렉트)

/webhook/* 경로는 n8n으로, /api/* 경로는 GPU 서버로 보낸다. GPU 서버 쪽에는 strip-api-prefix 미들웨어를 달아서 /api prefix를 벗긴 뒤 전달한다. GPU 서버는 K8s 클러스터 밖에 있지만 headless Service와 Endpoints로 K8s 내부 서비스처럼 추상화해놨기 때문에 Traefik에서 바로 라우팅할 수 있다.

XHR 업로드 진행률

브라우저에서 수백MB 파일을 직접 올리다 보니 업로드 진행률이 필수가 됐다. fetch API로는 업로드 progress 이벤트를 받을 수 없어서 XMLHttpRequest를 썼다.

const apiBase = '/api';  // Traefik이 GPU 서버로 라우팅
function uploadForm(url, fd) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', url);
    xhr.upload.onprogress = e => {
      if (e.lengthComputable) {
        const pct = Math.round(e.loaded / e.total * 100);
        updateProgress('업로드 중... ' + pct + '%');
      }
    };
    xhr.send(fd);
  });
}

sandbox iframe이라는 복병

리팩터링 끝나고 배포했더니 API 호출이 전부 CORS 에러로 막혔다. n8n v1.103.0부터 XSS 방지를 위해 Webhook HTML 응답을 sandbox iframe으로 감싸는데, 이러면 Origin이 null이 된다. GPU 서버에 Access-Control-Allow-Origin: *를 설정하거나, N8N_INSECURE_DISABLE_WEBHOOK_IFRAME_SANDBOX=true 환경변수로 sandbox를 비활성화해야 한다.

n8n 문서 어디에도 나오지 않는 동작이라 디버깅에 시간을 꽤 썼다. iframe 안에서 DevTools 열어보고 나서야 원인을 알았다.

도구의 역할을 최소화하는 것도 설계다

n8n은 노코드 도구다. 뭐든 할 수 있을 것 같은 느낌을 준다. 실제로 PDF 번역 서비스는 n8n이 API 프록시 역할까지 잘 해냈다. 그래서 동영상도 같은 패턴으로 가려 했는데 파일 크기라는 물리적 한계 앞에서 무너졌다.

돌이켜보면 n8n에서 파일을 만지게 한 것 자체가 설계 실수였다. 메모리 1Gi짜리 컨테이너에 수백MB 파일 프록시를 시키면 안 된다. 노코드 도구든 마이크로서비스든 각 컴포넌트가 감당할 수 있는 범위를 먼저 따져야 한다. 메모리 한도, 파일 크기, 처리 시간. 이 세 가지가 역할 경계를 정하는 기준이 됐다.

15개 노드에서 2개로 줄인 건 기능을 줄인 게 아니다. 책임을 적절히 분리한 것이다. n8n은 HTML만 내려주고 무거운 일은 GPU 서버가 직접 받는다. 오히려 더 깔끔해졌다.

자주 묻는 질문

n8n으로 대용량 파일을 중계하면 OOM이 발생하는 이유는?
n8n은 워크플로우 데이터를 전부 메모리에 올리고, Base64 인코딩 시 원본의 2.3배로 불어납니다. K8s 1Gi 메모리 제한에서 수백MB 파일을 중계하면 한계를 넘깁니다.
n8n 워크플로우 노드를 줄이면서 기능은 유지하는 방법은?
n8n은 HTML 페이지만 서빙하고, 브라우저 JavaScript가 GPU 서버 API를 직접 호출하는 구조로 전환하면 됩니다. Traefik PathPrefix로 경로를 분리해 하나의 도메인에서 두 백엔드를 사용합니다.
n8n sandbox iframe에서 CORS 에러가 나는 이유는?
n8n v1.103.0부터 XSS 방지를 위해 Webhook HTML 응답을 sandbox iframe으로 감쌉니다. Origin이 null이 되어 CORS가 차단되며, GPU 서버에 Access-Control-Allow-Origin: *을 설정하거나 sandbox를 비활성화해야 합니다.
n8n 자동화편(1/2)
Next

n8n으로 PDF 논문 번역 서비스 만들기