tag: 개발 이야기

Gatsby v3 마이그레이션 후기

2021년 7월 17일

Gatsby v3가 기존 v2에 비해 변경된 점이 많아서 마이그레이션을 계속 미루고 있었다. 기존 사이트는 모노리포 아래에 메인 웹사이트와 한국어 웹사이트를 별도 패키지로 구성하고 있었는데 공유하는 컴포넌트를 양쪽에서 사용할 수 있는건 좋긴 했지만 매번 글을 작성할 때마다 지나치게 많은 시간과 리소스를 사용했었다. 여름학기가 끝난 기념으로 재빨리 웹사이트를 정리했다.

계획

일단 마이그레이션 문서를 확인했다. 변경된 부분이 참 많았다. 하하하. 그냥 새 스타터를 받아서 하나씩 옮기는 쪽으로 전략을 바꿨다. 마이그레이션 과정에서 해야 할 일을 정리했다.

  • static 폴더 정리하기: 기존 워드프레스에서 옮겨올 때 대부분 리소스를 wp-content 폴더 안에 저장한 그대로 옮겨온 탓에 워드프레스도 아닌데 wp-content가 계속 존재했다. 각 리소스를 사용하는 포스트를 폴더 형태로 리소스까지 함께 보관하도록 변경한다.
  • mdx 적용
  • 분리된 사이트를 통합: 템플릿을 쉽게 적용할 수 있는 구조를 만든다.
  • 플러그인 마이그레이션: 직접 만든 플러그인, 설치해서 사용하는 플러그인 확인하기.
  • sitemap, rss를 영문과 국문 별도 설정
  • noindex 설정 추가
  • 답답하지 않은 디자인

과정

파일 정리

먼저 static 폴더를 정리하는 일을 가장 하고 싶었다. 그동안 사이트가 분리되어 있던 탓에 늘 어떤 방식으로든 경로 처리 문제가 있었다. 웹사이트 프리픽스를 지정할 수 있도록 gatsby에서 지원하고 있긴 하지만 플러그인마다 동작이 보장되어 있질 않았다. 그런 이유로 gatsby-remark-sharp 같은 플러그인은 제대로 동작하지 않았다. 이참에 wp-content 폴더도 정리하고 모든 정적 리소스도 함께 관리하도록 구조를 변경했다. sharp에서 지원 안하는 파일용으로 gatsby-remark-copy-linked-files도 설치했다.

스크립트를 작성해서 해당 리소스를 사용하는 포스트를 검색해서 리소스와 포스트를 폴더로 묶었다. 처음엔 쉘스크립트로 작성하다가 답답해서 js로 작성했고 생각보다 금방 할 수 있었다. (문자열 찾는 작업은 ripgrep-js를 활용했다.) 불필요한 크롭 이미지와 어디에도 연결되지 않은 리소스 모두 제거할 수 있었다.

Gatsby 관련

mdx는 가장 기대하고 얼른 설치해보고 싶은 것 중 하나였는데 생각보다 손봐야 할 부분이 많아서 일단은 remarkMarkdown을 유지하기로 했다. 다음에 기회가 된다면 0순위 작업이 될 것 같다.

Gatsby는 폴더 구조대로 url을 생성하는 것이 기본이지만 frontmatter 에서 주소를 직접 지정하는 플러그인을 쓰고 있다. 파일 구조는 언제든 바꿔도 url이 유지되었으면 해서 만든 플러그인이었는데 v3에서도 큰 문제 없이 동작했다.

lang과 type을 기준으로 템플릿을 찾아서 페이지를 만들도록 작성했다. 카테고리와 태그 페이지 생성도 기존 코드에서 아주 조금 바꾸는 것으로도 충분했다.

sitemap, rss는 동일한 플러그인을 여러 번 config에 넣는 것으로 여러 페이지를 만들 수 있었다. 다만 gatsby-plugin-sitemap은 생각과 다르게 filterPages가 동작하는 문제로 excludes만 적용했는데 해당 플러그인에서 사용하는 minimatch가 생각처럼 동작하지 않았다. 차라리 정규표현식으로 되어 있으면 편할 텐데. 일단은 sitemap이 크게 급한 상황은 아니니까 대략 숨겨야 하는 페이지만 숨기도록 수정했다. 이 과정에서 noindex 페이지도 몇 설정했다.

전 디자인도 크게 답답하지는 않았는데 너무 삭막한 것 같아서 조금 손봤다. styled-components로 작성했던 컴포넌트를 쉽게 옮겨 올 수 있었다. 다크 테마 설정은 추후에 하기로 하고 일단 뒀다.

대부분 플러그인은 문제 없이 동작했다.

  • gatsby-remark-attrremarkjs의 판올림으로 더이상 동작하지 않는다. 자주 쓰는 기능 중 하나였는데 아쉽다. common markdown의 directives를 지원하는 remark-directive를 설정했는데 attr처럼 동작하지는 않지만 그럭저럭 문제 없이 동작하긴 한다.
  • 그 사이에 netlify도 성능이 많이 좋아졌고 기능도 많아졌다. 빌드 이미지도 변경하고 캐시도 설치했다. 예전엔 gatsby-plugin-netlify-cache가 있었는데 deprecated 되었고 netlify에서 만든 플러그인, netlify-plugin-gatsby를 설치했다. 기존 플러그인과 동시에 설정하면 충돌이 있어서 둘 중 하나를 골라야 한다.

Netlify 관련

Netlify에서 사용중인 빌드 이미지가 오래되어서 해당 설정도 변경했다. 다만 빌드 중에 설치된 라이브러리를 제대로 인식하지 못하는 문제가 있었다. 내 경우에는 설치된 ruby 버전을 인식 못해서 새로 받는 일이 반복되었다. Site setting > build & deploy > build setting 에서 Edit setting 후 동일한 리포지터리를 다시 설정하고 저장하면 해결 된다.

Netlify Forms를 사용하지 않는다면 Site setting > build & deploy > form detection 설정을 끈다. 배포 속도가 향상된다.

결과

전체 배포 시간이 평균 4분~6분에서 1분 30초 선으로 줄어들었다.


워드프레스에서 Gatsby로 변경한 이후로 버전 업데이트는 좀 번거롭지만 보안과 서버 관리에 대한 걱정은 확실히 없어졌다. 다만 서드파티 플러그인이 버전을 따라가지 못하거나 하는 문제는 어디나 비슷한 것 같다. 그런 면에서 주요 플러그인을 모노리포에서 같이 관리하는 gatsby의 전략이 참 멋지다.

블로그를 처음 옮길 때와 달리 정적 호스팅 선택지가 많아졌다. 지금은 netlify를 계속 쓰고 있긴 하지만 Gatsby Cloud라든지 Azure 에서 제공하는 서비스도 확인해보고 싶다.

Remark에 흥미가 많이 생겼다. Rehype이나 mdx를 어떻게 지원하는지, 조만간 더 살펴보고 기능과 구조를 정리해보고 싶다.

늘 그렇듯 업데이트도 한 김에 좀 더 부지런히 글도 쓰고 생각도 정리하는 기회가 되었으면 좋겠다.

비주얼 타이머 앱 개발하며 배운 것들

React native로 지난 1년여 비주얼 타이머 개발한 과정 정리

2020년 8월 16일

visual timer 1

비주얼 타이머를 개발해서 2019년 6월에 출시했고 1년여 시간이 지나 1만 건 다운로드를 넘겼다. 현재는 목표로 했던 기능은 대부분 구현했고 성능 문제 개선, 사용자 피드백 적용을 고민하고 있다. 개발을 시작하면서 어렴풋이 정한 마일스톤인데 달성해서 기분 좋다.

비주얼 타이머는 남은 시간을 직관적으로 확인할 수 있도록 진행 상황을 표시해주는 시각화 타이머로, 소리, 색상, 문구 등 다양한 개인화 기능을 제공한다. 앱은 React native를 사용했고 공지 등 서버 자원은 Azure Function App, 이슈와 리퍼지터리는 Github에서 관리했다.

appstore connect

1만 달성 🙌

개발 동기

  • 생산성 도구를 만들어보고 싶다
  • 프로젝트로 실무 감각을 유지하고 싶다
  • 사이드 프로젝트로 작은 앱은 만들어 봤으니 조금 큰 앱을 만들고 싶다
  • 애플 개발자 등록 비용이라도 벌고 싶다

개발 전

이전부터 생산성 도구에 관심이 많아 만들어보고 싶었다. 타이머는 나도 자주 사용하는 앱 중 하나였고 앱스토어에서 새로운 앱은 없나 종종 검색하기도 했었다. 생산성 앱을 만들겠다는 결정을 하고 나서 다음 과정을 거쳤다.

이전에 시험 삼아 만들어본 팁 계산기 앱에서 얻은 경험을 바탕으로 무슨 앱을 만들지 결정했다.

  • 앱 이름에서 제공하는 기능을 명확하게 알 수 있도록: 별도로 마케팅할 가능성이 작으니까 나는 앱스토어 검색에 최적화하는 쪽으로 기울었다. 앱스토어에서 다양한 키워드로 검색해보니 검색 키워드가 앱 이름이면 유명한 앱보다 먼저 검색 결과에 노출되는 경우가 많았다.
  • 앱스토어에서 검색했을 때 경쟁 앱이 적당히 있어야: 큰 카테고리에서는 검색 순위에 밀릴 수 있어도 조금이라도 카테고리가 좁아지면 상위 순위에 나와야 한다. 검색했을 때 경쟁 앱이 전혀 없다면 사람들이 검색하지 않는 키워드일 가능성도 크다.
  • 기존 앱에서 개선할 만한 부분 확인하기: 다른 앱도 사용해보고 그 앱 리뷰도 보고 업데이트 로그를 보니 이런 앱을 만들며 겪을 피드백과 개선 과정을 이해하는 데 도움이 되었다. (물론 로그를 대충 적는 앱, 이해하기 힘든 리뷰도 많지만)

타이머 자체 기능도 당연히 중요하지만 사소한 개인화 기능을 요청하는 리뷰가 많았다. 그래서 다양한 설정을 할 수 있는 타이머를 만들어보기로 했고 가능한 한 대부분의 설정을 변경할 수 있도록 계획했다. 그 중에도 아동을 대상으로 한 개인화 기능은 유료로 제공해서 수익을 만들기로 했다.

개발 과정

프로젝트는 React native를 사용하기로 했다. 웹앱은 생각보다 느린 반응이 눈에 보여서 Flutter와 RN 사이에서 고민했는데 지금은 많이 개선되었지만 당시에 Flutter로 만들었다는 앱을 써보니 웹앱보다는 빠르지만 약간 느린 인상을 받아서 RN으로 결정하게 되었다.

Expo도 편의 라이브러리가 많아서 좋지만, 결정 당시에는 한국어 입력 문제가 있어서 바로 react native를 사용하기로 했다.

visual timer 2

초기에 고려하지 않은 탓에 큰 작업이 되었던 분할 화면

처음에는 공통으로 사용하는 컴포넌트를 만들어서 다른 프로젝트를 하게 되더라도 활용할 수 있도록 계획을 세웠다. 설정 저장/불러오기나 국제화 지원과 같이 전역에서 사용되는 라이브러리는 큰 문제 없이 계획한 방식대로 만들 수 있었다. 다만 생각처럼 되지 않은 부분은 표현 컴포넌트였다. RN에서 기본적인 컴포넌트를 많이 지원하긴 하지만 iOS 스타일의 크고 작은 표현 컴포넌트는 다른 라이브러리를 사용하거나 직접 만들어야 했다. 공개된 라이브러리가 많이 있지만 필요한 기능은 극히 일부인 데다 스타일 일관성을 맞추는 노력이 생각보다 많이 필요로 해서 직접 만들게 되었다. 이렇게 부분적으로 기능하는 컴포넌트는 당장에는 잘 동작하지만, 특정 상황만 고려해서 만든 탓에 다른 프로젝트에서 쓰기엔 조금 부족했다.

디자인은 iOS 기본 컴포넌트를 거의 따라가서 큰 막힘은 없었다. 아직도 약간씩 어색한 부분이 있지만 대부분 컴포넌트가 나름 비슷하게 동작한다. 그 외는... 좀 대충 만든 부분도 있지만 그래도 동작은 하니까 일단 뒀다. (타이머 순서 바꾸는 부분이라든지 매우 안 이쁘다.)

기능 중 완료 이미지 모음 리소스를 만드는 데 가장 많은 시간이 걸렸다. 완료 이미지 모음은 동물 등 이미지와 함께 이미지에 맞는 효과음이 타이머 완료 시에 표시되는 기능이다. 적합한 저작권을 가진 리소스를 찾고 정리해서 넣는 과정에 많은 시간을 썼다. 직접 일러스트라든지 그릴 수 있었다면 시간을 좀 더 줄일 수 있었을까.

서버는 Azure Function App을 사용하고 있는데 공지를 그냥 JSON 형식으로 내려주는 수준이라 별도의 데이터베이스 없이 작성했다. 그런 덕분에 응답도 빠르고 비용도 안 들어서 지금까지 그대로 유지되고 있다.

개발은 출시까지 4개월 정도 걸렸는데 개발은 1달 반에서 2달 정도 걸렸고 나머지는 앱 리소스(소리 효과, 이미지)와 앱스토어 메타 정보(앱 소개, 정책 등)을 준비하는 데 시간을 썼다.

main

사소하지만 품이 많이 드는 작업. 아이폰, 아이패드, 언어별로.

개발이 완료된 후에는 이상한모임 분들에게 테스트를 부탁해서 진행했다. 이 과정에서 앱이 어떤 맥락과 흐름에서 동작해야 하는지 피드백을 많이 주셔서 큰 도움이 되었다.

마케팅

홍보는 페이스북 그룹, 이상한모임 채널, 클리앙, 내 소셜 계정에서 진행했다. 소개 글과 함께 프로모션 코드도 나눴다. 다운로드 수는 확실히 이런 홍보를 할 때마다 오르긴 했지만 액티브 유저 수 추세는 큰 변동이 없이 항상 일정하게 증가했다. 돈을 쓰지 않아서 그랬을까, 프로모션 코드도 돈이라고 생각한다면 업데이트마다 꽤 지출했다고 볼 수 있겠다.

marketing

가장 큰 도움이 되었던 Back to the Mac 그룹

페이스북 타깃 광고는 제공하는 도구로 확인해보니 적어도 광고 본 사람 중 30% 이상은 받고 결제해야 손익 분기인 비용이라 집행하지 않았다...

그래도 적은 수지만 앱스토어에서 검색해 받는 사람도 꾸준히 있다. 메타 정보를 작성하는 일도 생각보다 쉽지 않았다. 업데이트마다 조금씩 변경하고 있지만, 검색 지표가 명확하게 공개돼 있지 않아서 솔직히 잘 모르겠다. (어떤 키워드로 검색했는지 등)

app units by source

검색 비중이 높다

개선과 피드백

앱 출시 이후 성능 문제를 계속 개선하고 있고 사용자 피드백도 그 과정에서 반영하고 있다. 초기에는 불안정하게 동작하는 부분이 있어서 못쓰겠다는 리뷰 받고 참담했는데 부지런히 안정화했다. 이 과정에서 Xcode의 프로파일링 도구가 큰 도움이 되었다.

피드백은 앱 내 이메일 보내기로도 오지만 대부분은 앱스토어 리뷰로 남긴다. 만족도를 물어보고 이메일로 보낼지 앱스토어 리뷰로 보낼지 구분하겠다고 백로그에 적어두고는 그동안 잊고 지냈... 그동안 작은 피드백도 많이 받았는데 이미 개인화에 중점을 두고 만든 덕분에 대부분 피드백은 간단하게 추가할 수 있었다.

review

좋은 리뷰와 탄탄한 피드백은 늘 힘이 된다

다만 구조적인 변화가 필요한 피드백은 백로그로 쌓고 있다. 가령 여러 타이머를 동시에 사용하거나 순서대로 동작하도록 하는 방식은 현재 구조에서는 처음에 염두에 두지 않았던 부분이라 구현이 어렵다. 좀 더 정리해서 차기 버전에서는 구현할 수 있는 구조로 가고 싶다.

아쉬운 점

사용자 추적

앱스토어커넥트에서 제공하는 분석 도구는 피상적인 수준이라 실제 데이터를 볼 수 없다는 점이 늘 불편하다. 실제 사용은 앱을 실행했을 때 버전 확인과 공지 데이터를 받기 때문에 애저 펑션앱 사용량으로 확인할 수 있지만, 여전히 부족하다. GA 등 추적 도구를 사용해서 어떤 기능을 주로 사용하는지 등 데이터를 모으면 개발 방향을 잡는 데 더 도움이 될 텐데 그러지 않았다. 처음부터 이 부분이 중요하다는 이야기를 들었는데도 지금까지 적용하지 않은 것은 정말 반성해야 한다.

약한 수익 모델

현재는 인앱 결제로 수익이 발생하고 있다.

  • 완료 시 나오는 이미지 모음
  • 개발 지원하기

처음 앱을 기획할 때는 아동용 타이머로 방향을 잡고 있어서 완료 이미지 모음을 인앱으로 제공했다. 앱을 공개한 이후에는 아동용 타이머 외에 일반 용도로 많이 사용하길래 커피 지원하기 같은 개발 지원 항목을 추가했다. 한국에서는 개발 지원이, 미국 스토어에서는 이미지 모음 결제가 많다. 내가 쓰는 시간을 생각하면 아직도 많이 부족하지만 그래도 앱개발자 등록 비용은 해결할 수 있게 되었다.

다음 버전에서는 인앱을 정리하고 유료로 전환하는 것을 고민하고 있는데 이런 결정은 어떻게 해야 하는 걸까, 사용자에게 무슨 가치를 주고 있는지, 그 비용은 어떻게 결정해야 할지 배우고 싶다.

안드로이드 지원

처음에는 RN이면 안드로이드 지원은 저절로 되겠지 생각했는데 실제로 돌려본 결과는 처참했다. iOS는 그래도 작게나마 해본 경험이 있어서 트러블슈팅도 큰 문제가 없었는데 안드로이드는 구동 자체도 쉽지 않았다. 차기 버전에서는 안드로이드도 같이 지원하고 싶다.

자동화와 테스트 부족

지금 앱은 일부 로직에 유닛테스트가 있지만, 아직 많이 부족하고 자동화도 없다. 앞으로 개선해야 하는 부분이다.

잘한 점

React Native

React로 많은 프로젝트를 해보지 않았는데 이 앱을 만들고 관리하면서 많이 배웠다. 정말 매달 새로운 기능과 라이브러리에 눈과 머리가 바쁘다. 출시 이후에도 react에 hook도 들어오고 많은 새 기능이 들어오고 있는데 업데이트 과정에서 조금씩 적용하고 개선하며 많이 배우고 있다.

확실히 js나 react에 경험이 있다면 큰 문제 없이 RN 프로젝트도 꾸릴 수 있다. 물론 타깃 플랫폼도 잘 알면 더 편하다. 특히 기능이나 용어는 각 타깃 플랫폼에서 제공하는 용어로 많이 설명되고 있어서 안드로이드와 iOS의 용어 차이를 알아두는 것도 도움이 됐다.

다국어 지원

처음부터 고려하지 않았다면 꽤 번거로운 작업이 되었을 텐데 지금 와서는 잘 한 결정 중 하나다. 현재 영어랑 한국어만 지원하지만 지금까지 지역 통계를 보면 확실히 도움이 되었다. 마케팅도, 프로모션도 대부분 한국에 했는데 다운로드 비율이나 수익 비중은 미국이 더 크다.

area

프로모션 코드는 한국에서 많이 뿌렸는데 조금 억울

우선순위와 시간 관리

혼자 하는 프로젝트는 내 마음대로 할 수 있다는 장점이 있지만, 한편으로는 정말로 제한적인 시간 자원을 어떻게 관리하는가 하는 점이 정말 큰 어려움이었다. 특히 내가 안 하면 아무것도 진행이 되지 않는다는 점이 당연하지만 피곤했다.

다행히 처음에 계획한 최소 기능은 대부분 만들어냈고 학업 하면서도 크고 작은 개선 작업을 진행했다. 출시 이후에는 적은 분량이라도 한 달에 한 번은 업데이트 할 수 있도록 노력했다. 지금까지 받은 피드백 중 적용된 것은 일부지만 개선 과정에서 좋은 리뷰도 많이 받을 수 있었다.


이 이전에도 앱을 만들어본 경험은 있지만, 하루 정도면 만들었던 수준의 앱과는 경험의 깊이가 달랐다. 특히 앱은 웹과는 확실히 다른 맥락과 인터페이스를 갖고 있었다. 앱을 출시하는 경험은 평소라면 별 생각 없이 쓰던 수많은 앱을 교과서처럼 보이게 했다. 아름답고 멋지고 편리한 기능을 모두 앱에 적용하고 싶었지만 작아도 정교한 기능은 보는 것과 다르게 훨씬 많은 시간을 필요로 했다. 쉽진 않겠지만 다음엔 그런 부분도 꼼꼼하게 구현해보고픈 욕심도 생겼다.

visual timer 3

지금 이렇게 되돌아보면 잘한 점보다 부족한 점도 많고 배워야 할 부분도 산더미다. 그래도 부족한 부분이 많지만 유용하게 사용하고 있다는 리뷰 하나하나가 큰 힘이 됐다. 앞으로 개선될 타이머와 만들어갈 많은 프로젝트에서도 이런 경험이 거름이 되었으면 좋겠다.

애플 앱스토어에서 보기

웹페이지 다크 모드 지원하기

prefers-color-scheme와 window.matchMedia 활용

2020년 7월 28일

웹페이지 다크 모드 지원은 prefers-color-scheme 미디어 쿼리를 사용해서 적용할 수 있다. 각 해상도에 따라 미디어 쿼리를 적용하는 방식과 크게 다르지 않다.

.title {
    color: #101010;
}

.desc {
    background-color: darkorange;
}

@media (prefers-color-scheme: dark) {
    .title {
        color: #EFEFEF;
    }
    .desc {
        background-color: peachpuff;
    }
}

이제 해당 페이지는 다크 모드로 지정했다면 해당 스타일로 적용된다. CSS 프로퍼티로 정의하면 자료와 구조를 분리할 수 있어 좀 더 깔끔해진다.

:root {
    --title-color: #101010;
    --desc-color: darkorange;
}

@media (prefers-color-scheme: dark) {
    :root {
        --title-color: #EFEFEF;
        --desc-color: peachpuff;
    }
}

.title {
    color: var(--title-color);
}

.desc {
    background-color: var(--desc-color);
}

사용자가 시스템에서 설정한 대로 보이긴 하지만 웹페이지에서 직접 변경할 수 있도록 옵션을 제공할 수도 있다. localStorage에 설정을 저장해서 다시 웹페이지에 접속할 때 마지막 설정을 불러올 수 있다.

:root {
    --title-color: #101010;
    --desc-color: darkorange;
}

@media (prefers-color-scheme: dark) {
    :root {
        --title-color: #EFEFEF;
        --desc-color: peachpuff;
    }
}

[data-theme="light"] {
    --title-color: #101010;
    --desc-color: darkorange;
}

[data-theme="dark"] {
    --title-color: #EFEFEF;
    --desc-color: peachpuff;
}

/* ... */

저장된 값이 있다면 페이지에 설정한다.

const theme = localStorage.getItem('theme');
if (theme) {
    document.documentElement.setAttribute('data-theme', theme);
}

시스템 설정을 확인하기 위해서 window.matchMedia() 함수를 사용할 수 있다. CSS의 미디어 쿼리가 현재 페이지에 해당하는지 확인하는 기능을 제공한다.

function toggleTheme() {
    // 저장된 값이 없다면 시스템 설정을 기준으로 함
    const currentTheme = localStorage.getItem('theme')
        || (
            window.matchMedia("(prefers-color-scheme: dark)").matches
            ? 'dark'
            : 'light'
        );
    const newTheme = currentTheme === 'dark' ? 'light' : 'dark';

    // 최상위 엘리먼트에 설정, 로컬 스토리지에 설정을 저장
    document.documentElement.setAttribute('data-theme', newTheme);
    localStorage.setItem('theme', newTheme);
}

만약 미디어 쿼리의 결과를 동적으로 반영해야 하는 경우에는 window.matchMedia()의 반환값인 MediaQueryList에 이벤트 리스너를 추가할 수 있다.

const mql = window.matchMedia("(prefers-color-scheme: dark)");

mql.addEventListener((e) => {
    if (e.matches) {
        // 해당 미디어 쿼리가 참인 경우
    } else {
        // 해당 미디어 쿼리가 거짓인 경우
    }
});

2019년 7월 업데이트

2019년 7월 20일

오랜만에 글을 쓴다고 자리에 앉았다. 작년에 미국으로 넘어 온 이후로 지금까지 많은 일이 있었는데 기록을 제대로 남기지 않았던 탓에 블로그도 휑하고 기억도 가물가물하다. 올해 7월도 조금밖에 남지 않았는데! 그래서 그동안 있던 일이라도 간단하게 정리해보고 싶었다.

왜 글을 잘 안 쓰게 되었나

미국으로 이사 온 이후로 안전에 대한 감각이 많이 달라졌다. 미국 밖에 있을 때는 총기 사건에 대한 뉴스를 드문드문 듣는 정도였지만 그렇게 외신으로 보도되는 사건은 매우 큰 사건에 한해서고 그보다 작은 사건도 꽤 빈번했다. 한국이나 호주라면 사소할 만한 사건에도 여기는 총기 문제가 늘 엮여서 사람 다치는 일이 흔했다. 바로 TV만 켜면 나오는 뉴스에도 그렇게 겁이 생기는데 오가면서 들은 뉴스나 사건에도 걱정이 앞설 수밖에 없었다.

결정적으로 집 앞에 멀쩡하게 세워져 있던 차량을 도난당하는 일이 있었다. 경찰에 신고했지만 끝내 찾을 수 없었다. 이 사건으로 동네 페이스북 커뮤니티와 Nextdoor에 가입하게 되었다. 이런 서비스를 통해 지역에서 일어나는 사건/사고를 알고 대응할 수 있다는 점은 분명 장점이지만, 한편으로는 이 지역에 산다면 내가 어디 사는지 명확히 알 수 있을 정도로 세세한 정보를 서비스에 제공하게 된다는 점이 걸렸다.

가장 큰 불안감은 내 온라인 활동이 사회공학(Social engineering)의 타겟이 될 수 있다는 점에서다. 예를 들어, 어딘가 여행을 가서 사진을 올린다면 집에 나 또는 가족이 없는 것을 알고 주거지에 침입할 수도 있다. 내 일상적인 동선을 파악해서 범죄 행위를 저지를 수도 있다. 내가 올린 사진에 걸린 태그를 보고 어느 지역에 사는지 알아낼 수도 있다. 소셜 네트워크의 사소한 요소조차도 내 불안함을 자극했다.

그래서 글도, 사진도 올리기 전에 여러 차례 고민할 수밖에 없었다. 정말 공유하고 싶을 때는 다녀온 지 한참 지나서야 올리기도 했다. 불안해서 어디인지 태그도 잘 하지 않았다. 이렇게 올린 글이나 사진은 적시성이 떨어져서 올리는 재미도 덜했다. 그래서 자연스럽게 아무것도 올리지 않는 쪽이 되었는데 늘 일상을 공유하던 습관 탓인지 고스란히 일상의 스트레스가 되었다.

최근 들어서 조금씩 올리긴 하지만 여전히 불안한 마음이 크다. 아마 이 글을 쓴 이후로는 다시 자제할 것이다. 내가 심은 꽃을 보세요! 하고 올리지만, 앞마당에 있는 꽃을 찍어 올린 것 보고 우리 집인걸 알면 어떡하지 걱정된다. 여전히 동네에 크고 작은 사건이 있는 것을 온갖 소셜 서비스를 통해 실시간으로 날아오기 때문이다. 그래서 아마 정말 기술적인 이야기 외에는 거의 쓰지 않을 것 같다.

코드, 코드, 코드

지난번에 팁 계산기 이후로도 여러 앱을 만들어서 올렸다. 지난 팁 계산기는 ionic을 사용했지만, 그 이후로는 모두 react-native를 사용했다. 리액트를 공부해도 실무에서 제대로 사용해본 적 없으니 리액트를 사용한 프로젝트를 해보고 싶었다. 그리고 팁 계산기 이후로 웹과는 다른 맥락에 재미를 느껴서 앱을 계속 만들고 싶었는데 리액트 네이티브가 좋은 선택지가 되었다.

현재 안드로이드 기기가 없어서 다 iOS로만 출시되어 있다. 매년 내는 애플세를 내고 나서 무얼 살 정도로 수익이 있으면 🤞 그때쯤 기기도 구입하고 안드로이드로도 낼 수 있지 않을까.

비주얼 타이머

비주얼 타이머는 리액트 네이티브로 가장 먼저 만들어본 앱이다. 남은 시간을 쉽게 확인할 수 있는 타이머로 다양한 설정을 통해 자신만의 타이머를 만들 수 있다.

  • 계획했던 기능을 5% 정도 빼고는 모두 구현했다.
  • 개발 기간 중간에 다른 많은 일정이 있었는데 끝을 냈다는 사실만으로도 감사하다.
  • 출시 전에 베타 테스트로 좋은 피드백을 많이 받아서 개선할 수 있었다.
  • 설정 할 수 있는 부분이 너무 많아서 혼란을 느끼는 사용자가 많았다. 다음엔 정말 필요한 기능만.
  • 어린이를 위한 종료 이미지와 효과음을 인앱으로 넣었는데 생각보다 인기가 있었다!
비주얼 타이머

팁 계산기

팁 계산기는 지난번에 만든 앱을 개선한 리액트 네이티브 앱이다. 한꺼번에 여러 팁 퍼센트를 확인하고 선택할 수 있다. 이전 버전과 다르게 계산 결과가 저장되며 메모를 남길 수 있다.

  • 기존 팁 계산기는 ionic에 angular였지만 중요 연산 로직은 대부분 재활용할 수 있었다.
  • JavaScriptCore의 처리 속도 지연을 처음 경험했다. 네이티브 코드로 넘겨서 연산하면 좋겠지만 memoize 하는 정도로 그쳤다.
  • 처음으로 admob을 달았는데 간단했다. 그런데 광고 붙인 이후로 앱이 영 안 이쁘다.

온도 변환 퀴즈

온도 변환 퀴즈는 화씨와 섭씨온도를 빠르게 바꿔서 선택하는 퀴즈 앱이다. 간단하게 하루 만에 끝내는 앱을 생각하다가 만들었다.

  • 늘 잘써보고 싶다고 했던 Animated를 가장 많이 썼다.
  • 간단한 게임은 리액트 네이티브도 정말 넘치는 수준이었다.
  • 광고 보면 체력을 하나 더 주는 리워드를 붙여봤는데... 광고 시청 바랍니다 😎

매일 투두

매일 투두는 시작 시각에 할 일을 적고 완료 시각에 할 일을 체크하는 앱이었다. 다른 앱과 다른 점이라면 하루가 지났을 때 모든 목록이 히스토리로 옮겨진다는 점이었다. 투두가 쌓이는 스트레스를 없애주는 신개념 투두앱🤔이었는데 만들어서 공유하니까 반응이 신통하지 않아서 침몰한 앱이다. (결국엔 나도 잘 안 썼고...)

블로그

내 블로그도 디자인을 변경했다. 전 디자인은 아이맥 기준처럼 만들어져서 작은 스크린에서는 답답하고 큰 스크린에서는 지나치게 글이 크게 보였다. 다시 마음을 다잡는 겸 블로그도 새로 정리했는데 깔끔해서 마음에 든다.

  • vw와 rem을 많이 사용했다. 그래도 픽셀 단위로 미디어쿼리 쓸 부분이 조금씩 있긴 했다.
  • 리액트 네이티브를 쓰며 익숙해진 flex를 많이 썼다.
  • 늘 제목의 계층을 서체 크기로 하는 게 불편하다고 생각했는데 css counter를 적용해서 책처럼 표시했다. 좀 더 명확한 것 같다.
  • 제목에 링크 붙여주는 플러그인을 넣었다.
  • 구조를 바꿔서 그리드 밖으로 나가는 것처럼 보이는 요소를 넣을 수 있게 되었고 (이 포스트에 이미지처럼) 이쁘게 잘 적용된 것 같다.
  • 배경색을 약간 어둡게 바꿔서 흰색을 강조로 사용할 수 있게 되었다.
  • 전체적으로 색을 부드럽게 조정했다. 너무 발색이 심한 웹페이지에 피로해서 내 자신도 블로그에 자주 안 들어왔나 싶다.

다녀온 곳

그동안 길고 짧게 다녀온 곳이 많았다. 아내와 가족과 좋은 시간을 보냈다. 사진은 생략하고 간단하게 정리했다.

  • LA: 갈 때마다 잘 놀고 오면서 복잡한 마음 생기는 곳.
  • 샌프란시스코 & 산호세: 분위기는 갈 때마다 좋지만, 여기서 살 수 있을까 깊은 고민.
  • 새크라멘토: Fire Wings 꼭 드세요.
  • 요세미티: 애플 배경화면을 눈으로 보니까 기분 이상했다. 기회가 있다면 또.
  • 그랜드캐니언 & 엔탈로프캐니언: 진짜 비현실적이고, 기회가 있다면 2.
  • 라스베가스: 오션스일레븐 생각하고 갔지만, 현실은 행오버랑 더 비슷한 분위기.
  • 플로리다: 4월인데도 제주의 여름처럼 후덥지근했다.
  • 샌디에고: 늘 좋은 기억 만든다.

그동안 있던 일들

순서 없이 목록으로 적어봤다.

  • 걱정을 많이 했던 영주권이 잘 나왔다.
  • 책을 많이 읽어야지 하고 잔뜩 샀는데 역시나 읽은 양이 너무 적다... 요즘 읽는 책은 The Shallows: What the Internet Is Doing to Our Brains.
  • Triplebyte를 통해서 인터뷰를 진행했었다. 프로세스도 좋았고 연결된 회사도 마음에 들었지만 리로케이션은 아무래도 힘들어서 다음을 기약했다.
  • 살쪘다. 운동이 절실하다.
  • 콘텍트렌즈를 맞췄다. 안경 없는 삶 어색하지만 매우 편하다.
  • 식물과 친해졌다. 씨를 뿌려 수확하는 재미를 알게 되었다. 그래도 나름 주기가 짧은 것 하는데도 기다림은 지루하다.
  • 오버워치를 하는데 맨날 비슷한 점수만 오간다. (팀 탓) 역할 고정 기능 기대하고 있다.
  • Friends를 다 봤다. 결말 너무 맘에 들지 않는다. 계속 보는 최근 드라마는 The 100.
  • 운전면허를 취득했다!
  • 학교에 등록해서 조만간 신학기가 시작될 예정이다.

목록으로 적으니까 금방 적는다 👏

나에게

나는 항상 지나치게 걱정하는 편이라서 걱정을 자제할 필요가 있다. 앞으로 학교에서의 생활에도 잡다한 고민은 치우고, 알고 싶었던 분야를 공부한다는 즐거움에 집중할 수 있었으면 좋겠다.

기록하는 습관을 잃어버린 것 같아 아쉽다. 글쓰기도 꾸준히 했으면 좋겠다.

리액트 훅(Hooks): 컨텍스트(context) API는 어떻게 되나요?

리액트의 새로운 기능(Hooks/Suspense)이 추가되면 컨텍스트 API에는 어떤 영향이 있는지 확인합니다. 번역

2019년 3월 25일

이 포스트는 Kent C. DoddsReact Hooks: What's going to happen to react context?를 번역한 글입니다.

2018년 초, 리액트 팀은 처음으로 공식 컨텍스트(context) API를 소개했습니다. 이 새 API에 대해서 글을 쓰기도 했고 많은 사람들이 이 기능에 꽤나 흥분했습니다.

이 API에 대한 가장 흔한 푸념 중 하나는 컨텍스트를 실질적으로 사용하려면 렌더링 프로퍼티 API를 사용해야 한다는 점이었습니다. 여러 컨텍스트를 사용하려고 하면 결국 (로직 재활용을 위해) 여러 렌더링 프로퍼티 API를 사용하게 되고 결과적으로 많은 중첩(nesting)이 발생하게 됩니다. 그래서 여러 렌더링 프로퍼티 기반 API를 조합해서 하나의 함수 컴포넌트를 만들어 컨텍스트를 소비하는 방식을 이전 블로그 포스트에서 제시했습니다.

const ThemeContext = React.createContext('light')
class ThemeProvider extends React.Component {
  /* code */
}
const ThemeConsumer = ThemeContext.Consumer
const LanguageContext = React.createContext('en')
class LanguageProvider extends React.Component {
  /* code */
}
const LanguageConsumer = LanguageContext.Consumer

function AppProviders({children}) {
  return (
    <LanguageProvider>
      <ThemeProvider>{children}</ThemeProvider>
    </LanguageProvider>
  )
}

function ThemeAndLanguageConsumer({children}) {
  return (
    <LanguageConsumer>
      {language => (
        <ThemeConsumer>{theme => children({language, theme})}</ThemeConsumer>
      )}
    </LanguageConsumer>
  )
}

function App() {
  return (
    <AppProviders>
      <ThemeAndLanguageConsumer>
        {({theme, language}) => (
          <div>
            {theme} and {language}
          </div>
        )}
      </ThemeAndLanguageConsumer>
    </AppProviders>
  )
}

리액트 컴포넌트의 강력한 합성 덕분에 위 예시처럼 문제를 해결할 수 있습니다. 이런 해법이 썩 맘에 들지 않습니다. 게다가 저만 그렇게 생각한 것이 아닙니다.

클래스 컴포넌트에서는 새 렌더링 프로퍼티 API를 적용하기 어렵다는 피드백을 받았습니다. 그래서 클래스 컴포넌트에서도 쉽게 컨텍스트 값을 사용할 수 있도록 새로운 편의 API를 추가했습니다. - React v16.6.0: lazy, memo와 contextType

이 새로운 편의 API는 클래스 컴포넌트에서 단 하나의 컨텍스트만 소비한다면 contextType이라는 정적 프로퍼티를 정의하는 것으로 사용하고 싶은 컨텍스트를 할당 받아 this.context로 접근할 수 있도록 기능을 제공합니다. 단일 컨텍스트만 필요로 한다면 일반적인 상황에서 꽤 깔끔하고 멋진 방법입니다.

이 편의 API는 정말 만족스럽습니다. 하지만 저는 리액트 컨텍스트의 미래에 영향을 줄 리액트 훅이 더 기대됩니다. 위에서 봤던 코드를 useContext 훅을 사용해서 다시 작성해보겠습니다.

const ThemeContext = React.createContext('light')
class ThemeProvider extends React.Component {
  /* code */
}
const LanguageContext = React.createContext('en')
class LanguageProvider extends React.Component {
  /* code */
}

function AppProviders({children}) {
  return (
    <LanguageProvider>
      <ThemeProvider>{children}</ThemeProvider>
    </LanguageProvider>
  )
}

function App() {
  const theme = useContext(ThemeContext)
  const language = useContext(LanguageContext)
  return (
    <div>
      {theme} and {language}
    </div>
  )
}

ReactDOM.render(
  <AppProviders>
    <App />
  </AppProviders>,
  document.getElementById('root'),
)

멋지지 않습니까? 렌더링 프로퍼티 기반으로 만든 코드만큼 강력하면서도 훨씬 쉽게 읽고, 이해할 수 있고, 리팩토링하고 관리하기 좋은 구현입니다. 단순히 적은 코드가 아닙니다. 간혹 코드 양을 줄이는 과정에서 코드가 주는 명료성을 해쳐 의사소통에 문제를 만들 때도 있습니다. 하지만 이 경우에는 적은 코드이면서도 동시에 쉽게 이해할 수 있습니다. 이 컨텍스트 API는 강력하며 동시에 새 훅 API에 큰 기능을 제공합니다.

리액트 훅의 또 다른 큰 기능은 완전히 선택적인 기능이며 하위호환성이 보장됩니다. 아마 페이스북은 세계에서 가장 크고 오래된 리액트 코드베이스를 운용하고 있으니 엔지니어에게 고통을 주는 결정을 할 수는 없었을 겁니다. 리액트가 점진적으로 새로운 훅의 세계로 이끌고 있다는 점이 너무나도 멋집니다. 리액트 팀에게 감사합니다. 공식 릴리즈가 기대되네요. (역자 주: useContext는 현재 16.8에 릴리즈되었습니다.)

결론

리액트의 멋진 점은 세세한 구현에 개의치 않고 실제 세계의 문제를 해결하는데 집중할 수 있도록 한다는 점입니다. 크로스 브라우징이나 성능 문제를 오랜 기간동안 겪었습니다. 이제는 리액트를 통해 더 나아가 문제를 단순화했습니다. 이제 간단히 읽고 이해할 수 있는 코드를 작성하고 리팩토링하며 유지관리만 하면 됩니다. 저도 제 코드를 통해서 다른 이들의 작업을 단순하게 할 수 있을 지 궁금하네요 🤔.

리액트 훅(Hooks): 렌더링 프로퍼티는 어떻게 되나요?

훅 기능이 렌더링 프로퍼티 컴포넌트와 비교했을 때 얼마나 코드 재사용성을 해결하는지 확인합니다 번역

2019년 3월 21일

이 포스트는 Kent C. DoddsReact Hooks: What's going to happen to render props?를 번역한 글입니다.

작년에 "프로퍼티 게터(prop getters)를 사용해서 사용자에게 컴포넌트 렌더링 제어를 넘겨주는 방법"이란 포스트를 작성했습니다. 이 글에서 (당시) react-toggled의 전체 구현을 보여줬는데 downshift에서 사용했던 패턴을 가르치기 위해서 작성했던 라이브러리입니다. 제가 downshift에서 훨씬 간단하고 단순한 컴포넌트를 동일한 패턴으로 구현해서 사용했기 때문에 프로퍼티 게터 패턴을 가르치기 아주 좋은 방법이었습니다.

react-toggled와 downshift는 둘 다 렌더링 프로퍼티 패턴을 사용해서 리액트 컴포넌트 간의 로직 코드를 공유했습니다. 다른 포스트인 "렌더링 프로퍼티를 사용하지 말아야 하는 경우"에서 렌더링 프로퍼티 패턴을 적용하기 좋은 경우를 확인했습니다. 하지만 리액트 훅을 적용하기 좋은 경우도 동일합니다. 리액트 훅은 클래스 컴포넌트와 렌더링 프로퍼티 조합보다 훨씬 간단합니다.

리액트 훅이 안정 버전에서 제공되기 시작하면 더이상 렌더링 프로퍼티를 쓸 필요가 없다는 의미인가요? 아닙니다! 여전히 렌더링 프로퍼티 패턴이 아주 유용하게 쓰일 수 있는 부분이 있으며 그 부분을 곧 살펴보도록 하겠습니다. 하지만 현재의 react-toggled를 훅 기반으로 구현한 코드와 비교해서 왜 훅이 더 간단하다고 주장하는지 비교해보도록 하겠습니다.

코드가 궁금하다면 현재의 react-toggled를 확인해보기 바랍니다.

가장 일반적인 react-toggled 사용 방법입니다.

function App() {
  return (
    <Toggle>
      {({on, toggle}) => <button onClick={toggle}>{on ? 'on' : 'off'}</button>}
    </Toggle>
  )
}

단순히 간단한 토글 기능이 필요한 경우라면 훅으로 작성한 코드는 다음과 같습니다.

function useToggle(initialOn = false) {
  const [on, setOn] = useState(initialOn)
  const toggle = () => setOn(!on)
  return {on, toggle}
}

이제 다음과 같은 방식으로 사람들이 사용할 수 있게 됩니다.

function App() {
  const {on, toggle} = useToggle()
  return <button onClick={toggle}>{on ? 'on' : 'off'}</button>
}

훨씬 간단하고 멋집니다. 하지만 react-toggled의 Toggle 컴포넌트는 이보다 더 많은 기능을 제공합니다. 하나를 예로 들자면 getTogglerProps라는 헬퍼(helper)를 제공합니다. 이 함수는 토글하는 엘리먼트에 올바른 프로퍼티를 전달하기 위해 사용합니다. (접근성을 위한 aria 속성도 포함합니다.) 이 기능도 포함해봅니다.

// 주어진 함수 목록을 호출하는 함수를 반환
const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args))

function useToggle(initialOn = false) {
  const [on, setOn] = useState(initialOn)
  const toggle = () => setOn(!on)
  const getTogglerProps = (props = {}) => ({
    'aria-expanded': on,
    tabIndex: 0,
    ...props,
    onClick: callAll(props.onClick, toggle),
  })
  return {
    on,
    toggle,
    getTogglerProps,
  }
}

이제 useToggle 훅에서 getTogglerProps 헬퍼도 사용할 수 있습니다.

function App() {
  const {on, getTogglerProps} = useToggle()
  return <button {...getTogglerProps()}>{on ? 'on' : 'off'}</button>
}

기존 방식보다 더 접근하기 좋고 편리합니다. 만약 getTogglerProps가 필요 없는 경우라면 어떨까요? 다음처럼 코드를 조금 더 나눌 수 있습니다.

// 주어진 함수 목록을 호출하는 함수를 반환
const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args))

function useToggle(initialOn = false) {
  const [on, setOn] = useState(initialOn)
  const toggle = () => setOn(!on)
  return {on, toggle}
}

function useToggleWithPropGetter(initialOn) {
  const {on, toggle} = useToggle(initialOn)
  const getTogglerProps = (props = {}) => ({
    'aria-expanded': on,
    tabIndex: 0,
    ...props,
    onClick: callAll(props.onClick, toggle),
  })
  return {on, toggle, getTogglerProps}
}

이제 react-toggled가 지원하는 getInputTogglerPropsgetElementTogglerProps도 같은 방법으로 구현할 수 있습니다. 이 접근 방식이라면 앱에서 사용하지 않는 추가적인 유틸리티를 쉽게 떨어낼 수 있고 어떤 면에서는 렌더링 프로퍼티 해결책이 오히려 사용하기 조잡하다고 느껴질 수도 있겠습니다. (불가능한 것은 아니지만 그냥 좀 깔끔하지 않습니다.)

하지만! 앱에 있는 모든 렌더링 프로퍼티 API를 새 훅 API로 모두 리팩토링하고 싶지 않아요! 라고 말할 수 있습니다.

두려워하지 마세요! 다음 코드를 보기 바랍니다.

const Toggle = ({children, ...props}) => children(useToggle(props))

여기 렌더링 프로퍼티 컴포넌트가 있습니다. 그냥 간단하게 예전 코드를 사용하면서도 시간이 흐른 후에 고쳐도 됩니다. 사실, 이 방식이 제가 커스텀 훅을 만든 후에 테스트할 때 추천하는 방법입니다.

여기에는 좀 더 내용이 있습니다. (마치 렌더링 프로퍼티 패턴에서 리액트 훅으로 바꾼 방식과 비슷합니다.) 더 설명하지 않고 이 글을 읽고 있는 당신이 발견하도록 조금 남겨두려고 합니다. 무엇인지 조금 더 생각해본 후에 제가 작성한 과정을 확인해보세요. 훅을 사용하면서 테스팅하는데 약간 달라지는 부분을 확인해보기 바랍니다. (자바스크립트 클로저 만세!)

렌더링 프로퍼티 패턴을 적용할 만한 경우

자, 이제 컴포넌트에 훅을 사용해서 리팩토링을 하면서도 여전히 렌더링 프로퍼티 기반 API로 리액트 컴포넌트를 공유할 수 있습니다. (관심 있다면 하이드라 패턴을 사용할 수도 있습니다.) 하지만 미래를 상상해봅시다. 렌더링 프로퍼티를 더이상 로직 재사용에 사용하지 않고 모두가 훅을 사용하고 있는 시대 말이죠. 그렇다면 계속 렌더링 프로퍼티 API를 작성하거나 사용해야 하는 이유가 있을까요?

그렇습니다! 보세요! 여기에 downshift에서 react-virtualized와 함께 사용한 예시입니다. 연관된 부분을 아래서 확인해보세요.

<List
  // ... some props
  rowRenderer={({key, index, style}) => (
    <div
    // ... some props
    />
  )}
/>

여기 rowRenderer 프로퍼티를 확인해보세요. 이 코드가 무엇인지 아세요? 바로 렌더링 프로퍼티입니다. 뭐라고요?! 🙀 바로 제어 역전을 실현한 렌더링 프로퍼티가 여기에 있습니다. 이 프로퍼티는 react-virtualized에서 목록에서 각 행을 렌더링하는 데 있어서 제어를 사용자에게 넘겨주기 위해 존재하는 코드입니다. 만약 react-virtualized를 훅으로 다시 작성한다고 하면 아마도 rowRendereruseVirtualized 훅의 인자로 받을 수 있을 겁니다. 하지만 이런 경우라면 렌더링 프로퍼티 패턴보다 이득이 있다고 말하기는 조금 어렵습니다. 그러니 이런 사용 예시에서는 이 패턴(과 이 방식의 제어 역전)을 사용하는 방법이 더 적합하다고 봅니다.

결론

이 내용이 흥미 있고 도움이 되었기를 바랍니다. 리액트 훅은 아직 알파 단계이며 변경 예정에 있습니다. (역자 주: 현재 16.8.0에 포함되어 배포되었습니다.) 이 기능은 철저히 선택 기능이며 리액트의 다른 API에 문제를 만들지 않습니다. 제가 보기엔 정말 좋은 기능이라고 생각합니다. 앱을 다시 작성하지 마세요! 대신 리팩토링을 하기 바랍니다. (훅이 안정 단계에 들어오면 말입니다.)