내가 2022년 상반기에 진행했던 프로젝트는 사내에서 PDP라고 불리던(이곳에서는 호텔,모텔,펜션,게스트하우스 등 국내 숙소의 상세 정보 페이지를 통칭한다) 상세 페이지를 완전히 새롭게 개발하여 기존 페이지를 대체하는 작업이었다. 개발하기로 하여 사전 회의에 모두 참여했던 담당자가 퇴사하는 바람에 갑자기 맡게 된 프로젝트로, 2명이 약 5개월간 개발을 진행했다. 나는 페이지에 노출되는 숙소의 위치, 정책 등 상세 정보, 후기, 그리고 쿠폰 정보 등을 주로 담당하여 개발했다.
이 프로젝트는 기존 하나의 프로젝트에서 노출하던 영역을 새로운 도메인이자 서비스로 분리하여 개발하는 마이크로 프론트엔드 개발이었다. Next.js를 이용하여 개발하고 AWS Beanstalk를 이용해 배포되었기에 새로운 도메인이 추가되어 SEO 처리도 필요했었는데, API Gateway를 이용하여 기존 도메인의 하위 URL로 잡았다면 기존에 노출되던 URL을 그대로 사용할 수 있었을 것이기에 아쉬움이 남는다.
프로젝트를 진행하며 크게 두가지, 개발 측면과 회사 분위기 측면에서 알게 된 것들이 있다. 회사 분위기, 혹은 사회생활 측면에서 알게된 것들은 굳이 글로 남기지 않으려고 한다. 그냥 혼자 삭히며 고민해볼 문제니까. 여하튼, 개발 중 새삼스레 알게 된 것들을 적어놓는다.
개발 측면에서 알게 된 것들
1. Next.js
유지보수를 위해 계속 사용하고는 있었지만, 신규 프로젝트 개발에 처음부터 사용한 것은 이번이 처음이다. 이전 담당자가 프로젝트를 세팅하고 퇴사하였기에 직접 초기 세팅을 하지는 않았지만 직접 했어도 프로젝트 자체는 큰 차이는 없었을 것으로 보인다. Next.js는 React를 기반으로 이런 저런 기능들이 포함된 하나의 프레임워크다(개인적으로는 React도 본연의 라이프사이클이 있어 프레임워크라고 생각하지만). 특히 직접 개발하고 관리하는 회사가 있고 꽤나 오래된 프레임워크이다 보니 지원하는 기능도, 이슈에 대한 대응도 굉장히 빠른 편이다. 그렇기 때문에 오픈 소스로 관리되는 Vue.js와는 큰 차이를 보인다.
Next.js는 항상 이슈가 되는 SSR을 비롯하여 SPA 구성을 위한 Router, HTML의 헤더 부분을 손쉽게 변경할 수 있는 Next/head 등 유용한 라이브러리가 많아 개발에 큰 도움이 되었다. 하지만 백엔드 레벨에서의 지원은 아쉬운 점이 많았다. Next.js 12버전부터 Middleware를 지원하기는 하지만 미들웨어에서 오류 발생시 폴백 처리가 제대로 되지 않아 고생한 경험이 있다. 백엔드 코드임에도 HMR등을 지원하니 직접 Express같은 것을 띄우는 것보다는 훨씬 좋지만 아무래도 백엔드쪽은 아쉬움이 느껴진다.
이전부터 리액트에 대한 불만이 있고 이 프로젝트를 진행하면서도 해소되지는 않았지만, 퍼블리셔 없이 프론트엔드를 개발하는 전담 팀이 있다면 Next.js만큼은 굉장히 좋다고 생각하게 되었다. (Nuxt.js는 왜 아직도 그 모양인지 모르겠음)
2. TRPC (React-query + @)
사실 TRPC에 대해서라기 보다는 리액트 쿼리에 대한 경험이라고 봐야 한다. TRPC는 프론트엔드에서 호출할 수 있는 API 개발에 사용되는 라이브러리로, 백엔드에서 구현한 End-point를 URL이 아닌 쿼리명으로 프론트엔드에서 호출할 수 있도록 해주는 라이브러리다. 타입스크립트 기반으로 요청 데이터나 응답 데이터의 타입, 범위 등을 체크하는 도움을 주지만 개발해야 하는 주요 부분에 아주 큰 차이가 있지는 않다.
TRPC의 프론트엔드 클라이언트 부분(Next.js와 함께 사용할 때)은 React-Query를 래핑한 라이브러리라고 생각해도 무관한데, 리액트 쿼리 특유의 훅 처리로 리팩토링에 한계가 있었다. 불필요한 커스텀 훅이 많이 만들어져 코드 복잡도가 높아지고 유지보수 편의성이 떨어졌고, 비교적 자유롭지 못한 함수 호출에 대한 불편함도 있었다. 물론 컴포넌트에 사용할 데이터를 불러오는 아주 일반적인 케이스에서는 리액트 쿼리 특유의 편리함을 느꼈지만 (심지어 TRPC로 구현한 백엔드의 쿼리명만 입력하면 되므로) 계속해서 요청 파라미터가 변하는 무한 스크롤 같은 경우 반 강제로 파라미터를 리코일에 넣어야 한다던지 하는 귀찮은 문제들이 발생했다. 프로젝트 개발 막바지에 이르러서는 익숙해져 불편함이 없었지만, 사용해보지 않은 개발자와 함께 프로젝트를 진행해야 한다면 선택이 망설여질 것 같다.
3. Next.js의 getServerSideProps와 Recoil Store
Next.js는 SSR이 아주 손쉽게 지원되는데, 페이지 컴포넌트에 선언한 getServerSideProps라는 함수에서 사용할 데이터를 가져와 페이지 컴포넌트로 전달한다. 이 값을 직접 사용하던지 리코일 등 스토어에 삽입하여 페이지를 구성할 수 있다. 이 프로젝트에서는 전달받은 데이터를 리코일에 삽입하는 부분을 더 쉽게 처리하기 위해 페이지 컴포넌트가 export되는 부분을 감싼 일종의 고차 함수(라기엔 래핑함수?)를 작성했다. 페이지 컴포넌트를 '실행하면 Recoil에 데이터를 삽입하는 컴포넌트'로 바꾼 것이다. 이렇게 작성하니 일반적인 상황에서는 문제가 없었지만 Next/Router를 이용하여 다른 페이지로 이동할 때 문제가 발생했다.
Next.js는 이동하고자 하는 페이지의 getServerSideProps 실행 결과를 받아 그 내용을 기반으로 페이지를 구성한 이후 이동하는데, 이 때 작성한 고차 함수(페이지 컴포넌트 실행시 리코일 스토어에 데이터를 삽입하는)가 실행되지 않는 것처럼 보이는 것이었다.
나중에 찬찬히 확인해보니 고차 함수나 로직 자체에 문제가 있던 것이 아니라, getServerSideProps 데이터를 리코일 스토어에 삽입한 이후, 다시 useEffect 훅에서 처리해야하는 구문이 있어 이 부분이 실행되지 않는 것이었다. 리액트의 데이터 처리는 Vue.js에서의 처리와 달리 함수형으로 값이 변경되었을 때 로직을 재실행해야 하는데, 리코일 스토어의 데이터만 갱신되고 로직이 재실행되지 않아서 발생한 문제였다. 문제와 원인은 확인하였지만 이미 지나온 길이 너무 멀어 수정하지 못했던 것이 마음에 걸린다 (변명하자면 직접 개발한 부분이 아니기에 더욱...)
4. Atomic 디자인 패턴과 개발
이번 서비스를 위해 두가지 프로젝트를 동시에 개발하였는데, 사내에서 디자인 시스템이라고 부르는 단순 버튼이나 라디오 버튼, 모달, 팝업 등의 컴포넌트 프로젝트와 사용자에게 보여질 서비스 프로젝트 이렇게 두가지였다. 디자인 시스템은 npm private 리포지토리에 배포하고 서비스 프로젝트에서 가져다 쓰는, 무난한 방식이다.
컴포넌트 구성 요소를 4개 레벨로 나누어 진행하는 Atomic 패턴은 어디서 유행이라도 시켰는지 다들 최신 기술로 인지하고 사용해보고 싶어했다. 하지만 개인적으로는 굉장히 탐탁치 않게 보았는데, 그 우려하던 문제가 이 프로젝트를 진행하며 절실하게 다가왔다. 디자인 시스템 프로젝트를 개발할 때에는 공통된 부분이 많고 그 구성 요소를 재활용하는 일이 많아 그럭저럭 굴러갔지만 서비스 프로젝트를 진행하다 보니 아무런 의미 없이 4개 요소의 디렉토리만 늘어나기 시작했다. 당연했다. 이미 재활용할 수 있는 컴포넌트는 디자인 시스템으로 분리하여 개발했고, 사용자에게 보여지는 부분에서는 컴포넌트의 재활용이 거의 일어나지 않는다. 사용자의 구매 여정에는 중복된 요소가 있을 이유도, 필요도 없기 때문이다. 이렇게 점점 늘어난 재활용의 가능성도 없는 컴포넌트 디렉토리는 너무나 많아졌고, 프로젝트 개발이 종료된 이 시점에서는 뭐가 어디에 있는지조차 알 수 없게 되어버렸다. 스토리북을 작성했음에도 내가 아닌 다른 개발자가 이 프로젝트를 이어받는다면 컴포넌트 재활용의 가능성은 완전히 없어질 것이라 생각한다.
패턴을 잘못 사용했다거나, 분류를 잘못했다는 둥 이야기가 나올 수 있다는 것은 안다. 하지만 확실한 것은 페이지 구성 요소를 구조화하는,일반적인 컴포넌트 Structure 패턴을 이용했다면 이런 고민도, 해결책도 필요가 없었을 것이다. 차라리 서비스 프로젝트는 일반적인 컴포넌트 Structure 패턴으로 개발하고 자주 사용되는 컴포넌트가 생긴다면 상위 레벨로 끌어올려 재활용할 수 있도록 하는 방향이 더 낫지 않을까? 이전 회사에서 이미 그렇게 개발하여 잘 진행하였고, 퇴직한 이후에도 잘 굴러가고 있는 프로젝트를 보니 더욱 고집을 부리고 싶어진다.
5. hashbang을 이용한 팝업 처리
하이브리드 웹으로 노출되는 프로젝트이다 보니 '뒤로 가기'에 대한 고민이 필요했다. 다른 프로젝트에서는 모두 backkey에 대한 이벤트 핸들링을 통해 처리하였지만 아무래도 이벤트 처리이다 보니 관리가 어려웠던 모양이다. 이 프로젝트에서는 해시뱅을 이용하여 '뒤로 가기' 동작시 열렸던 모달, 팝업 등을 닫을 수 있게 개발했다. 상당히 좋은 아이디어였다. URL 변경에 따라 화면에 무엇인가를 노출하거나 감추면 되기 때문에 동작이 명확하고, 브라우저의 기본 동작과도 꽤 잘 어울렸다. 하지만 이 URL을 변경한다는 점이 생각하지 못했던 까다로운 문제를 야기했다. 글을 읽으며 바로 떠올릴 수 있는 것은 새로고침일 것이다. 새로고침을 하면 보여지던 모달/팝업이 다시 노출되는게 맞을까?
또, JS로 직접 window.location.hash를 변경하니 이상한 오류가 발생했던 경험도 있다. 이것은 Next/Router와의 충돌로 보이는데, Rotuer 객체 내에서 관리하는 해시와 실제 주소창에 노출되는 해시가 불일치하는지 모달/팝업이 나타났다 사라지는 등 예측하지 못한 이상한 문제가 발생했다. 결과적으로는 Next/Router 사용시 직접 location 객체를 컨트롤하지 않고 모두 Router 객체로 컨트롤하여 해결할 수 있었다. 하지만 여기저기 널려있는 Router.replace 구문을 보니 꽤 신경쓰이기는 한다. 다른 개발자도 같은 실수를 할 것 같고...
6. 튜닝
늘 그렇듯 무엇인가 변경이 있으면 모두가 욕을 먹는다. 이 프로젝트는 그 중에서도 꽤나 많이 먹은 편인데, 원래 네이티브로 제공되던 페이지가 하이브리드로 전환되었기 때문이다. 당연하게도 노출이나 반응이 느려졌고, 사용자뿐 아니라 사내에서도 이걸 왜 한거냐라는 식의 반응이 많았다. 어찌되었건 이런 상황에서 시작한 튜닝은 꽤나 골치아팠다. '느리다'는 것은 너무 다양한 문제가 복합적으로 연결되는 문제였기 때문이다. Next.js에 힘입어 SSR은 자연스럽게 지원하고 있지만, SPA의 고질적인 문제인 JS 파싱 -> Hydration 과정이 느린 것이 문제였다. 고질적인 문제이자 한계점이기 때문에 어느정도 감안해야 했지만 이상할 정도로 느렸다. 그래서 크게 두 부분으로 나누어 진행했다.
첫번째는 FE측면에서의 번들 체크다.
꽤나 급하게 개발되었기 때문에 미처 체크하지 못했던, 사용하지 않거나 제외할 수 있는 라이브러리를 떼어내고 통합했으며, 가장 큰 용량을 차지하고 있던 AWS SDK 라이브러리의 버전을 올림으로서 상당한 용량을 줄여낼 수 있었다. 이곳은 AWS의 Cognito 자격 증명과 Kinesis를 이용하여 클라이언트 로그(사용자 통계)를 쌓는데, 기존 사용하던 라이브러리는 AWS-SDK v2로 각 기능별 분리가 되어있지 않아 기능을 두개 사용하는데도 엄청나게 큰 용량을 차지했다. 이를 v3로 올리고 그에 따라 변한 코드를 재작성하였다(대충 작업하다 발생한 장애는 덤).
또, lighthouse나 Performance 툴을 이용해 발견한 각종 수치를 줄여나갔다. 하나씩 확인하고 처리하는 일종의 노가다기에 딱히 할 말은 없다. 하지만 화면의 높이 등을 이용해 계산하는 배너가 있었는데, 이 배너의 높이를 고정 픽셀값으로 바꾸니 생각보다 큰 차이가 있었다. CLS (레이아웃 이동)로 구분되는 값들은 대체로 사용자에게 노출되는 속도에 큰 영향이 없었는데, 이번 경우에는 꽤나 큰 영향을 줬다는 점이 눈여겨볼만 했다. 이런 다양한 작업을 통해 결과적으로 Sentry Performance를 통해 LCP, SpeedIndex등 성능 지표의 응답 속도가 30%가량 감소한 것을 확인할 수 있었다. 상당히 고무적인 결과다.
두번째는 백엔드-API 측면에서의 체크였다.
먼저 API중 가장 느리고, 응답이 불안정한 '추천' 영역을 SSR에서 제외했다. UI 상에서도 가장 하단에 노출되기 때문에 큰 무리가 없었다. 또, 어찌된 이유인지 API 비동기 처리를 위한 Promise.all 구문에서 빠져있던 API도 확인하여 추가했다. 여기서 약간의 트릭을 사용하였는데, 실패한 경우 사용자에게 노출할 필요가 없는 API는 로직 상단에서 catch 구문을 걸어둔 것이다. 아주 간단하지만 타입 처리에 아주 곤란한 Promise.settle을 피할 수 있었다.
튜닝에 끝은 없지만, 이 작업들을 통해 위에서 말했듯 Sentry Performance로 모니터링 해보았을 떄 충분히 '사용할만한' 상태로 끌어올리는데 성공했다. 추가로 몰랐던 문제를 제보받아 알게 되었는데, IOS의 스와이프백을 지원하지 않는 경우 웹뷰를 닫을 수 없다는 문제였다.
IOS를 사용하지 않아 몰랐던 것인데, IOS에서 스와이프백을 지원하지 않는 경우 웹뷰가 느려지거나 락이 걸렸을 때 창을 닫을 수 있는 방법은 스와이프백뿐이다. 이건 앱에서 스와이프 백을 지원하지 않게 설정한 경우 상당히 골치아픈 문제로 이어지는데, 왼쪽 상단 '창 닫기' 버튼의 동작이 JS로 작동되므로 JS가 파싱-실행되기 전에는 웹뷰를 닫을 수 없다는 것이다. 안드로이드는 하드웨어 백키가 있어 자연스럽게 닫게 되지만 IOS는 앱을 강제로 종료하거나 작동될때까지 기다려야 한다는 점이 굉장한 불만 요소로 들어왔다. 항상 '정상적인' 인터넷 상태임을 보장할 수가 없으니까.
이번 프로젝트에서는 객실 정보를 열 때 새 창으로 띄운다던지, 모달이 있는 경우 스와이프백을 사용하지 못하게 한다던지 하는 꽤나 많은 작업 끝에 앱에서 스와이프백을 지원하도록 변경하긴 했지만, 기본적으로는 최상단 '닫기' 버튼이 있는 영역은 앱에서 직접 관리하는 것이 맞는 것으로 보인다. 웹뷰만을 띄운다면 이번 케이스처럼 인터넷이 매우 느린 상황에서 웹뷰가 로드중일 때 닫을 수 있는 방법은 스와이프백 뿐일테니.
마치며
갑자기 맡게된 프로젝트였지만 어찌되었건 잘 마무리 지었고, 최종적으로는 평가도 나쁘지 않았다. 숙소에 대한 정말 많은 데이터를 노출하는 작업으로 비즈니스 로직에 가까운 용어나 처리 방식에 대해서도 익숙해졌다고 생각한다. 무엇보다 React-Next.js로 하나의 상업 서비스 프로젝트를 마무리 지었다는 것이 큰 도움이 될 것이라는 생각이 든다. 그런데 자꾸 '난 할만큼 했어'라는 생각이 드는건 왜일까?
'ETC' 카테고리의 다른 글
내 문장이 그렇게 이상한가요? 를 읽고 (0) | 2022.11.15 |
---|---|
이벤트 대응을 위해 인스턴스 개수 늘리기(Scale in/out) (0) | 2022.09.26 |
함수형 프로그래밍에 대해 (0) | 2022.02.26 |
전략과 전술 (0) | 2022.01.29 |
책임과 기술, 그리고 오버엔지니어링 (0) | 2022.01.26 |