Speak Five
직무와 상황에 맞는 영어 문장 5개를 듣고, 따라 말하고, 영어를 가린 채 회상하는 학습 앱의 초기 골격 - 18시간, 커밋 8개로 선택 화면·시간대 규칙·테스트 게이트까지만 세웠고, 문장은 아직 0개다. 제품 이름도 가칭이다.
- React
- TypeScript
- Vite
- Capacitor
- Playwright
- Temporal
- QA
오늘의 학습 - 직무·상황을 고르면 그 조합의 문장을 보여주는 자리. 검수된 문장이 0개라 빈 상태를 그대로 보여준다
직무 선택 - 5직군으로 묶은 16직무 드롭다운. ID는 도메인 타입 한 곳에서만 정의한다
상황 선택 - 직무마다 5개, 전체 80상황. 날짜는 KST 달력 날짜로, 저장은 UTC Z로 가른다
Setup
- Problem
BRD의 출발 사례는 하나다. 미국 회사에 입사한 QA 엔지니어가 업무 영어와 졸업 말하기 점수를 같이 고민하는 상황. 업무 내용을 설명할 지식은 있는데, 영어로 진행 상황·문제·기대 결과·협업 요청을 꺼내는 데 어려움을 겪는 사람이다. BRD는 이 한 사례를 전체 시장의 수요로 일반화하지 않는다고 적어뒀고, 초기 고객 후보로 "한국어를 사용하며 해외 동료와 협업하는 직장인"을 제안만 해뒀다. PRD의 한 줄은 "업무에서 꺼내 쓸 표현을 짧게 반복하는 제품"이다. 첫 사용자 스토리도 "QA 엔지니어로서 오늘 버그를 설명할 표현을 골라 연습하고 싶다"다. 사업 상태는 BRD 머리에 그대로 적혀 있다. 고객 실험과 매출이 아직 검증되지 않은 기획.
- Context
2026년 10월 3일 22:58에 첫 커밋, 다음 날 16:56에 여덟 번째 커밋. 18시간 안에 들어간 작업이다. 문서는 10개(README, SSOT, BRD, PRD, ARCHITECTURE,
DATA_API_SPEC,CONTENT_SPEC,QA_TEST_PLAN, ROADMAP,PROJECT_STRUCTURE)고 버전은 0.1.7 Draft다. 제품 이름 Speak Five는 가칭이고 배포용 앱 식별자는 미확정이다. 로드맵은 R0 기획·콘텐츠 준비, R1 공통 웹·웹뷰 체험판, R1 고객 실험(20명·14일), R2 계정·유료 운영, R3 확장 순이다. 월 4,900원이라는 가격도 문서에는 "검증 전 제안"으로만 적혀 있다. 지금 위치는 R1 골격이고, README는 "이 골격은 R1 완료나 출시 준비를 뜻하지 않는다"고 먼저 말한다.- Users
가설상 사용자는 한국어로 일하면서 해외 동료와 협업하는 직장인이고, 그중 첫 사례가 QA 엔지니어다. 실제 사용자는 0이다. 검수된 문장이 하나도 없어서 지금 화면은 직군·직무·상황 선택과 빈 콘텐츠 상태만 보여주고, 선택값은 메모리에만 있어 새로고침하면 사라진다.
- Hypothesis
직무·상황으로 고른 문장 5개를 듣고, 따라 말하고, 영어를 가린 채 회상하는 짧은 반복이 업무 영어를 꺼내 쓰는 데 실제로 도움이 된다면, 검수된 문장 200개(4직군 × 50)만으로 20명·14일 실험에서 신호가 나온다. 실험 전까지는 어느 것도 확인된 게 없다.
Build
- What I did
- React·TypeScript·Vite 실행 환경에 린트·단위 테스트·E2E·GitHub Actions CI를 먼저 붙였다. npm run verify 하나가 린트(경고 0)·단위 테스트·빌드·E2E를 순서대로 돈다
- 5직군·16직무·80상황 선택 화면 - 직군으로 묶은 직무 드롭다운과 공통 커스텀 Select. ID는 domain/profile/
types.ts한 곳에서만 정의하고 콘텐츠 트리는 그 ID를 참조한다 - 시간대 규칙을 먼저 코드로 못박았다 - 저장·API·로그는 UTC Z 표기, 표시·학습일·복습일은 Asia/Seoul,
day_key는 timestamp가 아니라 학습 시간대의 달력 날짜.calendar.ts는 Z가 아닌 timestamp를 RangeError로 거부한다 - 세션 선택·복습 일정·보고서 집계를 어댑터를 모르는 순수 도메인 모듈로 떼어냈다. 의존은 한 방향이고 domain은 adapters를 참조하지 않는다
- 콘텐츠 스키마 검증·도메인 타입·저장/오디오/생명주기/콘텐츠 어댑터 계약, Capacitor 설정과 플랫폼 생성 절차까지 - 단, 네이티브 빌드와 실기기 테스트는 하지 않았다
- 마지막 커밋에서 Temporal 경로를 둘로 나눠 테스트하게 했다 - CI의 Node 26은 Temporal을 내장해서 폴리필 경로가 검증되지 않고 있었다
- Product decisions
- 미검수 문장과 가짜 완료 결과를 앱에 넣지 않는다 - 그래서 카탈로그는 bootstrap-empty-v1, 문장 0개다. 선택 화면이 빈 상태를 보여주는 건 버그가 아니라 결정이다
- 웹으로 공통 기능을 만들고 iOS·Android는 웹뷰 앱으로 낸다. 빌드 타깃은 iOS 16.4 WebView에 맞췄고, native/에는 Capacitor 설정만 두고 빈 가짜 프로젝트는 만들지 않는다
- R1에는 업무 Backend가 없다. backend/에는 UTC Z 계약만 적어두고 실행 서버는 두지 않는다
- 제품 이름·앱 식별자·가격을 확정하지 않은 채로 둔다. 모르는 것을 확정한 것처럼 적지 않기 위해서다
- 다음 단위는 기능이 아니라 콘텐츠다 - 4직군 × 1상황 × 5문장, 검수된 20문장과 오디오로 핵심 흐름을 한 번 잇는 것
- QA considerations
- 출시 코드와 테스트 코드가 같은가 - Playwright는 dev 서버가 아니라 production 빌드(preview, 4174 포트)에 붙는다. 설정 파일 주석이 이유를 적어뒀다. 출시되는 건 production 빌드이기 때문
- CI 환경이 숨기는 경로가 있는가 - Node 26은 Temporal을 내장하니 폴리필 경로는 CI에서 한 번도 돌지 않고 있었다. vitest 프로젝트를 runtime과 polyfill 둘로 나눠 같은 단위 테스트 296개를 두 번(592개) 돌리고, E2E는 addInitScript로 Temporal을 지운 뒤 2026-10-03T15:00Z가 "10월 4일"로 그려지는지 본다
- 날짜 경계에서 같은 입력에 같은 결과인가 - UTC Z 저장과 KST 달력 날짜를 분리하고, Z가 아닌 timestamp는 아예 받지 않는다. 자정 근처의 하루 어긋남을 함수 하나의 계약으로 막는 방식
- 테스트 통과를 완료로 읽지 않는가 -
QA_TEST_PLAN은 FR-001~010 추적표와 기기·오디오 매트릭스를 갖췄지만 모든 칸이 "미실행"이다. 문서 스스로 "구조 테스트 통과를 전체 수용 기준 통과로 취급하지 않는다", "테스트 더블을 실제 저장·음성 검증 완료로 취급하지 않는다"고 적어뒀다 - 실기기 검증이 빠져 있다 - README가 "이 테스트는 실제 iOS·Android WebView 검증을 대체하지 않는다"고 명시한다. 음성·권한은 시뮬레이터로 대체하지 않는다는 게 계획서의 원칙이다
Outcome
- Metrics
- 커밋 8개, 2026-10-03 22:58 ~ 10-04 16:56 KST, 단일 저자
- 문서 10개, 버전 0.1.7 Draft. 콘텐츠 트리 5직군·16직무·80상황, 검수 문장 0개
- 단위 테스트 파일 9개(선언 179개, Temporal 2경로로 592개 실행), E2E 스펙 2개(25개 통과·desktop 전용 1개 건너뜀), 통합 테스트 0개. 수치는 마지막 커밋 메시지에 적힌 실행 결과
- CI - push·PR마다 Node 26에서 npm run verify, 실패 시 trace·스크린샷 7일 보관
- 사용자·학습·매출 지표 전부 측정 전. QA 계획의 릴리스 게이트 네 칸 모두 미실행·미제작·미준비
- Result / Learning
결과는 없다. 18시간 만에 남긴 건 "무엇을 넣지 않을 것인가"가 코드로 고정된 골격이다. 문장이 검수되기 전에는 화면이 비어 있고, CI가 숨기던 폴리필 경로는 이제 매번 돈다. 다음 단위가 기능이 아니라 검수된 20문장이라는 것도 문서에 적혀 있다. 이 항목은 완성 기록이 아니라 시작점 기록이다. 고객 실험(20명·14일)이 돌고 나면 그 숫자를 여기 적는다.
- Retrospective
- 문서 10개 1만 자 가까이를 하루에 썼는데 검수된 문장은 0개다. 이 제품의 병목은 코드가 아니라 콘텐츠 검수라는 걸 로드맵도 알고 있다. 다음에 손대는 건 코드가 아니어야 한다.
- Temporal 폴리필 경로가 CI에서 검증되지 않고 있었다는 건 마지막 커밋에서야 알았다. "CI 통과"가 어떤 환경의 통과인지는 처음부터 물었어야 했다.
- Tech stack
- React
- TypeScript
- Vite
- Vitest
- Playwright
- Temporal (@js-temporal/polyfill)
- Capacitor
- GitHub Actions