Refactoring English: 8개월 차
원문은 Michael Lynch님이 에 게재했습니다. 이 블로그 구독하기
한 줄 요약
아마 모든 사람이 내 포커스 그룹에 참여하고 싶어 하지는 않는 것 같다.
하이라이트
- 얼리 액세스로 책을 구매한 독자라고 해서 모두가 거친 초고에 대해 피드백을 주고 싶어 하는 건 아니라는 걸 알게 됐다.
- 내 시간이 어디에 쓰이는지 파악하고 시간 낭비를 줄일 방법을 고민했다.
- 원래 300시간이 걸려 만들었던 웹 앱을 처음부터 다시 만드는 데 10시간을 썼다.
- Gleam으로 함수형 프로그래밍을 계속 배우고 있지만, 어쩌면 편법을 쓰고 있는지도 모른다.
목표 성적표
매달 초에 달성하고 싶은 목표를 정한다. 이번 달 목표 달성 결과는 다음과 같다.
한 번도 대화한 적 없는 독자 10명 이상과 이야기하기
- 결과: 독자 7명에게 이메일 발송, 3명으로부터 답장, 1명과 실시간 대화 진행
- 성적: C
막상 세어 보기 전까지는 응답률이 훨씬 더 낮다고 생각했는데, 실제로는 꽤 많은 독자가 답장을 줬다. 문제는 내가 충분히 많이 연락하지 않는다는 것이다.
아마 메일 한 통마다 AI가 쓴 티가 나지 않는 독창적인 내용을 담으려고 너무 오래 쓰고 있는 것 같다. 그러다 보면 상대방 블로그를 한 시간씩 파고드는 토끼굴에 빠지곤 한다.
밀린 마케팅 아이디어 처리하기
- 결과: 하려던 일의 약 70% 완료
- 성적: C
대부분의 작업은 처리했지만, 1년 전에 녹음한 인터뷰는 아직 공개하지 못했으니 다음에는 꼭 마무리하고 싶다.
Refactoring English 새 챕터 공개하기
- 결과: “Underused Techniques for Effective Emails” 공개
- 성적: A
이번 챕터 결과물은 만족스럽지만, 소셜 미디어에 공유하는 건 도박이 될 거라는 걸 알고 있었다. Lobsters에서는 반응이 좋았지만 Hacker News에서는 전혀 주목받지 못했다.
Refactoring English 지표
| 지표 | 2025년 6월 | 2025년 7월 | 변화 |
|---|---|---|---|
| 순 방문자 수 | 6,574 | 8,061 | +1,487 (+23%) |
| 사전 주문 수익 | $597.24 | $800.04 | +$202.80 (+34%) |
| 컨설팅 수익 | $242.45 | $0.00 | -$242.45 (-100%) |
| 스폰서 수익 | $48.25 | $48.25 | $0.00 (0%) |
| 총수익 | $887.94 | $848.29 | -$39.65 (-4%) |
웹사이트 방문 수는 늘었다. 인기 있는 새 글을 올린 것도 아닌데 방문 수가 유지된다는 건 긍정적인 신호다. 기존 샘플 발췌문을 읽으러 오는 사람들만으로도 방문 수가 건강하게 유지되고 있다는 뜻이기 때문이다.
총수익은 소폭 하락했지만, 유료 컨설팅이 한 건에서 0건으로 줄었기 때문이라 큰 변화는 아니다. 오히려 6월 대비 사전 주문이 34% 늘었다는 점이 더 고무적이다.
모두가 그냥 책을 ‘사는 기분’만 좋아하는 거라면 어떡하지?
독자들에게 연락해 화상 통화를 하다 보면 반복해서 듣는 말이 있다. “아직 읽기 시작 안 했어요.”
며칠 전에 구매한 사람에게서도, 몇 달째 책을 볼 수 있었던 사람에게서도 같은 말을 듣는다.
내 가장 큰 두려움은 이 책이 ‘진통제가 아니라 비타민’이라는 점이다. 사람들은 좋은 글쓰기를 중요하지만 급하지 않은 일로 여긴다. “다음 코딩 인터뷰 완벽하게 준비하는 법” 같은 책이라면 인터뷰를 앞두고 바로 읽겠지만, 글쓰기 실력을 키우는 일은 얼마든지 뒤로 미룰 수 있다.
책을 쓰기도 전에 미리 판매한 이유 중 하나는 과연 돈을 내고 살 만큼 수요가 있는지 확인하기 위해서였다. 수요는 있었고 지금도 계속 팔리고 있지만, 그게 내가 생각했던 만큼 예측력 있는 신호가 아닐까 봐 걱정된다.
내 책이 사람들이 실제로 시간을 들이지 않고도 글쓰기에 투자하고 있다는 기분을 느끼려고 사는 종류의 것이라면 어떡하지? 등록만 하고 실제로는 헬스장에 가지 않는 사람들 덕분에 돈을 번다는, 그 유명한 헬스장 프랜차이즈 Planet Fitness 같은 거라면?

내가 쓰고 있는 게 책으로 된 Planet Fitness라면 어떡하지?
Photo by Mike Mozart, CC-BY-2.0 라이선스에 따라 사용
설령 책으로 된 Planet Fitness가 될 수 있다 해도 지속 가능하지는 않다. 이 책이 재정적으로 성공하려면 책을 읽고 마음에 들어 한 사람들의 입소문에 의존해야 한다. 유명한 블로거들이 내 책을 도움이 된 자료로 인용해주길 바란다. 개발자가 글쓰기를 개선하고 싶다고 말할 때, 사람들이 당연하게 추천하는 책이 내 책이 되었으면 한다.
어쩌면 고객들은 내 포커스 그룹에 참여하고 싶지 않은 걸지도 모른다
책을 미리 판매한 또 다른 이유는 사전 주문한 독자들이 집필 과정에서 특히 열정적으로 피드백을 줄 거라고 예상했기 때문이다.
고객이기도 한 친구들과 이야기해보니, 대부분은 프로젝트를 응원하는 마음으로 미리 구매했고 완성된 책을 읽고 싶어 할 뿐, 꼭 포커스 그룹에 참여하거나 거친 초고에 피드백을 주고 싶어 하지는 않는다고 한다. 그냥 완성본을 읽고 싶은 거고, 그건 충분히 이해할 만하다.
그냥 한 사람씩 계속 연락하는 게 답이다
고객 한 명 한 명에게 직접 연락해 실시간 통화를 잡아보니, 일부 독자는 매우 열정적이고 피드백을 주고 싶어 한다는 걸 알게 됐다. 아직 몇 명밖에 찾지 못했지만, 그들이 준 피드백은 정말 훌륭했다.
열정적인 독자를 찾는 가장 효과적인 방법은 한 명씩 이메일을 보내는 것이었으니, 앞으로도 계속 그렇게 할 생각이다.
내 시간은 어디로 가는 걸까?
매년 한 번쯤 , 지난 한 달 동안 뭘 했는지 돌아보다가 “잠깐, 지난달에 한 게 고작 이거라고? 그럼 나머지 시간엔 뭘 한 거지?”라는 생각이 든다.
7월이 바로 그런 달이었다. 평소대로 풀타임으로 일했는데도 한 달을 돌아보니 “새 챕터 하나밖에 못 냈네?”라는 생각이 들었다.
그래서 월간 목표 외에 내 시간이 어디로 새고 있는지 한번 돌아보려 한다.
챕터에 과도하게 투자하기
몇 달 전, 챕터가 80~90% 완성됐을 때 독자에게 바로 전달하기보다 문장 다듬는 데 너무 많은 시간을 쓰고 있다는 걸 깨달았다. 그래서 챕터마다 시간 제한을 두고, 그 예산 안에서 완성할 수 있는 만큼만 쓰기로 했다.
7월에 쓴 이메일 챕터는 예산이 5시간이었는데, 실제로는 17.5시간을 썼다. 일부러 그렇게 한 부분도 있다. 목표를 정한 뒤에 이 챕터가 웹사이트에 독립적인 발췌문으로 쓰기에 좋겠다는 걸 깨달았기 때문이다. 발췌문으로 쓰고 다시 책에 녹여내는 데는 시간이 더 오래 걸린다.
하지만 지금 쓰고 있는 챕터에서도 같은 일을 반복하고 있다. 6시간 예산 중 이미 6.5시간을 썼는데, 독자에게 공유할 만하다고 느끼려면 아마 3시간은 더 써야 할 것 같다. 너무 다듬으려는 문제이기도 하지만, 책에서 가장 중요하고 첫 번째인 챕터에 예산을 너무 적게 잡은 탓도 있는 것 같다.
해결책: 공개 발췌문으로 쓸 챕터에는 추가 시간을 배정하고, 집필 시간을 정해진 한도 내로 엄격히 제한한다.
책 외 블로그 글
배운 걸 바로 블로그 글로 정리하는 걸 좋아하지만, 일주일에 20시간을 블로그에 쏟아도 쓰고 싶은 글을 다 쓰지 못할 것 같다.
내 블로그 글 중 일부는 내 책을 읽을 만한 독자를 끌어오지만, 다른 글들은 그렇지 않을 가능성이 크다. “그냥 재미로 쓰는” 글에 얼마나 시간을 투자할지 신중해야 한다.
지난달에 새 블로그 글 “Migrating a ZFS pool from RAIDZ1 to RAIDZ2”를 올렸다. 책에는 도움이 안 될 거라는 걸 알았지만, 몇 시간이면 쓸 수 있을 것 같았고, 아무도 제대로 설명하지 않은 내용을 풀어낼 수 있을 거라 생각했다.
실제로 ZFS 글은 작성하는 데 7시간이 걸렸으니, “재미로 쓰는” 글 치고는 과한 투자였다. 특히 더 많은 사람이 좋아할 만한 Refactoring English 챕터를 공유하지 못한 달에는 더욱 그렇다.
해결책: 책과 무관한 블로그 글은 더 엄격히 선별하고, 내 책을 읽을 만한 독자를 끌어올 수 있는 글을 우선한다.
나쁜 소셜 미디어 습관
어려운 글이나 소프트웨어에 대해 깊게 고민할 의욕이 떨어지거나 지루해질 때마다 나도 모르게 소셜 미디어를 확인하곤 한다. 잠깐만 보고 바로 일하러 돌아오겠다고 다짐하지만, 글 하나에 빠져버린다. 댓글이라도 달면 답글이 달렸는지 강박적으로 계속 확인하게 된다.
보통 내 글이 Hacker News나 reddit에서 크게 화제가 되면 댓글에 답하는 날을 따로 잡는다. ZFS 글은 크게 화제가 되지 않았는데도 하루를 댓글 대응에 써버렸다.
한때 나쁜 소셜 미디어 습관을 고치려고 LeechBlockNG라는 브라우저 확장 프로그램을 썼는데, 메모리를 잡아먹고 브라우저 전체를 느리게 하는 것 같아 꺼두었었다. 다시 써보고 있는데 아직 메모리 누수는 보이지 않으니 이번에는 잘 작동할지도 모르겠다.
해결책: 생산성 관련 브라우저 확장 프로그램을 다시 사용해 시간 낭비 사이트에 대한 진입 장벽을 높인다.
수면 방해에서 회복하기
어린아이가 잠을 잘 못 자서 아내와 나도 잠을 제대로 못 자고 있다.
수면 방해 자체는 문제의 작은 부분일지도 모른다. 더 큰 문제는 내가 수면 부족을 핑계로 게으른 행동을 정당화한다는 것이다. 글쓰기 세션을 건너뛰거나 소셜 미디어를 들여다보면서 “어젯밤 잠을 너무 못 잤으니 오늘은 열심히 안 해도 돼”라고 생각한다. 사실 평소의 80~90% 정도로는 충분히 일할 수 있는데도, 못 잔 걸 핑계로 대충 넘어가 버린다.
해결책: 수면 부족을 게으름의 핑계로 삼지 않는다.
유료 편집 일을 미루기
Refactoring English와 관련해 제공하는 서비스의 일환으로 편집 일을 하고 있다.
다른 사람의 블로그 글을 편집하는 건 좋아하지만 정신적으로 꽤 소모된다. 내 글 편집도 어려운데 남의 글을 편집하는 건 더 어렵다. 내 글은 왜 고치는지 설명하지 않아도 감으로 편집할 수 있지만, 다른 블로거에게 편집 노트를 줄 때는 초고의 어떤 점이 약한지, 왜 내 제안이 더 나은지를 명확하게 설명해야 한다.
편집 일을 자꾸 미루게 된다. 하루면 끝낼 수 있는 일도 며칠씩 끌곤 한다. 편집 일을 미루면 내 글쓰기도 함께 미뤄진다. 고객 일을 내 일보다 우선하고 싶기 때문이다.
해결책: 편집을 미루는 것이 많은 시간을 잡아먹는다는 걸 인정하고 더 빨리 처리한다.
사이드 프로젝트
300시간짜리 Vue 앱을 10시간 만에 정적 사이트 생성기로 교체하기
2019년에 What Got Done이라는 비즈니스를 만들려고 시도했다. 팀원들이 서로 일주일간의 업무 요약을 공유할 수 있는 앱이었다.

What Got Done은 사람들이 일주일간의 업무 업데이트를 공유할 수 있는 웹사이트였다.
Google에 있을 때 Snippets라는 내부 도구가 있었는데, What Got Done과 똑같은 역할을 했다. 나는 그 도구를 정말 좋아했고, Google을 나온 뒤에도 혼자 일하면서 매주 업데이트를 계속 작성했다.
What Got Done은 결국 고객을 찾지 못해서 오픈소스로 공개했고 지난 6년간 취미 프로젝트로 유지해 왔다.
처음에 What Got Done은 Vue, Firestore, AppEngine으로 만들었는데, 이제는 그 기술들을 정말 싫어하게 됐다. Firestore를 SQLite로, AppEngine을 fly.io로 교체하는 데 오랜 시간을 썼지만 Vue는 그대로 남아 개발을 불쾌하게 만들었다.
매주 What Got Done에 업데이트를 올릴 때마다 VS Code와 Hugo로 하는 블로그 작업 방식이 훨씬 좋다는 생각을 했다. 그래서 어느 주말에 What Got Done을 Hugo로 만든 단순한 정적 사이트로 아예 다시 구현했고, 지금은 weeks.mtlynch.io에서 호스팅하고 있다.

What Got Done을 Hugo로 생성할 수 있는 정적 사이트로 다시 구현했다.
6년 동안 Go + Vue + SQLite + fly.io 앱으로서 What Got Done을 구현하고 유지하는 데 아마 300시간 정도 썼을 것이다. 단순한 Markdown 파일과 Hugo로 정적 사이트로 다시 만드는 데는 10시간밖에 걸리지 않았다.
새 버전은 나만을 위한 앱이기 때문에 git 커밋에서 주간 업데이트를 미리 채워 넣는 같은 개인화된 기능을 추가할 수 있다. 그리고 물론 그냥 소스 컨트롤로 관리되는 정적 사이트일 뿐, 프론트엔드, 백엔드, 데이터베이스가 각각 다른 기술 스택으로 구성된 본격적인 웹 앱이 아니니 호스팅하고 유지보수하고 백업하는 것이 몇 배나 더 간단하고 저렴하다.
What Got Done 종료하기
What Got Done을 영원히 유지하고 싶지는 않다. 특히 이제는 나조차 사용하지 않으니 더욱 그렇다.
활성 사용자가 몇 명뿐이라 해도, 내가 만든 걸 쓰기 시작한 사람들을 버리고 싶지 않아서 What Got Done의 종료 경험을 최대한 매끄럽게 만들려고 했다.
- 웹사이트에 What Got Done이 연말에 서비스를 종료한다고 공지했다.
- 사용자가 게시물을 Markdown 형식으로 내보낼 수 있는 기능을 추가했다.
- 내 데이터를 Hugo로 옮기기 위해서 어차피 필요했던 기능이라, 웹 앱 자체에 넣어 모든 사용자가 쓸 수 있게 하면 좋겠다고 생각했다.
- 종료 후에도 사용할 전달 주소를 설정할 수 있는 기능을 추가했다.
- 예를 들어 내 프로필 페이지인
whatgotdone.com/michael을 weeks.mtlynch.io로 영구 리다이렉트되도록 설정해 두었다.
- 예를 들어 내 프로필 페이지인
Gleam으로 만드는 AIM 로그 파서 진행 상황
Gleam 프로그래밍 언어를 오래된 AIM 로그를 위한 파서를 만지작거리며 계속 배우고 있다. 가장 기본적인 로그는 이렇게 생겼다.
Session Start (AIM - DumbAIMScreenName:Jane): Mon Sep 12 18:44:17 2005
[18:44] Jane: hi
[18:55] Me: hey whats up
Session Close (Jane): Mon Sep 12 18:56:02 2005
타임스탬프 파싱하기
6월에는 위 로그를 받아 발신자와 메시지를 이렇게 추출할 수 있는 수준으로 파서가 기본적으로 동작했다.
[
Message(sender: "Jane", body: "hi"),
Message(sender: "Me", body: "hey whats up"),
]7월에 진전이 있어서 파서가 이제 타임스탬프도 이해할 수 있게 됐다. 세션 메타데이터의 날짜와 메시지에 있는 단순한 HH:MM 정보를 결합해야 해서 조금 까다롭다. 그래서 이제 로그 파서는 위 로그를 이렇게 변환할 수 있다.
[
Message(
timestamp: must_parse_rfc3339("2005-09-12T18:44:00-04:00"),
sender: "Jane",
body: "hi",
),
Message(
timestamp: must_parse_rfc3339("2005-09-12T18:55:00-04:00"),
sender: "Me",
body: "hey whats up",
),
]렉싱과 파싱을 단일 단계로 통합하기
파서도 별도의 렉싱과 파싱 대신 단일 패스로 동작하도록 단순화했다.
처음에는 로그를 토큰 목록으로 나눈 뒤 그 토큰들을 파싱하는 것이 더 올바르고 우아하다고 생각했다. 그래서 파서가 Session Start (AIM - DumbAIMScreenName:Jane): Mon Sep 12 18:44:17 2005 같은 한 줄을 보고 바로 파싱하는 대신, 렉서가 먼저 이를 다음과 같은 일련의 토큰으로 바꾸길 원했다.
[
SessionStart,
Word("(DumbAIMScreenName:Jane)"),
ColonSpace,
Word("Mon"),
Word("Sep"),
...
]하지만 그러려면 토큰나이저가 인식할 수 있는 ["Session", " ", "Start"] 같은 부분 문자열로 나누는 숨겨진 첫 번째 패스가 필요했고, Gleam 내장 라이브러리에는 문자열을 부분 문자열로 나누면서 구분자까지 유지하는 방법이 없어서 직접 문자열 분할 로직을 구현해야 했다. 예를 들어 줄바꿈으로 나눈 뒤 공백으로 나누면 문자열 목록은 얻겠지만, 구분자가 공백이었는지 줄바꿈이었는지는 추적할 수 없게 된다.
결국 입력을 세 번 파싱하는 느낌이었다. 내 커스텀 문자열 분할을 위해 한 번, 렉싱을 위해 한 번, 그리고 실제 파서를 위해 한 번. 처음에는 내가 함수형 언어나 텍스트 파서에 대해 잘 몰라서 그런 거고, 더 우아한 렉싱과 파싱 방법을 찾게 될 거라고 생각했다.
이 혼란을 핑계로 마침내 Crafting Interpreters 종이책을 샀다. 내가 본 소프트웨어 책 중 가장 아름답게 디자인된 책이다.

Gleam 프로젝트 덕분에 마침내 Crafting Interpreters 종이책을 살 명분이 생겼다.
책의 렉싱 장을 읽고 나서, 내 AIM 로그는 렉싱을 할 만큼 구조화되어 있지 않다는 결론을 내렸다. 모든 걸 입력을 한 글자씩 읽는 파서 하나로 합쳐 보니 그게 더 간단하게 느껴졌다.
한 글자씩 파싱하는 것의 단점은 Gleam의 패턴 매칭이 훨씬 더 지저분해 보인다는 것이다. 예전 구현에서는 이렇게 문자열 패턴을 찾을 수 있었다.
case contents {
["Session", "Start", ..rest] -> tokenize_list(rest, [SessionStart, ..acc])
["Session", "Close", ..rest] -> tokenize_list(rest, [SessionClose, ..acc])
그리고 이제는 더 길고 지저분한 패턴 매치가 되었다.
case state.remaining_graphemes {
[
"S",
"e",
"s",
"s",
"i",
"o",
"n",
" ",
"S",
"t",
"a",
"r",
"t",
...
Gleam에 클래스를 억지로 끼워 맞추고 있는 걸까?
파서를 진행하면서 똑같은 시그니처의 함수를 계속 반복해서 쓰고 있다는 걸 알게 됐다.
fn parse_tokens_with_messages(
tokens: List(Token),
messages: List(Message),
) -> List(Message) {
내 함수 중 상당수가 같은 매개변수를 받고 같은 반환 값을 가졌고, 코드를 추가할수록 매개변수와 반환 값 목록은 점점 더 길어졌다.
그래서 ParseState 타입을 만들어 대신 전달하도록 했다.
type ParseState {
ParseState(
last_timestamp: timestamp.Timestamp,
messages: List(Message),
remaining_graphemes: List(String),
)
}
fn parse_graphemes(state: ParseState) -> ParseState {
하지만 그러자 Gleam에 객체 지향 클래스를 몰래 끼워 넣는 기분이 들었다. Go였다면 코드가 이렇게 생겼을 것이기 때문이다.
type Parser struct {
LastTimestamp time.Time
Messages []Message
RemainingGraphemes []rune
}
func (p Parser) Parse() {
함수형 프로그래밍 고수분들, 제가 편법 쓰는 건가요? 아니면 이게 함수형 언어에서 상태를 전달하는 올바른 방법인가요?
마무리
무엇을 했나?
- “Underused Techniques for Effective Emails”를 공개하고 확장판을 얼리 액세스 독자들에게 발송했다.
- Refactoring English의 웹 전용 콘텐츠 중 마지막 남은 부분을 전자책으로 옮겼다.
- 블로그 글 “Migrating a ZFS pool from RAIDZ1 to RAIDZ2”를 공개했다.
- ScreenJournal을 위한 개선된 비밀번호 재설정 플로우를 만들었다.
- PicoShare에서 게스트용 파일 만료 옵션을 추가했다.
- 편집 서비스 홍보를 위해 피드백을 공개하는 조건으로 예정된 블로그 글에 대해 무상 편집을 진행했다.
- What Got Done 종료 계획을 세우고 내 데이터를 weeks.mtlynch.io로 이전했다.
배운 점
- Refactoring English의 모든 초기 독자가 피드백을 주고 싶어 하지 않아도 괜찮다. 계속 독자들에게 연락하다 보면 더 적극적으로 참여하고 싶어 하는 소수를 찾을 수 있다.
- 시간을 낭비하는 방식을 점검하고 이를 완화할 방법을 고민하는 것은 언제나 유용하다.
다음 달 목표
- 한 번도 대화한 적 없는 독자 20명에게 개인화된 이메일 보내기.
- Refactoring English 새 챕터 공개하기.
- 남은 마케팅 작업 마무리하기.
도움 요청
글쓰기를 개선하고 싶은 개발자이거나 그런 사람을 알고 있다면, 연락해 주세요.
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기