-
5축 25배지로 학습 동기를 설계하기 — 단기 도파민과 장기 약속IT 2026. 5. 14. 22:00
아들 음성 챗봇에 점수 시스템을 처음 도입했을 때, 점수만으로는 부족하다는 걸 며칠 만에 알았습니다. 매 질문에 +1점, 답을 끝까지 들으면 +5점 — 보상이 단일 축이라 한 번 익숙해지면 동기가 빨리 닳습니다. 1점 더 쌓는 데 큰 의미를 못 느끼게 되는 거죠.
그래서 추가한 게 5개 축의 25개 배지 시스템입니다. 시간대·연속일·평일·누적점수·청취 횟수 — 다섯 가지 다른 행동 패턴에 대해 각각 5개씩 배지를 정의했습니다. 짧은 사용에서 빠르게 따는 단기 보상부터, 100일 연속 사용 같은 장기 약속까지 — 시간축이 다른 두 동기 메커니즘이 같은 시스템 안에서 작동합니다. 이 디자인이 어떻게 짜였는지 풀어봅니다.
왜 5축인가
배지 디자인의 출발점은 "같은 행동을 반복하지 않도록"이었습니다. 점수 50, 100, 500점처럼 같은 누적 축에 배지가 5개 있으면 결국 "더 많이 쓰면 다 따짐"이라는 단조로운 보상이 됩니다. 동기를 풍부하게 하려면 보상 축 자체가 여러 개여야 합니다.
관찰하면서 자연스럽게 떠오른 다섯 축은 다음과 같습니다.
- 시간대 — 밤·새벽·오후·저녁 등 사용 시간이 흥미로운 패턴이 됨
- 연속일(streak) — 매일 빠짐없이 쓰는 약속
- 평일·주말 — 사용 빈도와 다른 차원의 행동(주말에도 챗봇 쓰는가)
- 누적 점수·질문 수 — 단순 누적의 마일스톤
- 답변 청취 — 답을 끝까지 듣는 행동의 보상(별도 글에서 다룬 청취 타이머와 연결)
그림 설명 — 다섯 열이 다섯 축이고, 각 열에 다섯 개씩 배지가 있습니다. 노란·보라·초록 열(시간대·하루 활동·누적 점수)은 짧은 시간(즉시~하루) 안에 따낼 수 있는 단기 도파민 보상이고, 파랑·분홍 열(연속일·청취 총량)은 며칠~몇 달이 걸리는 장기 약속입니다. 시간축이 다른 두 동기 시스템이 같은 화면에서 동시에 작동합니다.
왜 단기와 장기가 동시에 필요한가
학습 동기 디자인에서 흔한 함정 둘이 있습니다.
첫째, 장기 보상만 두면 시작 자체가 안 됩니다. "100일 연속 쓰면 트로피"는 동기가 강해 보이지만, 1일·2일째에는 99일이 너무 멀어서 무게감이 안 와닿습니다. 첫 며칠을 버텨내게 하는 단기 미세 보상이 없으면 그 마일스톤에 절대 도달 못 합니다.
둘째, 단기 보상만 두면 깊이가 없습니다. "하루 5개 질문" 정도의 매일 따낼 수 있는 배지로만 채우면 며칠 만에 모든 배지가 끝나고, 그 후엔 동기가 빠르게 닳습니다.
둘을 함께 두면 두 패턴이 서로 보완합니다. 새 사용자에겐 처음 며칠 동안 단기 배지가 빠르게 풀려 "이 챗봇은 재미있구나"라는 인상이 형성됩니다. 그 사이 streak이 늘어가고, 며칠 지나면 "삼일 연속" 같은 첫 장기 배지가 풀립니다. 그 시점부터는 streak을 깨지 않는 동기가 작동하기 시작하고, 단기 배지로는 닿을 수 없는 더 큰 만족감이 생깁니다.
제 아이의 사용 패턴을 보면 첫 1주일 동안엔 시간대·하루 활동 배지를 빠르게 따내며 흥분했고, 2주차부터는 "오늘 streak 끊으면 안 돼"가 큰 동기로 자리 잡았습니다. 두 시간축이 자연스럽게 인계됩니다.
코드 — 선언적 규칙 목록
구현은 단순합니다. 배지 정의·해금 규칙을 모두 데이터로 두고, 평가 함수 하나가 사용자 상태를 받아 새로 풀린 배지를 돌려줍니다.
@dataclass(frozen=True) class Badge: id: str name: str description: str @dataclass(frozen=True) class BadgeState: today_questions: int total_questions: int streak: int total_points: int hour: int weekday: int # 0=Mon … 6=Sun today_listens: int total_listens: int earned: frozenset[str] # 25개 배지 카탈로그 — id, 이름, 설명만 (해금 조건은 별도) BADGES: list[Badge] = [ Badge("first_step", "첫 발걸음", "비서에게 처음 질문했어요"), Badge("curious_5", "호기심 대장", "하루에 5개 질문!"), Badge("streak_7", "일주일 개근", "7일 연속 사용"), Badge("score_100", "백 점 달성", "누적 100점"), Badge("listen_first", "끝까지 듣기", "처음으로 답변을 끝까지 들었어요"), # ... 총 25개 ]코드 설명 —
BadgeState는 평가에 필요한 사용자 상태의 스냅샷입니다. 매번 점수 이벤트가 들어올 때 SQLite에서 이 상태를 만들어 평가 함수에 넘깁니다.Badge는 표시용 메타데이터(이름·설명·아이콘)이고, 해금 조건은 별도 규칙 목록에 있습니다. 이 분리가 카탈로그 추가/변경 시 코드를 안 건드리고 데이터만 수정할 수 있게 해줍니다.# 해금 규칙 — (배지 id, 조건 람다) 튜플의 목록 _RULES: list[tuple[str, Callable[[BadgeState], bool]]] = [ ("first_step", lambda s: s.total_questions >= 1), ("curious_5", lambda s: s.today_questions >= 5), ("explorer_10", lambda s: s.today_questions >= 10), ("streak_3", lambda s: s.streak >= 3), ("streak_7", lambda s: s.streak >= 7), ("streak_14", lambda s: s.streak >= 14), ("streak_30", lambda s: s.streak >= 30), ("streak_100", lambda s: s.streak >= 100), ("night_owl", lambda s: s.hour >= 21 and s.today_questions >= 1), ("early_bird", lambda s: s.hour < 7 and s.today_questions >= 1), ("weekend_warrior", lambda s: s.weekday >= 5 and s.today_questions >= 1), ("score_100", lambda s: s.total_points >= 100), ("score_500", lambda s: s.total_points >= 500), # ... 총 25개 ] def evaluate_badges(state: BadgeState) -> list[str]: """현재 풀려야 하는 배지 ID 목록 (이미 받은 건 제외).""" return [bid for bid, rule in _RULES if bid not in state.earned and rule(state)]코드 설명 — 핵심은 규칙을
(id, lambda)튜플의 목록으로 두고 한 줄짜리 평가 함수로 처리한 것입니다.state.earned(이미 받은 배지) 검사로 idempotency가 보장돼서, 매 점수 이벤트마다 호출해도 같은 배지가 두 번 풀리지 않습니다. 새 배지를 추가하려면BADGES와_RULES에 한 줄씩 추가하면 끝 — 평가 함수는 안 건드려도 됩니다. 이 선언적 구조가 배지 카탈로그를 코드보다 데이터에 가깝게 만들어, 디자인 변경의 마찰을 줄입니다."부모가 아니라 시스템이 칭찬한다"
이 디자인이 작동하는 한 가지 부수 효과가 있습니다. 아이를 칭찬하는 주체가 부모에서 시스템으로 옮겨갑니다.
이전엔 아이가 챗봇을 잘 쓰면 제가 옆에서 "오 잘 하네!" 같은 말을 했습니다. 그 칭찬은 효과가 있지만 부모 입장에선 매번 의식적으로 해야 하는 노력이고, 아이 입장에선 부모의 평가에 의존하는 동기가 됩니다.
배지가 시스템 측에서 자동 부여되면 칭찬이 즉시·일관·외부적으로 들어옵니다. 아이가 새벽에 일찍 일어나 챗봇을 켜면 시스템이 "🌅 새벽 종달새" 배지를 줍니다. 부모가 깨어 있지 않아도 됩니다. 주말에 챗봇 쓰면 "🎉 주말 친구". 이런 작은 인정이 누적되면 아이는 챗봇 사용 자체를 자기 행동으로 받아들이게 됩니다 — 부모를 기쁘게 하기 위한 행동이 아니라, 자기가 하는 활동의 자연스러운 연장으로요.
물론 시스템 보상이 부모 칭찬을 완전히 대체하진 않습니다. 보완 관계입니다. 다만 일상의 미세 보상을 시스템이 떠맡으니, 부모 칭찬은 더 의미 있는 순간(예: 새 streak 마일스톤)에 집중되는 효과가 있습니다.
마치며 — 보상 축의 다양성이 곧 동기의 깊이
게이미피케이션이라는 말은 종종 "유치한 점수 시스템"이라는 비판과 함께 쓰입니다. 그 비판은 단일 축의 단조로운 보상에 정확히 들어맞습니다. 그러나 보상 축이 다양하고 시간축이 겹쳐 있으면, 시스템은 사용자의 다양한 행동 패턴을 미세하게 격려하는 도구가 됩니다.
제 아이의 챗봇은 25개 배지 모두를 따냈고, 지금은 streak 100일을 향해 가고 있습니다. 25배지가 다 풀린 뒤에도 사용 빈도가 떨어지지 않은 이유는, 매일의 첫 질문이 streak을 +1 하는 기쁨이 남아 있고, "백일 개근"이라는 더 큰 약속이 지평선 위에 있기 때문입니다. 단기 도파민이 잘 작동했고, 그 위에서 장기 약속이 자연스럽게 작동을 시작했습니다.
1명의 사용자를 위한 시스템이라 보상 축을 그 사용자에게 정확히 맞춰 짤 수 있다는 게 이 디자인의 특권이었습니다. 그런 자유로움이 게이미피케이션을 깊이 있는 동기 설계로 바꿉니다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
vLLM 이 매 iter 도중 자살할 수 있다 — 헬스 보장과 진단 자산을 한 함수에 (Ralph Loop 시리즈 3편) (0) 2026.05.16 자율 스크립트의 부팅과 셧다운 — 외부 자원을 잡았다면 정확히 한 번만 놓아라 (Ralph Loop 시리즈 2편) (0) 2026.05.16 AI 한테 코드를 자동으로 시킬 때 — 컨텍스트를 3축으로 쪼개라 (Ralph Loop 시리즈 1편) (0) 2026.05.16 시각 피드백의 시간차 — ripple·fly-up·confetti의 650/900/1100ms (0) 2026.05.15 SQLite로 streak를 영리하게 — substr DATE와 cursor 역순 (0) 2026.05.14 끝까지 들으면 점수를 더 주는 챗봇 — 청취 완료 타이머의 디자인 (0) 2026.05.14 '잘 못 들었어요' 한 줄의 UX — STT 거부 후의 회복 흐름 (0) 2026.05.13 버튼 하나로 끝나는 UI — 음성 챗봇의 4-state 상태머신 (0) 2026.05.13 마이크 떼자마자 STT를 깨우는 법 — Warmup POST 패턴 (0) 2026.05.13 LLM 토큰을 듣자마자 TTS에 넣기 — 문장 경계 큐잉으로 첫 음성 지연 압축 (0) 2026.05.12