독후감 카테고리는 방치된 지 벌써 2년에 가까워지고 있는 반면 만들어진 지 얼마나 됐다고 회고록 카테고리에는 프로젝트 하나를 더 들이고 있다. 공부보단 일단 뭐든 만들고 보길 선호하는 성격을 가져서 그런가. 아무튼, 요즘 다시 구직활동에 전념할 시기가 찾아온 탓에 나 자신을 되돌아보는 순간이 잦아지긴 했다. 그 덕분에 사람이 너무 우울해하고 있더라. '지난 몇 년간 뭐 했지...' 이런 부정적인 기운을 한 번 찍어 눌러 볼 겸 이번 글은 활기차게 시작해 보자.
도마도(domado) - 뽀모도로 타이머
자, 오늘 소개할 제품은 바로 웹에서 동작하는 간단한 뽀모도로 타이머, 이름하여 도마도입니다!
뽀모도로 타이머
작년 하반기쯤부터 집중력이 많이 떨어졌단 생각이 들어 뽀모도로 타이머라는 물건을 사용하고 있었다. 25분 집중, 5분 휴식, 반복하다가 15분 긴 휴식. 뽀모도로라는게 사실 크게 복잡한 규칙이 없는 방법론이라서 만들기가 크게 어렵진 않다. 인터넷에 검색을 조금만 해도 각종 어플리케이션과 웹 사이트가 제공되고 있단 걸 알 수 있다. 마침 이렇게 회고록을 쓰는 김에 검색을 좀 해봤는데, pomofocus란 사이트가 여러모로 내가 만든 서비스랑 비슷하군.
아니 이제 와서 비슷한 사이트가 이미 있다는 말이 무엇이냐, 사실 이 프로젝트 자체가 아주 가벼운 마인드로 시작했다는 반증이다. 도마도는 웹 어플리케이션 형태로 구현한 뽀모도로 타이머다. 뽀모도로, 짧은 휴식, 긴 휴식 세 가지 형태의 타이머를 제공해준다. 어... 더 설명할 기능은 딱히 없다. 이 프로젝트를 시작한 이유는, 앞서 원래 내가 쓰고 있다고 말했던 뽀모도로 타이머 어플리케이션이 갑자기 나에게 돈을 요구했기 때문이다. 이렇게 말하니까 뭔가 위험한 어플리케이션을 사용한 것처럼 들리는데, 사실 그 어플리케이션은 구독 형태로 사용하는 서비스였고, 나는 그 체험판을 쓰고 있었던 것이었다.
헛돈 나가는 건 절대 안 되지
나는 이 프로젝트를 작년 12월 말에 시작해서 올해 3월 초까지 진행했다. 사실 그냥 혼자 만족하면서 쓸 수 있는 수준은 이미 예전에 도달했는데, 계속 이것저것 기능들을 덧붙이고 버그들을 수정하다 보니 어느덧 3달이나 지나 있더라. 그리고 지금은 개발을 조금 멈춰둔 상태지만(취직해야지 취직) 아직도 계속 개발하려면 할 수 있는 상태라고 보고있다. 어떻게 보면 참 기구하군, 만약 구독 형태의 어플이 아니라 다른 무료 서비스를 이용했다면 이런걸 만들어봐야겠단 생각조차 없었을 지도 모르는 일인데. 아무튼, 워낙 기능적으로는 소개할게 없는 서비스다 보니 이렇게 사담을 길게 섞어서 시작했다.
도마도의 탄생
궁상 그만 떨고 프로젝트 기획에 대해서 더 말해보자. 디자인이 꽤 이쁘게 나왔습니다. 이공계 출신 프로그래머의 디자인 감각이라곤 도저히 상상조차 못 할 색상 감각.
뽀모도로(pomodoro)는 이탈리아 사람이 만들었는데, 이탈리아어로 토마토를 뜻한다. 그래서 인터넷에 검색하면 위 사진과 같은 타이머를 찾아볼 수 있다. 일종의 아이디어 상품. 나는 처음 프로젝트를 떠올렸을 때 머릿속에 함께 그려진 이 토마토 모양 타이머를 그대로 디자인에 채용했다.
이렇게
도마도는 3가지 종류의 타이머를 제공해야 한다. 제일 중요한 집중 시간에 사용할 빨간색 뽀모도로 타이머와 함께 짧은 휴식과 긴 휴식을 나타내는 타이머도 필요하다. 나는 이를 표현하기 위해 색상 2개를 더 추가했다. 짧은 휴식을 나타내는 색상은 토마토 꼭지에서 가져와 초록색으로, 긴 휴식을 나타내는 색상은 나가서 좀 걷고 오라는 뜻에서 하늘색으로.
Drafts
여기서부터는 다시 개발 블로그의 자세로 돌아오자. 나는 이번 프로젝트를 진행하면서 개발 기간을 "드래프트(Draft)"라는 단위로 나누어 관리했다. 프로젝트의 개발 흐름은 이렇게 진행됐다. 드래프트를 시작하기 전에 먼저 어떤 기능들을 만들지 기획하고 그림을 그려본다. 그렇게 도출한 기능들을 기반으로 이슈를 생성하고 개발을 진행하면서 그 이슈들을 하나씩 제거한다. 모든 이슈가 제거되면 드래프트 끝. 꼭 드래프트가 끝나지 않았더라도 어느 정도 기능이 해당 드래프트의 dev 브랜치에 쌓이면 main 브랜치에 병합해 배포도 틈틈이 진행한다. 그렇게 운영하다 발생하는 버그들은 dev 브랜치에서 바로 hotfix를 진행하고 병합한다.
드래프트라는 단어에 대해 밑그림이 차곡차곡 쌓여서 완제품이 된다는 의미라고 좋게 해석할 수 있는데, 사실 그냥 관성적으로 선택한 단어다. 뒤늦게 실제 사용례를 찾아봤는데 조금 안 어울리는 측면이 있어서 민망하군. 그냥 개발 기간이라고 하지 뭘 또 저렇게. 아무튼, 약 3개월 동안 프로젝트를 진행하면서 드래프트는 총 2회가 있었고, 이번 글에서 그 두 번의 개발 기간을 모두 회고해볼 생각이다.
첫 번째 Draft
첫 번째 드래프트의 목표는 프로토타이핑과 개발 환경 세팅. 특히 여기서 집중해 보고 싶은 내용은 개발환경과 관련된 내용들이다. 아무리 개인 만족을 위한 프로젝트일지라도 항상 제자리걸음이 아닌 이전보다 한 걸음이라도 더 나아가는 것을 목표로 삼고 있다. 도마도의 경우 기능 그 자체에는 구현에 큰 어려움이 없을 것으로 예상되었기 때문에 나는 시도해 보지 않았던 개발 환경을 적극적으로 도입해 보기로 했다.
npm 대신 yarn
특히 yarn berry, 예전부터 node_moduels/
디렉토리에 대한 감정이 좋지 못했기 때문에 yarn berry를 도입했다. 패키지를 압축된 상태로 설치하고 그 패키지들을 저장소에 직접 푸시한다는 아이디어가 새로웠다. 다만 한 가지 애로사항은 있다. 일단 vscode에 yarn 환경이 바로 적용되지 않는 문제가 있는데, yarn dlx @yarnpkg/sdks vscode
명령어를 입력하면 쉽게 해결된다. 그런데 개발 기간 도중 새로 구매한 맥북에 개발 환경을 다시 세팅할 일이 있었는데 해당 명령어가 .yarn/sdks/
폴더에 새로운 커밋을 발생시키더라. 그냥 그 변경사항을 저장소에 커밋하고 푸시하면 될 것 같지만, 다시 WSL 환경에서 같은 명령어를 입력할 경우 동일한 변경사항이 또 다시 발생한다. 아주 크리티컬한 이슈는 아니었기 때문에 일단 이런 변경사항이 발생할 경우 그것을 discard 하는 방식으로 처리하고 있지만, 장기적으로 해결책을 찾아야 할 것 같다. 그래도 yarn berry에 대해선 전반적으로 만족하면서 사용하는 중.
Redux 대신 Recoil
상태 관리 라이브러리에 대한 경험이 많다고 하기엔 좀 민망하지만 그래도 쓸 일이 있으면 Redux(같은 Flux 기반 상태 관리 라이브러리라는 점에서 Vuex도 포함)를 주로 사용해 오고 있었다. 다만 Redux는 지금까지 몇 번 써왔음에도 그 구조가 머릿속에 잘 그려지지 않는다. 괜히 러닝 커브가 단점으로 꼽히는 게 아니야. 그래서 이번에 Recoil을 새로 도입했다. 확실히 쉽긴 하더라. 좀 더 React스러운 면이 있달까. Recoil로 만든 상태를 사용하는 방법도 useState
와 그 형태가 비슷한 면이 있어 쉽게 배우고 쉽게 사용할 수 있었다.
처음 Recoil Atom들을 구현하면서 그 형태를 어떻게 만들어야 할지 고민했던 기억이 난다. 여러 상태값들이 타이머를 위해 필요하게 될텐데, 이를 하나의 큰 Object atom으로 관리해야 하느냐, 분리된 많은 원시 타입 atom으로 관리해야 하느냐를 두고 고민했었다. 내 판단은 많은 원시 타입 atom들로 관리하는 방법. Object 형태에선 상태 간의 의존성이 불필요하게 많이 생기는 것이 좋지 않아 보이더라.
개발 환경 세팅에는 이 자료를 많이 참고했다. 때마침 내가 선택한 도구들을 다 묶어서 설명해주시더라구.
두 번째 Draft
77일 전에서 갑자기 48일 전으로 점프
(커밋 내역 상으로는 두 번째 드래프트에 포함되긴 하지만) 첫 번째 드래프트와 두 번째 드래프트 사이엔 회색지대라고 부를 만한 기간이 있었다. 첫 번째 드래프트의 결과물을 Vercel에 배포하고 직접 사용해 봤는데 그 만족감이 좋더라. 보통 좋은 게 아니라 '이거 모바일 지원만 추가하면 정말 괜찮겠는데?' 그래서 바로 모바일 지원을 중점으로 두 번째 드래프트를 기획하고 개발도 조금 진행했었다. 그리고 갑작스럽게 발생한 1달의 공백. 하필 올해 1월에 극심한 독감이랑 개인적인 일이랑 이것저것 다 겹쳐버려서...
지금 돌이켜보면 이때 까딱하면 프로젝트가 드랍될 수도 있었다는 생각이 든다. 그러다 2월 초, 오랜만에 만난 친구들과 대화를 나누다가 Vercel에 올라간 도마도를 보여주게 되었는데 거기서 한 친구가 아주 좋은 반응을 보내주더라. 심지어 실제로 써보면서 어떤 걸 고치면 좋을지 피드백도 넣어 주더라고. 너무 고맙..😭 아무튼 나는 이렇게 얻은 소중한 첫 번째 사용자를 기점으로 다른 친구들에게도 도마도를 보여주고 피드백을 수집했다. 그렇게 나온 두 번째 드래프트의 목표들:
개발 일지에서 발췌
두 번째 드래프트를 진행하면서 도마도는 PWA(Progressive Web Application)를 일부 지원하기 시작했다. 지난 글에서 도마도를 살짝 언급하면서 다뤘던 Screen Wake Lock API를 사용해 디바이스를 제어하고, Notification API를 사용해 디바이스의 OS가 타이머 완료 알림을 보내도록 만들고, 그리고 브라우저가 백그라운드로 넘어가도 타이머가 계속 흘러가도록 하는 등 두 번째 드래프트가 요구하는 기능들을 구현하면서 나는 도마도를 약간이나마 PWA스럽게 만들어냈다.
PWA 자랑 겸 새로 산 맥북 자랑 ㅎㅎ 😆
갤럭시에서도 쓸 수 있답니다.
PWA로 구현한 덕에 이렇게 디바이스에 설치해서 사용할 수도 있다. 진보적인 웹 어플리케이션이라니, 그 이름 참 거창하단 생각도 들지만 한 번 만들어서 써보니 여러모로 마음에 드는 것 같다.
냉정해질 시간
여기까진 이 도마도 프로젝트가 그저 낭만적으로만 보인다. 하지만 이 회고 카테고리를 만든 진짜 이유. "내가 이렇게 잘했음!" 이라고 자랑하는 회고록은 여기까지. 가뜩이나 개발 기간이 그리 길진 않았던 프로젝트였던 탓에(첫 번째 드래프트에 며칠, 두 번째 드래프트에 1달 남짓) 투박하고 어딘가 잘못된 것 같은 코드가 여럿 보인다. 그리고 근본을 뒤흔드는 문제도 하나 있고.
지금 완전 빚쟁이 다 됐음
투박하고 어딘가 잘못된 것 같은 코드들이 만든 기술 부채가 너무 커졌다. 이전에 내가 실패했던 프로젝트들은 설계만 너무 오래 붙잡고 있다가, 혹은 이것저것 온갖 고민을 다 하다 정작 기능이 늦게 완성되며 흐지부지되었던 기억이 있다. 이번 프로젝트는 그 반대에 가까웠다. 일단 만들고 보자. 그렇게 정신없이 개발을 진행하다 두 번째 드래프트 막바지쯤엔 React Hook들이 너무 얽혀있어서 기능 하나 추가하다가 머리에 쥐가 나려 하더라고.
지금 도마도의 컴포넌트는 대략 이렇게 구성되어 있다. 그리고 이들 중 문제아는 바로 <GlobalTimer />
. 타이머는 화면이 전환되어도 계속 돌아가야 한다. 그래서 나는 <App />
컴포넌트 바로 아래에 null
을 반환하는 Dummy 컴포넌트를 두고 거기서 타이머 관련 전역 상태들을 관리하도록 만들었다. 이 구조 자체가 동작에 오류를 만들진 않았으나, HTML은 만들지 않고 로직만 사용하는 React 컴포넌트라니. 아니 이거 그냥 React 훅으로 만들었어야 하는 거 아니야? 아니면 <GlobalTimer />
가 <AppBody />
를 감싸는(Wrapping) 구조로 만들었어야 하는 거 아닌가?
더 큰 문제는 <GlobalTimer />
내부에 있다. 우선 나는 이걸 일종의 코어 모듈로 생각하면서 개발했다. 타이머 어플리케이션에 제일 중요한 건 타이머 기능이니까. 시간 계산도 여기서 제어하도록 만들었고, 타이머의 시작, 정지, 종료 등의 상태 변화도 여기서 감지한다. 이 컴포넌트의 중앙에 (실제 주석 발췌)/* 타이머 상태 변화를 감지하는 useEffect 훅 */
이 있다. 그리고 이게 지금 너무 크다. 위에 첨부한 저 캡처, 저거 지금 그 useEffect
훅의 의존성 배열(Dependency Array)이다. 기능 구현하다 상태 하나 잘못 건드리는 순간, 이 "코어 모듈" 이 바로 고장 나 버린다. 드래프트 막바지 기능 추가를 위해 이 훅을 건드릴 때 마다 나는 버그에 치를 떨었다.
<GlobalTimer />
만의 문제로 끝날 게 아니라, 전반적으로 컴포넌트화가 험악하게 되어있다. <MainScreen />
은 너무 뭉텅이로 컴포넌트화가 되어있고 <PreferenceScreen />
은 컴포넌트화가 하나도 되어있지 않다. 세 번째 드래프트의 메인 테마는 채무 상환이 되어야 할 것 같다. 테스트 코드 작성과 리팩토링.
처음으로 돌아가서, 왜 웹이었지?
사실 앞서 두 번째 드래프트를 이야기할 때 은근슬쩍 넘어간 부분이 있다. 백그라운드 지원. 우선 JavaScript로 타이머는 어떻게 만들어야 할까? setInterval
을 써서 1초마다 타이머에서 1초씩 빼주면 될까?
사실 JavaScript의 Timeout들은 시간을 그렇게 정교하게 계산하지 않는다. '아 몇초 정도 오차는 괜찮아.'라는 생각이 드는가? 브라우저는 부하를 줄이기 위해 비활성화된 탭에선 Timeout의 최소 딜레이를 강제한다. 나는 25분짜리 타이머가 50분이 지나도 완료되지 않는 것을 경험했다. 이뿐만이 아니다. 백 그라운드로 보내진 웹 브라우저는 몇 분 뒤 JavaScript의 동작을 정지시켜 버린다.
나는 1초마다 타이머의 숫자를 줄이는 방식이 아니라 시작 시각 과 현재 시각의 차이를 계산하는 방식으로 타이머의 오차를 줄였고, Web Worker를 사용해 백그라운드에서도 계속 타이머가 돌 수 있도록 만들었다. 하지만 완벽한 해결책은 아니었다. 모바일 디바이스 환경에서 웹 브라우저가 백그라운드로 보내졌을 때, 5~10분 정도의 타이머는 잘 동작해서 알람을 정상적으로 울렸다. 하지만 25분짜리 타이머는 그대로 웹 페이지의 자바스크립트와 함께 침묵을 유지해 버리더라.
아무리 PWA라고 한들 결국은 웹 페이지로 제공되는 웹 서비스. 기기 제어나 백그라운드 제어 등에 있어서 실제 네이티브 어플리케이션이 주는 사용자 경험과 차이가 있을 수밖에 없다. 타이머를 만들어 봐야겠단 생각을 했을 때, 그 플랫폼을 결정하는 데 있어 너무 관성적인 선택을 했던 것은 아닐까 싶은 생각도 든다. 물론 그렇다고 이제 와서 플랫폼을 갈아탈 생각은 없고, 웹 워커를 가지고 몇 가지 시도는 더 해봐야겠단 생각은 든다. 웹 워커 자체를 공부할 필요도 있겠군.
잘 포장하면 이렇게 말할 수는 있겠지. "휴대폰으로 도마도 뽀모도로 타이머를 켜 두세요. 뽀모도로 타이머가 실행되는 동안 휴대폰을 사용하면 타이머가 제대로 동작하지 않습니다. 집중할 시간엔 딴짓하지 맙시다." 😂
자신감을 갖자
와 전국구!
온 대한민국이 사용하고 있는 도마도! 라고 하기엔 좀 부족하긴 하다. 그치만 이런 마인드도 필요하댔어. 특히 이번 프로젝트는 좋게 바라봐 준 한 친구 덕에 더 힘을 얻을 수 있었다. 사실 나는 나름 숲이나 도마도 말고도 꾸준히 뭔가를 만들어오고 있는 사람이긴 한데, 또 그것을 남들에게 공개하는 건 두려워하는 경향이 있다. 부정적인 의미의 완벽주의 성격을 가진 사람이 이런다는 것 같더라고. 조금 자신감을 가지는 게 좋지 않을까 싶은 생각도 든다.
근데 사실 그 친구가 제안한 게 하나 더 있었는데, 이 디스콰이엇이란 사이트에 도마도 소개 글을 올려 보는 것. 그리고 위에서 "자신감을 가지는 게 좋지 않을까"라고 말한 주제에 아직도 이걸 망설이고 있다. 특히 글 초반에 말했던 그 pomofocus를 발견하고 나서 망설임이 더 심해진 상태. 허허 쉽지 않군. 나중에 세 번째 드래프트를 완료하고 쓰러 온 회고록에선 좀 더 홍보를 시도한 상태로 만날 수 있었으면 좋겠다.