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

무색

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

연락처

contact@museck.com

사업자 정보

상호: 무색

대표: 배성재

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

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

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관·연락처
INCHEON, KR
n8n PDF 번역 — pencil-sketch 스타일 키 비주얼
n8n 자동화편
2026. 1. 20.

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

n8nwebhookpdf-translationkubernetes

팀 내부에서 쓰던 PDF 논문 번역 도구가 있었다. GPU 서버에서 TranslateGemma를 돌려서 학술 논문 PDF를 한글로 번역하는 건데, 꽤 잘 됐다. 문제는 이걸 외부에도 열어주려고 하면서 시작됐다.

n8n으로 빠르게 프로토타입을 만들었는데, 막상 실사용에 넣으니 Form Trigger의 한계에 부딪히고, 대용량 PDF에서 OOM이 터지고, 에러 전파 문제까지 줄줄이 나왔다. 노코드 도구로 프로덕션 서비스를 만들 때 만나는 전형적인 삽질기를 공유한다.

전체 구조

브라우저에서 PDF를 업로드하면 n8n Webhook이 받아서 GPU 서버로 번역 요청을 보낸다. 번역 결과는 Obsidian Vault에 자동 저장되고, 사용자는 브라우저에서 진행률을 확인하며 기다렸다가 완료되면 다운로드한다.

Form Trigger가 안 되는 이유

처음엔 n8n의 Form Trigger를 썼다. 파일 업로드 폼을 자동으로 만들어주니까 편했다. 그런데 한 가지 치명적인 제약이 있었는데, Respond to Webhook 노드를 지원하지 않는다는 거다.

무슨 말이냐면, Form Trigger는 워크플로가 끝날 때 자동으로 응답을 보내는 구조라서 중간에 커스텀 HTTP 응답을 줄 수가 없다. 번역은 수 분이 걸리는 작업인데 브라우저가 그동안 마냥 기다리고 있어야 한다. 타임아웃도 문제고, 사용자 경험도 영 아니다.

결국 Webhook 트리거로 전환했다. 구조가 확 달라진다.

  • PDF 업로드 시 taskId를 즉시 반환
  • 클라이언트는 1초 간격으로 진행률 폴링
  • 번역 완료 시 GET 엔드포인트로 결과 다운로드

비동기로 바꾸니 오히려 더 유연해졌다. 프로그레스 바도 붙일 수 있고, 여러 건을 동시에 처리하는 것도 자연스럽게 됐다.

프로그레스 바 구현

사용자가 "지금 어디쯤이지?"를 알 수 있어야 했다. zotero-server가 /status API를 이미 제공하고 있었기 때문에 n8n에서 이걸 프록시해주면 됐다.

GET /pdf-translate-progress?taskId=xxx 엔드포인트를 하나 추가하고, n8n의 HTTP Request 노드로 zotero-server의 status를 그대로 전달한다. 클라이언트에서는 이렇게 폴링한다.

async function pollProgress(taskId) {
  while(true) {
    await new Promise(r => setTimeout(r, 1000));
    const r = await fetch(progressUrl + '?taskId=' + taskId);
    const d = await r.json();
    if (d.status === 'success') {
      showDownloadButtons(taskId, d.fileList);
      return;
    }
    const pct = Math.round(d.progress || 0);
    updateProgressBar(d.stage, pct);
  }
}

"용어 추출 중 (20%)" 같은 단계별 진행률이 표시된다. 사소해 보이지만 이게 있고 없고의 체감 차이가 크다.

OOM 크래시와 메모리의 벽

여기서 진짜 삽질이 시작됐다. 작은 PDF는 잘 되는데 30MB 넘어가는 논문을 올리면 n8n 파드가 죽는다. K8s에서 메모리 리밋을 1Gi로 잡아놨는데 그걸 훌쩍 넘어버리는 거다.

원인을 추적해보니 prepareBinaryData라는 n8n 내부 함수가 문제였다. 이 함수는 바이너리 데이터를 n8n의 워크플로 데이터 구조에 넣기 위해 버퍼를 한 번 더 복사한다.

50MB PDF 기준으로 계산하면 이렇다.

  • 원본 PDF: 50MB
  • Base64 인코딩: ~115MB (2.3배)
  • prepareBinaryData 버퍼 복사: ~115MB 추가
  • 합계: ~230MB, 원본의 4.6배

1Gi 메모리에서 이건 감당이 안 된다. n8n 런타임 자체가 먹는 메모리도 있으니까.

해결은 간단했다. prepareBinaryData를 거치지 않고 Code 노드에서 fs.writeFileSync로 디스크에 직접 쓴다.

const fs = require('fs');
const buffer = Buffer.from(base64Content, 'base64');
fs.writeFileSync(
  '/shared/obsidian-vault/zotero-zotmoov/public/' + fileName,
  buffer
);

Base64 디코딩만 메모리에서 하고 파일은 바로 NFS로 내리니까 메모리 사용량이 절반 이하로 줄었다.

병렬 노드의 함정

Vault에 원본 PDF를 저장하는 것과 번역 요청을 보내는 것은 독립적인 작업이다. 그래서 병렬로 연결했는데, Vault 저장이 실패하면 번역 요청까지 같이 죽어버렸다.

n8n에서 병렬로 연결된 노드 중 하나가 에러를 던지면 전체 워크플로가 중단된다. continueOnFail: true를 설정해야 한쪽이 실패해도 나머지가 계속 진행된다. PDF 번역이 본 작업이니 Vault 저장 실패 때문에 멈추면 안 되는 거다.

워크플로를 코드로 관리하기

n8n 워크플로를 JSON으로 내보내서 Git에 넣고, deploy.sh로 배포한다. 그런데 n8n API가 좀 까다로워서, 내보낸 JSON을 그대로 PUT하면 400 에러가 난다. id, active, tags 같은 read-only 필드가 body에 들어있으면 거부한다.

BODY=$(python3 -c "
import json, sys
d = json.load(open('\$WORKFLOW_FILE'))
for k in ('id', 'tags', 'active', 'createdAt', 'updatedAt',
          'versionId', 'staticData', 'meta', 'pinData',
          'activeVersionId', 'versionCounter', 'triggerCount',
          'isArchived', 'activeVersion', 'description'):
    d.pop(k, None)
json.dump(d, sys.stdout, ensure_ascii=False)
")

Python 원라이너로 필드를 제거한 뒤 API로 쏜다. 생성(POST)이든 업데이트(PUT)든 동일한 로직을 탄다.

HTTP Request 프록시 팁

n8n에서 백엔드 API를 프록시할 때 한 가지 주의점이 있다. HTTP Request 노드는 기본적으로 4xx/5xx 응답을 에러로 취급한다. 번역 서버가 404(작업 없음)나 500을 반환하면 워크플로가 멈춰버린다.

이걸 막으려면 HTTP Request 노드에서 neverError: true를 설정한다. 그러면 HTTP 상태와 무관하게 응답을 데이터로 넘겨준다. 에러 핸들링은 다음 노드에서 하면 된다.

정리

n8n으로 PDF 번역 서비스를 만들면서 배운 것을 요약하면 이렇다.

  • Form Trigger는 커스텀 응답이 안 된다. 비동기 작업은 Webhook + Respond to Webhook 조합을 써야 한다
  • n8n의 prepareBinaryData는 메모리를 많이 먹는다. 대용량 파일은 fs.writeFileSync로 직접 처리하자
  • 병렬 노드는 continueOnFail 설정이 필수다
  • 워크플로 JSON을 Git으로 관리할 때 read-only 필드 제거를 잊지 말자

노코드 도구가 프로토타이핑에 강력한 건 맞다. 하지만 프로덕션에 올리려면 메모리 관리, 에러 핸들링, 비동기 처리 같은 전통적인 엔지니어링 문제를 똑같이 풀어야 한다. 도구가 추상화해주는 부분과 직접 손대야 하는 부분의 경계를 아는 게 핵심이다.

자주 묻는 질문

n8n Form Trigger와 Webhook Trigger의 차이는?
Form Trigger는 워크플로 끝에 자동 응답하므로 중간에 커스텀 HTTP 응답이 불가합니다. 비동기 작업은 Webhook + Respond to Webhook 조합을 써야 합니다.
n8n에서 대용량 파일 처리 시 OOM이 발생하면?
n8n의 prepareBinaryData는 원본의 4.6배 메모리를 사용합니다. Code 노드에서 fs.writeFileSync로 디스크에 직접 쓰면 메모리를 절반 이하로 줄일 수 있습니다.
n8n 병렬 노드에서 한쪽 실패 시 전체가 중단되나요?
네, 기본적으로 병렬 노드 중 하나가 에러를 던지면 전체 워크플로가 중단됩니다. continueOnFail: true를 설정하면 나머지가 계속 진행됩니다.
n8n 자동화편(2/2)
Prev

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