Refactoring English: 8개월 차
한 줄 요약
아마 모든 사람이 내 포커스 그룹에 참여하고 싶어 하지는 않을지도 모릅니다.
하이라이트
- 얼리 액세스로 책을 구매한 독자라고 해서 모두가 초고에 대한 피드백을 주고 싶어 하는 것은 아니라는 걸 알게 되었습니다.
- 시간이 어디로 가는지 파악하고 시간 낭비를 줄일 방법을 고민했습니다.
- 원래 300시간이 걸렸던 웹 앱을 처음부터 다시 만드는 데 10시간을 썼습니다.
- Gleam으로 함수형 프로그래밍 학습을 계속하고 있지만, 어쩌면 편법을 쓰고 있는지도 모르겠습니다.
목표 평가
매달 초에 이루고 싶은 목표를 정합니다. 목표 대비 실적은 다음과 같습니다.
이전에 대화한 적 없는 독자 10명 이상과 이야기하기
- 결과: 독자 7명에게 이메일을 보냈고, 답장 3건, 실시간 대화 1건을 했습니다
- 등급: C
직접 세어 보기 전까지는 응답률이 훨씬 더 낮다고 생각했는데, 실제로는 꽤 많은 독자가 답장을 주고 있었습니다. 문제는 제가 충분히 많이 연락하지 않았다는 점입니다.
아마 이메일 한 통마다 너무 오래 고민하는 것 같습니다. 특별하면서도 AI가 쓴 티가 나지 않게 쓰려고 하다가, 상대방 블로그를 한 시간씩 읽는 토끼굴에 빠지곤 합니다.
쌓여 있던 마케팅 아이디어 backlog 해소하기
- 결과: 계획했던 일의 약 70%를 완료했습니다
- 등급: C
대부분의 작업은 처리했지만, 1년 전에 녹음한 인터뷰를 아직 공개하지 못했습니다. 다음에는 꼭 마무리하고 싶습니다.
Refactoring English 새 챕터 공개하기
- 결과: “효과적인 이메일을 위한 잘 알려지지 않은 기법들”을 공개했습니다
- 등급: A
이번 챕터 결과물은 만족스럽지만, 소셜 미디어에 공유하는 건 도박이 될 거라는 걸 알고 있었습니다. Lobste.rs에서는 긍정적인 반응을 얻었지만 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% 증가한 점이 더 반갑습니다.
모두가 그저 책을 사는 기분만 좋아하는 건 아닐까?
독자들에게 연락해 화상 통화를 하다 보면 반복해서 듣는 말이 있습니다. “아직 읽기 시작하지 못했어요.”
며칠 전에 구매한 사람에게서도, 몇 달째 책을 가지고 있는 사람에게서도 같은 말을 듣습니다.
가장 두려운 건 이 책이 “진통제가 아닌 비타민”이 되는 것입니다. 사람들은 글쓰기 실력을 중요하지만 긴급하지 않은 일로 여깁니다. “다음 코딩 인터뷰를 완벽하게 준비하는 방법” 같은 책이라면 다가오는 인터뷰를 앞두고 바로 읽겠지만, 글쓰기 실력 향상은 얼마든지 뒤로 미룰 수 있습니다.
책을 쓰기 전에 미리 판매한 이유 중 하나는 책값을 내고서라도 읽고 싶어 하는 사람이 충분히 있는지 확인하기 위해서였습니다. 실제로 그런 사람들이 있었고 지금도 계속 구매가 이어지고 있지만, 그 신호가 제가 생각했던 것만큼 예측력이 있는지는 걱정됩니다.
혹시 제 책이 사람들이 글쓰기에 투자하고 있다는 느낌을 받기 위해 사두기만 하고 실제로는 시간을 들여 아무것도 하지 않는 종류의 책은 아닐까요? 마치 등록만 해두고 한 번도 가지 않는 사람들 덕분에 돈을 버는 것으로 알려진 유명 헬스장 프랜차이즈 플래닛 피트니스처럼 말입니다.

혹시 내가 쓰고 있는 게 책 버전의 플래닛 피트니스인 건 아닐까?
사진 Mike Mozart 제공, CC-BY-2.0 라이선스에 따라 사용
설령 책 버전의 플래닛 피트니스가 될 수 있다 해도 지속 가능하지는 않습니다. 이 책이 재정적으로 유지되려면 책을 읽고 마음에 들어 한 사람들의 입소문에 의존해야 합니다. 유명한 블로거들이 제 책을 자신에게 도움이 된 자료로 언급해 주길 바랍니다. 개발자가 글쓰기를 개선하고 싶다고 말할 때 사람들이 가장 먼저 추천하는 책이 제 책이 되길 바랍니다.
어쩌면 고객은 내 포커스 그룹에 참여하고 싶지 않은 걸지도 모릅니다
책을 미리 판매한 또 다른 이유는 사전 구매한 독자들이 집필 과정에서 특히 적극적으로 피드백을 주고 싶어 할 거라고 예상했기 때문입니다.
고객이기도 한 지인들과 이야기해 보니, 대부분은 프로젝트를 후원하는 마음으로 미리 구매했고 완성된 책을 읽고 싶어 할 뿐, 굳이 포커스 그룹에 참여하거나 초고에 피드백을 주고 싶어 하지는 않는다고 합니다. 그냥 완성본을 읽고 싶다는 것이고, 그건 충분히 이해할 만한 일입니다.
그냥 한 명씩 계속 연락해야겠습니다
고객들에게 한 명씩 연락해 실시간 통화를 해보니, 일부 독자는 매우 열정적이고 피드백을 주고 싶어 한다는 걸 알게 되었습니다. 아직 몇 명에 불과하지만, 그들이 준 피드백은 정말 훌륭했습니다.
열정적인 독자를 찾는 가장 효과적인 방법은 한 명씩 직접 이메일을 보내는 것이었기 때문에 앞으로도 계속 그렇게 할 계획입니다.
내 시간은 어디로 가는 걸까?
매년 한 번쯤 돌아보면, 지난 한 달 동안 이룬 걸 보며 “어, 지난달에 한 게 이게 다라고? 그럼 나머지 시간에는 뭘 한 거지?”라는 생각이 듭니다.
7월이 딱 그런 달이었습니다. 정규 풀타임 근무 시간대로 일했지만, 한 달을 돌아보니 “새 챕터를 하나밖에 못 냈네?”라는 생각이 들었습니다.
그래서 월간 목표 외의 시간에 제 시간이 어디로 가고 있었는지 되짚어 보려 합니다.
챕터에 과도하게 투자하기
몇 달 전, 저는 글을 다듬는 데 너무 많은 시간을 쓰고 있다는 걸 깨달았습니다. 80~90% 정도 완성되면 바로 독자에게 전달하는 대신 계속 문장을 매만지고 있었던 것입니다. 그래서 챕터마다 시간 예산을 정하고 그 안에 완성할 수 있는 만큼만 쓰기로 했습니다.
7월에 쓴 이메일 챕터의 예산은 5시간이었지만 실제로는 17.5시간이 들었습니다. 일부는 의도적인 초과였습니다. 목표를 정한 뒤에 이 챕터가 웹사이트에 단독 발췌문으로 공개되면 좋겠다는 생각이 들어 그렇게 했고, 발췌문으로 쓰고 다시 책에 맞춰 넣는 과정은 더 오래 걸리기 때문입니다.
하지만 지금 쓰고 있는 챕터도 같은 패턴을 반복하고 있습니다. 예산 6시간 중 6.5시간을 이미 썼고, 독자에게 공개할 수준이 되려면 적어도 3시간은 더 필요해 보입니다. 제가 너무 다듬으려 하는 문제도 있지만, 책에서 가장 중요하고 첫 번째에 해당하는 챕터의 예산을 너무 낮게 잡은 탓도 있는 것 같습니다.
해결책: 공개 발췌문이 될 챕터는 예산을 더 넉넉히 잡고, 정한 집필 시간을 엄격히 지키겠습니다.
본업 외 블로그 글
배운 걸 바로 글로 정리해 두는 걸 좋아하지만, 쓰고 싶은 블로그 글을 다 쓰려면 일주일에 20시간을 써도 모자를 겁니다.
어떤 블로그 글은 제 책을 읽을 만한 독자를 끌어모으지만, 어떤 글은 그렇지 않을 가능성이 큽니다. “그냥 재미로 쓰는” 글에 얼마나 시간을 투자할지 신중해야 합니다.
지난달에 공개한 “RAIDZ1에서 RAIDZ2로 ZFS 풀 마이그레이션하기”가 그런 경우입니다. 책에는 도움이 되지 않을 거라는 걸 알았지만, 몇 시간이면 쓸 수 있을 것 같았고 제가 본 글 중에는 제대로 설명한 경우가 없어 직접 정리해 보고 싶었습니다.
실제로 이 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, App Engine으로 만들었는데, 지금은 그 기술들을 모두 별로 좋아하지 않게 되었습니다. Firestore를 SQLite로, App Engine을 fly.io로 교체하는 데 오랜 시간을 썼지만 Vue는 그대로 남아 개발을 불편하게 만들었습니다.
매주 What Got Done에 업데이트를 올릴 때마다 VS Code와 Hugo로 하는 블로그 작업 방식이 더 편하다는 생각이 들었습니다. 그래서 어느 주말에 Hugo로 만든 단순한 정적 사이트로 What Got Done를 그냥 다시 구현해 버렸고, 지금은 weeks.mtlynch.io에서 호스팅하고 있습니다.

Hugo로 생성할 수 있는 정적 사이트로 What Got Done를 다시 구현했습니다.
그렇게 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로 옮기기 위해 필요한 작업이었으니, 모든 사용자가 할 수 있도록 웹 앱 자체에 이 기능을 넣으면 좋겠다고 생각했습니다.
- 사용자가 What Got Done 종료 이후에도 사용할 전달 주소를 설정할 수 있는 기능을 추가했습니다.
- 예를 들어 제 프로필 페이지인
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() {
함수형 프로그래밍에 밝은 분들께 묻습니다. 제가 편법을 쓰고 있는 걸까요? 아니면 함수형 언어에서 상태를 전달하는 올바른 방법이 이런 것일까요?
마무리
무엇을 했나?
- “효과적인 이메일을 위한 잘 알려지지 않은 기법들”을 공개하고 얼리 액세스 독자들에게 확장 버전을 발송했습니다.
- Refactoring English의 웹 전용 콘텐츠 중 마지막 남은 부분을 전자책으로 옮겼습니다.
- 블로그 글 “RAIDZ1에서 RAIDZ2로 ZFS 풀 마이그레이션하기”를 공개했습니다.
- ScreenJournal의 더 나은 비밀번호 재설정 흐름을 만들었습니다.
- PicoShare에 게스트용 파일 만료 옵션을 추가했습니다.
- 다가오는 블로그 글에 대해 무상으로 첨삭을 진행하고 그 피드백을 제 첨삭 서비스 홍보용으로 공개하는 방식으로 교환했습니다.
- What Got Done의 종료 계획을 세우고 제 데이터를 weeks.mtlynch.io로 옮겼습니다.
배운 점
- Refactoring English의 모든 얼리 독자가 피드백을 주고 싶어 하지 않아도 괜찮습니다. 계속 독자들에게 연락하다 보면 더 적극적으로 참여하고 싶어 하는 소수를 찾을 수 있습니다.
- 시간을 낭비하는 방식을 점검하고 이를 완화할 방법을 고민하는 것은 언제나 유용합니다.
다음 달 목표
- 이전에 대화한 적 없는 독자 20명에게 개인화된 이메일을 보내기.
- Refactoring English의 새 챕터를 공개하기.
- 남은 마케팅 과제를 완료하기.
도움이 필요합니다
글쓰기 실력을 키우고 싶은 개발자이거나 그런 사람을 알고 있다면, 연락해 주세요.
글을 무작위로 읽기