cmod.ify

[월간 솔루션 토이] #1 - ATYPE: 타자 연습이 왜 이렇게 불편하지? 본문

Project

[월간 솔루션 토이] #1 - ATYPE: 타자 연습이 왜 이렇게 불편하지?

modifyC 2026. 4. 9. 16:24
728x90
반응형

월간 솔루션 토이는 매달 하나의 실제 문제를 직접 해결하는 토이 프로젝트를 만들어보는 시리즈입니다.

 

거창한 기술 스택이나 완벽한 설계보다는, 지금 당장 불편한 것을 빠르게 만들어 배포하고 실제 피드백을 받는 것을 목표로 하고 있습니다.

 

첫 번째 주제는 코딩 강사로서 직접 겪은 불편함에서 시작된 타자 연습 서비스, ATYPE입니다.

서비스 단축 링크: bit.ly/atype

https://atype-roan.vercel.app/

 

ATYPE

본문을 클릭 후 첫 글자를 입력하면 시작돼요

atype-roan.vercel.app


3일 만에 타자 연습 서비스 MVP를 만든 이야기

코딩 강사로 일하다 보면 직접 불편함을 느끼는 순간이 생깁니다.
그 불편함을 그냥 넘기지 않고 만든 서비스, ATYPE 이야기를 해보려 합니다.


1. 어떤 문제를 발견했나요

초등학교 저학년부터 중등 학생들을 가르치고 있습니다.

수업에서 타자 연습을 시킬 때마다 기존 사이트들에서 두 가지 문제가 반복적으로 불편했습니다.

 

첫째, 광고와 UI 문제입니다.
기존의 타자 연습 사이트들은 배너 광고가 넘쳐나고 UI도 복잡합니다. 어른인 저도 헷갈리는데, 초등학교 1~2학년 아이들이 스스로 사용하기엔 너무 어려웠습니다. 광고 내용도 어린이에게 적절하지 않은 경우가 있었습니다.

 

둘째, 검정 타이머 기능이 없었습니다.
학원에서는 종종 타자 대회를 진행합니다. 1분, 3분, 5분 기준으로 분당 타수(KPM)를 측정하는 건데, 이 조건에 딱 맞는 사이트가 없었습니다. 기존 사이트는 무한 연습 모드가 기본이라 시험처럼 활용하기 어려웠습니다.

직접 겪은 문제였기 때문에, 해결책도 명확했습니다. 그냥 만들기로 했습니다.


2. 구체적인 문제 상황

수업 중 일어났던 일들을 정리하면 이렇습니다.

  • 아이들이 타자 사이트를 열면 가장 먼저 광고 배너를 클릭해서 엉뚱한 곳으로 이동하는 경우가 많았습니다.
  • "선생님, 어디 눌러요?"라는 질문을 수업마다 반복적으로 들었습니다. UI가 너무 복잡해서 연습 시작 버튼조차 못 찾는 것이었습니다.
  • 타자 대회를 열 때마다 타이머를 따로 켜놓고, 화면을 보면서 수동으로 "그만!"을 외쳐야 했습니다. 비효율의 극치였습니다.
  • 한글 기준 KPM이 사이트마다 달라서 결과를 비교하기도 애매했습니다.

이 문제를 해결하려면 딱 두 가지만 있으면 됐습니다.

  1. 광고 없는 깔끔한 UI
  2. 1분/3분/5분 검정 모드 (타이머 + 자동 종료 + KPM 측정)

3. 기대 사항

MVP 목표는 단순했습니다.

"우리 학원 학생들이 쓸 수 있는, 광고 없는 타자 연습 사이트"

거창하게 시작하지 않기로 했습니다. 처음부터 완벽한 서비스를 만들려다가 아무것도 못 만드는 경우를 너무 많이 봤기 때문입니다.
빠르게 MVP를 배포하고, 실제 학생들의 반응을 보면서 개선하는 방향으로 계획했습니다.


4. 완성본 소개

주요 기능

기능 설명

타입별 연습 한글 / 영어 / 프로그래밍 코드 타자 연습
검정 모드 1분 / 3분 / 5분 타이머 + 자동 종료 + 결과 저장
연습 모드 타이머 없이 자유롭게 연습
정확한 KPM 한컴타자 기준 자모 수 기반 계산
기록 저장 정확도 70% 이상인 기록만 저장
실시간 랭킹 오늘 / 이번 주 기준 랭킹
카드 게임 타이핑 기반 카드 게임 (게이미피케이션)

기술 스택 및 UI

  • Next.js 14 (App Router) — 풀스택을 하나의 프로젝트에서 처리
  • Supabase — Auth + PostgreSQL + RLS 한 번에
  • Vercel — git push로 자동 배포
  • TypeScript — 타입 안정성 확보

UI는 glassmorphism 스타일로 만들었고, 타입별로 테마 색상을 다르게 적용했습니다.

어린 학생들도 "이건 한글 모드구나"를 색깔만 봐도 직관적으로 알 수 있도록 했습니다.

  • 한글 → 황금색 계열
  • 영어 → 보라색 계열
  • 코드 → 청색 계열

한글 모드 메인 화면
영어 타이핑

 

프로그래밍 타이핑

 

게임 모드

게임 시작 창입니다.

게임 시작

게임에서 타이핑을 치면 화면 중앙 카드가 체크 됩니다. 

 

왼쪽에 내가 따낸 카드들이 정리 되고,

오른쪽에 조합을 성공하면 색상이 표시 됩니다.

게임 진행화면

로그인 한 상태로 높은 점수를 얻으면 기록 할 수 있습니다.

게임 종료

 

 

아키텍처

 


5. 기능 우선순위는 어떻게 정했나요

노션에 버전별로 로드맵을 정리하면서 진행했습니다.

v1.0 (MVP) — 핵심 기능만

✅ 한글 / 영어 / 프로그래밍 타자 연습
✅ 1분 / 3분 / 5분 검정 모드
✅ 자모 기반 KPM 계산
✅ 회원가입 / 로그인
✅ 기록 저장 및 랭킹

처음에는 게임 기능이 없었습니다. "당장 학원에서 쓸 수 있는가?"를 기준으로 판단했을 때, 검정 모드가 가장 시급한 기능이었기 때문입니다.

 

v2.0 — 게이미피케이션

✅ 카드 게임 모드 추가

MVP를 배포하고 나서야 게임 기능을 추가했습니다. 학생들이 연습 모드를 지루해한다는 걸 직접 보고 나서 만든 기능입니다. 게이미피케이션에 관심이 많아서 이 부분이 특히 재밌었습니다.

 

왜 이 순서였나요?

사실 게임이 더 흥미로운 기능입니다. 하지만 게임을 먼저 만들면 핵심 문제(타이머 검정)가 해결되지 않은 채 오래 걸리게 됩니다. 풀고 싶은 기능보다 필요한 기능을 먼저 만드는 것이 MVP의 기본이라고 생각했습니다.


6. 해결 과정

스택 선택: 빠른 MVP를 위한 조합

예전이었으면 서버 세팅하고, DB 설치하고, 인증 구현하는 데만 며칠이 걸렸을 것입니다. 이번엔 다음 조합으로 그 시간을 대폭 줄였습니다.

  • Next.js — 프론트엔드, 백엔드, API Routes 전부 하나의 프로젝트에서 처리
  • Supabase — Auth, DB, RLS를 한 번에 제공. 직접 구현할 게 거의 없습니다
  • Vercel — GitHub에 push하면 자동으로 배포됩니다

이 세 가지 조합이면 아이디어를 서비스로 만드는 데 걸리는 시간이 체감상 절반 이하가 됩니다.

카드 게임 설계

게임 모드는 타이핑을 기반으로 한 카드 게임입니다. 화면에 카드들이 나타나고, 해당 단어를 타이핑하면 점수가 쌓입니다. 카드 조합에 따라 보너스 점수가 붙고, 레벨이 오를수록 타이머가 짧아집니다.

타이핑 실력이 곧 게임 실력으로 연결되도록 설계했습니다. 단순히 빠르기만 하면 안 되고, 정확하게 쳐야 높은 점수를 받을 수 있습니다.


7. 트러블슈팅

만들면서 가장 많이 고생한 부분 세 가지를 공유합니다.

트러블슈팅 1: 한글 IME 처리

문제

처음엔 단순하게 input 이벤트로 한글을 처리했습니다. 그런데 한글은 자모를 조합하는 중간 상태가 존재합니다. '한'을 입력할 때 'ㅎ' → '하' → '한' 순서로 조합되는데, 조합 중인 글자가 지문과 비교되면서 전부 오타로 처리됐습니다.

지문: 한글
입력: ㅎ (조합 중)
결과: ❌ 오타 처리 → 사용자는 분명히 맞게 치고 있는데

 

해결

compositionstart, compositionupdate, compositionend 이벤트를 조합해서 처리했습니다.

// 조합 중일 때는 파란색으로 미리 보여주기만 합니다
onCompositionUpdate={(e) => {
  setComposingChar(e.data) // 조합 중인 글자 따로 보관
}}

// 조합이 끝났을 때만 지문과 비교합니다
onCompositionEnd={(e) => {
  compareWithTarget(e.data) // 확정된 글자만 비교
  setComposingChar('')
}}

핵심은 조합이 끝나기 전까지는 비교하지 않는 것입니다. 조합 중인 글자는 파란색으로 미리 표시해서 사용자가 "지금 입력 중"임을 시각적으로 알 수 있게 했습니다.

 

교훈

한글 IME는 생각보다 훨씬 복잡한 영역입니다. 단독 자모 vs 결합 자모(ㄲ, ㅒ), 복합 종성 분해(ㄳ → ㄱ+ㅅ), 연음 처리까지. 단순해 보이는 기능 하나에도 한글의 특성을 제대로 이해해야 한다는 걸 배웠습니다.


트러블슈팅 2: 한글 타수 계산 오류

문제

처음엔 완성 글자 수 × 2로 KPM을 계산했습니다. 그랬더니 실제로 400타 실력인 학생이 190타로 나오는 황당한 상황이 벌어졌습니다.

입력: "안녕하세요"
잘못된 계산: 5글자 × 2 = 10타 → 너무 적음

해결

한컴타자 기준으로 자모 수를 직접 계산하도록 바꿨습니다.

function countKeystrokes(char: string): number {
  const code = char.charCodeAt(0) - 0xAC00
  if (code < 0) return 1 // 영문, 공백 등

  const jongCode = code % 28
  return jongCode === 0 ? 2 : 3 // 받침 없으면 2타, 있으면 3타
}

// "안" = ㅇ+ㅏ+ㄴ = 3타
// "아" = ㅇ+ㅏ = 2타
// " " = 공백 = 1타

이렇게 하니 한컴타자와 거의 동일한 결과가 나왔습니다.

교훈

타수라는 개념 하나도 기준이 다르면 결과가 완전히 달라집니다. 사용자에게 익숙한 기준(한컴타자)을 맞추는 게 중요합니다. 숫자 하나가 UX 전체를 망칠 수 있다는 걸 느꼈습니다.


트러블슈팅 3: useCallback 클로저 문제

문제

타자 검정이 끝나면 기록을 저장하는 finishTyping 함수가 있는데, 분명히 로그인이 돼있는데도 함수 내부에서 user 상태가 null로 찍히는 문제가 있었습니다.

// 문제가 있었던 코드
const finishTyping = useCallback(async () => {
  console.log(user) // null이 찍힘... 분명 로그인 됐는데?
  if (!user) return // 저장 안 됨
  await saveRecord(user.id, score)
}, []) // deps 비어있음 → 클로저에 초기값 고정

 

해결

userRef를 추가해서 항상 최신 값을 참조하도록 했습니다.

const userRef = useRef(user)

useEffect(() => {
  userRef.current = user // user가 바뀔 때마다 ref도 업데이트
}, [user])

const finishTyping = useCallback(async () => {
  const currentUser = userRef.current // ref로 최신 user 참조
  if (!currentUser) return
  await saveRecord(currentUser.id, score)
}, []) // ref는 deps에 안 넣어도 됩니다

 

교훈

useCallback과 클로저의 관계를 제대로 이해하지 못하면 이런 버그가 생깁니다. useCallback의 deps 배열이 비어있으면 함수 생성 당시의 값이 고정됩니다. 비동기 콜백에서 최신 state가 필요하다면 ref를 쓰는 것이 안전합니다.


트러블슈팅 4 (보너스): 키보드 가이드 색상 문제

문제

한글 입력 중 다음에 입력할 자모가 키보드 가이드에 표시되는데, 초성/중성/종성이 모두 같은 색으로 보였습니다. 사용자 입장에서는 "지금 뭘 눌러야 하지?"를 구분할 수가 없었습니다.

 

원인

getNextKey 함수가 다음에 입력할 키만 반환하고, 그게 초성인지 중성인지 종성인지 메타정보를 전달하지 않았습니다.

// 문제: 문자열만 반환
function getNextKey(): string {
  return 'ㅎ' // 초성인지 종성인지 알 수 없음
}

 

해결

타입 정보를 함께 반환하도록 구조를 바꿨습니다.

// 개선: 타입 정보 포함
function getNextKeyWithType(): { key: string; type: 'cho' | 'jung' | 'jong' } | null {
  if (typed === 0) return { key: tKeys[0], type: 'cho' }
  if (typed === 1) return { key: tKeys[1], type: 'jung' }
  if (typed >= 2) return { key: tKeys[2], type: 'jong' }
  return null
}

// 키보드에서 색상 매핑
const keyTypeColors = {
  cho:  { bg: '#dbeafe', color: '#1e40af' }, // 파란색 계열
  jung: { bg: '#fed7aa', color: '#92400e' }, // 주황색 계열
  jong: { bg: '#d1fae5', color: '#065f46' }, // 초록색 계열
}

 

교훈

단순히 값을 전달할 때도 그 값이 어떤 의미를 가지는지 메타정보를 함께 전달해야 할 때가 있습니다. 컴포넌트 간 데이터 설계는 처음부터 잘 잡아두는 게 나중에 훨씬 편합니다.


8. 결과: 기대했던 것과 실제는?

MVP 배포 후 학원에서 직접 써봤습니다.

 

잘 된 것들

  • 광고가 없으니 아이들이 헤매지 않았습니다. 실제로 수업 중 "선생님 어디 눌러요?" 질문이 없어졌습니다.
  • 검정 모드가 생기니 타자 대회 진행이 훨씬 수월해졌습니다. 타이머가 자동으로 멈추고 결과를 바로 보여줍니다.
  • 카드 게임 모드는 중간 학년 학생들한테 인기가 좋았습니다. "선생님 더 어려운 거 없어요?"라는 반응도 나왔습니다.

 

아쉬웠던 것들

  • 너무 어린 학생들 (초등 1~2학년): 손가락 가이드가 없어서 어느 손가락으로 어느 키를 눌러야 하는지 모르는 경우가 있었습니다.
  • 중학생 이상: 카드 게임 난이도가 너무 쉽다는 피드백이 왔습니다. 타이핑 실력이 이미 어느 정도 되는 학생들에게는 도전감이 없다는 것이었습니다.

예상했던 것과 실제 반응이 다른 부분들이 있었지만, 오히려 그게 더 좋았습니다. 배포하지 않았으면 절대 몰랐을 피드백이기 때문입니다.


9. 교훈

이번 프로젝트에서 배운 것들을 정리하면 이렇습니다.

 

기술적인 것들

  • Next.js + Supabase + Vercel 조합의 생산성은 생각보다 훨씬 좋았습니다. 3일 만에 MVP를 낼 수 있었던 건 이 스택 덕분이었습니다.
  • 한글 IME는 별도의 도메인입니다. 한글을 다루는 서비스를 만들 거라면 IME 이벤트 흐름을 따로 공부해야 합니다.
  • useCallback과 클로저 문제는 React를 쓰다 보면 꼭 한 번 만나게 됩니다. 미리 알아두면 디버깅 시간을 줄일 수 있습니다.

프로젝트 전반에 대한 것들

  • 직접 불편함을 느낀 문제를 풀 때 동기부여가 압도적으로 강했습니다. "이거 내가 쓸 거야"라는 감각이 개발 속도에 영향을 미쳤습니다.
  • MVP는 진짜 작게 만들어야 합니다. 게임 기능을 v1에 넣었다면 아마 3일이 아니라 2주가 걸렸을 것입니다.
  • 실제 사용자에게 배포해야 진짜 문제를 발견할 수 있었습니다. 머릿속에서 상상한 문제와 현실의 문제는 다릅니다.

10. 개선사항 (다음 버전 계획)

실제 반응을 보고 나서 다음 버전에 넣을 것들을 정리했습니다.

  • 손가락 가이드 추가 — 어린 학생들을 위한 홈포지션 안내
  • 난이도 조절 시스템 — 초등 / 중등 / 고급 레벨 구분
  • 대회 기능 — 학원 내 학생들끼리 실시간으로 겨루는 기능
  • 캐릭터 커스텀 — 게이미피케이션 요소 강화. 타이핑 실력이 오를수록 캐릭터가 성장
  • 고급 게임 모드 — 중학생 이상도 도전감을 느낄 수 있는 난이도

마무리

처음엔 "우리 학원 애들이나 쓸 간단한 사이트"로 시작했는데, 만들다 보니 생각보다 복잡한 문제들이 많았습니다. 특히 한글 IME는 예상보다 훨씬 깊은 영역이었습니다.

그래도 3일 만에 실제로 배포하고, 실제 학생들이 쓰는 모습을 본 것이 가장 뿌듯했습니다. 코드를 짜는 게 목적이 아니라, 문제를 푸는 게 목적이라는 걸 다시 한 번 느꼈습니다.

사이드 프로젝트를 시작하고 싶은데 망설이고 계신다면, 작게 시작하는 걸 추천드립니다. 완벽한 기획보다 배포 경험이 훨씬 많은 걸 알려줍니다.

🔗 서비스 링크: https://atype-roan.vercel.app/
🔗 GitHub: https://github.com/MODIFYC/ATYPE


다음 블로그 예고

다음엔 Neckster 개발 이야기를 써볼 예정입니다.

728x90
반응형