프로젝트로 돌아가기
ComfyUI API 파이프라인 구축 가이드15 min

worker.ps1 전체 코드 작성: ComfyUI API 호출부터 결과 저장까지

worker.ps1의 전체 코드를 한 줄씩 설명합니다. job.json 읽기, 워크플로우에 프롬프트 주입, ComfyUI API POST 요청, 생성 완료 대기(polling), 이미지 저장, result.json 작성까지 빠짐없이 다룹니다.

worker.ps1이 하는 일

worker.ps1은 이미지 생성 파이프라인의 핵심입니다. 이 스크립트 하나가 처음부터 끝까지 전부 처리합니다.

  1. job.json을 읽어서 프롬프트를 꺼냅니다.
  2. comfy_workflow.json을 읽어서 프롬프트를 교체합니다.
  3. ComfyUI API에 POST 요청으로 이미지 생성을 시작합니다.
  4. 생성이 완료될 때까지 폴링합니다 (최대 600초).
  5. 완료되면 이미지를 output 폴더에 저장하고 result.json을 씁니다.

전체 코드

$ErrorActionPreference = "Stop"

$BASE = "D:\016_CardNew"
$JOB_PATH = Join-Path $BASE "iojob.json"
$WF_PATH = Join-Path $BASE "iocomfy_workflow.json"
$OUT_DIR = Join-Path $BASE "output"
$RES_PATH = Join-Path $BASE "io
esult.json"

$COMFY = "http://127.0.0.1:8188"
$POS_NODE_ID = "6"
$NEG_NODE_ID = "7"

if (!(Test-Path $JOB_PATH)) {
    '{"status":"wait","message":"job.json not found"}' | Set-Content -Encoding UTF8 $RES_PATH
    exit 0
}
if (!(Test-Path $WF_PATH)) {
    '{"status":"error","message":"workflow not found"}' | Set-Content -Encoding UTF8 $RES_PATH
    exit 1
}

$job = Get-Content $JOB_PATH -Raw | ConvertFrom-Json
$wf = Get-Content $WF_PATH -Raw | ConvertFrom-Json

$wf.$POS_NODE_ID.inputs.text = [string]$job.image_prompt
if ($job.negative_prompt) {
    $wf.$NEG_NODE_ID.inputs.text = [string]$job.negative_prompt
} else {
    $wf.$NEG_NODE_ID.inputs.text = "blurry, low quality, watermark"
}

$body = @{ prompt = $wf } | ConvertTo-Json -Depth 100 -Compress
$q = $null
for ($try=1; $try -le 3; $try++) {
    try {
        $q = Invoke-RestMethod -Method Post -Uri "$COMFY/prompt" -ContentType "application/json" -Body $body
        break
    } catch {
        if ($try -eq 3) { throw }
        Start-Sleep -Seconds (2 * $try)
    }
}
$promptId = [string]$q.prompt_id
Write-Host "[INFO] promptId=$promptId"

$imageMeta = $null
for ($i=0; $i -lt 300; $i++) {
    Start-Sleep -Seconds 2
    $h = Invoke-RestMethod -Method Get -Uri "$COMFY/history/$promptId"
    if ($h.PSObject.Properties.Name -contains $promptId) {
        $outputs = $h.$promptId.outputs
        foreach ($p in $outputs.PSObject.Properties) {
            if ($p.Value.images -and $p.Value.images.Count -gt 0) {
                $imageMeta = $p.Value.images[0]
                break
            }
        }
    }
    if ($imageMeta) { break }
}

if (-not $imageMeta) {
    '{"status":"error","message":"timeout"}' | Set-Content -Encoding UTF8 $RES_PATH
    exit 1
}

$fn = [uri]::EscapeDataString([string]$imageMeta.filename)
$sf = [uri]::EscapeDataString([string]$imageMeta.subfolder)
$tp = [uri]::EscapeDataString([string]$imageMeta.type)
$viewUrl = "$COMFY/view?filename=$fn&subfolder=$sf&type=$tp"

if (!(Test-Path $OUT_DIR)) { New-Item -ItemType Directory -Path $OUT_DIR | Out-Null }
$ts = Get-Date -Format "yyyyMMdd_HHmmss"
$outPath = Join-Path $OUT_DIR ("result_" + $ts + ".jpg")

Invoke-WebRequest -Uri $viewUrl -OutFile $outPath -UseBasicParsing

@{
    status = "ok"
    prompt_id = $promptId
    output_image = $outPath
} | ConvertTo-Json -Depth 5 | Set-Content -Encoding UTF8 $RES_PATH

Write-Host "[OK] saved: $outPath"

블록별 분석

블록 1: $ErrorActionPreference = "Stop"

$ErrorActionPreference = "Stop"

PowerShell에서 기본 에러 처리는 에러가 나도 계속 실행되는 것입니다. 예를 들어 파일을 못 찾아도 경고만 출력하고 다음 줄로 넘어갑니다. "Stop"으로 설정하면 에러가 나는 즉시 스크립트가 중단됩니다. 이미지 생성 파이프라인에서는 중간에 실패했는데 계속 진행하면 더 큰 문제가 생길 수 있습니다. 명시적으로 Stop을 설정하는 것이 안전합니다.

블록 2: 경로 변수와 노드 ID

$BASE = "D:\016_CardNew"
$JOB_PATH = Join-Path $BASE "iojob.json"
$WF_PATH = Join-Path $BASE "iocomfy_workflow.json"
$OUT_DIR = Join-Path $BASE "output"
$RES_PATH = Join-Path $BASE "io
esult.json"

$COMFY = "http://127.0.0.1:8188"
$POS_NODE_ID = "6"
$NEG_NODE_ID = "7"

경로를 변수로 묶는 이유는 나중에 폴더를 옮길 때 $BASE만 바꾸면 되기 때문입니다.

Join-Path는 OS에 맞는 경로 구분자를 자동으로 씁니다. Windows에서는 백슬래시를 씁니다. 문자열 연결보다 안전합니다.

$POS_NODE_ID = "6"$NEG_NODE_ID = "7"은 comfy_workflow.json에서 확인한 노드 ID입니다. 워크플로우가 바뀌어서 노드 ID가 달라지면 이 두 줄만 바꾸면 됩니다.

블록 3: 파일 존재 확인

if (!(Test-Path $JOB_PATH)) {
    '{"status":"wait","message":"job.json not found"}' | Set-Content -Encoding UTF8 $RES_PATH
    exit 0
}
if (!(Test-Path $WF_PATH)) {
    '{"status":"error","message":"workflow not found"}' | Set-Content -Encoding UTF8 $RES_PATH
    exit 1
}

job.json이 없으면 status: "wait"를 result.json에 쓰고 exit 0으로 정상 종료합니다. exit 0은 에러 없이 종료했다는 신호입니다. Linux에서 SSH로 이 스크립트를 실행했을 때 exit code가 0이면 성공으로 인식합니다. job.json이 아직 안 도착한 것은 오류 상황이 아니기 때문입니다.

comfy_workflow.json이 없으면 status: "error"exit 1입니다. 워크플로우 파일은 사전에 배치되어 있어야 합니다. 없다는 것은 설정 오류이므로 실제 에러입니다.

블록 4: JSON 파싱

$job = Get-Content $JOB_PATH -Raw | ConvertFrom-Json
$wf = Get-Content $WF_PATH -Raw | ConvertFrom-Json

-Raw 옵션이 중요합니다. 이것 없이 Get-Content를 쓰면 파일 내용을 줄 단위 배열로 읽습니다. -Raw를 쓰면 파일 전체를 하나의 문자열로 읽습니다. ConvertFrom-Json은 문자열을 받아야 하기 때문에 -Raw가 없으면 작동하지 않습니다.

ConvertFrom-Json이 성공하면 $job.image_prompt처럼 점으로 필드에 접근할 수 있습니다.

블록 5: 프롬프트 주입

$wf.$POS_NODE_ID.inputs.text = [string]$job.image_prompt
if ($job.negative_prompt) {
    $wf.$NEG_NODE_ID.inputs.text = [string]$job.negative_prompt
} else {
    $wf.$NEG_NODE_ID.inputs.text = "blurry, low quality, watermark"
}

$wf.$POS_NODE_ID에서 $POS_NODE_ID는 문자열 "6"입니다. 따라서 $wf."6".inputs.text와 동일합니다. PowerShell에서 객체 프로퍼티 이름을 변수로 참조하는 방법입니다.

[string] 캐스팅을 쓰는 이유가 있습니다. ConvertFrom-Json이 JSON 값을 PowerShell 타입으로 변환할 때, 값이 숫자처럼 보이면 Int 타입으로 해석할 수 있습니다. [string]으로 명시적으로 문자열로 변환해야 ComfyUI가 올바르게 받습니다.

negative_prompt가 없을 때 기본값 "blurry, low quality, watermark"를 씁니다. job.json에 negative_prompt 필드가 없거나 빈 문자열이면 이 기본값이 들어갑니다.

블록 6: API 호출 (3회 재시도)

$body = @{ prompt = $wf } | ConvertTo-Json -Depth 100 -Compress
$q = $null
for ($try=1; $try -le 3; $try++) {
    try {
        $q = Invoke-RestMethod -Method Post -Uri "$COMFY/prompt" -ContentType "application/json" -Body $body
        break
    } catch {
        if ($try -eq 3) { throw }
        Start-Sleep -Seconds (2 * $try)
    }
}
$promptId = [string]$q.prompt_id
Write-Host "[INFO] promptId=$promptId"

ConvertTo-Json -Depth 100의 Depth 값이 핵심입니다. 기본값은 2입니다. 워크플로우 JSON은 여러 겹으로 중첩되어 있습니다. Depth가 작으면 깊은 곳의 값이 잘리고 "System.Object"라는 문자열로 대체됩니다. ComfyUI가 이 값을 받으면 에러가 납니다. 100처럼 충분히 크게 설정합니다.

-Compress는 JSON에서 공백과 줄바꿈을 제거해서 크기를 줄입니다. HTTP Body를 최소화하는 것이 좋습니다.

재시도 로직은 3번 시도합니다. 실패 후 대기 시간이 2 * $try초입니다. 1번째 실패 후 2초, 2번째 실패 후 4초를 기다립니다. 이를 지수 백오프(exponential backoff)라고 합니다. 서버가 잠시 바쁠 때 바로 재시도하면 더 부하가 걸립니다. 조금 기다렸다가 재시도하는 것이 좋습니다. 3번 모두 실패하면 throw로 에러를 다시 던집니다.

성공하면 응답에서 prompt_id를 꺼냅니다. 이 UUID로 다음 단계에서 생성 결과를 조회합니다.

블록 7: 생성 완료 폴링 (최대 600초)

$imageMeta = $null
for ($i=0; $i -lt 300; $i++) {
    Start-Sleep -Seconds 2
    $h = Invoke-RestMethod -Method Get -Uri "$COMFY/history/$promptId"
    if ($h.PSObject.Properties.Name -contains $promptId) {
        $outputs = $h.$promptId.outputs
        foreach ($p in $outputs.PSObject.Properties) {
            if ($p.Value.images -and $p.Value.images.Count -gt 0) {
                $imageMeta = $p.Value.images[0]
                break
            }
        }
    }
    if ($imageMeta) { break }
}

300번 반복 x 2초 = 최대 600초(10분)를 기다립니다. z_image_turbo는 steps=15이므로 보통 10~30초 안에 끝납니다.

$h.PSObject.Properties.Name -contains $promptId 구문이 낯설게 보입니다. PowerShell에서 동적 프로퍼티 이름(변수에 담긴 문자열)이 객체에 있는지 확인하는 방법입니다. 일반적인 $h.$promptId는 프로퍼티가 없으면 null을 반환하는데, 이 방식으로 먼저 존재를 확인합니다.

$outputs.PSObject.Properties로 outputs 안의 모든 노드를 순회합니다. SaveImage 노드(여기서는 노드 "9")의 출력에 images 배열이 있으면 생성이 완료된 것입니다. 첫 번째 이미지의 메타데이터(filename, subfolder, type)를 $imageMeta에 담습니다.

타임아웃이 되면 status: "error", message: "timeout"을 result.json에 쓰고 exit 1로 종료합니다.

블록 8: URI 인코딩과 이미지 다운로드

$fn = [uri]::EscapeDataString([string]$imageMeta.filename)
$sf = [uri]::EscapeDataString([string]$imageMeta.subfolder)
$tp = [uri]::EscapeDataString([string]$imageMeta.type)
$viewUrl = "$COMFY/view?filename=$fn&subfolder=$sf&type=$tp"

if (!(Test-Path $OUT_DIR)) { New-Item -ItemType Directory -Path $OUT_DIR | Out-Null }
$ts = Get-Date -Format "yyyyMMdd_HHmmss"
$outPath = Join-Path $OUT_DIR ("result_" + $ts + ".jpg")

Invoke-WebRequest -Uri $viewUrl -OutFile $outPath -UseBasicParsing

EscapeDataString으로 파일명의 특수문자를 URL 안전 형태로 변환합니다. 이 프로젝트의 파일명에는 /가 포함됩니다(Z_Image_Turbo/27-02-2026/image_00001_.png). URL 쿼리 파라미터에 /를 그대로 넣으면 경로로 해석됩니다. %2F로 인코딩해야 합니다. EscapeDataString이 이것을 처리합니다.

Invoke-WebRequest/view 엔드포인트에서 이미지 바이너리를 받아서 파일로 저장합니다. -UseBasicParsing은 HTML 파싱 없이 바이너리를 그대로 받겠다는 옵션입니다. 이미지 파일을 받을 때 필요합니다.

타임스탬프 파일명(result_20260322_105758.jpg)으로 저장하므로 여러 번 실행해도 덮어씌워지지 않습니다.

블록 9: result.json 작성

@{
    status = "ok"
    prompt_id = $promptId
    output_image = $outPath
} | ConvertTo-Json -Depth 5 | Set-Content -Encoding UTF8 $RES_PATH

Write-Host "[OK] saved: $outPath"

Set-Content -Encoding UTF8로 저장합니다. PowerShell에서 기본 인코딩은 시스템 설정에 따라 다릅니다. 명시적으로 UTF8을 지정하면 Linux에서 읽을 때 한글이 깨지지 않습니다.

마지막 줄 Write-Host "[OK] saved: $outPath"는 SSH로 실행했을 때 터미널에 보이는 성공 메시지입니다.

실행 방법

PowerShell 터미널에서 직접 실행합니다.

powershell -NoProfile -ExecutionPolicy Bypass -File "D:\016_CardNewscriptsworker.ps1"

-NoProfile은 PowerShell 프로필 스크립트를 로드하지 않습니다. 불필요한 시간을 줄이고 환경 오염을 막습니다. -ExecutionPolicy Bypass는 스크립트 실행 권한을 임시로 허용합니다. Windows 기본 설정에서는 외부 스크립트 실행이 막혀있기 때문입니다.

정상 실행 시 출력

[INFO] promptId=626d4d7c-6955-4577-a2b6-80d4cd80c172
[OK] saved: D:\016_CardNewoutput
esult_20260322_105758.jpg

이 두 줄이 나오면 성공입니다. result_날짜시간.jpg 파일이 output 폴더에 생겼는지 확인합니다.

worker.py — Python 대안

같은 역할을 Python으로 구현한 버전도 있습니다(D:\016_CardNewscriptsworker.py). 주요 차이점입니다.

폴링 횟수가 120회(x2초 = 최대 240초)입니다. ps1보다 짧습니다. 결과 이미지를 output/result.jpg 고정 파일명으로 저장합니다. 타임스탬프 없이 덮어씁니다. BASE 경로가 D:\015. Autothreads로 되어있습니다. 실제 사용 전에 D:\016_CardNew로 수정해야 합니다. .venv를 활성화해야 requests 패키지를 쓸 수 있습니다.

PowerShell이 없는 환경이거나, Python 코드로 파이프라인을 통합하고 싶을 때 worker.py를 씁니다. 일반적인 Windows 자동화에는 worker.ps1이 더 편리합니다.

리도 프로필

리도 인사이트

기술을 현장 언어로 다시 풀어 쓰는 사람

3D 설계, 광통신 인프라 장비 개발, 글로벌 현장 교육을 19년 넘게 다뤄왔고, 요즘은 AI 자동화, 꿈꾸는 카메라, 실무 채널 운영을 연결해 복잡한 일을 더 쉽게 만드는 방법을 기록하고 있습니다.

다음 대화

읽고 끝내지 말고, 실제 문제로 이어가도 좋습니다.

자동화, 설계, 교육, 콘텐츠 중 무엇이든 지금 필요한 문제부터 같이 정리해볼 수 있습니다.

편하게 문의하기