본문으로 건너뛰기

사주고사 제작기 13. 캐릭터 50개 확정과 사주 해석 사전 생성

·8 min read·13 / 13

캐릭터를 80개까지 늘렸다가 50개로 다시 줄였다. 늘리는 것보다 줄이는 판단이 더 어려웠다.

80개를 채우니 서사 밀도가 떨어졌다

줄인 이유는 둘이다.

사주 해석 사전 생성이 감당이 안 됐다. Gemini 무료 플랜이 하루 20건이라 80개를 채우려면 나흘 넘게 걸린다. 그동안 사주 토글은 계속 꺼둔 채로 둬야 한다.

그리고 개수를 채우면서 캐릭터 하나하나의 서사가 얇아졌다. 비슷한 궤적이 섞이기 시작했고, 그럴 거면 50개를 제대로 만드는 쪽이 낫다고 봤다.

pnpm generate:saju를 다시 돌려 나머지를 채우고 50개를 완료했다. c51부터 c80까지는 배치 파일 일곱 개를 통째로 지우고, 걸쳐 있던 배치 하나는 남길 캐릭터만 두고 다시 만들었다. 문제 데이터에서도 해당 항목을 뺐다. 4,000줄 정도가 사라졌고 번들도 같이 줄었다.

사전 생성 스크립트를 캐릭터 하나 끝날 때마다 저장하게 만들어둔 게 여기서 값을 했다. 일일 한도로 끊겨도 다시 실행하면 이어서 간다. 며칠에 걸쳐 나눠 돌리는 게 애초에 전제였는데, 그 전제를 코드에 반영해둔 덕에 재실행 명령 한 줄로 끝났다.

AI가 좋은 말만 했다

lib/gemini-saju.ts의 프롬프트가 "종합적인 해석을 제공하세요" 수준이었다. 결과가 "좋은 에너지를 가지고 있습니다", "대인관계가 원만합니다" 같은 문장으로 채워졌다. 누구에게 갖다 붙여도 맞는 말이라 읽을 이유가 없다.

LLM은 그냥 두면 긍정 쪽으로 기운다. 약점을 말하라고 명시하지 않으면 말하지 않는다. 프롬프트에 규칙을 박았다. 약점과 불리한 구조를 장점만큼 충분히 언급할 것, 막연한 위로 대신 구체적인 리스크를 적을 것, "~할 수 있습니다" 같은 애매한 표현을 쓰지 말 것, 상충하는 오행 조합은 내면 갈등으로 직접 서술할 것.

결과가 눈에 띄게 달라졌다. "재물운이 있습니다"가 "금(金)이 과다해 욕심이 앞설 때 오히려 손재수가 생깁니다"로 바뀌었다.

결과 화면이 중간부터 보였다

추리 모드에서 마지막 라운드를 풀고 결과로 넘어갈 때 스크롤 위치가 그대로 남았다. 게임 중에 아래로 내려뒀으면 결과 화면 중간부터 보인다.

// detective-client.tsx — finished 상태 진입 시
useEffect(() => {
  if (gameState.phase === "finished") {
    window.scrollTo(0, 0);
  }
}, [gameState.phase]);

공유 결과 페이지는 서버 컴포넌트라 useEffect를 쓸 수 없다. 스크롤만 올리는 작은 클라이언트 컴포넌트를 따로 만들어 마운트 시점에 실행하게 했다. 이것 하나 때문에 페이지 전체를 클라이언트로 바꿀 이유는 없다.

타이핑할 때 렉이 걸렸다

에세이 입력창에서 글자를 칠 때마다 버벅였다. 원인이 둘이었다.

먼저 sticky 헤더에 backdrop-blur-sm이 붙어 있었다. blur는 GPU 합성 레이어를 만들고, 그 아래에서 텍스트가 바뀔 때마다 배경을 다시 계산한다. 긴 글이 들어 있는 textarea 위에서 스크롤하거나 타이핑하면 매번 다시 그린다. 모바일에서 특히 심했다. 장식이라 그냥 뺐다.

그리고 useDeferredValue 적용 범위를 넓혔다.

// 변경 전
const deferredEssay = useDeferredValue(essay);
 
// 변경 후
const deferredEssay = useDeferredValue(essay);
const deferredCharCount = useDeferredValue(essay.length); // 카운터도 지연

글자 수 카운터가 매 입력마다 갱신되고 있었다. 카운터가 한 박자 늦게 따라오는 건 티가 안 나는데, 입력 자체가 밀리는 건 바로 느껴진다. 급하지 않은 갱신을 뒤로 미루는 게 useDeferredValue가 하는 일이다.

에세이 화면 상단의 진행률 바는 모바일에서 숨겼다. 에세이 모드는 한 문제짜리 단일 화면이라 진행률을 보여줄 게 없는데 자리만 차지하고 있었다.

CLAUDE.md가 300줄을 넘었다

파일 하나에 기획과 파일 구조, DB 설정, 진행 현황이 다 들어 있었다. 매번 전부 읽히니 정작 필요한 부분의 비중이 낮아졌다.

주제별로 나눴다.

CLAUDE.md (8줄 — @import 참조만)
  └── docs/rules/overview.md      # 기획·기술스택·결정사항
  └── docs/rules/structure.md     # 파일 구조 트리
  └── docs/rules/data-security.md # DB·보안 설정
  └── docs/rules/status.md        # 완료·미구현 현황

Claude Code가 @파일명 형태의 import를 지원해서 CLAUDE.md에서 참조만 걸어두면 된다. 파일이 작으면 필요한 것만 골라 읽을 수 있다.

막힌 지점과 원인

문제원인해결
사주 해석 사전 생성이 며칠씩 걸림캐릭터 80개에 일일 20건 한도50개로 축소해 완료
캐릭터 서사가 얇아짐개수를 채우는 데 무게가 실림c51~c80 삭제, 약 4,000줄 정리
AI 해석이 좋은 말만 함프롬프트에 약점을 말하라는 지시가 없음약점·리스크 명시 규칙 추가
결과 화면이 중간부터 보임화면 전환 시 스크롤 위치 유지finished 진입 시 최상단 이동, 서버 컴포넌트는 별도 클라이언트 컴포넌트
타이핑 시 렉sticky 헤더의 backdrop-blur가 매 입력마다 리페인트 유발blur 제거 + 카운터도 useDeferredValue 적용

backdrop-blur가 이 정도로 비쌀 줄은 몰랐다. 보기 좋으라고 넣은 한 줄이 입력 반응성을 갉아먹고 있었다.

생성 문제 96개의 해설을 만세력 기반으로 보강하는 일, 문제와 인물 데이터 이관, rate limiter를 영구 저장소로 옮기는 일이 남았다.