이미지 자동 생성하기: 템플릿과 job, 그리고 webhook
이 질문에 대한 답 대부분은 텍스트 상자 스크린샷입니다. prompt를 치고, 버튼을 누르고, 기다리고, 내려받고, 반복합니다. 그건 자동화가 아닙니다. 여러분이 손으로, 더 빨리 일하는 겁니다.
자동이란 루프 안에 사람이 없다는 뜻입니다. 데이터 한 행이 도착하고, 이미지가 나오고, 아무도 그 광경을 지켜보지 않습니다. 여기에는 네 부분이 필요합니다. 구멍이 뚫린 prompt 템플릿, 그 구멍을 채울 데이터, 블로킹 없이 일감을 받아 주는 큐, 그리고 결과를 둘 지속적인 장소입니다. 저희는 그 한가운데에서 API를 돌리고 있어서, 사람들이 만드는 파이프라인과 출시 3주 뒤에 조용히 깨지는 파이프라인을 둘 다 봅니다.

아무도 먼저 말해 주지 않는 것: 이제 쌉니다
숫자부터 시작하겠습니다. 여러분이 무엇을 만들 마음을 먹느냐가 여기서 바뀌기 때문입니다.
Nano Banana 2 Lite로 이미지 천 장이면 $23.80입니다. 2026년 8월 26일 저희 가격으로 1,000 × $0.0238입니다. 제품 컷 십만 장 — 진짜 카탈로그 하나, 모든 SKU, 모든 컬러웨이 — 이면 $2,380입니다.
여러분이 저울질하실 티어들의 같은 물량입니다.
| 이미지 수 | Nano Banana 2 Lite | Nano Banana 2 | Nano Banana Pro |
|---|---|---|---|
| 100 | $2.38 | $4.00 | $7.50 |
| 1,000 | $23.80 | $40.00 | $75.00 |
| 10,000 | $238.00 | $400.00 | $750.00 |
| 100,000 | $2,380.00 | $4,000.00 | $7,500.00 |
가격과 제품 조건은 시간이 지나면서 변경될 수 있습니다. 구매를 결정하기 전에 각 프로바이더의 현재 가격을 확인하시기 바랍니다. Google의 Batch 또는 Flex 가격은 스케줄링, 가용성, 처리 조건이 다르기 때문에 표준 온디맨드 API request와 직접 동등하지 않으며, 따라서 이 비교에서는 제외했습니다. 이는 특정 범위에 한정된 비교이며, E2X가 모든 구성에서 세계에서 가장 저렴하다는 주장은 아닙니다.
이미지 생성을 어떻게 자동화하느냐고 물으시는 분 대부분은 마음속으로 이미 "시도해 보기엔 너무 비싸다"고 값을 매겨 두셨습니다. $23.80짜리 질문입니다. 만드십시오.
1단계: prompt 쓰기를 멈추고 템플릿을 쓰십시오
한 번 타이핑하는 prompt는 문장입니다. 기계가 만 번 돌리는 prompt는 슬롯이 있는 템플릿이고, 그 차이가 이 일의 전부입니다.
규칙은 이렇습니다. 변하는 것은 전부 슬롯으로, 동일하게 유지되어야 하는 것은 전부 고정 텍스트로 갑니다. 그 경계를 잘못 잡으면 세트가 흐트러집니다. "배경"이 가변 쪽으로 새어 나가서 절반은 흰 바탕, 절반은 회색 바탕이 되는 식입니다.
TEMPLATE = (
"{colour} 색상의 {product_name}을(를) 담은 스튜디오 제품 사진, "
"무지 웜그레이 배경 정중앙, 왼쪽 위에서 들어오는 부드러운 방향성 키라이트, "
"얕은 피사계 심도, 에디토리얼 카탈로그 스타일링, "
"텍스트 없음, 로고 없음, 인물 없음"
)
슬롯은 둘입니다. 조명, 배경, 프레이밍, 배제 조건은 얼어붙어 있습니다. 400장이 400건의 사고가 아니라 한 번의 촬영처럼 보이게 만드는 것이 바로 그 얼어붙은 절반입니다.
가변 절반에서 네 가지는 빼 두십시오. 매체, 광원 방향, 배경, 그리고 부정 지시입니다. 그것들이 여러분의 시각적 정체성입니다. 그것들이 행마다 움직이면 정체성은 없습니다.

prompt 작성 자체는 별도의 글입니다. 자동화에서 중요한 것은 형태입니다. 고정된 틀, 이름 붙은 구멍, 뜻밖의 것 없음.
2단계: 데이터가 실제 제품입니다
템플릿은 사소합니다. 일이 사는 곳은 데이터이고, 그 데이터는 보통 이미 갖고 계신 무엇입니다.
- 제품 카탈로그 테이블. SKU당 한 행, 이름·색상·소재·카테고리 열.
- 블로그 글의 CMS 익스포트. 제목과 주제가 글마다 헤더 이미지를 만드는 슬롯이 됩니다.
- 애플리케이션 데이터베이스의 행들. 사용자가 올린 리스팅, 이벤트 페이지, 레시피.
- 광고 변형 스프레드시트. 헤드라인 여섯에 배경 넷이면 스물네 장이고, 그걸 손으로 브리핑하는 사람은 없습니다.
소스가 무엇이든 API에 닿기 전에 딕셔너리 리스트로 정규화하시고, 모든 행에 안정적인 ID를 유지하십시오. 결과를 레코드에 다시 붙이려면 필요하고, 이걸 건너뛰면 webhook이 도착하는 바로 그 순간에 아픕니다.
3단계: 제출하시고 기다리지 마십시오
저희 API는 의도적으로 비동기입니다. job을 POST하시면 job ID가 즉시 돌아오고, 생성은 저희 쪽에서 일어납니다.
curl -X POST https://api.e2x.ai/v1/jobs/submit \
-H "Authorization: Bearer $E2X_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "google/nano-banana-2-lite/text-to-image",
"input": {
"prompt": "매트 블랙 색상의 호두나무 데스크 램프를 담은 스튜디오 제품 사진...",
"aspect_ratio": "1:1"
},
"webhookUrl": "https://your-app.example.com/hooks/e2x"
}'
테이블을 job으로 바꾸는 워커는 서른 줄 남짓입니다. CSV를 읽고, 행마다 템플릿을 채우고, 제출하고, 행 ID에 job ID를 기록해서 미아가 생기지 않게 합니다.
import csv
import os
import time
import requests
API = "https://api.e2x.ai/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['E2X_API_KEY']}",
"Content-Type": "application/json",
}
TEMPLATE = (
"{colour} 색상의 {product_name}을(를) 담은 스튜디오 제품 사진, "
"무지 웜그레이 배경 정중앙, 왼쪽 위에서 들어오는 부드러운 방향성 키라이트, "
"얕은 피사계 심도, 에디토리얼 카탈로그 스타일링, "
"텍스트 없음, 로고 없음, 인물 없음"
)
MAX_IMAGES = 500 # 하드 상한, 비용 절 참고
UNIT_COST = 0.0238 # Nano Banana 2 Lite, 2026년 8월 26일 확인
def submit(row):
body = {
"model": "google/nano-banana-2-lite/text-to-image",
"input": {
"prompt": TEMPLATE.format(**row),
"aspect_ratio": "1:1",
},
"webhookUrl": "https://your-app.example.com/hooks/e2x",
}
for attempt in range(4):
r = requests.post(f"{API}/jobs/submit", json=body, headers=HEADERS, timeout=30)
if r.status_code < 300:
return r.json()["data"]["jobId"]
if r.status_code == 429 or r.status_code >= 500:
time.sleep(2 ** attempt)
continue
raise RuntimeError(f"row {row['id']} rejected: {r.status_code} {r.text}")
raise RuntimeError(f"row {row['id']} still failing after 4 attempts")
with open("catalog.csv") as fh:
rows = list(csv.DictReader(fh))[:MAX_IMAGES]
print(f"submitting {len(rows)} jobs, budget ${len(rows) * UNIT_COST:.2f}")
submitted = {}
for row in rows:
try:
submitted[row["id"]] = submit(row)
except RuntimeError as err:
print(f"skipped: {err}")
print(f"{len(submitted)}/{len(rows)} accepted")
저 루프가 하지 않는 일을 보십시오. 행 하나가 실패했다고 멈추지 않습니다. 500개짜리 batch에서 212번 행의 color가 null이라면, 새벽 3시의 스택 트레이스와 이미지 0장이 아니라 이미지 499장과 기록된 skip 하나를 받으셔야 합니다.
slug는 보이는 것보다 중요합니다. google/nano-banana-2-lite/text-to-image에는 capability 접미사가 붙지만, Nano Banana 2의 텍스트 slug는 아무것도 붙지 않은 맨 google/nano-banana-2입니다. 추측하시면 다 끝난 것처럼 보이는 파이프라인에서 404를 받으십니다. 모델마다 파라미터 목록을 기계가 읽는 스펙 파일로 공개합니다.
4단계: polling보다 webhook, 그리고 그 이유
polling하실 수 있습니다. GET /v1/jobs/{JOB_ID}는 pending, processing, completed, failed, cancelled를 돌려주고, 일회성 스크립트라면 그걸로 충분합니다.
파이프라인에서는 잘못된 선택이며, 이유는 우아함이 아닙니다. polling은 여러분 프로세스의 수명을 batch에서 가장 느린 job에 묶습니다. Lite 생성은 저희 쪽에서 60초 안팎이 걸립니다. 500개를 제출하시면 poller는 꼬리가 끝날 때까지 상태를 쥔 채 살아 있어야 합니다. 배포를 지나, 컨테이너 재시작을 지나, 여러분이 올라타 있는 서버리스 플랫폼의 15분 timeout을 지나서요. 그러다 죽으면, 어느 job이 진행 중이었는지에 대한 유일한 기록을 쥔 채로 죽습니다.
webhook은 그걸 뒤집습니다. 제출하고 나옵니다. 나중에 완료된 job과 함께 POST가 도착하고, 여러분은 정확히 그 job 하나를 무상태로 처리합니다.
// Express 핸들러. 멱등해야 합니다. 같은 jobId가 두 번 도착할 수 있습니다.
app.post("/hooks/e2x", express.json(), async (req, res) => {
res.status(200).end(); // 먼저 응답하고, 일은 그다음에
const job = req.body.data;
if (job.status !== "completed") {
await markFailed(job.jobId, job.error?.message ?? "unknown");
return;
}
const already = await findAsset(job.jobId);
if (already) return;
await storeImage(job.jobId, job.outputs[0].url);
});
손으로 스크립트를 돌리며 지켜보고 계실 때는 polling하십시오. 아무도 지켜보지 않을 때는 webhook을 잡으십시오. 그게 바로 자동화의 정의입니다.
5단계: 출력 URL은 임시이며, 파이프라인은 이렇게 깨집니다
이겁니다. 나머지를 다 훑어 넘기시더라도 이 절은 두 번 읽으십시오.
data.outputs[0].url은 전달용 URL이지 저장소가 아닙니다. 지금은 됩니다. 영원히 되지는 않습니다. 이것을 영구적인 것으로 다루는 무엇이든 — product_image_url 컬럼, CMS 필드, 클라이언트에게 메일로 보낸 링크 — 스테이징에서는 멀쩡히 렌더링되고 2주 뒤에 깨진 이미지 아이콘을 보여 줍니다.
이 고장이 고약한 이유는 지연되기 때문입니다. 쓰는 시점에는 아무것도 에러가 나지 않습니다. 테스트는 통과합니다. batch는 성공 500건을 보고합니다. 그러다 가장 오래된 레코드부터 이미지가 사라지기 시작하고, 누군가 알아차릴 때쯤이면 job 전체를 다시 돌리지 않고는 재생성할 수 없습니다. prompt도 저장해 두지 않았으니까요.
바이트를 내려받으십시오. 여러분의 버킷에 넣으십시오. 여러분의 URL을 저장하십시오.

import requests
import boto3
s3 = boto3.client("s3")
def store_image(job_id, row_id, source_url):
img = requests.get(source_url, timeout=60)
img.raise_for_status()
key = f"products/{row_id}.jpg"
s3.put_object(
Bucket="my-assets",
Key=key,
Body=img.content,
ContentType=img.headers.get("content-type", "image/jpeg"),
)
return f"https://cdn.example.com/{key}"
들어간 김에 prompt를 이미지 옆에 저장하십시오. 텍스트 컬럼 하나이고, "그 변형을 다시 뽑아"와 "처음부터 다시"를 가르는 차이입니다.
첫날부터 들일 만한 습관이 둘 더 있습니다. 내려받은 것이 에러 페이지가 아니라 이미지 바이트인지 확인하십시오. HTML이 담긴 200도 여러분 버킷에 기꺼이 쓰이니까요. 그리고 저장 키를 저희 job ID가 아니라 여러분의 행 ID로 잡으십시오. 그래야 재생성이 미아를 만들지 않고 올바른 오브젝트를 덮어씁니다.
6단계: 물량이 커지면 실제로 어긋나는 것들
작은 batch는 모든 것을 숨깁니다. 수백 장을 넘기면 나타나는 것들입니다.
부분 실패는 정상입니다. 큰 실행에서는 일정 비율이 실패합니다. 콘텐츠 필터에 걸리는 prompt, 형식이 깨진 행, 일시적인 상위 오류 같은 것들입니다. 500 중 497을 성공 케이스로 다루시고, 그 세 행만 다시 돌리는 명령 하나를 스스로에게 마련해 두십시오.
재시도에는 지출 상한이 필요합니다. 상한 없는 재시도 루프는 청구서를 만드는 버그입니다. 한 번에 $0.0238은 무해하게 들리다가, 갇힌 루프가 사만 번 돌리면 달라집니다. 코드에 하드 카운트를 박으십시오. 위의 MAX_IMAGES 줄이 여기서 가장 싼 보험입니다.
멱등성은 선택이 아닙니다. webhook은 두 번 이상 전달될 수 있습니다. 하나의 job ID로 두 번 돌려도 안전하지 않은 핸들러는 중복 자산과 중복 행을 만들고, 그 사실을 가장 바쁜 주에 알게 됩니다.
비용에는 월간 청구서가 아니라 실시간 카운터가 필요합니다. 저희 API의 금액 값은 마이크로센트로 돌아옵니다. 1,000,000이 $1입니다. batch마다 합산하시고 끝날 때 총액을 로그로 남기십시오. 매일 밤 "이미지 412장, $9.81"을 보고하는 파이프라인은 아무도 감사할 필요가 없는 파이프라인입니다.
aspect ratio 기본값이 여러분을 놀래킬 겁니다. Lite의 기본값은 1:1이고, Nano Banana 2와 Pro의 기본값은 9:16입니다. 템플릿에 고정하십시오. 그러지 않으면 카탈로그 절반이 소리 없이 세로가 됩니다.
watermark는 파일을 따라다닙니다. Google은 자사 이미지 모델이 만드는 모든 것에 SynthID를 찍습니다. 보이지 않고, 플래그가 아니며, 이후 파이프라인이 바이트에 무슨 짓을 하든 살아남습니다. 여러분 쪽에서 브랜드나 법무를 맡은 사람이 AI 생성 자산을 승인해야 하는 시점은 파이프라인이 버킷에 오브젝트 2만 개를 쓴 뒤가 아니라 파이프라인이 존재하기 전입니다.

파이프라인을 어느 모델에 겨눌 것인가
Nano Banana 2 Lite를 기본으로 두시고, 구체적인 무언가가 강제할 때만 벗어나십시오.
이건 얼버무림이 아니라 산수입니다. $0.0238은 저희가 파는 것 중 이미지를 만드는 가장 싼 것이고, 그것이 대체한 원조 Nano Banana보다 한 세대 새로우며, 파이프라인 물량에서는 가격 격차가 복리로 불어나 재무가 보는 유일한 숫자가 됩니다. 다른 어디에도 없는 aspect ratio 넷도 딸려 옵니다. 1:4, 4:1, 1:8, 8:1이고, 출력이 배너와 세로 레일이라면 정확히 원하시던 것입니다. 특이점 하나. resolution 파라미터가 아예 없고, 보내면 조용한 무시가 아니라 에러입니다.
이 네 경우에는 다른 데로 보내시고, 그 밖에는 아닙니다.
- 1K를 넘는 출력. Lite는 1K 전용입니다. Nano Banana 2는 기본 $0.04에 4K까지 가며, 2K는 ×1.5, 4K는 ×2입니다.
- 이미지 안의 읽히는 텍스트. 가격이 박힌 제품 카드, 현지화된 인포그래픽. 그건 $0.075의 Nano Banana Pro에 삽니다. 거기서는 1K와 2K의 값이 같으니 2K를 가져가십시오. 공짜입니다.
- 투명 배경. 컷아웃과 스티커 시트에는
background: transparent가 필요하고, 저희가 취급하는 모델 중 그것을 한 번의 호출로 해내는 것은 GPT Image 2뿐입니다. 카탈로그 원가 숫자가 아니라 1K 기준 $0.0525로 예산을 잡으십시오.quality가 모든 티어에서 7을 곱합니다. - 여러 프레임에 걸친 같은 얼굴 또는 같은 병. 물량 문제가 아니라 참조 이미지 문제이고, 일관성에 관한 별도의 글이 있습니다.
혼합 워크로드라면 대부분을 Lite로 돌리시고, 텍스트나 충실도가 필요한 2%만 데이터의 컬럼 하나로 Pro에 분기하십시오. 파이프라인 하나, 모델 둘, Pro 가격은 값을 하는 자리에서만.
submit과 poll 계약을 파라미터별로 훑는 것은 Nano Banana Pro API 튜토리얼에, 티어별 분해는 모델 비교에 있습니다. 둘 다 모델을 이미 고르셨다고 전제합니다. 아직이시라면 text-to-image 모델에 가격이 함께 나열되어 있고, 카탈로그가 영상 쪽까지 더합니다.
전체를 순서대로
슬롯이 있는 템플릿. 안정적인 ID가 붙은 데이터. 하드 상한 아래에서 job으로 제출. webhook 받기. 바이트를 여러분 저장소로 내려받기. 실패와 지출을 로그로. 여섯 단계이고 어려운 것은 없으며, 언제나 건너뛰는 것은 5단계입니다.
자주 묻는 질문
이미지를 어떻게 자동으로 생성합니까?
prompt를 이름 붙은 슬롯이 있는 템플릿으로 쓰시고, 제품 카탈로그나 CSV 같은 데이터 소스에서 그 슬롯을 채우신 뒤, 채워진 prompt를 각각 비동기 job으로 이미지 API에 제출하십시오. 결과는 polling이 아니라 webhook으로 받으시고, 이미지 바이트를 여러분의 저장소로 내려받으십시오. 그 루프의 어느 것도 사람을 필요로 하지 않으며, 그것이 이 일을 빠른 수작업이 아니라 자동으로 만듭니다.
이미지 1,000장을 자동으로 생성하는 데 얼마가 듭니까?
2026년 8월 26일 E2X 가격 기준으로, Nano Banana 2 Lite를 통한 1,000장은 $23.80입니다. request당 $0.0238이기 때문입니다. 같은 물량이 Nano Banana 2에서는 $40.00, Nano Banana Pro에서는 $75.00입니다. 더 높은 해상도는 그 위에 곱해지지만, 표준 1K 출력이라면 이미지당 숫자가 계산의 전부입니다.
이미지 생성 job에는 webhook을 써야 합니까, polling을 써야 합니까?
무인으로 돌아가는 것에는 webhook을 쓰십시오. polling은 여러분 프로세스의 수명을 batch에서 가장 느린 job에 묶어서, 배포나 서버리스 timeout이 어느 job이 아직 돌고 있는지에 대한 유일한 기록을 쥔 poller를 죽입니다. webhook은 제출하고 빠져나온 다음, 완료된 job을 각각 무상태로 처리하게 해 줍니다. 실행되는 걸 지켜보고 계신 스크립트라면 polling으로 충분합니다.
생성한 이미지가 얼마 뒤에 로딩되지 않는 이유는 무엇입니까?
이미지 대신 API의 출력 URL을 저장하셨기 때문입니다. 그 URL은 저장소가 아니라 임시 전달용이라서, 그것을 담은 컬럼은 처음에는 제대로 렌더링되다가 나중에 깨집니다. job이 완료되는 즉시 바이트를 내려받아 여러분의 버킷에 쓰시고, 여러분의 URL을 저장하십시오. 이미지 파이프라인이 깨진 채로 출시되는 가장 흔한 경로입니다.
자동화된 이미지 파이프라인은 어느 모델을 기본으로 삼아야 합니까?
이미지당 $0.0238인 Nano Banana 2 Lite입니다. 저희 카탈로그에서 가장 싼 이미지 모델이자 원조 Nano Banana보다 한 세대 새로우며, batch 물량에서는 가격이 다른 모든 고려를 압도합니다. 1K를 넘는 출력에만 Nano Banana 2로, 읽히는 이미지 내 텍스트에만 Nano Banana Pro로, 투명 배경에만 GPT Image 2로 옮기십시오.
batch의 이미지 일부가 실패하면 어떻게 됩니까?
언제나 일부는 실패하며, 파이프라인은 그것을 치명적이 아니라 정상으로 다뤄야 합니다. 거부된 행 이후에도 계속 제출하시고, 실패한 ID를 에러와 함께 로그로 남기시고, 실패분만 돌리는 재시도 경로를 만드십시오. 재시도 횟수는 상한을 두십시오. 한 번 한 번이 실제 과금이니까요.