
Flutter와 Claude API로 마작의 '무엇을 버릴까?' 퀴즈 앱을 만든 이야기
요약
Flutter와 Claude API를 활용하여 마작 타패 연습을 위한 퀴즈 앱을 개발한 사례를 소개합니다. 고정 문제와 AI 생성 문제를 혼합하여 구성하였으며, 복잡한 게임 상황을 프롬프트에 구조화하여 전달하는 설계 노하우를 다룹니다.
핵심 포인트
- Claude API를 활용한 동적 문제 생성 및 프롬프트 구조화
- Flutter와 Riverpod을 이용한 앱 상태 관리 및 UI 구현
- AI 응답의 JSON 파싱 에러 및 데이터 무결성 검증 로직
- Unicode 이모지 사용 시 폰트 깨짐 방지를 위한 텍스트 폴백 구현
서론
이번에는 상당히 취미적인 개발 이야기입니다.
마작을 막 배운 사람이 "더 연습하고 싶다"고 생각했을 때, 딱 적당한 연습 앱이 좀처럼 없더라고요.
그래서 "무엇을 버리면 좋을까"를 3지선다로 답하는 퀴즈 앱을, Flutter와 Claude API를 조합하여 만들어 보았습니다.
만든 것
- 100문제를 출제하는 「무엇을 버릴까?」 퀴즈 앱
- 고정 문제 50문항 (JSON으로 관리) + Claude API 생성 50문항의 혼합 방식
- 3지선다 형식으로 최선의 타패 (打牌)를 선택하면, 정오 피드백과 해설이 나옴
- 패는 Unicode 이모지 (🀇🀈🀉…)로 시각적으로 표시
기술 스택
| 용도 | 채택 기술 |
|---|---|
| 프레임워크 | Flutter 3.x |
| ... |
앱의 구성
문제의 종류는 두 가지가 있습니다.
고정 문제 (50문항): assets/questions.json에 미리 정의해 둔 것입니다. beginner / intermediate / advanced로 균형 있게 나누어, 마작의 기본 패턴을 한 차례 모두 망라하도록 했습니다.
AI 생성 문제 (50문항): Claude API를 호출하여 기동 시 동적으로 생성합니다. 매번 다른 문제가 나오기 때문에, 반복해서 플레이해도 질리지 않는 것이 포인트입니다.
// questions.json의 1문제 포맷
{
"id": "fixed_001",
...
공들인 부분: 「장의 상황」을 어떻게 표현할 것인가
설계에서 가장 머리를 쓴 것이, 마작의 「장의 상황」을 데이터로 표현하여 AI에게 전달하는 부분이었습니다.
마작의 타패 판단은 손패(手牌)만으로 결정되는 것이 아니거든요. 버림패의 흐름, 도라 (ドラ) 정보, 리치 (リーチ) 유무, 남은 매수… 다양한 요소가 얽혀 들어옵니다. 그것을 어떻게 AI 프롬프트 (Prompt)에 녹여낼지, Claude Code를 사용하며 구현했습니다.
컨텍스트를 프롬프트에 구조화하여 전달하기
장의 상황을 구조화하여 프롬프트에 전달함으로써 문제의 질을 담보하고 있습니다.
// ai_question_service.dart (발췌)
String _buildSystemPrompt() {
return '''
...
포인트는 「제약을 명시적으로 적는 것」입니다. 특히 「choices는 손패에 포함된 패로만 구성한다」라는 규칙을 넣어두지 않으면, 손패에 없는 패가 선택지에 혼입되는 버그가 흔히 발생했습니다. AI라 할지라도 대충 다루면 제대로 어긋나버리네요.
검증 (Validation)은 필수입니다
AI의 응답이 반드시 올바른 JSON이 된다는 보장이 없으므로, 파싱 에러나 손패가 13장이 아닌 경우에 대비한 검증 로직을 넣었습니다.
List<Question> _validateAndParse(String jsonStr) {
final List<dynamic> raw = jsonDecode(jsonStr);
return raw
...
API 실패 시에는 고정 문제에서 랜덤하게 보완하도록 했기 때문에, 크래시 (Crash)는 발생하지 않습니다.
패의 Unicode 표현
패의 표시는 Unicode 이모지 (U+1F000번대)를 사용하고 있습니다.
// utils/tile_emoji.dart
const Map<String, String> tileEmojiMap = {
'1m': '🀇', '2m': '🀈', '3m': '🀉', '4m': '🀊', '5m': '🀋',
...
다만, 단말기나 폰트에 따라 표시가 깨지는 경우가 있습니다. 그래서 텍스트 폴백 (Fallback, 「일만」, 「이통」 등)도 함께 구현해 두었습니다. Android 실기기에서 테스트했을 때 이모지가 깨져서(豆腐) 나온 적이 있는데, 이 폴백 덕분에 살 수 있었습니다.
상태 관리: Riverpod으로 문제 흐름을 관리
퀴즈의 진행 상태는 Riverpod으로 일원 관리하고 있습니다.
// providers/quiz_provider.dart (발췌)
class QuizState {
final List<Question> questions;
...
QuizPhase로 화면의 상태를 명확히 구분함으로써, UI의 깜빡임이나 중복 응답 버그를 방지할 수 있었습니다. 「피드백 표시 중」과 「다음 문제로 이행 중」 사이의 버튼 조작 부분이 특히 깔끔해졌다고 느낍니다.
스코어와 랭크
100문제가 종료된 후 스코어와 랭크를 표시합니다.
| 스코어 | 랭크 |
|---|---|
| 90〜100 | SS (雀聖, 작성성) |
| ... |
스코어는 shared_preferences를 사용하여 베스트 스코어를 저장하고, 홈 화면에도 표시하고 있습니다.
회고
잘된 점
- Claude API를 이용한 문제 생성은 예상보다 품질이 높아서, 프롬프트(Prompt)를 제대로 작성하면 거의 정확한 마작 문제를 생성할 수 있었습니다.
- **Riverpod을 이용한 상태 관리 (State Management)**는 퀴즈와 같이 "페이즈 전환 (Phase Transition)이 많은 UI"에 매우 궁합이 좋았습니다.
- 고정 문제 + AI 생성 하이브리드 방식은 최초 실행 시의 로딩을 최소화하면서도 문제의 다양성을 확보할 수 있어 밸런스가 좋았다고 생각합니다.
다음에 한다면 바꾸고 싶은 점
- **문제 캐싱 (Caching)**을 로컬에 저장할 수 있도록 하여, 오프라인에서도 플레이할 수 있게 만들고 싶습니다.
- 난이도 선택을 사용자가 직접 고를 수 있게 한다면, 초보자부터 상급자까지 폭넓게 사용할 수 있을 것 같다는 생각을 하고 있습니다.
마치며
Flutter와 Claude API를 조합함으로써, 문제 콘텐츠 생성을 AI에게 맡기면서 퀴즈 앱을 만들 수 있었습니다. "AI에게 문제를 만들게 하는" 접근 방식은 마작과 같이 전문 지식이 필요한 도메인에서도 충분히 활용 가능하다는 것을 느꼈습니다.
마찬가지로 "전문 지식 계열 퀴즈 앱"을 만들어 보고 싶은 분들에게 참고가 된다면 기쁘겠습니다!
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기