본문으로 건너뛰기

사주고사 제작기 12. 시나리오 80개 확장과 Edge 번들 초과

·11 min read·12 / 13

시나리오를 40개에서 80개로 늘렸다. 콘텐츠만 두 배가 된 게 아니라 배포가 막히고 콘솔에 경고가 뜨고 API가 크래시했다.

다양성 축을 정해놓고 늘렸다

캐릭터 40개로는 재방문할 때 금방 본 시나리오를 다시 만난다. 그냥 숫자만 채우면 비슷한 인물이 늘어날 뿐이라 축을 먼저 정했다.

기존신규 확장
오행주요 조합 위주희귀 조합 (화와 금 충돌, 수와 금 공존 등)
시대1955~20051945~2010 (해방세대부터 알파세대까지)
직업예술·IT·사업 중심군인, 외과의, 바둑, 프로게이머, 형사, 셰프, 번역가
궤적5가지 패턴권력 추락, 귀농, PTSD 회복, 자수성가, 윤리적 딜레마

배치 파일 4개 단위로 서브에이전트 10개를 병렬로 돌렸다. 각 배치가 pnpm type-check를 통과한 뒤에 메인 파일에 import를 추가하는 순서로 갔다. 한 번에 다 만들고 마지막에 검사하면 어디서 깨졌는지 찾기가 어렵다.

나이 순서에 묶여 있어서 재미있는 걸 못 물었다

기존 라운드는 유년기, 10대, 20대, 30대 순으로 나이를 따라갔다. 그러다 보니 연애 스타일이나 재정 위기 같은 인생의 핵심 사건을 원하는 자리에 넣기 어려웠다. 30대 라운드에서 물어볼 게 없는 캐릭터도 있고, 반대로 20대에 사건이 몰린 캐릭터도 있다.

시간 축을 버리고 주제 축으로 바꿨다.

성장환경 → 청소년기 → 연애 → 직업 → 위기 → 재물 → 결혼 → 말년

도메인마다 물어볼 게 정해지니 질문이 뾰족해졌다. 위기 도메인에는 카지노 파산이나 SNS 역풍, 우울증 같은 구체적인 사건명이 들어가고, 재물 도메인에는 3억 손실이나 월 매출 2,000만원처럼 금액이 붙는다. 추상적인 서술 대신 숫자와 사건이 들어가니 선택지끼리 구별도 쉬워졌다.

기존 80개를 전부 새 구조로 다시 만들었다. LifePhase 타입에는 구버전 값도 남겨서 이미 저장된 결과가 깨지지 않게 했다.

배포가 1MB 제한에 걸렸다

Error: The Edge Function "result/[submissionId]/opengraph-image" size is 1.08 MB
and your plan size limit is 1 MB.

import 체인을 따라가 보니 이랬다.

OG 이미지를 만드는 데 필요한 건 제출 결과 하나인데, repository를 거치면서 시나리오 데이터 전체가 딸려 들어갔다. 40개일 때 간신히 통과하던 게 80개가 되며 넘었다.

OG 이미지 세 개의 런타임을 edge에서 nodejs로 바꿨다. Node.js 런타임은 번들 크기 제한이 없다. 콜드 스타트가 Edge보다 느릴 수 있는데, OG 이미지는 사람이 아니라 크롤러가 주로 요청하는 것이라 체감 차이가 없다.

근본 원인은 repository가 quiz와 detective 도메인을 한 파일에서 다루는 것이다. 도메인별로 쪼개면 Edge로 돌아갈 수 있다.

게임 중에 Gemini를 부르는 건 너무 느렸다

게임 화면에서 캐릭터의 사주팔자와 AI 해석을 접었다 펼 수 있는 토글을 만들었다. 힌트를 단서로 쓰게 유도하는 게 목적이다. 생년월일시와 사주팔자 카드, 해석 다섯 섹션이 들어간다.

처음엔 유저가 열 때마다 런타임에 Gemini를 호출했다. 80개 캐릭터를 사람이 만날 때마다 부르는 건 느리고 비싸다.

사전 생성 스크립트로 옮겼다.

pnpm generate:saju
# tsx --env-file=.env.local scripts/generate-detective-saju.ts

80개를 순차 처리하는데, 캐릭터 하나가 끝날 때마다 즉시 파일에 저장하고 이미 있는 건 건너뛴다. 중간에 끊겨도 다시 돌리면 이어서 간다.

이 구조가 바로 필요했다. Gemini 무료 플랜 일일 한도가 하루 20건이라 한 번 실행에 17개만 생성됐다. 전부 만들고 마지막에 저장하는 방식이었으면 한도에 걸린 순간 다 날아갔다.

API 라우트는 사전 생성분을 먼저 보고, 없으면 런타임 호출로 폴백하고, 그것도 실패하면 502를 준다. 80개가 다 채워질 때까지는 토글을 화면에서 임시로 감춰뒀다.

섹션 색도 나눴다. 처음엔 다섯 섹션이 다 회색이라 구분이 안 됐다. 종합운은 보라, 성격은 하늘색, 직업과 재물은 황금색, 대인관계는 장미색, 조언은 에메랄드로 배정하고 열린 섹션 왼쪽에 컬러 보더를 줬다.

셔플되는 리스트에 index를 key로 썼다

배포하고 콘솔을 보니 경고가 떠 있었다.

Encountered two children with the same key, `금 (金) 과다`

trait.element를 key로 쓰고 있었는데 같은 오행이 두 번 나올 수 있었다. 전체를 훑어 세 군데를 고쳤다.

파일수정 전수정 후
character-intro.tsxkey={trait.element}key={element-i}
round-card.tsxkey={i}key={choice.originalIndex}
saju-pillars-card.tsxkey={label}key={label-i}

round-card.tsx가 특히 중요했다. 선택지는 매 게임 셔플되는데 배열 index를 key로 쓰면, 순서가 바뀔 때 React가 같은 자리에 있는 다른 항목을 같은 것으로 본다. DOM 요소를 재사용하면서 내용만 바꾸기 때문에 애니메이션이나 포커스가 엉뚱한 곳에 남는다. 셔플 전 원래 인덱스를 쓰면 항목마다 고정된 값이 된다.

클라이언트가 원인을 알 수 없는 에러를 받았다

/api/quiz/submit에서 createSubmission에 try-catch가 없었다. Supabase 연결이 끊기면 서버가 빈 응답으로 죽는다.

Failed to execute 'json' on 'Response': Unexpected end of JSON input

클라이언트에는 이렇게 찍힌다. 서버가 왜 죽었는지 알려주는 정보가 하나도 없다. createSubmission을 감싸서 의미 있는 에러 JSON을 돌려주게 했다.

에러 문구도 손봤다. 전부 "잠시 후 다시 시도해주세요"로 통일돼 있었는데, Supabase 일일 한도 초과나 점검 중일 때는 잠깐 기다려도 안 된다. 서버에 일시적인 오류가 있으니 나중에 다시 시도해달라는 쪽으로 바꿨다.

추리 결과 저장에 실패하면 결과 화면은 그대로 보여주고 토스트로 공유 링크를 쓸 수 없다고만 알린다. 저장 실패가 게임 결과를 못 보게 만들 이유가 없다.

막힌 지점과 원인

문제원인해결
Edge Function 1MB 초과로 배포 실패OG 이미지가 repository를 거쳐 시나리오 전체를 번들링런타임을 nodejs로 전환
게임 중 사주 해석이 느림유저가 열 때마다 Gemini 런타임 호출사전 생성 스크립트 + 파일 우선 조회
사전 생성이 17개에서 멈춤Gemini 무료 플랜 일일 20건 한도건별 즉시 저장으로 이어서 실행 가능하게
React key 중복 경고중복 가능한 값과 배열 index를 key로 사용안정적인 고유값으로 교체
클라이언트가 JSON 파싱 에러만 봄Supabase 실패 시 서버가 빈 body로 크래시try-catch로 감싸 에러 JSON 반환

콘텐츠를 두 배로 늘렸더니 인프라 제약이 먼저 나타났다. 정적 데이터가 큰 프로젝트에서는 import 체인이 곧 번들 크기라는 걸 배포가 막히고 나서야 봤다.

나머지 63개 사전 생성과 토글 재활성화, repository 도메인 분리가 남았다. 시나리오가 더 늘면 정적 데이터를 DB로 옮기는 것도 검토해야 한다.