1. ChatGPT App에서 무엇을 테스트하고 무엇을 테스트하지 않는가
클래식 웹 애플리케이션은 단순합니다: UI → 백엔드 → DB. 함수에는 unit 테스트, API에는 통합 테스트, 전체 사용자 플로우에는 E2E를 작성합니다.
ChatGPT App에서는 그림이 조금 더 복잡합니다:
사용자 ↔ ChatGPT UI ↔ 위젯(Apps SDK, React)
↘
MCP 서버 (tools/resources)
↘
ACP / 백엔드 / 외부 API
ChatGPT 내부의 모델이 언제 여러분의 suggest_gifts를 어떤 인자로 호출할지, MCP에서 온 structuredContent를 어떻게 렌더링할지, 그리고 언제 위젯을 보여줄지를 결정합니다.
테스트 관점에서는 세계를 두 층으로 나눠 두는 것이 편합니다:
- Infrastructure tests — 이 강의에서 다루는 부분입니다. 우리는 다음을 확인합니다:
- 사용자 클릭 시 위젯 코드가 깨지지 않는지;
- MCP 도구가 스키마에서 약속한 형식대로 데이터를 받고 반환하는지;
- ACP 엔드포인트와 웹훅이 살아 있고, 대표적인 JSON에서 실패하지 않는지.
- AI behavior evals — 모듈 20에서 다룹니다. 이때는 모델이 무엇을 답하는지, 설명이 적절한지, 의미에 맞게 선물을 고르는지, 환각은 없는지를 봅니다.
오늘의 러프한 공식:
“LLM 자체가 아니라 LLM 주변을 테스트한다”.
그래서 커리큘럼에서도 이 주제에서 별도로 강조합니다: “우리는 GPT의 답변을 문자 그대로 테스트하지 않고, 그 주변 인프라와 데이터 계약을 테스트한다”.
복잡도를 줄이기 위해 GiftGenius에 간단한 “테스트 피라미드”를 적용합니다.
graph TD A["Unit 테스트
utils, tools의 비즈니스 로직"] --> B[Contract 테스트
Zod/JSON Schema, webhooks] B --> C[E2E / UI 테스트
ChatGPT 없이 위젯 + MCP] C --> D["CI의 Smoke
“진짜 살아 있나?”"] style A fill:#e0f7fa,stroke:#00838f,stroke-width:1px style B fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px style C fill:#fff3e0,stroke:#ef6c00,stroke-width:1px style D fill:#ffebee,stroke:#c62828,stroke-width:1px
이제 각 레벨을 살펴보며 학습용 GiftGenius에 테스트를 추가해 봅시다. 마지막에는 ChatGPT App 테스트에서 자주 보이는 전형적인 실수 체크리스트를 정리합니다.
2. Unit 테스트: GiftGenius를 작은 조각으로 나누기
ChatGPT App에서 unit은 무엇인가
우리 스택에서 unit 테스트는 작고 고립된 로직 조각을 검증하는 것입니다. 실제 네트워크나 DB 없이, 가능하면 MCP 프레임워크 호출도 없이 진행합니다.
GiftGenius에서는 예를 들어 다음과 같습니다:
- ‘선물 관련성’ 점수를 계산하는 함수;
- 가격이 없거나 통화가 맞지 않는 상품을 제거하는 필터;
- 통화 변환기;
- ‘원시’ 상품 객체를 UI의 GiftCardProps로 매핑하는 매퍼.
가능하면 MCP 도구의 로직도 분리하는 것이 좋습니다. MCP 라우트 핸들러는 얇은 래퍼로 두고, 비즈니스 로직이 담긴 순수 함수를 호출하게 합니다. unit 테스트에서는 그 순수 함수를 테스트합니다.
예시: 선물 랭킹 함수
가격 범위와 인기도를 바탕으로 ‘점수’를 매기는 유틸리티 scoreGift가 있다고 해봅시다:
// src/lib/scoreGift.ts
export type Gift = {
id: string;
price: number;
popularity: number; // 0..1
};
export function scoreGift(gift: Gift, maxPrice: number): number {
if (gift.price > maxPrice) return 0;
const priceScore = 1 - gift.price / maxPrice;
return Math.round((priceScore * 0.6 + gift.popularity * 0.4) * 100);
}
Jest로 unit 테스트를 작성합니다(Vitest도 거의 동일합니다):
// src/lib/scoreGift.test.ts
import { scoreGift } from './scoreGift';
test('scoreGift는 비싼 선물의 점수를 낮춘다', () => {
const cheap = { id: 'c', price: 50, popularity: 0.5 };
const expensive = { id: 'e', price: 100, popularity: 0.5 };
const max = 100;
const cheapScore = scoreGift(cheap, max);
const expensiveScore = scoreGift(expensive, max);
expect(cheapScore).toBeGreaterThan(expensiveScore);
});
여기서는 기본적인 “Arrange–Act–Assert”가 보입니다(데이터 준비 → 함수 호출 → 결과 검증). 이는 더 복잡한 테스트에서도 권장되는 구조적 접근입니다.
MCP 핸들러에서 비즈니스 로직 분리
지금 여러분의 코드에 아마 다음과 비슷한 것이 있을 겁니다:
// app/mcp/route.ts — 크게 단순화
import { createMcpServer } from '@modelcontextprotocol/sdk';
import { scoreGift } from '@/lib/scoreGift';
server.tool('suggest_gifts', {
// ...
handler: async ({ input }) => {
const gifts = await fetchFromCatalog(input);
const scored = gifts
.map(g => ({ ...g, score: scoreGift(g, input.maxPrice) }))
.sort((a, b) => b.score - a.score);
return { gifts: scored.slice(0, 10) };
},
});
scoreGift에 대한 unit 테스트는 이미 작성했지만, “선물 목록을 받아 상위 10개를 점수 순으로 반환하는 함수” 자체도 테스트하고 싶습니다. 이를 별도 모듈로 뽑아보겠습니다:
// src/lib/rankGifts.ts
import { scoreGift, Gift } from './scoreGift';
export function rankGifts(gifts: Gift[], maxPrice: number) {
return gifts
.map(g => ({ ...g, score: scoreGift(g, maxPrice) }))
.sort((a, b) => b.score - a.score)
.slice(0, 10);
}
그리고 테스트:
// src/lib/rankGifts.test.ts
import { rankGifts } from './rankGifts';
test('rankGifts는 score 내림차순 상위 10개 선물만 반환한다', () => {
const gifts = Array.from({ length: 20 }, (_, i) => ({
id: `g${i}`,
price: 10 + i,
popularity: 0.5,
}));
const result = rankGifts(gifts, 100);
expect(result).toHaveLength(10);
expect(result[0].score).toBeGreaterThanOrEqual(result[9].score);
});
이런 unit 테스트는 빠르고 비용이 적으며 즉각적인 피드백을 제공합니다. 그래서 MCP 서비스의 “테스트 피라미드 넓은 기반”으로 권장됩니다.
MCP 도구용 unit 테스트: 외부 API 모킹
흔한 실수는 실제 HTTP 요청(카탈로그, Stripe 등)까지 포함해 MCP 도구 핸들러를 “unit 테스트”하려는 것입니다. 이런 테스트는 느리고 취약해집니다.
가장 좋은 방법은 핸들러에는 ‘연결(wiring)’만 남기고, 복잡한 로직은 이미 별도로 테스트한 함수로 빼는 것입니다. 그래도 핸들러 자체를 꼭 테스트하고 싶다면, 의존성을 모킹하세요. 이는 MCP 테스트 관련 심층 가이드에서도 권장하는 접근으로, 도구 핸들러에서 외부 API를 모킹하라는 것입니다.
3. Contract 테스트: Zod/JSON Schema를 모델과 ACP의 ‘계약’으로
우리 컨텍스트에서 contract 테스트란
유닛 로직은 통제했습니다. 이제 피라미드의 다음 층 — 서비스들이 JSON 계약을 통해 여전히 서로를 이해하는지 확인해야 합니다. 이것이 바로 contract 테스트입니다.
계약 테스트는 데이터를 주고받는 두 측이 여전히 서로를 이해하는지를 확인합니다. 내부 알고리즘이 아니라 JSON의 형태와 의미(필드, 타입, 필수 여부)에 집중합니다.
ChatGPT App에는 이런 계약이 많습니다:
- ChatGPT ↔ MCP: MCP 도구의 inputSchema와 outputSchema.
- MCP ↔ commerce API(ACP): create_checkout_session 요청 형식과 응답 구조.
- ACP ↔ 우리 백엔드(웹훅): order.created, payment_failed 등.
스키마를 바꿨지만 코드를 업데이트하지 않거나(또는 그 반대) 하면 조용한 단절이 생깁니다. 모델은 예전 JSON을 보내는데 코드는 새 필드를 기다리게 되어 런타임에 실패합니다. 이런 상황을 contract 테스트가 프로덕션 전에 잡아줘야 합니다.
Zod를 단일한 진실의 근원으로
JavaScript/TypeScript 생태계에서는 MCP와 함께 이미 사용한 Zod가 매우 적합합니다. SDK가 Zod 스키마를 JSON Schema로 변환해 도구 선언에 활용할 수 있습니다.
예를 들어, 선물과 추천 결과의 스키마를 정의해 봅시다:
// src/schemas/gift.ts
import { z } from 'zod';
export const GiftSchema = z.object({
id: z.string(),
title: z.string(),
price: z.number().nonnegative(),
currency: z.string().length(3),
url: z.string().url(),
});
export const SuggestGiftsResultSchema = z.object({
gifts: z.array(GiftSchema).min(1),
});
코드용 타입은 z.infer로 얻습니다:
export type Gift = z.infer<typeof GiftSchema>;
export type SuggestGiftsResult = z.infer<typeof SuggestGiftsResultSchema>;
이것만으로도 일종의 컴파일 타임 contract 테스트가 됩니다. 어디선가 currency: 123를 할당하려 하면 TypeScript가 “string이어야 한다”고 알려줄 것입니다.
스키마의 런타임 contract 테스트
실제(또는 실제에 가까운) 데이터 예제를 스키마로 검증하는 런타임 테스트는 더 강력한 보호막을 제공합니다.
// src/schemas/gift.test.ts
import { GiftSchema, SuggestGiftsResultSchema } from './gift';
test('GiftSchema는 유효한 상품을 허용한다', () => {
const sample = {
id: '123',
title: '고양이 머그컵',
price: 19.99,
currency: 'USD',
url: 'https://example.com/gift/123',
};
expect(() => GiftSchema.parse(sample)).not.toThrow();
});
test('SuggestGiftsResultSchema는 빈 선물 목록을 거부한다', () => {
const badResult = { gifts: [] };
expect(() => SuggestGiftsResultSchema.parse(badResult)).toThrow();
});
왜 중요한가:
- 프롬프트/문서에 모델용 JSON 예제를 보여준다면, 그 예제를 그대로 이런 테스트에 넣어 “예제가 거짓말하지 않음”을 보장할 수 있습니다;
- 스키마를 바꾸면(예: url을 필수로 변경) 즉시 오래된 예제와 픽스처가 더 이상 유효하지 않음을 테스트가 알려줍니다.
Apps SDK 공식 권장사항에서도 structured content가 선언된 outputSchema를 준수해야 모델이 이해할 수 있다고 강조합니다. 스키마 테스트는 이러한 불일치를 막는 1차 방어선입니다.
웹훅과 ACP의 계약
같은 원칙을 웹훅과 ACP 엔드포인트에도 적용합니다. 예를 들어 OrderCreated가 있다고 합시다:
// src/schemas/acp.ts
import { z } from 'zod';
export const OrderCreatedSchema = z.object({
id: z.string(),
userId: z.string(),
totalAmount: z.number(),
currency: z.string().length(3),
status: z.literal('created'),
});
테스트:
// src/schemas/acp.test.ts
import { OrderCreatedSchema } from './acp';
test('OrderCreatedSchema는 웹훅 샘플을 검증한다', () => {
const sample = {
id: 'ord_1',
userId: 'user_42',
totalAmount: 59.99,
currency: 'USD',
status: 'created',
};
expect(() => OrderCreatedSchema.parse(sample)).not.toThrow();
});
웹훅 핸들러에서는 가장 먼저 OrderCreatedSchema.parse(body)를 수행해, 이후에는 유효한 객체만 다룬다는 확신을 가질 수 있습니다.
OpenAI의 App 회귀 체크리스트에서도 앱이 발전함에 따라 스키마를 항상 최신으로 유지할 것을 권장합니다. Contract 테스트는 이를 잊지 않도록 보장해 줍니다.
4. 위젯 테스트와 ‘준 E2E’: chatgpt.com 없이 진행하기
Unit 테스트는 로직을, contract 테스트는 서비스 간 데이터 형식을 지켜줍니다. 하지만 피라미드는 여기서 끝이 아닙니다. 위젯과 MCP를 거치는 사용자 경로가 하나의 전체로 실제 동작하는지도 확인해야 합니다. ChatGPT App에서는 이를 ‘준 E2E’ 형식으로 진행합니다.
왜 Playwright로 ChatGPT를 직접 돌리면 안 되는가
직관적으로는 이렇게 생각할 수 있습니다: https://chatgpt.com을 열어 위젯을 띄우고 Playwright로 “선물 고르기 → 결제하기” 전체 시나리오를 돌리면 ‘진짜 E2E’ 아니냐고요.
안 됩니다.
문제점:
- chatgpt.com에 대한 자동화 실행은 ToS를 위반합니다;
- (Cloudflare, 2FA 등) 보호 장치가 CI의 봇을 매우 싫어합니다;
- 모델의 행동이 가변적입니다. 오늘은 suggest_gifts를 호출하다가도, 내일은 텍스트 답변만 할 수 있습니다.
따라서 ChatGPT App의 E2E 테스트는 더 넓게 해석합니다. 즉, 우리 애플리케이션 내부의 전체 경로 — 위젯 + MCP + ACP — 를 실제 ChatGPT UI와 실제 모델 없이 테스트합니다.
자세한 가이드에서도 제안하는 전략은 같습니다. MCP 서버는 headless 클라이언트로 별도 테스트하고, 위젯은 window.openai를 모킹한 “테스트 호스트”에서 테스트합니다.
React 컴포넌트로서의 위젯 테스트
기본 옵션은 React Testing Library입니다. 우리가 할 일:
- GiftGeniusWidget 컴포넌트를 렌더링합니다.
- 필요한 메서드(callTool, openExternal 등)가 있는 가짜 window.openai를 주입합니다.
- 사용자인 척합니다: 버튼 누르기, 텍스트 입력 등.
- callTool이 올바른 인자로 호출되었고 UI가 예상 결과를 보여주는지 확인합니다.
간단한 위젯이 있다고 가정해 봅시다:
// src/app/GiftGeniusWidget.tsx
'use client';
import React from 'react';
export function GiftGeniusWidget() {
const [loading, setLoading] = React.useState(false);
async function handleClick() {
setLoading(true);
await (window as any).openai.callTool('suggest_gifts', {
occasion: 'birthday',
});
setLoading(false);
}
return (
<div>
<button onClick={handleClick}>선물 추천하기</button>
{loading && <p>잠시만요, 아이디어를 모으는 중…</p>}
</div>
);
}
테스트:
// src/app/GiftGeniusWidget.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { GiftGeniusWidget } from './GiftGeniusWidget';
test('버튼이 window.openai.callTool을 통해 suggest_gifts를 호출한다', async () => {
const callToolMock = vi.fn().mockResolvedValue({});
(window as any).openai = { callTool: callToolMock };
render(<GiftGeniusWidget />);
const button = screen.getByText('선물 추천하기');
await fireEvent.click(button);
expect(callToolMock).toHaveBeenCalledWith('suggest_gifts', {
occasion: 'birthday',
});
});
여기서는 실행 환경을 완전히 통제합니다:
- 실제 ChatGPT 없음;
- 네트워크 없음;
- “UI → window.openai” 연결을 검증하는 깔끔하고 빠른 테스트.
Apps SDK 문서에서도 권장합니다. 위젯 테스트 시 window.openai를 모킹해 실제 환경에 의존하지 않도록 하라고요.
Playwright로 하는 E2E-light: Next.js + MCP
다음 레벨에서는 로컬에서 Next.js 애플리케이션(Dev Mode처럼)을 띄우되, ChatGPT를 통해서가 아니라 테스트 브라우저에서 직접 접근합니다.
확인할 가치가 있는 시나리오:
- /widget(또는 프로젝트 구조에 따라 /) 페이지를 엽니다.
- 최소한의 단계만 흉내냅니다: 선물 타입 선택, “아이디어 보기” 버튼 클릭.
- 위젯이 선물 카드들을 표시했는지 확인합니다.
- (선택) 카드 클릭 → “결제로 이동” 클릭 → ACP 모킹이 성공을 반환하는지 확인.
Playwright 테스트 미니 예시:
// tests/e2e/gift-flow.spec.ts
import { test, expect } from '@playwright/test';
test('사용자는 선물을 선택하고 결과를 볼 수 있다', async ({ page }) => {
await page.goto('http://localhost:3000/widget');
await page.click('text=생일 선물');
await page.click('text=추천');
await page.waitForSelector('[data-testid="gift-card"]');
const cards = await page.locator('[data-testid="gift-card"]').all();
expect(cards.length).toBeGreaterThan(0);
});
실제 프로젝트에서는 다음이 추가됩니다:
- Playwright의 beforeAll에서 npm run dev 또는 별도의 test-server를 띄우기;
- 프로덕션 서비스를 건드리지 않도록 MCP/ACP 모킹.
이 정도 단순한 시나리오만으로도 위젯과 MCP 사이의 전형적인 “단선”들을 잡을 수 있습니다. 잘못된 URL, CORS 오류, 부정확한 structuredContent 등.
5. CI의 Smoke 테스트: “이게 아예 올라오긴 하는가” 확인
피라미드의 맨 위, 가장 가벼운 층은 smoke 테스트입니다. E2E-light처럼 전체 시나리오를 검증하진 않고, 배포 전에 애플리케이션이 살아 있고 올라오는지만 확인합니다.
Smoke vs 풀 E2E
수동 smoke 테스트는 2번째 모듈에서 이미 언급했습니다. 그때는 첫 “Hello GiftGenius”를 띄우고, 위젯이 렌더링되는지, ChatGPT가 보이는지, 버튼이 링크를 여는지를 확인했죠. 목표는 Dev Mode + 터널 + Apps SDK 설정이 올바른지 확인하는 것이었습니다.
이번 과제는 비슷하지만, 자동화되어 있고 CI에서 수행됩니다:
- 모든 사용자 시나리오를 모사하려고 하지 않습니다;
- 실제 ChatGPT와 통신하지 않습니다;
- 그저 다음만 확인합니다:
- Next.js 앱이 기동되는지;
- MCP 서버가 최소한 tools/list / tools/call에 응답하는지;
- ACP 엔드포인트가 테스트 JSON에 200을 반환하는지.
프로덕션 배포나 Store 제출 전 특히 중요합니다. “아예 안 올라온다”를 CI에서 잡는 편이 사용자에게서 듣는 것보다 훨씬 낫습니다.
MCP 도구용 smoke 테스트 예시
테스트에서 MCP 서버를 띄우거나 SDK의 MCP 클라이언트를 사용하는 헬퍼 모듈이 있다고 가정해 봅시다. 테스트의 개념은 다음과 같습니다:
// tests/smoke/mcp-tools.smoke.test.ts
import { createTestMcpClient } from './testClient';
test('MCP가 tools.list와 tools.call(suggest_gifts)에 응답한다', async () => {
const client = await createTestMcpClient(); // 서버를 띄우거나 연결
const tools = await client.listTools();
expect(tools.some(t => t.name === 'suggest_gifts')).toBe(true);
const result = await client.callTool('suggest_gifts', {
occasion: 'birthday',
budget: { currency: 'USD', max: 50 },
});
expect(result.gifts.length).toBeGreaterThan(0);
});
MCP 테스트 심화 자료에서도 정확히 이런 방식을 권장합니다. 테스트에서 MCP 클라이언트를 사용해 JSON‑RPC 전체 사이클 — list → call → 응답 — 을 검증하라고요.
createTestMcpClient 구현은 유틸로 감출 수 있습니다. 같은 프로세스에서 서버를 띄우거나, 이미 실행 중인 인스턴스에 연결합니다.
ACP/checkout용 smoke 테스트
실제 결제를 모사하지 않고도 commerce 레이어용 간단한 테스트를 작성할 수 있습니다:
// tests/smoke/acp.smoke.test.ts
import fetch from 'node-fetch';
test('ACP test-intent가 200을 반환한다', async () => {
const res = await fetch('http://localhost:3000/api/acp/test-intent', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
amount: 10,
currency: 'USD',
}),
});
expect(res.ok).toBe(true);
});
여기서 test-intent가 무엇을 하는지는 중요하지 않습니다. DB 접근만 확인하고 {"status":"ok"}를 반환해도 됩니다. 중요한 건 CI가 다음을 잡아낸다는 점입니다:
- 빠뜨린 env 키;
- 깨진 라우트;
- 서툰 JSON 파싱.
최소한의 CI 파이프라인
CI/CD 상세는 배포 모듈에서 다루겠지만, 기본 파이프라인은(예: GitHub Actions) 이렇게 구성할 수 있습니다:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main ]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm test # unit + contract
- run: npm run test:e2e # e2e/ui
- run: npm run test:smoke # smoke mcp/acp
npm run test:e2e와 npm run test:smoke는 내부에서 dev 서버를 띄우고 준비 완료를 기다린 뒤 Playwright / Node 스크립트를 실행하도록 구성할 수 있습니다.
6. GiftGenius를 위한 미니 테스트 지도
혼란을 피하기 위해 각 레벨에서 무엇을 테스트하고 어떤 질문에 답하는지 표로 정리합니다.
| 레벨 | GiftGenius 예시 | 도구 | 무엇을 검증하나 |
|---|---|---|---|
| Unit | scoreGift, rankGifts, 예산 유효성 검사기 | Jest / Vitest | 로직이 올바르게 계산되는가? |
| Contract (schemas) | Zod 스키마 Gift, SuggestGiftsResult, OrderCreated | Zod, AJV | 여전히 GPT/ACP와 같은 JSON “언어”로 대화하는가? |
| UI/Component | 클릭 시 위젯 동작, window.openai.callTool 호출 | React Testing Library | UI가 올바른 동작을 트리거하는가? |
| E2E‑light | 사용자가 선물 선택 플로우를 통과하고 카드를 본다 | Playwright/Cypress | GiftGenius의 모든 부분이 하나의 작동 플로우로 모이는가? |
| CI의 Smoke | MCP가 tools.list/call에 응답, ACP test-intent 200 | Node 스크립트, MCP client | 애플리케이션이 살아 있고 연결되었는가? |
이 조합이 바로 모듈 계획에서 말한 ChatGPT App을 위한 “최소 실행 가능 테스트 세트”입니다. 엔터프라이즈급 QA 팀 없이도, 프로덕션이 사소한 변화마다 무너지지 않도록 하는 기본 보증을 제공합니다.
7. ChatGPT App 테스트에서 흔한 실수
오류 №1: 모델의 답변을 결정론적으로 테스트하려 한다.
가끔 개발자들이 “여기 선물 아이디어 5가지” 같은 정확한 문자열 응답을 기대하는 테스트를 쓰곤 합니다. 이런 테스트는 본질적으로 취약합니다. 모델은 문구를 그대로 반복할 의무가 없고, 모델 자체가 업데이트될 수도 있습니다. 이 모듈에서는 답변 내용은 전혀 건드리지 않습니다 — 도구 호출, 스키마 유효성, 플로우의 안정성만 확인합니다. 텍스트 품질 평가는 별도의 영역(모듈 20, LLM evals)입니다.
오류 №2: MCP 스키마에 대한 contract 테스트 부재.
Zod 스키마를 한 번 작성하고 잊어버리기 쉽습니다. 그러다 도구 결과에 discount 필드를 추가하면서 코드는 업데이트했지만 스키마를 갱신하지 않는 경우가 생깁니다. 모델은 예전 포맷을 계속 보내고, 코드는 새 필드를 기대해 프로덕션에서 알 수 없는 오류가 나기 시작합니다. Zod/JSON Schema 기반 contract 테스트는 이런 “조용한” 고장을 예방하므로, 이를 무시하는 것은 흔하지만 매우 아픈 실수입니다.
오류 №3: CI에서 chatgpt.com을 대상으로 E2E를 시도한다.
누군가는 여전히 시도합니다. 실제 ChatGPT를 상대로 Playwright를 돌려 로그인하고 UI를 클릭합니다 — 그리고 Cloudflare 차단, 불안정한 테스트, 약관 위반 가능성만 남습니다. 올바른 길은 Next.js 호스트와 MCP를 격리해 테스트하고, window.openai와 외부 API를 모킹하는 것입니다. 이는 Apps SDK와 MCP 가이드에서도 권장합니다.
오류 №4: E2E만 쓰고 unit 레벨을 잊는다.
애플리케이션의 절반을 클릭하는 “거대한” E2E 테스트 하나만 있고 unit 테스트가 0인 프로젝트를 보곤 합니다. 이런 접근은 잘못된 안전감을 줍니다. 테스트는 초록/빨강 중 하나일 뿐, 원인 국소화는 거의 불가능하고, 매 실행에 몇 분이 걸립니다. 수십 개의 빠른 순수 함수용 unit 테스트와 핵심 경로에 대한 몇 개의 깔끔한 E2E-light가 훨씬 효과적입니다.
오류 №5: 일반 테스트에서 실제 외부 API를 사용한다.
Stripe, 외부 카탈로그, CRM 등은 통제된 환경의 통합 테스트에는 좋지만, 일상적인 npm test에는 부적절합니다. 테스트가 네트워크, 타사 rate limit, 타인의 프로덕션 서버에 의존하면, 여러분의 코드와 무관한 이유로 실패합니다. 최선은 외부 API를 모킹(nock, msw 등)하고, 별도의 특수 환경에서만 몇 가지 “실제” 확인을 두는 것입니다.
오류 №6: 배포 전에 smoke 테스트를 잊는다.
기능을 합치고, MCP 스키마를 업데이트하고, UI를 고치고, “Deploy”를 눌렀습니다 — 그런데 누군가 next.config를 망가뜨렸거나 .env를 지워서 Next.js가 기동하지 않습니다. 자동화된 smoke 테스트가 없으면 CI는 이런 명백한 실패를 프로덕션까지 통과시킵니다. “서버가 올라왔는지”, “MCP가 기본 호출에 응답하는지”, “ACP 테스트 엔드포인트가 200을 주는지”를 확인하는 간단한 smoke 스위트 하나가, 전장 디버깅 시간을 절약하고 스트레스를 크게 줄여줍니다.
오류 №7: 초기 단계에 테스트 환경을 과도하게 복잡하게 만든다.
대기업의 모범 사례에 고무되어, 여러 환경, 데이터 생성이 수반된 복잡한 계약 테스트, 부하 시나리오 등을 한 번에 도입하고 싶을 수 있습니다. 하지만 그러다 팀은 몇 주를 인프라에 쓰고 기능 출시를 멈추게 됩니다. ChatGPT App의 시작에는 우리가 말한 “Sanity Suite” — unit + contract + 두어 개의 E2E-light + CI의 smoke — 만으로 충분합니다. 이후 트래픽과 요구가 늘어나면 그에 맞춰 발전시키면 됩니다.
GO TO FULL VERSION