Seedance + n8n: 제한된 비동기 폴링 (Bounded Async Polling) 워크플로우 구축하기
요약
n8n을 사용하여 비동기 비디오 생성 API를 위한 '제한된 비동기 폴링(Bounded Async Polling)' 워크플로우를 구축하는 방법을 설명합니다. 무한 루프를 방지하기 위해 폴링 횟수와 대기 시간을 명시적으로 설정하는 안정적인 자동화 패턴을 다룹니다.
핵심 포인트
- 비동기 API 작업 시 task_id와 루프 상태를 보존하는 것이 중요함
- 무한 루프 방지를 위해 폴링 횟수와 타임아웃 등 상한선(Upper Bound) 설정 필수
- n8n의 HTTP Request 노드 사용 시 병렬 상태 분기를 통해 데이터 유실 방지
- 보안을 위해 API 키는 n8n Credentials를 통해 관리 권장
이 기사는 원래 XINGOD.ME에 게시되었습니다. 이 DEV 에디션은 자동화 빌더(automation builders)를 위해 설명을 조정하면서도 구현 내용을 완전하게 유지합니다.
비디오 생성 API는 설계상 비동기적 (asynchronous)입니다. 첫 번째 요청은 완성된 MP4를 반환하지 않습니다. 대신 제공자가 작업을 대기열에 추가하고, 처리하고, 최종적으로 완료하거나 실패할 때까지 보존해야 하는 작업 식별자 (task identifier)를 반환합니다.
이는 자동화 문제를 변화시킵니다. 프로덕션 환경에 적합한 워크플로우는 단순히 "완료될 때까지 폴링(poll)하기" 이상의 것을 수행해야 합니다. 반드시 다음과 같은 기능을 갖춰야 합니다:
- 정확히 한 번만 제출 (submit);
- 모든 노드에 걸쳐
task_id및 루프 설정 (loop configuration) 보존; - 상태 요청 사이에 대기;
- 성공 또는 실패 시 즉시 중단;
- 엄격한 폴링 제한 (hard polling limit) 강제;
- 최종 타임아웃 (timeout)을 관찰 가능하게 만듦.
이 튜토리얼에서 기본 폴링 예산은 5초 × 120회 폴링, 즉 약 10분입니다. 두 값 모두 설정 가능하지만, 루프는 항상 제한된 (bounded) 상태를 유지합니다.
상태 머신 (The state machine)
높은 수준에서 워크플로우는 다음과 같습니다:
submit
→ task_id 및 루프 상태 (loop state) 저장
→ wait
...
테스트된 n8n 워크플로우는 다음과 같은 구체적인 노드들을 사용합니다:
manualTrigger
→ setFields
→ submitTask
...
pollTask와 carryState 주변의 분기 (branching)가 핵심적인 부분입니다. n8n의 HTTP Request 노드는 현재 아이템을 HTTP 응답으로 교체합니다. 병렬 상태 분기 (parallel state branch)가 없다면, 응답이 다음 반복 (iteration)에 필요한 루프 카운터와 설정을 지워버릴 수 있습니다.
1. 요청 및 폴링 예산 설정
setFields라는 이름의 Edit Fields 노드로 시작합니다:
model = Doubao-Seedance-1.5-pro
prompt = A timelapse of a flower blooming at sunrise
size = 1280x720
...
intervalSeconds와 maxPolls는 숨겨진 상수가 아니라 설정 값입니다. 다른 워크플로우는 더 짧은 간격이나 다른 마감 기한이 필요할 수 있지만, 여전히 명시적인 상한선 (upper bound)을 가져야 합니다.
인증 정보는 재사용 가능한 Header Auth 자격 증명(Credential) 등을 사용하여 n8n Credentials에 저장하십시오. 실제 API 키를 내보낸 워크플로우 JSON, 스크린샷 또는 코드 예제에 절대 붙여넣지 마십시오.
2. 한 번 제출하고 task_id 정규화하기
submitTask를 HTTP Request 노드로 구성합니다:
POST https://vancine.com/v1/video/generations
Content-Type: application/json
JSON 바디(body)에 구성된 모델(model), 프롬프트(prompt), 크기(size)를 전송합니다. 지원되는 정확한 모델과 요청 필드는 변경될 수 있으므로, 워크플로우를 프로덕션(production) 환경에서 사용하기 전에 최신 문서를 확인하십시오.
제출 직후, saveTaskId가 루프 엔벨로프(loop envelope)를 생성합니다:
task_id = {{ $json.task_id || $json.id || ($json.data && $json.data.task_id) || ($json.data && $json.data.id) }}
base_url = {{ $('setFields').first().json.base_url || 'https://vancine.com' }}
intervalSeconds = {{ $('setFields').first().json.intervalSeconds || 5 }}
...
제출 응답은 영수증과 같습니다. task_id는 이후의 모든 상태 요청(status request), 에러 메시지 및 감사 기록(audit record)을 위한 지속적인 전달 값(durable hand-off value)입니다.
3. 대기 후 응답과 상태 분기하기
saveTaskId를 waitInterval이라는 이름의 Wait 노드에 연결합니다. 대기 시간은 다음과 같습니다:
{{ $json.intervalSeconds || 5 }}
대기 후, 동일한 아이템을 두 개의 노드로 보냅니다:
pollTask는 상태 요청(status request)을 수행합니다.carryState는 다음 반복(iteration)에 필요한 상태(state)를 유지합니다.
pollTask는 다음을 호출합니다:
GET {{$json.base_url}}/v1/video/generations/{{$json.task_id}}
carryState는 다음을 출력합니다:
task_id = {{ $json.task_id }}
poll_index = {{ $json.poll_index || 0 }}
maxPolls = {{ $json.maxPolls || 120 }}
...
두 분기 모두 append 모드로 구성된 Merge 노드로 입력됩니다. 결과 아이템 리스트에는 API 응답과 유지된 루프 상태(loop state)가 포함됩니다.
4. 로컬에서 병합하고 정확히 한 번만 증가시키기
mergeState Code 노드는 네트워크 요청을 수행하지 않습니다. 이 노드는 추가된 두 아이템을 식별하여 병합하고, 유지된 아이템으로부터 카운터(counter)를 정확히 한 번 증가시킵니다:
const items = $input.all();
const response =
...
중요한 불변량(invariant)은 다음과 같습니다:
const pollIndex = (carried.json.poll_index || 0) + 1;
카운터는 초기 상태 노드(initial state node)가 아니라, 가장 최근의 전달된 상태(carried state)로부터 가져옵니다. 이를 통해 워크플로우가 maxPolls에 도달할 수 있게 하며, 종료되지 않는 작업(non-terminal task)이 무한 루프에 빠지지 않도록 보장합니다.
5. 완료, 실패, 타임아웃을 별도로 라우팅하기
종료 체크(terminal checks)를 순서대로 실행합니다.
ifCompleted
completed와 기존 값인 SUCCESS를 성공으로 처리합니다:
$json.status === 'completed' ||
$json.status === 'SUCCESS' ||
($json.data &&
...
True(참) 분기는 outputResult로 이동하며, 여기서 result_url을 반환하거나 영구 저장할 수 있습니다.
ifFailed
ifCompleted의 False(거짓) 분기는 ifFailed에 도달합니다. failed와 FAILURE를 종료 에러(terminal errors)로 처리합니다:
$json.status === 'failed' ||
$json.status === 'FAILURE' ||
($json.data &&
...
True 분기는 Stop And Error 노드로 보냅니다. 생성 실패는 비즈니스 측면의 종료 상태이며, 폴링(polling)을 계속해야 할 이유가 아닙니다.
ifTimeout
종료되지 않았고 실패하지도 않은 아이템만이 ifTimeout에 도달합니다:
($json.poll_index || 0) >= ($json.maxPolls || 120)
True 분기는 타임아웃 에러와 함께 중단됩니다. False 분기는 continuePolling에 도달합니다.
continuePolling은 최신(latest) 값들을 다시 루프(loop)로 복사합니다:
task_id
poll_index
maxPolls
...
그 후 waitInterval에 다시 연결됩니다. 기본 설정을 사용할 경우, 120번째의 종료되지 않은 응답은 121번째 반복을 시작하는 대신 타임아웃 분기로 진입하게 됩니다.
6. 전송 재시도(transport retries)를 작업 폴링(task polling)과 분리하기
여전히 processing 상태인 작업은 일시적인 429 또는 5xx 응답을 받은 HTTP 요청과는 다릅니다.
두 개의 별도 예산(budget)을 사용하세요:
- 일시적인 전송 실패(transient transport failures)를 위한 백오프(backoff)가 포함된 작고 명시적인 재시도 정책
- 유효한 작업 상태 응답을 위한
poll_index / maxPolls예산
이러한 분리는 네트워크 노이즈(network noise)가 비즈니스 타임아웃(business timeout)을 조용히 변경하는 것을 방지합니다. 또한 다음 경로들을 독립적으로 테스트하세요:
- 완료 (completed);
- 실패 (failed);
- 타임아웃 (timeout);
task_id누락;429및5xx에러;- 잘못된 형식(malformed) 또는 알 수 없는 상태 값.
task_id, poll_index, 마지막 상태(last status), 그리고 경과 시간(elapsed time)을 로그로 남기세요. Authorization 헤더는 절대 로그에 남기지 마십시오.
테스트된 워크플로우 시도하기
공개된 Seedance API Starter Kit에는 테스트된 n8n 워크플로우와 함께 Node.js, Python, cURL, Postman 예제가 포함되어 있습니다.
추가 리소스:
Vancine으로 시작하기
새로운 Vancine 계정은 신용카드 등록 없이 $1의 무료 크레딧을 받습니다. 여기에서 계정을 생성할 수 있습니다.
크레딧 사용량은 선택한 모델, 입력값, 지속 시간 및 현재 가격에 따라 달라집니다. $1의 크레딧이 특정 비디오 생성이 해당 금액 내에서 완료될 것을 보장하지는 않습니다.
제한된 폴링(Bounded polling)은 운영 측면에서 큰 이득을 주는 작은 아키텍처적 선택입니다. 모든 실행은 검사 가능한 상태를 가지며, 모든 최종 결과는 명시적인 분기(branch)를 가지며, 모든 루프는 알려진 실패 예산(failure budget)을 가집니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기