리디북스 프론트엔드 웹을 개발하는 한준희라고 합니다. 처음 입사해 직접 코드를 봤을 때 무척 놀랐어요. 10년이나 된 서비스라 오래된 코드도 많았거든요. 그걸 새롭게 바꾸려 한 시도 역시 지층처럼 고스란히 쌓여있었고요. 프론트엔드 기술 스택을 하나로 통합하는 React 리팩토링 프로젝트, 지금부터 소개합니다.
리액트에 진심인 개발자 영상 시리즈 ▼
프론트엔드 기술 통합
React 리팩토링
프론트엔드 기술 스택을 하나로 통합하는 “리팩토링(refactoring) 프로젝트”를 시작했습니다. 핵심 목적은 생산성과 유지보수성이었어요. 디자인은 그대로 유지하지만, 구현은 완전히 새로 개발하는 것이 골자였습니다.
리액트를 리팩토링하는 것이 가장 효율적이라고 판단했어요. 리액트가 기존 리디북스에서 가장 높은 비중을 차지하고 있었기 때문입니다. 기존에는 여러 프론트엔드 팀이 각각의 도메인에 해당하는 프론트엔드를 운영했었습니다. 그래서 어떤 서비스는 아예 기술 스택이 다르기도 했고, 스택은 같지만 컨벤션이 전혀 다른 경우도 있었어요. 하지만 이번 기회로 리디의 전체 서비스 기술은 대동단결하게 되었습니다. 앱은 React-Native로, 웹은 React로 말이죠.
저를 포함해 총 5명의 프론트엔드 엔지니어가 프로젝트를 진행하고 있습니다. 당연히 비즈니스 피처 개발이 우선순위가 높기에, 피처 개발 약 60%, 리팩토링 40% 의 비중으로 운영하고 있어요. 새롭게 개발하거나 기존 페이지를 수정해야 하는 경우, 되도록 리팩토링도 같이 진행하려 하고요.
React 장단점?
jsx 문법과 component
처음 이런 프론트엔드 뷰 라이브러리를 접했던 건 앵귤러(Angular)였어요. 그때 접한 앵귤러의 문법은 서버사이드의 문법과 비슷하다고 느꼈는데요. 렌더와 액션을 처리하는 부분이 template와 controller로 나뉜 구조죠. 하지만 리액트는 jsx 문법으로, 한 파일 안에서 렌더와 액션 모두를 한눈에 보며 개발할 수 있어서 좋았어요. 특히 jsx 문법과 그 당시 리액트가 보여주었던 빠른 퍼포먼스에 반했던 기억이 나요.
반대로 리액트의 단점으로 생각하는 부분도 있는데, 지금은 많이 개선되었어요. 먼저, 타입스크립트가 없었을 때는 propTypes로 처리하다 보니 여러 가지로 불편했어요. 다행히 타입스크립트를 도입하고 나서는 타입 관련 문제들이 거의 해결되었습니다. 또, class component로 개발할 때는 라이프사이클 관리가 비교적 쉬웠는데요. 상태 변화에 따른 최적화, componentWillUpdate와 같은 메소드, (지금은 다른 함수로 대체되었지만) 컴포넌트 상태가 변경될 때 최적화에 손이 참 많이 갔었죠.
지금은 모두 function component로 개발하고 있습니다. 훅을 통해 거의 모든 걸 구현할 수 있어 코드를 간결하고 직관적으로 짤 수 있고, 커스텀 훅으로 재사용성도 높아지게 되었는데요. 하지만 더 복잡해진 면도 있다고 생각해요. useEffect를 사용하면서 말 그대로 사이드이펙트에서 한 번 더 고려해야 하고, 상태에 따른 최적화를 위해 useRef나 useMemo를 사용하니까요. 또 상태관리 라이브러리를 도입하고 SSR을 위해 Next.js를 도입하는 등 이것저것 필요한 게 많아지죠.
결국 리액트 하나만 가지고는 아무것도 못한다는 것. 그게 아이러니인 것 같아요.
우선순위에 따른
점진적 리팩토링
리디북스 웹서비스는 크게 네 가지 — 서점, 웹 뷰어, 내서재, 셀렉트 — 로 나눌 수 있습니다. 각 서비스는 마이크로서비스로, 도메인 및 레파지토리 자체가 분리되어 있어요. 이중 가장 비즈니스 우선순위가 높고 효율적인 페이지를 먼저 고려했어요. 특히 리디북스 서점 일부는 이미 react로 마이그레이션 되어 있기도 했거든요. 앞으로 비즈니스 요구사항이 많아질 페이지이기도 해서, 리디북스 서점 중 고객이 가장 접근하기 쉬운 페이지들을 리팩토링하기 시작했어요.
페이지 단위로 리팩토링을 하다보니 고려사항도 많았는데요. 예를 들어 같은 ridibooks.com 서점 안에서도 회원가입 페이지는 리팩토링된 서버로 트래픽을 돌리고, 로그인 페이지는 기존 PHP 서버로 트래픽을 돌려야 하는 상황이었죠.
여러 해결책이 있었지만, 저희는 SRE 팀의 도움으로 손쉽게 해결할 수 있었어요. 당시 SRE팀도 인프라 개선작업을 하고 있었는데, 그 일환으로 Cloudflare와 더불어 AWS AppMesh를 도입하였습니다. 단순히 URL 뿐만 아니라 header 값이나 그외 여러가지를 통해 트래픽 제어가 가능해졌죠. 덕분에 아주 복잡한 라우팅 룰을 손쉽게 처리할 수 있었어요.
다만 아쉬운 부분은 페이지에서 페이지로 이동할때, 어느 페이지가 리팩토링 되고 어느 페이지가 아직 PHP 로 서비스되는지 확정할 수 없다는 거예요. 그래서 리팩토링에서는 CSR를 배제하고 SSR만으로 서비스 하고 있어요. 다행히도 Next.js를 통해 수월하게 개발하고 있습니다.
기존 웹과 새로운 웹의
조화로운 작동법
1. 디렉터리 구조
리디북스 웹의 디렉토리 구조는 최대한 복잡하지 않게 하려고 했어요. 기본적으로 Next.js 기반이다 보니, pages 디렉토리를 중심으로 각 페이지가 디렉토리로 구성되어 있습니다. 또 도메인별로 API 통신을 위한 models·services 디렉토리와 redux Slice를 위한 features 디렉토리, 마지막으로 각 컴포넌트를 위한 components 디렉토리가 있어요.
2. API 상태 관리
기본적으로 redux 기반으로 상태 관리를 합니다. 여러 API 를 조합해 페이지를 구성하는 경우가 많아, 여러 API 를 저희 프로젝트 내에서 규격화할 수 있도록 model과 service 레이어를 구성하게끔 설계했어요. 클라이언트 사이드에서는 redux-toolkit의 createAsyncThunk액션과 slice를 만들어 통신하고 있어요.
3. 스타일 관리
스타일은 emotion을 사용하고 있어요. 리팩토링 대상페이지가 emotion을 사용 중이기도 했거든요. 리팩토링 과정에서 실제로 디자인 시안은 거의 존재하지 않았습니다. 따라서 실제 코드와 브라우저 스타일을 보며 작업해야 했고, 동시에 진행되다 보니 비슷한 페이지를 다른 사람이 작업하기도 했습니다. 여기서 발생할 수 있는 CSS 충돌 같은 작업은 emotion을 통해 완벽히 해결되었어요. 스타일 코드 자체도 재사용할 수 있어서 효율적으로 작업할 수 있었습니다.
4. 테스트 코드 작성
유닛테스트는 기본이라고 생각하고 리팩토링 프로젝트를 시작했어요. 이 프로젝트가 끝난다고 리팩토링 자체가 끝나는 건 아니니까요. 코드를 계속 개선하려면 테스트코드는 필수이자 기본이죠. 그래서 저희 팀 내부적인 목표는 커버리지를 100% 달성하는 겁니다. 물론 아직 달성하지 못했고, 특히 피처 개발처럼 일정이 결정된 경우 테스트코드를 못 짤 때도 있습니다. 그래도 매주 체크하며 조금씩이나마 확실한 100%를 향해 가고 있어요.
프로젝트 초반에는 테스트 코드로 react-test-renderer를 사용했었어요. 리액트 공식페이지에서도 가이드를 제공하는 터라 자연스럽게 사용하게 되었는데요. 쓰다 보니 불편한 점이 많아 최근에는 testing-library/react로 모두 전환했어요.
리팩토링 프로젝트에서는 일반 유닛테스트 뿐만 아니라 컴포넌트 테스트도 진행해요. 특히 엘리먼트를 찾는 부분에 있어서 react-test-renderer가 불편한 점이 많았어요. 찾는 엘리먼트가 없을 때 에러 발생도 신경써야 하고요. 또 저희가 emotion을 사용하고 있기 때문에 emotion으로 작성되는 엘리먼트를 접근할 때는 emotion이 생성한 wrapper가 있어 또한 react-test-renderer에서는 따로 처리해야 하는 불편함이 있었어요.
그러던 중, 리디 프론트엔드 세미나에서 추천 받아 testing-library/react 를 도입하게 되었어요. testing-library 도입 후 불편한 점이 많이 해결되었어요. 특히 redux 스토어를 동반한 테스트의 경우에도 testing-library/react 에서 제공해주는 것이 많아 비교적 쉽게 테스트 코드를 작성할 수 있었죠.
리디북스 웹 배포 프로세스
그리고 캐싱
리디북스 웹의 배포는 모두 자동화되어 있습니다. 개발자가 따로 뭔가를 하지 않아도, github의 master 브랜치로 코드가 병합되면 github action을 통해 AWS Codepipeline이 실행되면서 빌드 후 ECS의 blue-green 배포까지 진행됩니다. 물론 진행 과정은 모두 슬랙(Slaack)을 통해 알림과 공유가 되고요. 이런 인프라 세팅은 리디 SRE팀의 든든한 지원 하에 수월하게 할 수 있었습니다.
리디 SRE(Site Reliability Engineering)은 개발자가 만든 소프트웨어를 인프라 및 운영에 활용할 수 있도록 시스템을 구축하고 자동화하는 팀입니다. DevOps를 비롯하여 여러 업무를 수행합니다.
리팩토링을 하며 가장 많이 신경 쓴 것 중 하나가 ‘퍼포먼스’였는데요. 겉으로는 바뀐 게 없어야 하는 리팩토링이다보니, ‘성능이라도 끌어올려보자!’라는 생각으로 튜닝을 많이 했어요. 특히 메인 홈페이지는 고객이 가장 먼저 접하는 페이지기도 해서 더 신경썼는데요. 기존에도 퍼포먼스를 많이 고민한 흔적이 있더라고요.
그래서 나온 해결책이 캐싱(Caching)이었습니다. 기존에는 리액트와 Next.js를 serverless 형태로 구축해 rendering 용도로 사용하고, 결과물을 redis와 메모리에 캐시하는 방식이었는데요. 메모리에 캐시하다 보니 TTFB 는 정말 빨랐어요. 리팩토링 프로젝트에서도 적어도 기존 TTFB 정도의 속도는 나와야 했기에, 비슷한 방식으로 캐시를 사용했습니다. 하지만 기존과는 다르게 serverless 형태가 아니라 Next.js 서버가 직접 요청을 받아 캐싱된 렌더링 결과물에 요청마다 다르게 처리해야 하는 부분을 따로 처리하는 방식으로 커스터마이즈했습니다. 그 결과 TTFB는 거의 비슷한 수준까지 끌어올렸고, CSR 퍼포먼스는 오히려 더 향상시킬 수 있었어요.
개발 과정의 최대 난제
리디북스 서점 홈에서 ‘책’이란 아이템은 정말 다양하게 표현되는데요. 예를 들어 신간 섹션에서는 커버 이미지와 타이틀만 노출되지만, 추천 섹션에서는 커버와 캐치프레이즈가 노출됩니다. 리디북스 메인 홈페이지를 보시면 바로 이해하실 거예요.
책을 일반화하여 컴포넌트 하는 데 굉장한 공을 들였는데요. 전에도 책을 컴포넌트화하는 시도가 몇 번 있었어요. 디자인시스템을 통해 도출된 디자인 라이브러리도 있었고, 리엑트를 통해 구현된 라이브러리도 있었습니다. 그 코드들을 참고해 더 확장성 있고 사용하기 쉬운 책 컴포넌트를 설계했어요.
기본적으로 BookDefinition 컴포넌트를 이용해 책을 그리게 됩니다. 기존에 정의된 책에 대한 공통적인 정책, 가로형·세로형 등의 정의는 presets을 통해 재사용할 수 있어요. 동일한 책 컴포넌트의 정의를 공유하는 BookDefinitionContext를 통해 각 책의 세부 컴포넌트를 제어할 수 있습니다. 또 모든 컴포넌트는 modularComponent라는 타입으로 정의되는데, 이 타입은 컴포넌트 정의 시점에 설정된 option과 메소드를 통해 기존의 세부 컴포넌트를 커스터마이즈 할 수 있습니다. 최근 세어보니 책 1개를 구성하는 컴포넌트가 모두 합쳐 40개 가량 되더라고요.
제가 짠 코드를 언제나 개선해야 한다고 생각해요. 하지만 이미 배포된 코드를 고치는 건 여러 가지로 무서운 일이죠. 혹시라도 버그가 생기거나 사이드이펙트가 발생한다면 정말 끔찍하죠. 그래서 점점 리팩토링을 두려워하게 되는 것 같아요.
그래서 “리팩토링을 두려워하지 않는 환경”이 중요하다고 생각해요. 작게는 코드의 근거가 될 수 있는 테스트부터 시작해, 문서화·자동화 등 철저한 시스템과 환경이 필요합니다. 개발자가 리팩토링의 두려움을 잊고 언제나 더 나은 코드를 위해 도전할 수 있게 해주는 원동력이 아닐까 합니다.
고객과 발맞춰 새로운 콘텐츠 경험을 선보이는
리디와 함께할 당신을 기다립니다.