
프롬프트 박스에서 상태 유지형(Stateful) AI 워크플로우로: AI 제품 광고 스튜디오 구축하기
요약
단순한 프롬프트 입력 방식을 넘어, 반복 가능한 AI 제품을 만들기 위한 상태 유지형(Stateful) 워크플로우 설계 방안을 다룹니다. Photo2Ads 구축 사례를 통해 이미지/비디오 초안 분리, 브라우저 저장소 활용 등 아키텍처 결정 사항을 설명합니다.
핵심 포인트
- 프롬프트를 프로젝트 전체가 아닌 하나의 지침(instruction)으로 정의
- 이미지와 비디오 초안 상태를 분리하여 데이터 충돌 방지
- localStorage와 IndexedDB를 활용한 설정 및 파일 관리
- 선언적 기능 계약 및 출처 인식(Provenance-aware) 설계 적용
대부분의 AI 크리에이티브 제품은 동일한 인터페이스, 즉 크고 비어 있는 프롬프트 박스(prompt box)에서 시작합니다. 하지만 상태 유지형(stateful) AI 워크플로우에는 그 이상의 것이 필요합니다.
프롬프트 박스는 실험에는 유용합니다. 하지만 반복 가능한 제품을 만들기 위한 기초로는 취약합니다.
AI 제품 광고 스튜디오인 Photo2Ads를 구축하면서, 저는 동일한 제품 및 엔지니어링 문제에 계속 직면했습니다. 모델은 출력을 빠르게 생성할 수 있었지만, 애플리케이션은 사용자에게 그 출력을 중심으로 프로젝트를 계속 재구성하도록 요구했습니다.
제품 이미지, 크리에이티브 방향(creative direction), 모델 기능(model capabilities), 모드별 설정, 그리고 결과물의 출처(provenance) 모두가 단 한 번의 요청보다 더 오래 유지되어야 했습니다.
이 글에서는 제품을 단순한 프롬프트 래퍼(prompt wrapper)에서 상태 유지형(stateful) AI 워크플로우로 전환하기 위한 아키텍처 결정 사항들을 다룹니다:
- 이미지와 비디오 초안 상태(draft state)의 분리
- 직렬화 가능한(serializable) 설정을 위한
localStorage사용 - 실제
File객체를 위한 IndexedDB 사용 - 생성 모델을 위한 선언적 기능 계약(Declarative capability contracts)
- 워크벤치(workbench)의 씨앗이 될 수 있는 출처 인식(Provenance-aware) 예시
- 브라우저 저장소를 사용할 수 없을 때를 대비한 점진적 향상(Progressive enhancement)

1. 프롬프트를 프로젝트가 아닌 지침(instruction)으로 취급하기
프롬프트는 제품 광고 생성 요청의 일부일 뿐입니다.
실제 프로젝트에는 다음 항목들도 포함됩니다:
- 제품 참조 이미지
- 선택 사항인 인물 참조
- 크리에이티브 스타일
- 종횡비(Aspect ratio) 및 해상도
- 이미지 또는 비디오 모델
- 비디오 훅(hook) 및 설정
- 지속 시간 및 오디오 선호도
- 입력과 출력 사이의 관계
만약 애플리케이션이 프롬프트만 저장한다면, 모드를 전환하거나 페이지를 새로고침할 때 사용자의 작업 대부분이 파괴됩니다.
첫 번째 중요한 결정은 이미지와 비디오를 별도의 초안(drafts)으로 모델링하는 것이었습니다.
interface ProductAdSettingsDraft {
image: {
prompt: string;
...
돌이켜보면 이는 당연해 보이지만, 놀라울 정도로 흔하게 발생하는 버그 유형을 방지해 줍니다.
하나의 상태(state) 객체가 두 가지 모드 사이에서 공유될 때 다음과 같은 문제가 발생합니다:
- 비디오 프롬프트를 편집할 때 이미지 프롬프트가 덮어씌워질 수 있음
- 비디오 전용 훅(hook)이 이미지 생성 과정에 유출됨
- 모델을 전환할 때 지원되지 않는 설정이 선택된 상태로 남을 수 있음
- 모드로 돌아갔을 때 처음부터 다시 시작하는 것처럼 느껴짐
제품은 개념적으로 공유되지만, 각 모드는 자신만의 작업 상태(working state)를 가져야 합니다.
React에서는 하위 초안(drafts)을 병합하지 않고도 활성 상태를 선택할 수 있습니다:
const [imageProducts, setImageProducts] = useState<ProductReference[]>([]);
const [videoProducts, setVideoProducts] = useState<ProductReference[]>([]);
...
UI는 모드를 전환하지만, 초안은 온전하게 유지됩니다.
2. File 객체를 localStorage에 넣지 마세요
설정 초안은 JSON 직렬화(JSON-serializable)가 가능하므로 localStorage로도 충분합니다:
function writeSettingsDraft(value: ProductAdSettingsDraft) {
try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(value));
...
하지만 제품(product)과 인물(person) 참조는 다릅니다.
업로드된 에셋(asset)은 단순한 메타데이터가 아닙니다. 브라우저는 새로고침 후에도 다음과 같은 작업을 수행하기 위해 실제 File 객체가 필요할 수 있습니다:
- 새로운 미리보기 URL 렌더링
- 생성이 시작될 때 참조(reference) 업로드
- 원본 파일 이름 및 MIME 타입 보존
- 파일을 다시 요청하지 않고 이미지와 비디오 모드 간 이동
File을 JSON으로 직렬화하면 바이너리 데이터가 손실됩니다. blob: URL만 유지하는 방식 또한 실패하는데, 왜냐하면 객체 URL(object URL)은 현재 문서의 생명주기(lifecycle)에 종속되기 때문입니다.
IndexedDB는 File 및 Blob 객체를 포함하여 구조적 복제(structured-clone)가 가능한 값들을 저장할 수 있습니다.
최소한의 객체 저장소(object store)만으로도 충분합니다:
const DATABASE_NAME = 'photo2ads-product-ad-drafts';
const STORE_NAME = 'drafts';
const ASSETS_KEY = 'workbench-assets-v1';
...
에셋 초안은 이미지와 비디오 참조를 분리하여 유지합니다:
interface ProductAdAssetsDraft {
imageProductReferences?: ProductReference[];
videoProductReferences?: ProductReference[];
...
이를 작성하는 것은 단일 IndexedDB 트랜잭션으로 처리됩니다:
async function writeAssetsDraft(value: ProductAdAssetsDraft) {
const db = await openDraftDatabase();
...
하이드레이션 (Hydration) 시, 영속화된 File 객체는 새로운 오브젝트 URL (Object URL)을 받습니다:
function restoreReference(
reference: ProductReference
): ProductReference | undefined {
...
이것이 오브젝트 URL 정리 (Cleanup)가 중요한 이유이기도 합니다:
function revokeObjectUrl(url?: string) {
if (url?.startsWith('blob:')) {
URL.revokeObjectURL(url);
...
정리를 하지 않으면, 긴 편집 세션 동안 참조 이미지를 반복적으로 교체할 때 메모리 누수 (Memory leak)가 발생할 수 있습니다.
3. 자동 저장하기 전에 하이드레이션(Hydrate) 하세요
초안을 자동 저장하는 것은 레이스 컨디션 (Race condition)을 유발합니다.
기존 초안이 하이드레이션 되기 전에 영속화 효과 (Persistence effects)가 실행되면, 초기 빈 React 상태가 저장된 초안을 덮어쓸 수 있습니다.
워크벤치 (Workbench)는 명시적인 하이드레이션 플래그 (Hydration flag)를 사용합니다:
const [draftHydrated, setDraftHydrated] = useState(false);
useEffect(() => {
...
모든 영속화 효과는 이 플래그를 확인합니다:
useEffect(() => {
if (!draftHydrated) return;
...
짧은 디바운스 (Debounce)를 사용하면 초안의 반응성을 유지하면서 모든 키 입력마다 쓰기가 발생하는 것을 방지할 수 있습니다.
규칙은 간단합니다:
복구 (Restoration)가 완료되기 전에 기본 클라이언트 상태가 영속화 저장소에 쓰여지도록 방치하지 마세요.
4. 모델의 기능은 흩어진 조건문이 아니라 데이터에 있어야 합니다
비디오 생성 모델들은 동일한 입력을 지원하지 않습니다.
다음 요소들에 따라 다를 수 있습니다:
- 지속 시간 (Duration)
- 해상도 (Resolution)
- 종횡비 (Aspect ratio)
- 오디오 지원 여부
- 참조 이미지의 수
- 가격 티어 및 크레딧 비용
만약 이러한 규칙들이 UI 컴포넌트와 API 핸들러 전반에 흩어져 있다면, 규칙이 어긋나게 됩니다.
선언적인 모델 계약 (Declarative model contract)을 사용하는 것이 추론하기 더 쉽습니다:
interface VideoModelConfig {
id: VideoModel;
displayName: string;
...
UI는 이 계약(contract)을 읽어 유효한 옵션을 표시합니다. 검증 레이어(validation layer)는 요청이 제출되기 전에 동일한 계약을 읽습니다.
function validateVideoInput(input: VideoInput, config: VideoModelConfig) {
if (!config.supportedDurations.includes(input.duration)) {
throw new Error(`${config.displayName} does not support that duration.`);
...
참조 제한(Reference limits)은 인물 참조(person reference)가 사용 가능한 슬롯 중 하나를 소비할 때 특히 중요해집니다.
const productReferenceLimit = Math.max(
1,
model.maxReferenceImages - (hasPersonReference ? 1 : 0)
...
인터페이스는 원격 제공자(remote provider)가 요청을 거부할 때까지 기다리는 것이 아니라, 생성 전에 이를 사용자에게 전달해야 합니다.
5. 예시에서 출처(provenance) 보존하기
AI 쇼케이스는 보통 마케팅 콘텐츠로 취급됩니다.
하지만 이것이 실행 가능한 상태(executable state)의 소스가 될 때 더욱 유용해집니다.
Photo2Ads의 쇼케이스 데이터는 결과물을 프롬프트(prompt), 크리에이티브 스타일(creative style), 그리고 해당 케이스에 진정으로 속하는 모든 참조(references)와 연결된 상태로 유지합니다:
interface ShowcaseExample {
slug: string;
name: string;
...

Recreate를 클릭하면 빈 생성기로 이동하는 대신 워크벤치(workbench)에 데이터가 주입(seed)됩니다:
function applyExample(example: ShowcaseExample) {
workbench.seed({
mediaMode: 'video',
...
null 값은 중요합니다.
만약 특정 제품이나 인물 참조가 바인딩되지 않은 상태로 예시가 생성되었다면, 애플리케이션은 인계(handoff)가 완벽해 보이도록 하기 위해 임의로 값을 만들어내서는 안 됩니다.
그렇게 함으로써 사용자에게 더 정직한 계약을 제공할 수 있습니다:
- 기존 입력값은 그대로 전달됨
- 누락된 입력값은 누락된 상태로 유지됨
- 정확한 프롬프트(prompt)를 검사할 수 있는 상태로 유지됨
- 사용자가 무엇을 교체해야 하는지 이해할 수 있음
이를 통해 갤러리는 실행 가능한 문서(executable documentation)의 한 형태로 변모합니다.
6. 지속성(Persistence)을 점진적 향상(Progressive Enhancement)으로 만들기
브라우저 저장소(Browser storage)는 실패할 수 있습니다.
시크릿 모드(Private browsing), 저장 정책, 할당량 제한(Quota limits), 그리고 브라우저 동작 방식 등으로 인해 localStorage나 IndexedDB를 사용할 수 없게 될 수 있습니다.
초안 저장(Draft persistence)이 실패했다고 해서 생성 흐름(Generation flow) 자체가 사용 불가능해져서는 안 됩니다.
Photo2Ads에서는 저장 헬퍼(Storage helpers)가 실패를 포착하여 undefined를 반환합니다. 그러면 현재의 인메모리(In-memory) 세션이 계속 작동합니다.
async function readAssetsDraft<T>(): Promise<T | undefined> {
try {
const db = await openDraftDatabase();
...
이를 통해 유용한 계층 구조가 형성됩니다:
- 인메모리 상태(In-memory state)는 현재 상호작용을 위해 필수적입니다.
- 브라우저 지속성(Browser persistence)은 연속성을 향상시킵니다.
- 원격 저장소(Remote storage)는 사용자가 생성 요청을 제출할 때만 시작됩니다.
초안 시스템은 도구 사용을 위한 전제 조건이 되지 않으면서, 작업 손실을 줄여주는 역할을 해야 합니다.
7. 워크플로우 내부에 검토(Review) 프로세스 유지하기
상태(State)와 출처(Provenance)는 단순히 편의를 위한 기능이 아닙니다.
이 기능들은 AI 출력물을 더 쉽게 검토할 수 있게 해줍니다.
제품 광고 모델은 다음과 같은 오류를 범할 수 있습니다:
- 라벨을 잘못 읽음
- 패키징을 변경함
- 로고를 왜곡함
- 브랜드 색상을 변경함
- 시각적 주장(Visual claims)을 지어냄
- 비디오 프레임 전반에 걸쳐 일관되지 않은 제품을 생성함

참조 자료(References)를 결과물 근처에 두면 검토자가 즉각적인 비교를 할 수 있습니다. 프롬프트와 설정값을 함께 유지하면 예상치 못한 동작을 진단하기가 더 쉬워집니다.
사람의 검토는 워크플로우가 실패했다는 증거가 아닙니다. 그것은 실제 제품을 위한 광고를 만드는 시스템의 필수적인 단계입니다.
다른 생성형 제품을 만든다면 재사용할 것들
이러한 패턴들은 제품 광고에만 국한된 것이 아닙니다.
만약 제가 다른 상태 유지형(Stateful) AI 도구를 만든다면, 동일한 원칙들을 재사용할 것입니다:
-
지속 가능한 객체(Durable object)를 식별하십시오.
프롬프트와 출력이 변하는 동안 무엇이 안정적으로 유지되어야 합니까?
-
모드별 상태(Mode-specific state)를 분리하십시오.
하나의 워크플로우가 다른 워크플로우를 조용히 덮어쓰게 두지 마십시오.
-
데이터 유형에 따라 영속성(Persistence) 방식을 선택하십시오.
JSON은
localStorage에 적합하며, 바이너리 자산(Binary assets)은 IndexedDB 또는 원격 저장소(Remote storage)에 속합니다. -
자동 저장하기 전에 하이드레이션(Hydrate)을 수행하십시오.
비어 있는 초기 상태가 실제 초안을 덮어쓰는 것을 방지하십시오.
-
모델의 역량을 데이터로서 기술하십시오.
UI와 검증기(Validator)가 동일한 계약(Contract)을 공유하게 하십시오.
-
출력물에 출처(Provenance)를 첨부하여 유지하십시오.
입력값이 없는 결과물은 재현하거나 신뢰하기 어렵습니다.
-
영속성을 사용할 수 없을 때는 부드럽게 실패(Fail softly)하십시오.
연속성(Continuity)은 향상된 기능일 뿐입니다. 핵심 상호작용은 여전히 작동해야 합니다.
진짜 제품은 워크플로우입니다
AI 기능의 가치를 생성 지연 시간(Generation latency)이나 가장 뛰어난 출력물의 시각적 품질로 판단하기는 쉽습니다.
하지만 사용자는 해당 모델 호출(Model call) 주변의 모든 것을 경험합니다:
- 반복되는 설정
- 모드 전환
- 참조 제한(Reference limits)
- 유실된 업로드 파일
- 유효하지 않은 조합
- 결과 검토
- 재현 및 인계(Handoff)
빠른 모델이 자동으로 빠른 제품을 만드는 것은 아닙니다.
Photo2Ads를 구축하며 얻은 더 큰 교훈은 프롬프트 박스가 아키텍처의 중심이 되어서는 안 된다는 것입니다. 지속 가능한 객체(Durable object)가 중심이 되어야 합니다.
제품 광고의 경우, 그 객체는 바로 제품입니다.
작동 구현 방식과 실제 사례는 photo2ads.com에서 확인하실 수 있습니다.
만약 여러분이 생성형 도구를 만들고 있다면, 여러분의 애플리케이션은 사용자에게 어떤 정보를 계속해서 다시 입력하도록 강요하고 있습니까?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기