혼자서 Bookverse를 만든 6개월
혼자 소프트웨어를 만드는 일에는 낭만적인 버전이 있다. 깔끔한 책상, 에스프레소 한 잔, 그리고 점심때쯤이면 사람들이 사랑하는 무언가를 출시하는 것. 하지만 내가 지난 6개월 동안 실제로 살아온 버전은 *“일곱 번째 엣지 케이스가 방금 당신을 물어버려서, 똑같은 Nginx 설정을 네 번 다시 쓴다”*에 더 가깝다.
이것은 회고다 — 출시한 것들의 체크리스트도, 튜토리얼도 아니다. 그저 6개월을 보낸 뒤의 메모일 뿐이다.
왜 시작했나
나는 수년간 언어 앱들을 이따금 써왔는데, 한 가지 구체적인 점을 발견했다. 인기 있는 게임화 앱에서 30일 연속 기록을 채울 수 있으면서도 여전히 원어로 된 문단 하나를 읽지 못했다. 피드백 루프는 촘촘했고 게임화는 매끄러웠지만, 진척의 단위가 한 문장, 때로는 한 단어였다. 그리고 언어를 읽는 일은 — 실제로 읽어내는 일은 — 페이지 단위에서 일어난다.
나는 교과서를 교과서답게 다루는 앱을 원했다. 1장을 펼친다. 읽는다. 듣는다. 다시 말해본다. 남은 것을 공부한다. 내일은 2장.
제품의 형태는 첫날부터 분명했다. 함정은 “첫날부터 형태가 분명함”이 소프트웨어를 만드는 일의 5% 정도밖에 책임지지 않는다는 것이다. 나머지 95%는 화려할 것 없는 중간 구간이다.
화려할 것 없는 중간 구간
이번 분기에 내가 한 일 중, 그 어떤 릴리스 노트에도 없는 것들의 불완전한 목록:
- 전체 모노레포 경로 트리를
app_v54_01에서bookverse로 이름 변경했다 (그리고 systemd 유닛 이름에 밑줄을 넣고 싶어져서 다시 이름을 바꿨다). - Nginx vhost용 Terraform 설정을 네 가지 버전으로 작성한 끝에,
null_resource트리거가 입력 변수뿐 아니라 렌더링된 템플릿의 해시도 포함해야 한다는 걸 알아냈다. EXPO_PUBLIC_*환경 변수 값이 iOS에서는 잘 작동하는데 웹 번들에서는 왜 빈 문자열로 resolve되는지 추적하는 데 일주일을 썼다. (Babel의preset-expo는node_modules에 대해 인라이닝을 건너뛴다. 게시된 패키지에서는 환경 변수가 아니라 팩토리 함수를 써야 한다.)- API URL 헬퍼의 호출부 17곳을 리팩터링했다. 처음에 그것을 잘못 중앙화했기 때문이다.
이 중 어느 것도 앱에 관한 마케팅 글에는 실리지 않을 것이다. 하지만 전부 실제 작업이었다.
혼자 개발하는 일에 대해 아무도 경고해주지 않는 것은, 지루한 절반을 떠맡아 줄 사람이 없다는 점이다. “플랫폼 팀이 배포를 처리한다”고 말할 수 없다. 왜냐하면 당신이 곧 플랫폼 팀이기 때문이다. 당신은 모든 모자를 서툴게 쓴다 — 충분히 오래 써서 제대로 쓰게 될 때까지.
음성 인식이라는 토끼굴
지난 6개월 중 가장 많은 교훈을 준 일화는, 중국에서 휴대폰으로 말하기 연습이 작동하게 만드는 것이었다.
계획은 이랬다: 한 줄을 탭하고, 원어민이 그것을 말하는 걸 듣고, 다시 탭해서
자기 목소리를 녹음하고, 얼마나 비슷한지 본다. 표준적인 모바일 음성 인식이면
된다 — iOS에서는 expo-speech-recognition, Android에서는 시스템 서비스,
끝.
현실은: 대상 청중의 상당 부분이 중국 지역 OS 변형(gmsconfig.china
오버레이)이 적용된 Lenovo Motorola 휴대폰을 쓰는데, 이것은 Google Mobile
Services를 통째로 제거해버린다. 시스템 음성-텍스트 변환이 없다. 앱은
에뮬레이터에서는 완벽하게 작동했다. 베이징의 Moto XT2507에서는 조용히
크래시 났다.
수정에는 약 3주가 걸렸다:
- 앱이 플랫폼별로 음성 백엔드를 교체할 수 있도록 어댑터 패턴을 구축했다 — 가능한 곳에서는 시스템 인식, 대체 수단으로는 클라우드 전사.
- Whisper 스타일의 클라우드 백엔드를 추가했다 (DashScope를 통한 Alibaba의
qwen3-asr-flash. 중국 지역 기기는 VPN 없이도 도달할 수 있기 때문이다). - 말을 마치면 녹음이 자동으로 멈추도록 음성 활동 감지를 조정했다. (-25 dBFS 임계값, 600ms 침묵 윈도우, 다섯 개 샘플에 걸쳐 평활화 — 다른 모든 조합은 단어 중간에 끊거나 아예 멈추지 않았다.)
- React Native의
FormData업로드에서expo-file-system의uploadAsync로 전환했다. 같은 기기들에서 RN의FormData가 오디오 blob에 대해 불안정했기 때문이다.
저 네 항목 중 절반은 내가 보통 한 주에 쓰는 것보다 많은 코드다. 그 어느 것도 “기능”이 아니다. 하지만 전부 그 기능을 필요로 하는 사람들에게 그것이 존재하기 위해 필요했다.
그 교훈은 — 대략 2주마다 반복되는데 — 어려운 부분은 당신이 어려울 거라 생각한 부분인 경우가 드물다는 것이다. 나는 음성 인식에 하루를 잡았다. 스무 날이 걸렸다.
끝맺음의 규율
혼자 만들 때의 유혹은 새롭고 반짝이는 것을 좇는 것이다. 새 기능은 신난다. 기존 흐름에 대한 47번째 손질은 신나지 않는다. 그래서 당신은 기능들을 쌓아가는데, 그중 어느 것도 제대로 끝나지 않아서, 제품이 절반쯤 지어진 것들의 무덤처럼 느껴진다.
내가 거듭 다시 배우는 규율: 다음 것을 시작하기 전에 한 가지를 제대로 끝내라. 만다린 먼저. 만다린을 정말 잘 만들어라. 그다음 한국어. 그다음 영어. 이 플랫폼은 구조화된 코스를 전달하기 위해 만들어졌다 — 하지만 첫 코스는 간판이 되는 코스, 학습자가 실제로 끝낼 수 있는 코스여야 한다.
5%만 지어진 계획된 한국어 코스는 누구에게도 도움이 되지 않는다. 누군가가 처음부터 끝까지 읽을 수 있는 완성된 만다린 코스가 진짜 제품이다.
기능도 마찬가지다. 말하기 연습은 출시되기까지 석 달 동안 로드맵에 있었다. 늦게 출시됐지만, 제대로 출시됐다 — 중국 지역 케이스를 다루는 것은 그것이 쓸 만해지기 위한 하중을 견디는 요소였지, 미뤄도 될 마감 손질 같은 게 아니었다.
11월의 나에게 해주고 싶은 말
세 가지. 이 일을 시작한 나의 버전에게 엽서를 보낼 수 있다면.
이름 바꾸기를 멈춰라. 이름은 첫날에 틀려도 괜찮은 것이다. 이름을 다시 바꾸는 것은 대개 그 값어치보다 비싸다. (나는 지금까지 이것을 정확히 네 번 했다.)
구조화된 책을 사라. 나는 포럼의 어휘 목록들 사이를 오가며 몇 주를 낭비했다. HSK 3.0 표준은 실제로 출판된 문서다. 구조화된 책을 사서 그것을 따르는 편이, 그것들이 무엇을 다루는지 역설계하는 것보다 빠르다.
청중은 인내심이 있다. 나는 빨리 출시하지 않으면 사람들이 흥미를 잃을 거라고 계속 생각했다. 진실은 정반대다: 언어 학습자는 정의상 인내심이 있다 — 그들은 이미 1년의 연습을 약속하고 있다. 그들은 셋째 날에 데모가 필요하지 않다. 그들은 삼백째 날에 작동하는 무언가가 필요하다.
다음은
만다린은 계속 자라고 있다 — Band 1과 Band 2는 운영 중이고, Band 3은 지금 감수 중이다. 한국어 코스 콘텐츠 저작은 만다린 Band 3이 출시된 뒤에 시작된다. 플랫폼 쪽은 6개월 전보다 잠잠해졌고, 이는 실제 학습 콘텐츠에 더 많은 시간이 들어간다는 뜻이다 — 바로 그곳에 시간이 들어가야 한다.
Bookverse를 쓰고 있다면: 고맙다. 아직 안 쓰고 있다면: 언젠가 한 챕터를 펼쳐보길. 첫 번째는 무료다.