Haechi는 한국어 문법을 FSRS-5 간격 반복 알고리즘으로 학습하는 앱이다. 문법 페이지를 탐색하고, 플래시카드 세트를 생성하고, 과학적 스케줄링으로 복습한다. 지난 몇 주간 세 번의 메이저 릴리스를 거쳤다.
v0.1.0 — 비주얼 통합
첫 번째 과제는 앱이 블로그와 같은 사람이 만든 것처럼 보이게 하는 것이었다. 기존에는 450줄의 바닐라 CSS로 급하게 스타일링한 상태였다.
다크 퍼스트 디자인. 다크 모드를 기본으로 설정했다. localStorage에 저장하고 시스템 설정을 존중하며, 로드 시 잘못된 테마가 깜빡이지 않는다. 라이트 모드는 토글로 전환 가능.
이중 타이포그래피 시스템. UI 크롬(내비게이션, 라벨, 버튼, 통계)에는 Geist Mono를, 한국어 콘텐츠에는 Pretendard Variable을 사용한다. 하나의 시스템에 두 가지 정체성이 공존한다.
이중 악센트 컬러. 문법 탐색 모드에는 파란색(#7eb8f0), 복습 세션에는 보라색(#c4a7e7). 복습 페이지에서 CSS 변수 스코핑으로 자동 전환된다.
대시보드 홈페이지. 복습 대기 카드 수, 예상 소요 시간, 세션 횟수, 다음 복습 시간을 실시간으로 보여주는 통계 그리드. 신규 사용자에게는 온보딩 메시지, 복습할 것이 없으면 “모두 완료” 상태 표시.
Tailwind CSS 4. 450줄의 바닐라 CSS를 블로그와 동일한 디자인 토큰 시스템으로 교체. @tailwindcss/vite 플러그인을 통해 rgba() 배경과 보더를 사용하는 반투명 표면 시스템을 구축했다.
v0.1.1 — 레이아웃 확장과 문법 검색
비주얼 통합 직후 실제로 사용해 보니 부족한 부분이 보였다.
960px 와이드 레이아웃. 홈페이지와 복습 페이지를 960px로 확장하고, 문법 상세 페이지는 768px 읽기 칼럼을 유지했다. 프로그레스 바와 카운터는 전체 너비, 카드는 480px 중앙 배치.
문법 검색과 필터. 한국어 제목이나 영어 정의로 검색 가능. 초급/중급/고급 필터 필(문법 항목이 5개 이상일 때 표시). 홈페이지에서 바로 학습 상태 배지를 확인할 수 있다.
일관된 대시보드. 신규 사용자든, 활발히 학습 중이든, “모두 완료” 상태든 동일한 4개 통계 그리드와 복습 버튼을 본다.
v0.2.0 — 인증과 서버 데이터베이스
가장 큰 변화. 브라우저 로컬 저장소만으로는 한계가 명확했다. 폰에서 공부하다 노트북으로 옮기면 데이터가 없다. 지하철에서 복습하다 오프라인이 되면 리뷰가 날아갈 수 있다.
인증
better-auth로 이메일/비밀번호 인증. 30일 세션, 미들웨어 기반 인증 가드. 공개 경로(로그인, 회원가입, API 헬스 체크)를 제외한 모든 페이지가 인증 필요.
로그인/회원가입 페이지는 기존 다크 에디토리얼 디자인을 따른다. Study 버튼에 인증 게이트를 추가해서 비로그인 사용자는 리다이렉트 대신 인라인 회원가입 안내를 본다.
서버 데이터 영속성
AstroDB(Turso/libSQL) 로 서버 측 데이터베이스 구축. 스키마: User, Session, Account(better-auth 관리 테이블) + Card, ReviewLog, ReviewStats(앱 데이터).
5개 API 엔드포인트:
cards/generate— 문법 페이지에서 플래시카드 생성cards/due— 복습 대기 카드 조회review— FSRS 서버 측 스케줄링으로 리뷰 제출sync— 오프라인 데이터 동기화health— 연결 상태 확인
DataProvider 추상화
가장 중요한 아키텍처 결정이었다. 세 가지 구현체:
- LocalDataProvider — Dexie.js/IndexedDB. 오프라인 전용 또는 비로그인 모드.
- ServerDataProvider — Fetch API로 서버에 직접 요청.
- SyncingDataProvider — 하이브리드. 로컬에서 읽고, 서버에 쓰고, 백그라운드로 동기화.
세션 쿠키 유무에 따라 적절한 Provider를 반환한다. 모든 컴포넌트(ReviewSession, StudyButton, DashboardStats, ExportImport)가 DataProvider를 통해 동작하므로, Dexie를 직접 임포트하는 코드가 사라졌다.
오프라인 우선 동기화
지하철에서 복습해도 데이터를 잃지 않는 것이 핵심 요구사항이었다.
SyncingDataProvider는 앱 초기화 시, 온라인/오프라인 이벤트 발생 시, 페이지 가시성 변경 시 자동으로 동기화를 시도한다. Last-write-wins 전략에 UUID 중복 제거. 토스트 알림으로 동기화 상태를 피드백한다.
리뷰 로그에 state_before/state_after FSRS 상태 스냅샷을 저장한다. 현재는 last-write-wins지만, 향후 충돌 해결 전략을 정교화할 수 있는 기반을 마련했다.
기타 변경
- Astro 5 → 6 업그레이드. 하이브리드 모드가 기본이 되면서
output: 'hybrid'설정이 불필요해졌다. - Date → number 마이그레이션. 모든 타임스탬프를 유닉스 밀리초로 변환. Dexie가 자동 마이그레이션.
grammarId→grammarSlug리네이밍. 코드베이스 전체에서 의미를 명확하게.- 모든 페이지 서버 렌더링. 인증 상태에 따라 헤더를 다르게 보여주기 위해 필요.
테스트 인프라
E2E 테스트를 위해 임시 Turso 데이터베이스를 생성하고, 테스트 실행 후 정리하는 스크립트를 만들었다. 단일 명령어로 DB 생성 → 서버 기동 → Playwright 테스트 → DB 삭제까지 자동화.
현재 42개 단위 테스트(Dexie 연산, FSRS 스케줄링)와 33개 E2E 테스트(인증 플로우, 문법 페이지, 복습 세션, 내보내기/가져오기)가 통과한다.
왜 이렇게 만들었는가
Haechi는 의도적으로 미니멀하다. 듀오링고의 반대편에 서 있다. 게이미피케이션 없음, 장식 없음. 타이포그래피와 깊이감만으로 인터페이스를 구성한다.
이 접근 방식에는 이유가 있다. 늦은 밤 공부할 때 화려한 애니메이션과 알림은 방해가 된다. 차분하고, 집중할 수 있고, 신뢰할 수 있는 도구가 필요했다. 데이터가 내 것이고, 오프라인에서도 동작하고, 여러 기기에서 이어서 할 수 있으면 된다.
FSRS-5를 선택한 것도 같은 맥락이다. Anki의 SM-2보다 과학적으로 더 정확하고, 학습자의 기억 상태를 더 세밀하게 추적한다. 알고리즘이 언제 복습할지 결정하면, 나는 그냥 앉아서 공부하면 된다.
다음 단계는 문법 콘텐츠를 확대하는 것이다. 현재 두 개의 문법 항목(~아/어서, ~으면서)만 있다. 인프라는 준비되었고, 이제 콘텐츠를 채울 차례다.