CodeGym /행동 /ChatGPT Apps /단계 간 컨텍스트 저장과 복원

단계 간 컨텍스트 저장과 복원

ChatGPT Apps
레벨 11 , 레슨 2
사용 가능

1. 워크플로 컨텍스트란 무엇이며 왜 필요한가

일반적인 웹‑애플리케이션에서는 상태가 어디에 있는지 비교적 명확합니다: 데이터베이스, 캐시, 그리고 프런트의 Redux나 로컬 React 상태 같은 것들. ChatGPT App에서는 더 복잡합니다. 상태가 세 가지 세계로 퍼져 있습니다 — 모델 내부(대화 기록), 위젯 내부(UI 상태), 그리고 여러분의 서버/MCP(비즈니스 데이터).

워크플로 컨텍스트란 “우리가 지금 어느 단계에 있는가”와 “이미 무엇을 알고 있는가”에 답하기 위해 필요한 전체 데이터 집합을 의미합니다. 우리가 다루는 학습용 GiftGenius를 예로 들면, 컨텍스트에는 다음이 포함됩니다:

  • 선물 수신자 프로필: 나이, 성별, 관심사;
  • 예산과 필요한 경우 통화;
  • 생성된 아이디어 목록과 그중 사용자가 좋아요를 눌렀거나 숨긴 항목;
  • 기술 요소: 세션 또는 워크플로 식별자, 상태(“profile_collected”, “ideas_shown”, “checkout_started”).

이 컨텍스트는 백엔드 개발자인 여러분만을 위한 것이 아닙니다. 모델 자체도 필요로 합니다. 어떤 질문이 이미 나왔는지, 어떤 도구가 호출되었는지, 지금 무슨 얘기를 하고 있는지를 이해하기 위해서입니다. 그리고 사용자가 채팅으로 돌아왔을 때 처음부터 다시 시작하지 않도록 사용자에게도 필요합니다.

사용자는 직관적으로 “ChatGPT가 다 기억한다”고 생각합니다. 실제로 모델이 기억하는 것은 대화 텍스트뿐이며, 그것도 컨텍스트 창에 담기는 동안만 가능합니다. order_id, cart_id, 또는 “좋아요한 아이디어 목록” 같은 구조화된 정보는 여러분의 서버에 보관해야 합니다. 그렇지 않으면 자신만만하지만 틀린 답변을 양산하는 기계를 얻게 됩니다.

2. 세 가지 상태 계층: UI, LLM, 비즈니스

컨텍스트 보존은 세 개의 상태 계층 모델로 이해하는 것이 가장 편리합니다. 일명 “State Triad”입니다.

계층 표

간단한 표를 보겠습니다:

계층 어디에 저장되는가 수명 주기 역할 GiftGenius 예
UI State 위젯(React, widgetState) 채팅/위젯 메시지가 열려 있는 동안 시각적 상태, 로컬 입력 하이라이트된 카드, 폼 상태
LLM Context OpenAI의 채팅 기록 메시지가 컨텍스트 창에 ‘들어갈’ 때까지 대화 이해와 추론 “엄마 선물 찾는 중, 예산 $50”
Business State MCP / 백엔드(데이터베이스/Redis) 원하는 만큼(영속) 진실 소스: 검증된 데이터, 상태 { step: "ideas", budget: 50, liked: [42, 51] }

UI 계층은 빠르고 반응성이 좋지만 아주 취약합니다. 사용자가 기록 상단으로 스크롤하면 ChatGPT가 위젯이 들어 있는 iframe을 “언마운트”했다가 나중에 다시 마운트할 수 있습니다. 바로 이럴 때 widgetState가 필요합니다. React 컴포넌트보다 조금 더 오래 살고 ChatGPT 호스트 클라이언트와 동기화됩니다.

LLM 계층은 모델에게 끊김 없는 대화의 느낌을 주지만, 텍스트와 도구 호출(tool call)만 보관합니다. 여러분의 장바구니 JSON을 거기에 넣을 수는 있지만, 사실상 텍스트 안에 JSON을 삽입하는 것일 뿐이며 모델이 이를 데이터베이스처럼 취급하지는 않습니다.

비즈니스 계층은 엔지니어인 여러분이 통제할 수 있는 곳입니다. 검증된 데이터, 인덱스, 주문 상태 등이 여기에 있습니다. 선물 추천, 예약, 학습 같은 진지한 시나리오가 생기는 순간, 바로 이 계층이 상태에 대한 기본 진실 소스가 되어야 합니다.

가장 큰 엔지니어링 과제는 이 세 계층이 제각각 다른 방향으로 흐르지 않도록 하는 것입니다. 사용자가 위젯에서 예산을 바꿨는데 모델은 여전히 예전 예산을, 데이터베이스에는 또 다른 값이 들어 있는 상황 — 전형적인 이상 동작의 레시피입니다.

3. 무엇을 저장할 것인가: WorkflowContext 구조

구체적으로 이야기하기 위해 GiftGenius의 컨텍스트 인터페이스를 TypeScript로 정의해 봅시다. 이미 몇 가지 단계(프로필 수집, 예산 선택, 아이디어 생성, 보기/좋아요)가 있다고 가정하겠습니다.

간단한 구조로 시작해 보겠습니다:

// backend/types/workflow.ts
export type GiftWorkflowStep =
  | "profile"
  | "budget"
  | "ideas"
  | "checkout";

export interface GiftWorkflowContext {
  id: string;              // workflowId — 시나리오 식별자
  userId?: string;         // 인증이 설정되어 있는 경우
  currentStep: GiftWorkflowStep;
  profile?: {
    age?: number;
    gender?: string;
    interests?: string[];
  };
  budget?: {
    min?: number;
    max?: number;
    currency: string;
  };
  ideas?: {
    id: string;
    title: string;
  }[];
  likedIdeaIds: string[];
  hiddenIdeaIds: string[];
  updatedAt: number;       // TTL/정리를 위한 타임스탬프
}

최종 스키마는 아니지만 중요한 요소는 이미 들어 있습니다. 포함되는 항목은 다음과 같습니다:

  • 워크플로 식별자 — 이 값으로 컨텍스트를 찾아올 것입니다;
  • 지금까지 어디까지 왔는지 위젯과 모델 모두가 알 수 있게 해 주는 현재 단계;
  • 각 단계에서 채워질 필드 집합;
  • 업데이트 시간 같은 서비스 필드.

식별자에 대해 따로 설명합니다. 이 강의에서 workflowId는 우리 backend/MCP 내부의 특정 시나리오 식별자를 의미합니다. ChatGPT 대화의 식별자(sessionId)와 같을 수도 있지만, 그것에 의존하지는 않습니다. userId는 여러분의 인증 시스템(있다면)에서 온 사용자 식별자입니다. 한 사용자가 여러 개의 활성 워크플로를 가질 수 있습니다. id 필드에 바로 이 workflowId가 들어 있으며, 이 값으로 컨텍스트를 찾고 업데이트합니다.

다음 섹션에서는 세 가지를 살펴봅니다. 컨텍스트 객체를 어디에 보관할지, 어떻게 기록할지, 그리고 어떻게 다시 꺼내올지 — 위젯과 모델 모두를 대상으로 합니다.

4. 상태는 어디에 저장할까: 옵션과 절충

상태 보존은 두 축으로 생각하면 편합니다: 어디에 저장할지와 얼마나 오래 보관할지. 이 섹션에서는 저장 위치에 집중하고, 수명은 체크리스트와 흔한 오류 파트에서 다시 다룹니다.

먼저 저장 위치를 살펴보겠습니다.

대화 내부(프롬프트에 삽입)

때로는 “매번 현재 상태를 담은 JSON을 모델에 반환해서, 모델이 알아서 처리하게 하자”고 말하고 싶어질 수 있습니다. 아주 간단한 시나리오와 짧은 단계 체인에서는 작동하지만, 곧 두 가지 문제에 부딪힙니다. 컨텍스트 길이 제한과 데이터 무결성 보장의 부재입니다.

또한 MCP 프로토콜은 본질적으로 stateless입니다. HTTP처럼 기본적으로 요청 간 상태를 저장하지 않습니다. 도구 호출을 특정 세션에 묶으려면 도구 인수나 메타데이터/헤더를 통해 워크플로 또는 세션 ID를 명시적으로 전달해야 합니다.

따라서 비즈니스 상태를 대화에만 저장하는 것은 아키텍처라기보다는 학습용 실험에 가깝습니다.

위젯에서: UI + widgetState

UI 레벨에서는 일반적인 React 상태(useState, useReducer 등)를 사용하지만, 앞서 말했듯 컴포넌트는 언마운트될 수 있습니다. 이를 위해 Apps SDK에는 widgetState 메커니즘이 있으며, React 밖에서 살아 있고 ChatGPT 호스트와 동기화됩니다. 위젯 마운트 시 저장된 값을 꺼내오고 변경 시 다시 넣으면, 로컬이지만 꽤 편리한 저장소가 됩니다.

이 저장소는 순수한 시각적 상태에 적합합니다. 지금 어떤 카드가 접혀 있는지, 어떤 탭에 있는지, 사용자가 “다음”을 누르기 전 폼에 무엇을 입력했는지 같은 것들입니다. 하지만 서버를 대체할 수는 없습니다. 사용자가 다른 기기에서 혹은 일주일 뒤에 채팅을 열면 widgetState가 도움이 안 될 수 있습니다. 여기에 비즈니스 로직을 얹는 것도 논쟁의 여지가 있습니다.

서버/MCP: Map, Redis, DB

마지막으로 프로덕션의 기본 옵션입니다. 우리는 GiftWorkflowContext를 MCP 서버나 백엔드 서비스 쪽에 저장합니다. MCP 클라이언트와 서버는 프로토콜상 stateless이므로, 어떤 컨텍스트를 업데이트할지 구분하기 위해 매 도구 호출마다 workflowId(또는 state_token)를 전달해야 합니다.

구현 옵션은 여러 가지가 있습니다:

  • Node.js의 in‑memory Map — 데모나 개발 환경에 적합: 빠르지만 재시작 시 사라집니다;
  • TTL이 있는 Redis 같은 in‑memory 캐시 — 몇 단계로 이루어진 짧은 위저드 시나리오에 적합: 1~2시간 살게 하고 이후 삭제할 수 있습니다;
  • 일반적인 SQL/NoSQL 데이터베이스 — “일주일 후에 돌아오기”, “임시저장/장바구니” 같은 시나리오에는 필수입니다.

이 강의에서는 특정 데이터베이스에 깊이 들어가지 않고, 인터페이스와 무엇을 담아야 하는지 이해에 집중합니다.

5. 가장 단순한 MCP 서버 저장소: workflowId 기반 Map

현실적으로 시작해 봅시다. MCP 서버에서 키를 workflowId로 하는 in-memory Map을 사용합니다. 학습용 데모에서는 이를 대화의 sessionId와 같게 설정해도 되지만, 프로덕션에서는 workflowId를 시나리오의 독립된 식별자로 유지하는 것이 좋습니다. 이 Map의 값은 GiftWorkflowContext입니다. 실제 프로덕션에서는 Redis나 DB로 대체하겠지만 API는 동일하게 유지됩니다.

MCP 서버가 TypeScript라고 가정합시다. 초기화 근처에 다음을 추가합니다:

// mcp/workflowStore.ts
import { GiftWorkflowContext } from "../backend/types/workflow";

const workflows = new Map<string, GiftWorkflowContext>();

export function getWorkflow(id: string): GiftWorkflowContext | undefined {
  return workflows.get(id);
}

export function saveWorkflow(ctx: GiftWorkflowContext): void {
  workflows.set(ctx.id, { ...ctx, updatedAt: Date.now() });
}

그다음은 수신자 프로필을 저장하는 도구입니다. 중요한 점은 이 도구가 workflowId와 프로필 데이터를 입력으로 받고, 내부에서 해당 컨텍스트를 생성/업데이트한다는 것입니다:

// mcp/tools/setProfile.ts
import { jsonSchema } from "@modelcontextprotocol/sdk"; // 별칭
import { getWorkflow, saveWorkflow } from "../workflowStore";

export const setProfileTool = {
  name: "gift_set_profile",
  description: "선물 수신자 프로필을 저장합니다",
  inputSchema: jsonSchema.object({
    workflowId: jsonSchema.string(),
    age: jsonSchema.number().optional(),
    gender: jsonSchema.string().optional(),
    interests: jsonSchema.array(jsonSchema.string()).optional()
  }),
  async run(input: any) {
    const existing = getWorkflow(input.workflowId);
    const ctx = existing ?? {
      id: input.workflowId,
      currentStep: "profile",
      likedIdeaIds: [],
      hiddenIdeaIds: []
    };
    ctx.profile = {
      age: input.age,
      gender: input.gender,
      interests: input.interests ?? []
    };
    ctx.currentStep = "budget";
    saveWorkflow(ctx);
    return {
      structuredContent: {
        type: "profileSaved",
        workflowId: ctx.id,
        profile: ctx.profile,
        nextStep: ctx.currentStep
      }
    };
  }
};

이 도구는 이미 두 가지를 수행합니다. 프로필을 저장하고 currentStep을 다음 단계로 진행시키는 것입니다. 실제 프로젝트에서는 “데이터 저장”과 “단계 전환”을 별도 도구로 분리하고 싶을 수 있지만, 개념 이해에는 이 정도로 충분합니다.

인수의 workflowId에 주목하십시오. 바로 이 매개변수가 도구 호출을 해당 컨텍스트에 묶어 줍니다. 클라이언트(위젯 또는 에이전트)는 어딘가에 이를 저장해 두고 계속 전달해야 합니다.

6. Apps SDK와의 연동: workflowId와 sessionId는 어디서 얻나

ChatGPT Apps에서 “workflowId를 어디서 얻나”는 약간 철학적인 질문입니다. 인증 사용 여부, MCP 직접 사용 여부, 또는 Agents SDK 사용 여부에 따라 달라집니다. 대체로 서버에서 첫 도구 호출 시 생성하거나, 위젯에서 생성해 아래로 전달하는 방식이 있습니다.

학습 예제에서는 첫 단계가 워크플로를 생성하는 MCP 도구 호출이고, 이후 위젯이 해당 id를 받아 이어서 사용한다고 가정합시다.

가장 단순한 예는 다음과 같습니다:

// mcp/tools/startWorkflow.ts
import { randomUUID } from "crypto";
import { saveWorkflow } from "../workflowStore";

export const startWorkflowTool = {
  name: "gift_start_workflow",
  description: "새 선물 추천 워크플로를 생성합니다",
  inputSchema: { type: "object", properties: {} },
  async run() {
    const id = randomUUID();
    saveWorkflow({
      id,
      currentStep: "profile",
      likedIdeaIds: [],
      hiddenIdeaIds: [],
      updatedAt: Date.now()
    });
    return {
      structuredContent: {
        type: "workflowStarted",
        workflowId: id,
        currentStep: "profile"
      }
    };
  }
};

모델은 도구의 응답에서 workflowId를 받은 뒤 다음과 같이 할 수 있습니다:

  • 컨텍스트에 숨겨진 형태로 이를 유지합니다;
  • structuredContent를 통해 위젯으로 전달하여, 위젯이 이 값을 widgetState에 저장하고 이후 도구 호출에 계속 포함하도록 합니다.

위젯 쪽 코드는 대략 다음과 같을 것입니다.

7. 위젯에서 workflowId와 로컬 UI 상태 보관

아이디어 목록 위젯이 있다고 가정해 봅시다. 이 위젯은 어떤 워크플로를 표시 중인지 알고 싶고, 컴포넌트가 언마운트되더라도 로컬 좋아요 상태를 기억하고자 합니다. 단순화된 예시는 다음과 같습니다:

// app/widgets/GiftIdeasWidget.tsx
import { useEffect, useState } from "react";

interface Idea {
  id: string;
  title: string;
}

interface WidgetProps {
  widgetId: string;
  workflowId: string;   // structuredContent에서 전달됨
  ideas: Idea[];
}

interface UiState {
  liked: string[];
}

export function GiftIdeasWidget(props: WidgetProps) {
  const [uiState, setUiState] = useState<UiState>({ liked: [] });

  useEffect(() => {
    window.openai.getWidgetState<UiState>(props.widgetId).then(saved => {
      if (saved) setUiState(saved);
    });
  }, [props.widgetId]);

  function toggleLike(id: string) {
    const exists = uiState.liked.includes(id);
    const next: UiState = {
      liked: exists
        ? uiState.liked.filter(x => x !== id)
        : [...uiState.liked, id]
    };
    setUiState(next);
    window.openai.setWidgetState(props.widgetId, next);
    // 여기에서 MCP 도구 "gift_like_idea"를 호출할 수 있습니다
  }

  return (
    <ul>
      {props.ideas.map(idea => (
        <li key={idea.id}>
          {idea.title}
          <button onClick={() => toggleLike(idea.id)}>
            {uiState.liked.includes(idea.id) ? "★" : "☆"}
          </button>
        </li>
      ))}
    </ul>
  );
}

여기서 widgetState는 UI 계층으로만 사용됩니다. 어떤 아이디어가 강조되어 있는지를 기억하죠. 바람직하게는 좋아요를 서버에도 전송해야 합니다(예: MCP 도구나 Next.js의 API 엔드포인트를 통해) — 그래야 비즈니스 계층도 사용자의 선택을 알 수 있습니다.

widgetState만으로 전체 워크플로를 구성하려 해서는 안 됩니다. 서버의 비즈니스 컨텍스트를 보조하는 추가 레이어로 사용해야 합니다.

8. 시나리오 복원: 사용자가 돌아왔을 때

이제 더 흥미로운 케이스로 넘어가 봅시다. 사용자가 ChatGPT를 닫고 몇 시간 또는 며칠 후 같은 채팅을 다시 열었습니다. 무엇이 일어나야 할까요?

이상적인 UX는 다음과 같습니다. 모델과 App이 해당 사용자의 미완료 워크플로가 있는지 인지하고, 그 컨텍스트를 불러온 뒤 “프로필과 예산을 이미 입력하셨네요. 아이디어 선택부터 계속하겠습니다.” 같은 메시지를 보여줍니다.

아키텍처적으로는 다음과 같습니다:

  1. 서버에는 어떤 userId 또는 적어도 내부 workflowId에 묶인 GiftWorkflowContext가 저장되어 있습니다.
  2. 새 요청(또는 대화 내 첫 도구 호출) 시 App은 서버에 “이 사용자에게 활성 워크플로가 있나요?”라고 묻습니다.
  3. 있다면 서버는 그것을 반환하고, 모델이 응답에 사용할 수 있도록 resume 같은 특수 플래그를 포함할 수 있습니다.

간단한 모놀리식 데모에서는 MCP 서버와 Next.js 애플리케이션이 같은 저장소(혹은 프로세스)를 공유한다고 가정할 수 있으므로, MCP의 workflowStore를 API 라우트에서도 재사용합니다.

Next.js에서는 다음과 같은 간단한 API 라우트가 될 수 있습니다:

// app/api/gift/workflow/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getWorkflow } from "@/mcp/workflowStore"; // 이 데모에서는 MCP와 Next.js가 동일한 저장소를 공유합니다

export async function GET(req: NextRequest) {
  const id = req.nextUrl.searchParams.get("workflowId");
  if (!id) return NextResponse.json({ error: "Missing workflowId" }, { status: 400 });

  const ctx = getWorkflow(id);
  if (!ctx) return NextResponse.json({ exists: false });

  return NextResponse.json({
    exists: true,
    context: ctx
  });
}

위젯(또는 MCP 도구)은 상태를 갱신해야 할 때(예: 첫 마운트 시나 단계 전환 시) 이 엔드포인트를 호출할 수 있습니다. 학습 구성에서는 workflowId + Map 저장소면 충분합니다. 실제 프로덕션에서는 여기에 인증과 사용자 소유권 검증을 추가해야 합니다.

Agents SDK나 더 복잡한 오케스트레이션을 사용한다면 이 아이디어를 “체크포인트”로 확장할 수 있습니다. 큰 단계의 끝에서 상태를 저장하고, 에이전트가 재시작 시 그 지점부터 이어가는 방식입니다. 하지만 이는 다음 모듈의 주제입니다.

9. 앞뒤 이동과 단계 히스토리

“이전 단계로 돌아갈 수 있나요?”라는 질문은 필연적으로 나옵니다. 사용자는 예산을 수정하고, 관심사를 고치고, 추천에서 불필요한 상품을 제거하고 싶어합니다.

기술적으로는 두 가지를 의미합니다:

  • 현재 단계뿐 아니라 내려진 결정의 히스토리도 저장해야 합니다;
  • 되돌린 후 파생 데이터를 신중하게 재계산해야 합니다.

한 가지 방법은 컨텍스트에 단계 스냅샷을 담는 history 필드를 추가하는 것입니다. 예를 들면:

export interface StepSnapshot {
  step: GiftWorkflowStep;
  payload: any;          // 해당 단계의 구체 데이터
  createdAt: number;
}

export interface GiftWorkflowContext {
  // ...이전 필드
  history: StepSnapshot[];
}

사용자가 프로필을 작성하면 step"profile"인 스냅샷을 히스토리에 추가합니다. 예산을 변경하면 또 하나의 스냅샷을 추가합니다. 프로필로 되돌릴 때는 다음을 수행합니다:

  • currentStep = "profile"로 업데이트;
  • 필요하면 히스토리를 해당 인덱스까지 잘라냄;
  • 파생 값 재계산(예: 예산에 의존한다면 아이디어와 좋아요를 초기화).

모델 레벨에서는 동기화가 중요합니다. 사용자가 위젯에서 “뒤로” 버튼을 눌렀다면, 비즈니스 컨텍스트를 업데이트하고 응답에서 새 상태를 명시적으로 돌려주는 도구 호출을 보내야 합니다. 그렇지 않으면 UI는 2단계를, 모델은 3단계를 보여주는 전형적인 불일치가 발생합니다.

위젯 레벨에서의 롤백은 단순한 버튼처럼 보일 수 있습니다:

async function goBackToProfile() {
  await fetch("/api/gift/workflow/back", {
    method: "POST",
    body: JSON.stringify({ workflowId, targetStep: "profile" })
  });
  // UI를 갱신하고 로컬 상태를 정리합니다
}

서버가 컨텍스트에서 무엇을 정리할지, 그리고 도구 응답을 통해 모델에게 어떤 메시지를 보낼지 결정하면 됩니다.

10. 모델과 연결하기: 추론을 위한 컨텍스트

우리가 상태로 하는 모든 작업은 결국 사용자뿐 아니라 LLM에도 필요합니다. 모델은 다음을 이해해야 합니다:

  • 이미 알려진 사실(예: 수신자 프로필과 예산);
  • 이미 진행한 단계;
  • 미완료 프로세스가 있는지 여부.

이 정보를 모델에 전달하는 방법은 App 아키텍처에 따라 달라집니다. system 프롬프트에 주입하거나, ToolOutput으로 구조화된 형태로 반환하거나, SDK가 지원한다면 _meta/annotations 같은 특수 필드를 사용할 수 있습니다.

전형적인 패턴은 다음과 같습니다:

  1. MCP 도구가 structuredContent에 컨텍스트 요약(현재 단계, 핵심 필드, 필요시 workflowId)을 담아 반환합니다.
  2. Apps SDK는 이를 위젯이나 텍스트 + 숨김 데이터로 변환합니다.
  3. 모델은 structuredContent를 보고 시나리오가 이어졌음을 이해하고, 그에 따라 다음 행동을 계획합니다.

가끔 모델이 중요한 매개변수를 “잊거나” 환각을 시작하면, 컨텍스트를 강제로 갱신할 수 있습니다. 최신 상태를 반환하는 전용 도구를 호출하면 모델이 “다시 컨텍스트에 들어옵니다”.

전체 GiftWorkflowContext를 마지막 필드까지 모델에 쏟아 넣으려 하지 마십시오. 핵심만으로 충분합니다. 누구를 위한 선물인지, 예산은 얼마인지, 몇 개의 아이디어를 이미 보여줬는지, 미완료 결제가 있는지 등입니다.

11. WorkflowContext 설계를 위한 미니 체크리스트

흔한 오류로 넘어가기 전에, 워크플로 컨텍스트를 설계할 때 스스로 답해야 하는 작은 질문 목록을 정리해 둡시다(인터페이스 옆에 메모해도 좋습니다):

  • 시나리오에 어떤 단계가 있으며 각 단계에 필요한 최소 데이터는 무엇인가?
    불필요한 거대한 JSON 괴물을 예방합니다.
  • 하나의 채팅 범위에서만 기억하면 되는 것과, 세션/기기 간에도 유지해야 하는 것은 무엇인가?
    전자는 widgetState와 프롬프트에 남겨둘 수 있고, 후자는 반드시 서버 DB에 저장해야 합니다.
  • 컨텍스트 식별자는 어떻게 생길 것인가?
    userId + scenario 조합, 별도의 workflowId, 혹은 둘 다일 수 있습니다. 중요한 것은 데이터베이스에서 컨텍스트를 단일하게 찾을 수 있어야 한다는 점입니다.
  • 오래된 워크플로는 어떻게 정리할 것인가?
    데모에서는 “정리 안 함”이 허용될 수 있지만, 프로덕션에서는 TTL이나 오래된 워크플로를 삭제하는 백그라운드 작업이 필요합니다.
  • 사용자에게 뒤로 가기가 필요한가? 어떻게 구현할 것인가?
    분기 트리를 저장할지, 선형 단계 목록만으로도 충분한지, 되돌리기 방식을 결정합니다.

마지막으로 “사용자가 일주일 후 다른 채팅에서 돌아왔다”는 시나리오를 머릿속으로 돌려보세요. App이 예전 워크플로를 어떻게 인지하고 무엇을 보여줄지 설명할 수 없다면, 영속 저장 부분을 강화해야 합니다.

12. 단계 간 컨텍스트 처리 시 흔한 오류

오류 1: 모든 것을 대화 기록에만 저장한다.
때로는 “모델이 텍스트에서 다 보니까 매번 프롬프트에 예산, 상품, 사용자가 선택한 것들을 나열하자”라는 유혹이 생깁니다. 이런 접근은 곧 컨텍스트 한계에 부딪히고, 무결성 보장을 전혀 하지 못합니다. 모델은 중요한 사실을 “잊거나” 식별자를 헷갈릴 수 있습니다. 비즈니스 크리티컬(돈, 예약, 주문)한 것들은 여러분의 backend/MCP에 진실 소스로 존재해야 합니다.

오류 2: 전체 워크플로를 widgetState만으로 구성하려 한다.
Apps SDK의 widgetState는 위젯 언마운트/리마운트 사이에 UI 상태를 살아남게 하는 장치이지, 워크플로를 장기 보관하는 수단이 아닙니다. 여기에 프로필, 장바구니, 단계 히스토리를 보관하려 들면, 기기 변경 시 혼란과 장기 복원의 불가능으로 이어집니다. 위젯은 시각적 편의와 로컬 컴포트에 책임이 있습니다. 시나리오 로직은 서버에 있어야 합니다.

오류 3: 명시적인 workflowId 또는 다른 키가 없다.
개발자가 conversation_id 같은 암시적 식별자에 의존하면서 자체 워크플로 개념을 도입하지 않는 경우가 있습니다. 그 결과, 시나리오를 구분할 수 없고, 여러 개의 병렬 워크플로를 나눌 수 없으며, 필요한 정확한 워크플로를 복원하기 어려워집니다. MCP처럼 프로토콜상 stateless한 환경에서는 도구와 API 엔드포인트 어디에서든 간단한 문자열 workflowId를 포함하는 것만으로도 많은 문제가 해결됩니다.

오류 4: UI 상태와 비즈니스 로직을 뒤섞는다.
전형적인 상황은 widgetState에 “어떤 탭이 열려 있는가”뿐 아니라 “장바구니에 어떤 상품이 있는가”까지 넣어두고, 서버에서 그 상태를 기반으로 의사결정을 하려는 것입니다. 그러면 아주 작은 비동기 타이밍 이슈만으로도(위젯은 그려졌지만 요청은 아직, 혹은 그 반대) 모델, UI, 데이터베이스가 서로 다른 현실을 보게 됩니다. 책임 경계는 명확해야 합니다. 서버는 비즈니스 데이터를 저장하고 검증하며, 위젯은 이를 표시하고 사용자가 쉽게 변경하도록 도와줍니다.

오류 5: 복원 및 롤백 시나리오가 없다.
사용자가 단계별로 이상적으로 진행하고, 아무 것도 실패하지 않고, ChatGPT가 재시작되지 않으며, 네트워크가 끊기지 않는 “행복 경로”를 그리기는 쉽습니다. 현실에서는 각 단계가 실패할 수 있고, 사용자는 중간에 떠났다가 일주일 후 돌아올 수 있습니다. WorkflowContext 구조를 설계하지 않았고, “활성” 워크플로를 어떻게 찾을지 고민하지 않았으며, “뒤로”와 “나중에 계속” 버튼을 고려하지 않았다면, 여러분의 시나리오는 취약하고 사용자에게 불편할 것입니다. 잘 설계된 컨텍스트는 내고장성의 기반이며, 이에 대해서는 다음 강의에서 다룹니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION