Skip to Content
1인개발기#02 Doki World

Doki World

AI와 대화하며 캐릭터와 관계를 쌓아가는 인터랙티브 비주얼 노벨

개발 기간: 2026-02-01 ~ 진행중


왜 만들게 되었나?

Thought Lab을 만들다가 멈춘 가장 큰 이유는, AI-native로 일하다 보니 투두리스트를 별도 도구에 적을 필요가 없어졌기 때문입니다. 에이전트 코딩으로 개발하면 플랜이나 고민을 프로젝트 안에서 바로 처리하게 되고, 정작 시간 추적이나 가설 관리 도구는 쓰지 않게 되었습니다.

다만 Thought Lab에서 실험했던 멀티 에이전트 대화 컨셉 자체는 재밌다고 느꼈습니다. 여러 AI 페르소나가 한 주제를 놓고 토론하는 구조 — 이걸 미연시(비주얼 노벨)로 가져오면 어떨까? 페르소나의 “의견”이 캐릭터의 “대사”가 되고, “토론 주제”가 “스토리”가 되는 구조가 자연스럽게 그려졌습니다.


주요 기능

듀얼 게임 모드

채팅 모드와 비주얼 노벨 모드를 실시간으로 전환할 수 있습니다.

  • VN 모드: 스프라이트 기반 클릭 진행, 배경 + 캐릭터 + 대사창
  • Chat 모드: 메시지 버블 UI + 스티커 + 선택지

VN 모드 - 캐릭터 스프라이트와 배경, 나레이션 텍스트가 표시됩니다

Chat 모드 - 사이드바에서 에피소드를 선택하고, 캐릭터와 대화합니다

모바일 Chat 모드 - 스티커와 함께 캐릭터와 자유롭게 대화

AI 에이전트 시스템

Master-Agent 아키텍처로 스토리를 구동합니다.

  • Master Agent: 플레이어 메시지를 라우팅하고, 스토리 진행을 판단하며, 선택지를 생성
  • Character Agent: 캐릭터별 고유한 말투와 감정으로 대화 생성 (감정 표현, 스티커, 속마음 포함)
  • Creative + Judge 파이프라인: OpenAI 2단계 호출 — 첫 번째가 생성하고, 두 번째가 검증/보정

SSE(Server-Sent Events) 스트리밍으로 character_start → character_end → narration → story_update → done 순서로 실시간 전달됩니다.

Beat 기반 스토리 시스템

스토리 계층 구조: Novel > Story(에피소드) > Scene > Beat

5종류의 Beat가 있습니다:

  • narration — AI가 서술하는 장면 묘사
  • dialogue — 캐릭터 간 정해진 대사
  • freeChat — 플레이어가 캐릭터와 자유 대화
  • trigger — 특정 조건 충족 시 발동
  • choice — 플레이어의 선택이 스토리를 분기

호감도 시스템

0~100 범위의 6단계 관계 시스템:

단계범위의미
rejection0이별 엔딩
guarded1-19경계
neutral20-39중립
friendly40-59우호
trust60-79신뢰
bond80-100유대

호감도가 목표에 미달하면 자동으로 스토리가 연장되고, 0에 도달하면 이별 엔딩이 발동됩니다.

10명의 캐릭터 × 3개 소설

이세계의 별 소설 표지

3개의 소설 세계관에서 10명의 캐릭터를 만날 수 있습니다:

  • 이세계의 별 — 아리아, 네루, 메이, 진숙, 루나 (판타지)
  • 월요일의 온도 — 소라, 지호 (현대)
  • 달빛 궁정 — 하루, 유리, 소하 (동양 판타지)

Ring 과금 시스템

  • 1 Ring = 10 채팅, 웰컴 보너스 3 Ring (30 채팅)
  • 듀얼 결제: 한국(토스페이먼츠) + 해외(Creem.io)
  • 첫 구매 2배 보너스, 리워드 광고 (1회 = 1채팅, 일 10회 한도)
  • 1년 유효기간 + 7일 환불

다국어 지원

  • 정적 텍스트: next-intl 기반 ko/en/ja 라우팅 (/ko/play/..., /en/play/...)
  • AI 응답: User.locale DB 필드로 캐릭터가 해당 언어로 대화
  • 시드 데이터도 3개 언어별 분리 관리 (prisma/data/{ko,en,ja}/)

에셋 생성 파이프라인

비주얼 노벨에는 캐릭터 스프라이트, 배경, 이벤트 CG가 필요합니다. 외주를 줄 수도 있지만, 스토리가 바뀔 때마다 에셋도 바뀌어야 하는 구조에서 외주는 현실적이지 않았습니다. 그래서 이미지 생성 AI로 직접 만들기로 했고, 별도 프로젝트(asset_generator)를 만들어 자동화했습니다.

스토리가 에셋을 결정한다

가장 먼저 해결해야 했던 문제는 **“어떤 에셋이 필요한지 어떻게 아는가?”**였습니다. 스토리를 쓰다 보면 “이 장면에 배경이 필요하네”, “여기에 CG가 있으면 좋겠다”가 계속 생기는데, 이걸 수작업으로 추적하면 반드시 빠뜨립니다.

해결책은 스토리 데이터 자체를 에셋 요구사항의 원천으로 삼는 것이었습니다. 에피소드의 scene.background, showSceneImage 트리거, NOVELS 배열에서 필요한 에셋을 자동으로 추출합니다. npm run check-assets 한 번이면 “배경 3개 미완성, CG 2개 누락” 같은 리포트가 나옵니다.

doki-world asset_generator ────────── ─────────────── 1. 스토리 작성 (prisma/data/) 2. npm run check-assets → 필요한 에셋 자동 추출 3. npm run asset-request → asset-push → 미완성 에셋만 패키징해서 전달 ──→ asset-requirement/ 수신 4. from-requirements 커맨드 → 미완성 항목만 배치 생성 → 완료 상태 자동 갱신 5. 생성된 에셋을 게임에 배포 ←────── 생성 결과물

같은 캐릭터를 일관되게 뽑는 고민

에셋 생성에서 가장 큰 고민은 **“같은 캐릭터가 매번 다른 얼굴로 나온다”**는 것이었습니다.

화풍 통일 — 처음에는 퀄리티가 높은 모델을 쓰고 싶었지만, 스프라이트와 이벤트 CG를 다른 모델로 생성하면 같은 게임 안에서 화풍이 따로 놀았습니다. 결국 하나의 모델(Animagine XL 4.0 Opt)로 통일하고, 추가 스타일 보정(LoRA)도 쓰지 않기로 했습니다. 퀄리티를 조금 포기하더라도 일관성이 더 중요했습니다.

표정 변형 — 캐릭터 스프라이트는 같은 포즈에서 표정만 바뀌어야 합니다. 매번 새로 생성하면 몸체까지 달라지니까, base 이미지를 먼저 만들고 얼굴 영역만 교체하는 합성 방식을 썼습니다. 캐릭터 1명당 약 12분이면 6가지 표정이 나옵니다.

basehappysadangry
basehappysadangry

레퍼런스 이미지 활용 시도 — “이 캐릭터 이미지를 참고해서 같은 캐릭터로 CG를 뽑아줘”라는 기능(IP-Adapter)을 붙이려 했지만, 모델 호환성 문제로 항상 실패했습니다. 결국 캐릭터의 외형 특징(머리색, 눈색 등)을 프롬프트에 직접 써넣는 방식으로 대체했는데, 오히려 이 쪽이 더 안정적이었습니다. “자꾸 다른 얼굴이 됨” 문제는 끝까지 완벽히 해결하지 못했지만, 실용적으로 충분한 수준에는 도달했습니다.

이벤트 CG: 시행착오의 연속

스토리 핵심 장면을 1장의 일러스트로 만드는 이벤트 CG가 가장 어려웠습니다. 초반에는 “멋진 구도로 드라마틱하게”를 시도했다가 많이 깨졌습니다.

  • 복잡한 카메라 앵글을 지시하면 그림체 자체가 무너짐 → 단순한 프레이밍만 사용하기로
  • 캐릭터를 1명만 그리라고 했는데 2~3명이 나옴 → 해당 지시를 강하게 명시해야 했음
  • 프롬프트에 지시를 너무 많이 넣으면 모델이 혼란 → 핵심만 15~25개로 제한
  • 한 번에 10장 뽑고 고르는 것보다, 1장씩 뽑고 → 확인 → 조정하는 루프가 훨씬 효율적

이런 시행착오를 거쳐 “잘 나온 CG”의 기준을 정하고, 이후 생성 시 벤치마크로 삼았습니다.

이벤트 CG 예시

이벤트 CG - 분위기 있는 장면

배경: 세계관마다 다른 화풍

3개 소설의 세계관이 다르니 배경 분위기도 달라야 했습니다. 이세계의 별은 판타지 애니메이션풍, 월요일의 온도는 실사풍, 달빛 궁정은 동아시아풍. 27개 위치를 프리셋으로 등록해두고, 배경 이름만 지정하면 해당 세계관의 화풍으로 자동 생성됩니다.

별빛 마을 배경

에셋 생성도 Claude Code로

doki-world 본체뿐 아니라 asset_generator에도 Claude Code 커맨드를 정의해뒀습니다. “캐릭터 이름과 외형 설명을 주면, 한국어 설명을 이미지 생성용 태그로 변환하고, 캐릭터 설정 파일을 만들고, 스프라이트를 배치 생성”하는 과정이 커맨드 하나로 돌아갑니다. 이벤트 CG도 벤치마크 이미지와 비교하며 1장씩 리뷰하는 인터랙티브 루프를 커맨드로 만들어뒀습니다.

doki-world 쪽의 npm run asset-request와 맞물려서, 스토리를 쓰면 → 필요한 에셋이 자동 추출되고 → asset_generator에서 배치 생성하는 파이프라인이 완성됩니다.

3차례 대전환

프로젝트 방향이 바뀔 때마다 캐릭터를 통째로 폐기하고 재생성했습니다. 22번의 커밋, 총 934건의 이미지 변경(548 생성, 39 수정, 347 삭제). 삭제 347건 중 98%가 3번의 대전환(커밋 #11, #14, #19)에서 발생했습니다.

  • 스토리 방향이 바뀌면 캐릭터 설정이 바뀌고, 설정이 바뀌면 에셋을 처음부터 다시 만들어야 했음
  • 대부분 캐릭터가 1회 이상 삭제→재생성 사이클을 겪음
  • 월요일의 온도는 실사풍으로 만들었다가 통째로 삭제하고 애니메이션풍으로 다시 만듦

이런 대전환을 겪으면서 자동화의 가치를 절감했습니다. 처음부터 다시 만들어야 할 때, 커맨드 한 번이면 되니까요. 수작업이었다면 3번의 전환을 버틸 수 없었을 겁니다.


Behind the Scenes

34일의 숫자들

  • 90개 커밋, 196개 Claude 세션, 24 작업일
  • 커밋 대비 세션이 2.2배 — 코드를 쓰는 시간보다 고민하고 디버깅하는 시간이 더 많았다
  • 커밋 없는 작업일이 5일 — 세션은 있었지만 커밋으로 이어지지 못한 날들
  • 가장 많은 커밋을 낸 날: 02-11 (11개), 03-03 (10개), 03-04 (10개)
  • 가장 많은 세션을 쓴 날: 03-04 (16개), 02-13 (15개), 02-10 (14개)

날짜별 상세 개발 행적은 회고 상세 페이지에 기록했습니다.

가장 어려웠던 것: AI 대화 통제

이 프로젝트에서 가장 많은 개발 세션을 투자한 부분은 스토리 안에서 AI 대화를 통제하는 것이었습니다.

자유 채팅이라면 간단합니다. 캐릭터 페르소나를 주고 “알아서 대화해”라고 하면 됩니다. 하지만 비주얼 노벨은 다릅니다. 특정 장면에서는 특정 이야기를 해야 하고, 호감도에 따라 반응이 달라져야 하며, 적절한 타이밍에 다음 장면으로 넘어가야 합니다. 자유도와 통제 사이의 줄다리기입니다.

구체적으로 어려웠던 포인트들:

  • 누가 다음 장면으로 넘기는가? — Master Agent의 beatCompleted 신호, 캐릭터의 action_suggestion 평가, freeChat의 maxTurns 도달, 3+ 메시지 fallback. 이 4가지 메커니즘이 동시에 존재하고, 우선순위가 충돌하는 엣지 케이스가 끊임없이 나타났습니다.

  • freeChat 구간의 딜레마 — 플레이어가 캐릭터와 자유롭게 대화하는 구간에서, AI가 스토리와 무관한 방향으로 대화를 끌고 갈 수 있습니다. scene.hintsgoal, keyInfo, forbidden, talkingPoints를 넣어서 가이드하지만, 너무 강하게 제한하면 자유 채팅의 의미가 없어집니다.

  • 호감도 판정의 모호함 — Judge 파이프라인이 대화 내용을 보고 호감도를 평가하는데, affinityTopics가 빠져있으면 Judge가 뭘 기준으로 평가할지 모릅니다. 반대로 기준을 너무 상세하게 주면 기계적인 대화가 됩니다.

  • 스토리 데이터 동기화 — 스토리 내용을 한 줄 바꾸려면 7단계를 거쳐야 합니다: 에피소드 데이터 수정 → 캐릭터 페르소나 업데이트 → seed.ts 등록 → DB seed 실행 → 스키마 변경 → 타입 검증 → 문서 업데이트. 한 단계라도 빠지면 AI가 옛날 페르소나로 대화합니다.

솔직히 말하면 아직 완성도가 높지는 않습니다. 스토리 흐름이 자연스럽게 이어지다가도, 갑자기 맥락을 잃거나 장면 전환이 어색한 순간이 있습니다. “AI가 이야기를 만든다”는 것과 “AI가 이야기를 따라간다”는 것 사이의 균형을 잡는 건 여전히 진행 중인 과제입니다.

재밌는 발견이 하나 있었습니다. 경쟁사(Zeta.ai, Risuai 등)를 분석하고 하드유저 커뮤니티를 살펴보니, AI 캐릭터 채팅 서비스에서 유저들이 가장 불만을 느끼는 지점이 일반 AI 챗봇과 똑같았습니다 — 메모리가 끊기고, 컨텍스트를 잃고, 이전에 했던 이야기를 까먹는 것. 하드유저들이 가장 원하는 건 화려한 연출이 아니라 “이 캐릭터가 나와 나눈 대화를 기억해주는 것”이었습니다. AI를 도구로 쓰든 캐릭터로 쓰든, 결국 핵심 과제는 같은 거였습니다.

끊임없이 갈아엎은 것들

34일 동안 핵심 인프라를 여러 차례 전면 교체했습니다.

DB 3번 변경:

  1. Qdrant (벡터 DB) → MySQL (02-09, “벡터 디비는 차용하지 않기로 했어”) → Supabase PostgreSQL (02-11, 2일만에 재전환)

AI 파이프라인 5번 변경:

  1. 단일 호출 → 2-call 아키텍처 (02-09) → 전면 리팩토링 (02-10) → chat-v2 리라이트 (02-17, “대화가 너무 맘에 안들어”) → Creative/Judge 2-call 최종 (02-19)

결제 PG 변경:

  1. Ring 재화 설계 (02-13) → Creem + 토스 구현 (02-24) → 토스 심사 대응으로 환불 시스템 급조 (03-03) → Ring 용어 제거, 채팅 횟수 직접 단위 (03-07) → Creem 밴 (03-09)

좌절의 순간들:

  • 02-08: “후.. 우리가 너무 어렵게 짠 것 같아..”
  • 02-17: “대체 zeta나 타 서비스 앱들처럼 대화 흐름이 자연스럽게 하려면 어떻게 해야될까? 나 진짜 모르겟어. 현재 대화가 너무 맘에 안들어..”
  • 02-21: “QA중인데 문제가 너무 많아보여”

결제: 예상 못한 난관

토스페이먼츠와 Creem.io 이중 결제 시스템을 구축하고, 법적 요건도 갖추었습니다:

  • 사업자 등록 및 통신판매업 신고 완료
  • 이용약관, 개인정보처리방침 작성 (ko/en/ja)
  • 성인인증 게이트 (첫 로그인 시 연령 확인 + 약관 동의)

코드는 준비되어 있었지만, PG 심사에서 연쇄적으로 막혔습니다. 토스페이먼츠는 환불 시스템과 유효기간을 요구했고, Creem은 콘텐츠 카테고리 문제로 밴당했습니다. “코드를 짜면 끝”이 아니라 사업적 허들이 더 컸습니다.

Claude Code와의 협업

.claude/commands/ 폴더에 에셋 체크, 스토리 업데이트, git push 등 반복 작업을 명세로 정의해두고, Claude가 그대로 실행하는 방식으로 개발했습니다. 196개 세션 중 세션 유형 분포:

  • 버그 수정/디버깅: ~45개 (23%)
  • 기능 구현 요청: ~40개 (20%)
  • 설계/기획 논의: ~25개 (13%)
  • QA/점검: ~20개 (10%)
  • 에셋 관련: ~18개 (9%)
  • 프롬프트 튜닝: ~15개 (8%)

PRD Agent 때와 마찬가지로, 명세서를 잘 쓰는 것이 곧 개발이라는 감각이 더 강해졌습니다. 특히 에셋 파이프라인처럼 반복적이고 규칙 기반인 작업에서 이 방식이 빛을 발했습니다.

한편 Claude가 .env.example에 실제 키를 넣어서 Git Guardian 경고를 받는 사고도 있었습니다. AI와 협업할 때 보안 규칙을 명시적으로 CLAUDE.md에 적어둬야 한다는 교훈을 얻었습니다.

핵심 교훈

도키월드는 개발이 늦어서 못 나온 게 아니라, 핵심 재미에 대한 확신 부족 때문에 계속 구조를 바꾸며 범위를 키워서 런칭이 밀렸다.

  1. IP 만들기 + 서비스 만들기가 겹쳐서 양쪽 모두 완성도가 낮아졌다
  2. 개발 속도는 느린 게 아니었다 — Day 2에 9,582줄, Day 3에 SSE+듀얼모드 완성
  3. 문제는 중간부터 제품 완성보다 구조 변경과 범위 확장이 계속 일어난 것
  4. 특히 대화 품질 불만 → 전면 리팩토링 → 결제/인증/i18n/운영요소 동시 추가가 런칭을 밀었다
  5. 이번 실패는 실행력 부족이 아니라 스코프 통제 실패

다음을 위한 프레임워크: 2주 런칭

도키월드의 경험에서 뽑아낸 규칙들:

대원칙:

  • 첫 아키텍처로 런칭까지 간다. 중간에 갈아엎지 않는다
  • 아이디어에 의심이 갈수록 런칭을 더 빨리 한다. 확신은 유저 데이터에서 온다
  • “나중에 추가”는 진짜 나중에 한다. v1에 없으면 없는 거다

타임라인:

Day 0 기획 (반나절) — NOT-DO 목록 확정 Day 1~3 코어 구현 — 핵심 루프 동작 + 1명에게 보여주기 Day 4~7 수익화 + 인증 — 돈 받을 수 있는 상태 Day 8~10 마감 — 다듬기만, 새 기능 금지 Day 11~14 버퍼 — 예상 못한 문제 대응 ───────────────────────────────────────────── Day 14 런칭

스코프 통제 규칙 (도키월드에서 실제로 일어난 패턴들):

패턴도키월드에서 일어난 일다음에 할 것
”품질이 불만족”파이프라인 5번 재작성런칭 후 데이터 보고 판단
”이것도 있으면 좋겠다”i18n, 어드민, 반응형, 광고 보상NOT-DO 목록 확인 후 거부
”더 좋은 구조가 있다”DB 3번 변경런칭 전 변경 금지
”경쟁사는 이렇게 한다”Zeta, Risuai 분석 → 리라이트분석은 런칭 후에

매일 자가 체크:

  1. 오늘 한 작업이 런칭을 앞당기나, 뒤로 미루나?
  2. 이 기능 없이 런칭할 수 있나? → Yes면 안 한다
  3. 이걸 지금 바꾸면 다른 것도 바꿔야 하나? → Yes면 런칭 후에

이 글은 계속 업데이트됩니다. 날짜별 상세 기록은 개발 회고에서 확인할 수 있습니다.

Last updated on