유대선

진행 중 · 2026년 5월 23일

Mimi

YouTube 영상 아무거나 간격반복 영어 쉐도잉으로 바꾸고, 저절로는 안 나오는 문장 패턴·콜로케이션·전치사를 매일 드릴로 굳히는 풀스택 영어 학습 앱. Java/Spring + Next.js, 1인 개발.

8,892 LOC · 49 endpoints · 121 tests · multi-tenant · AWS ECS Fargate

역할
Solo (Claude Code 페어 프로그래밍)
스택
Java 21 · Spring Boot 3.3 · PostgreSQL 16 · Next.js 16 · TypeScript · Gemini / Claude · AWS ECS
Mimi

영어 문서는 하루 종일 읽는데 막상 말하거나 쓰려면 굳는 개발자를 위한 풀스택 영어 학습 앱. Java 21 / Spring Boot API + Next.js / TypeScript 프론트엔드, 현재 iOS App Store에 Mimi — English Shadowing으로 출시되어 있다. 핵심은 기능 수가 아니라 마찰 제거 — 이미 소비하는 입력(YouTube)을 피하던 출력 연습으로 바꾼다.

서로 먹여주는 두 반쪽

엔지니어링 하이라이트

스택 & 인프라

Java 21 · Spring Boot 3.3 · PostgreSQL 16(Flyway, JSONB, raw-SQL CHECK) · Next.js 16 / React 19 / TypeScript strict · Tailwind 4 · shadcn/ui. 관측성: Micrometer + Actuator → Prometheus, MDC 요청/사용자 로깅. Docker(멀티스테이지, ffmpeg + yt-dlp); GitHub Actions CI; AWS ECS Fargate + RDS로 keyless OIDC 배포; 프론트는 Vercel. Testcontainers(실제 Postgres) + Vitest + Playwright(14 spec).

아직 안 하는 것

프로젝트 로그

이 프로젝트를 만들면서 남긴 트러블슈팅 · 회고 · 업데이트의 시간순 기록.

28 로그됨
마지막 업데이트: 2026년 7월 20일
필터
2026년 7월 20일 주2 · 2 기술 회고
  1. PAY-1 — free/pro 한 칸을 capability 테이블로 교체하고 AI 비용 구멍을 막다

    기술 회고

    2026년 7월 20일 · 3

    user_entitlements 복합 PK + AccessPolicy 중앙 판정으로 SHADOW/AI 접근을 분리하고, 클립 자동분석·regenerate·유료 벤더 경로의 게이트 구멍을 봉합했다. 193개 테스트 green, 적대 리뷰 10건 반영.

  2. PAY-2 — RevenueCat 연동에서 적대 리뷰가 진짜 경쟁 조건과 보안 오탐지를 잡다

    기술 회고

    2026년 7월 20일 · 4

    216개 테스트가 green인 상태에서 3-렌즈 적대 리뷰가 신규 entitlement 행의 실제 경쟁 조건과 sandbox 오탐지 버그(유료 고객이 프로덕션에서 안 보이게 됨)를 찾아 원자적 SQL upsert로 재설계했다.

2026년 7월 13일 주16 · 5 업데이트 · 2 트러블슈팅 · 3 기술 회고 · 6 사용성 회고
  1. AI 스파링을 앱의 중심으로 — 독립 중앙 탭 승격 (R2)

    업데이트

    2026년 7월 14일 · 1

    묻혀있던 스파링을 최상위 중앙 탭으로 올렸다. 'live' 코랄색의 부양 마이크 버튼 — 나머지 파란 앱과 다른 색·다른 자리로, 제품의 해자(말하기)가 가장 눈에 띄는 픽셀을 갖게 했다. Codex R2 트랙이 미산출이라 직접 구현.

  2. U1 mastery 데이터 레이어와 스파링 리포트 클라이언트

    업데이트

    2026년 7월 14일 · 2

    8개 정적 SRS 팩의 mastery를 한 selector로 집계하고 F1 스파링 리포트 API를 shared core에 연결했다. 오늘 reps를 주간 합계로 오인하지 않도록 표시 계약도 분리했다.

  3. U3 세션 goal-gradient와 peak-end

    사용성 회고

    2026년 7월 14일 · 4

    드릴에 후퇴하지 않는 진행바와 막판 남은 카드 강조를 추가하고, 낙관 채점 뒤 드릴·스파링을 공용 peak-end 요약으로 마무리했다. 공유 SRS 갱신이 세션을 재마운트하던 경합은 세션 snapshot으로 분리했다.

  4. U4 팩별 진행과 조용한 마일스톤

    업데이트

    2026년 7월 14일 · 3

    Practice 팩 카드에 U1 mastery 진행을 연결하고, 사용자별 로컬 receipt로 마스터·스트릭·첫 스파링 축하를 한 번만 큐잉한다.

  5. 공용 API 요청에 15초 deadline 추가

    트러블슈팅

    2026년 7월 13일 · 1

    공용 fetch client에 일반 요청 15초, FormData 60초 상한을 추가하고 caller 취소와 timeout을 구분했다. 공용 client를 우회하는 legacy 음성 업로드는 이번 범위에 포함되지 않는다.

  6. 신규 사용자를 무료 학습 루프로 안내

    사용성 회고

    2026년 7월 13일 · 4

    빈 Home의 일반 import CTA를 shadow, 기기 내 dictation, review로 이어지는 우선순위 경로로 바꾸고 초대 전용 AI 범위를 분명히 했다.

  7. 모바일 저장 실패를 보이게 — 카드 진행 전에 서버 응답 기다리기

    기술 회고

    2026년 7월 13일 · 3

    clip·영상 삭제·review·drill grade의 조용한 mutation 실패를 localized Alert로 드러내고, grade는 저장 성공 뒤에만 다음 카드로 진행하도록 바꿨다.

  8. 모바일 다크 입력창과 눌림 피드백 — 터치가 보이게

    기술 회고

    2026년 7월 13일 · 3

    지정된 12개 입력창을 theme 색으로 바꾸고, Track D 허용 화면의 Pressable 49개에 공용 opacity와 Android ripple을 연결했다.

  9. R3 Home 우선순위와 Practice 팩 그리드

    사용성 회고

    2026년 7월 13일 · 4

    Home 첫 화면을 streak와 one-tap Sparring 중심으로 재구성하고, Practice를 팩별 count·due 메타가 있는 균일 그리드로 바꿨다.

  10. R1 의미 토큰과 공용 Pressable 기반

    기술 회고

    2026년 7월 13일 · 3

    기존 색 값을 primary/live/mint 의미로 고정하고, light/dark 공용 Card·Chip·PrimaryButton·TalkButton과 press/focus helper를 추가했다.

  11. Review·Me·drill 채점 표면을 공용 토큰으로 통일

    사용성 회고

    2026년 7월 13일 · 2

    Review 카드 flip과 semantic grade 색을 추가하고, Me 섹션과 두 drill runner를 R1 Card·Chip·Button 계약으로 통일했다.

  12. 저장 세션을 네이티브 splash 아래에서 조용히 검증

    트러블슈팅

    2026년 7월 13일 · 2

    SecureStore 실패와 만료 토큰이 부팅을 멈추거나 Home을 잠깐 노출하지 않도록, 저장 토큰을 bounded me 요청으로 먼저 확인하고 401 fallback을 합쳤다.

  13. 스파링 연결에 끝을 만들다 — 12초 상한, 취소, 복구 문구

    사용성 회고

    2026년 7월 13일 · 2

    Realtime mint 또는 WebView 연결이 끝나지 않던 경로에 12초 상한과 Cancel을 추가했다. 내부 오류 원문은 로그에만 남기고 화면은 재시도 가능한 한국어·영어 안내로 통일했다.

  14. AI 403을 초대제 안내와 무료 연습 복귀로 바꾸기

    사용성 회고

    2026년 7월 13일 · 2

    AI 사용 권한이 없는 계정에서 서버 403 원문 대신 초대제 설명을 보여 주고, 비용 없는 Practice 흐름으로 돌아갈 수 있게 했다.

  15. 스파링에 신규 학습 표현 모드 — 낮은 box부터 입으로 꺼내기

    업데이트

    2026년 7월 13일 · 2

    복습일이 아직 오지 않은 낮은-box 표현을 우선 주입하고, 부족한 자리는 아직 본 적 없는 카드로 채우는 스파링 모드를 추가했다. 기존 오늘 복습 선택은 바꾸지 않았다.

  16. 기본 동사 스파링을 한 축으로 — 동사별·파티클·전치사별 하위 범위

    업데이트

    2026년 7월 13일 · 3

    기본 동사 스파링에서 put/take/get 같은 동사 그룹이나 to/in/with/up/out 같은 파티클·전치사 하나만 골라 대화 타깃을 만들 수 있게 했다. 선택 범위 밖 카드는 보충하지 않는다.

2026년 7월 6일 주2 · 1 업데이트 · 1 사용성 회고
  1. 대화가 어색한 이유 — AI에게 '목록 다 쓰지 마라'를 가르치다

    사용성 회고

    2026년 7월 12일 · 2

    스파링이 오늘 복습 카드를 섞어 6개 주입하니 운동·코딩 표현이 한 대화에 뒤섞여 어색했다. '전부 소화'가 아니라 '한 흐름에 붙는 부분집합만 쓰고 나머지는 이월'로 프롬프트를 바꿔 자연스러움을 회복. 근본 해결(주제 스코프 세션)은 별도 설계 중.

  2. 주제 골라 스파링 — 팩 하나로 대화를 한 결로 묶기 (③)

    업데이트

    2026년 7월 12일 · 2

    스파링 타깃이 주제를 넘나들어 어색하던 문제(① 프롬프트로 완화)를 근본적으로: 세션 시작에 주제(팩)를 고르면 타깃 6개가 그 도메인 안에서만 뽑힌다. 클라이언트 전용 변경이라 백엔드·런타임 비용 0.

2026년 6월 1일 주8 · 6 기술 회고 · 2 사용성 회고
  1. 모바일 사용성 갈아엎기 — 영상을 버리지 않고 '내 영상'으로 쌓는 TubeShad식 플로우

    사용성 회고

    2026년 6월 7일 · 4

    import하면 클립 하나 만들고 영상을 버리던 모바일 플로우를, 영상을 저장해 언제든 다시 열어 전체 자막을 보고 아무 줄이나 쉐도잉하는 구조로 바꿨다. 근본 원인은 '내가 import한 영상' 개념의 부재였다.

  2. 자막 캐시를 1000만 명 규모로 — 전역 공유 캐시의 두 빈틈(경합 insert + 무한 재긁기)

    기술 회고

    2026년 6월 7일 · 5

    1000만 명까지 가는 설계에서 '실시간 스크래핑'은 비용·차단·법적으로 불가능하다. 답은 캐싱. videos 테이블은 이미 전역 공유 캐시였지만, 동시 insert 경합과 UNAVAILABLE 무한 재긁기 두 빈틈이 스케일에서 터진다.

  3. 쉐도잉 루프 고도화 — 한 줄 반복 / A-B 구간 / 자동 다음 줄을 폴링 하나로

    사용성 회고

    2026년 6월 7일 · 3

    import한 영상 화면에서 TubeShad급 쉐도잉을 만들었다. 한 줄 반복, A-B 구간 반복, 줄별 N회 자동 진행 + 0.5~1.5배속을, 단일 폴링 루프에 모드 분기로 통합했다.

  4. 우리가 서울에 올린 AWS 49개 — 실제 배포된 리소스 전체 딥 정리

    기술 회고

    2026년 6월 4일 · 10

    terraform state에 실제로 박힌 49개 리소스(+ 데이터소스 7개)를 층별로 하나도 안 빼고 푼다. 각 리소스가 무엇이고 왜 있고 어디에 연결되는지.

  5. YouTube 자막 가져오기 7가지 방안 — 하나도 모르는 사람을 위한 딥 해설

    기술 회고

    2026년 6월 4일 · 9

    레지덴셜 프록시, CORS 프록시, 브라우저 확장, 큐레이션... 용어를 하나도 모를 때 각 방안이 정확히 무엇이고 왜 되거나 안 되는지를 바닥부터 푼다.

  6. YouTube 자막 — 검증된 방법 랭킹 + 폴백 런북 (이게 깨지면 어디로 가나)

    기술 회고

    2026년 6월 4일 · 5

    7개 방법을 적대적으로 실측 검증한 뒤 'bgutil POToken 사이드카'를 채택. 깨졌을 때 갈아탈 폴백 순서와 각 대안의 트레이드오프를 런북으로 남긴다.

  7. YouTube 자막의 진짜 벽은 IP가 아니라 POToken이었다 — 그리고 WebView 우회

    기술 회고

    2026년 6월 4일 · 4

    AWS 배포 후 깨진 YouTube import. '데이터센터 IP 차단'인 줄 알았는데, 가정 IP에서도 raw fetch는 빈 응답이었다. 진짜 벽은 POToken(BotGuard)이었고, yt-dlp만 통과한다. 폰은 yt-dlp를 못 돌리니 — 진짜 브라우저인 WebView로 우회한다.

  8. YouTube 자막을 어디서 가져올 것인가 — 서버 vs 클라이언트, 웹 vs 모바일 (CORS의 벽)

    기술 회고

    2026년 6월 4일 · 7

    AWS 배포 직후 YouTube 임포트가 깨졌다. yt-dlp가 데이터센터 IP에서 차단된 것. '그럼 클라이언트에서 가져오면?'을 끝까지 파보니, 웹앱은 CORS 때문에 사실상 불가능하고 모바일 네이티브만 가능하다는 비대칭이 드러났다. 7개 방안을 차원별로 비교하고 Mimi의 전략을 정한다.