FE

ZEP을 Angular.js에서 Next.js로 마이그레이션하며

jvn4dev 2024. 8. 22. 01:47

3월 말정도부터 시작됐던 본격적인 ZEP 플레이화면의 마이그레이션 프로젝트를 진행하면서 느낀 점들을 회고해보려고 한다. 약 두달이 넘는 시간동안 프론트엔드팀 전부가 꽤 많은 시간을 크런치모드 상태로 지냈던 것 같다.

사실 나는 신입이였기에 이게 얼마나 힘든 일인지 몸으론 느꼈지만 머리로 가늠은 하지 못하고 있는 상태에서 그저께 회식에서 팀장님이 말씀해주시길 자기의 커리어상 가장 힘들고 긴 크런치모드였다고…한다.

크리티컬한 문제가 있을만한 이슈들은 쳐내고 마이그레이션 TF를 마무리하는 시기에 느끼는 부분으로는 정말 힘들고 짧지않은 시간이였지만 꽤나 큰 뿌듯함이 몰려온다. 짧은 시간 내에 변경된 코드를 파악하고 기능을 구현해야했어서 어려운 부분이 많았고 온전히 내 것으로 만들었다는 확신도 아직은 들지 않지만 최대한 지금까지 내가 이해한 내용을 정리해두려고 한다.

나는 설정을 맡는다.

우리 프론트엔드 팀원은 총 8명이다. 이번 팀원 모두가 TF를 시작하기 3개월전쯤부터 우리의 임무로 3명의 팀원은 마이그레이션 프로젝트의 뼈대를 만들고 나머지(나 포함)는 기존 레거시 프로젝트의 추가되는 프로덕트 기능을 추가하는 것이였다.

그렇게 때가되어 프론트엔드 팀 전원이 마이그레이션에 투입이 되었고, 우리는 ZEP에 PLAY화면을 구성하는 부분을 총 8개로 나누어 한명씩 맡기로 했다. 우리의 미션은 주어진 두달안에 관련된 모든 기능을 오너쉽을 가지고 마무리할 것. 시간이 너무 없기에 팀장님까지 기능구현의 포함되어 각자가 자신이 구현해야할 기능에 책임감을 가지고 움직였어야했다.

나는 8개로 나누어진 지라의 컴포넌트에서 설정 컴포넌트를 담당하게 되었다. 처음 설정을 맡게되었을 때 조금의 막막함이 있었다. 설정이 기능상 스페이스의 거의 모든 부분에 관여되는 부분이기에 단순히 기능구현만이 대수가 아닐 것이라는 생각이 들었기 때문이다. 그래도 어찌하겠는가. 한번 부딫혀봐야지.

복잡하지만 명확했던 신규 프로젝트

처음 마이그레이션으로 신규 구성된 프로젝트 코드를 봤을 때 생각보다 이해가 되지 않아 힘들었다.

플레이화면은 결국 Phaser엔진이라는 웹게임엔진을 기반으로 만들어지기 때문에 플레이화면이 마운트되는 시점부터 생각보다 많은 부분들이 클래스기반의 OOP로 구성되어있었다.

개발을 유니티로 처음 접하여 OOP의 개념 자체는 이해하고 있었지만 웹개발을 시작한 뒤로 리액트 함수형 컴포넌트만 사용하다보니 클래스로 프로젝트가 구성되어있는 부분이 생각보다 쉽게 와닿지 않았다. 그래도 이 부분은 기존 레거시 코드와 개념은 공유하기에 생각보다 금방 이해할 수 있었다.

기존보다 정말 거대했던 클래스가 훨씬 구체적이고 명확한 클래스들로 나누어져서 인스턴스를 생성하니 마운트시점에 각각이 마운트되는 타이밍이 명확해져서 엄청난 이점이 생겼다.

이전에는 플레이화면이 마운트될 때 Phaser엔진에서 실행되는 게임이 웹의 정확한 마운트 시점을 알 수 없어 임의로 setTimeout으로 처리하는 등의 어려운 부분들이 존재했는데 이번에는 그런 걸 확실하게 잡고 가고자하는 의지가 설계에서 느껴졌다.

프로젝트에 사용된 스택으로는 다음과 같다.

Nx

반응형으로 설계된 모바일 웹과 별개로 현재 우리의 앱은 React-Native를 사용하고 있다. RN의 뼈대 위에 스페이스 진입 이후 대부분의 플레이화면은 웹뷰로 이루어져있기 때문에 웹과 공유되는 api모듈 및 공통컴포넌트가 많았다. 이를 해결하기 위해 Nx 모노레포를 활용하여 여러 리소스들을 하나의 레포에서 공유하고자 했다.

개인적으로 Nx는 처음 사용해봐서 개인적으로 좀 더 공부해보고 싶은 부분이다.

Nextjs

Nextjs가 지원하는 dynamic import를 적극적으로 활용하여 기존의 문제였던 번들사이즈를 줄여 빌드사이즈 및 첫 로딩 시간을 줄이고자 노력했다. ES2020에 새로 추가된 dynamic import는 앱에 필요한 모듈을 빌드타임이 아닌 런타임에 불러온다. 기존 Angularjs의 레거시 코드는 .Net Core 위에 Angularjs 코드로 이루어져있어 역시 일정부분 SSR의 효과를 누리고는 있었지만 커다란 번들링 사이즈를 갖고 있었기에 초기 로딩 속도가 빠르지 않았다. 우리의 서비스는 플레이화면에 입장한 후부터 실질적인 서비스의 진가를 느낄 수 있기에 빠른 초기로딩 속도가 관건이였다.

기존 레거시 프로젝트 중 정적페이지들은 이미 별도의 레포에 Nextjs로 마이그레이션(v2) 되어있는 상태였다. 기존 페이지들은 SEO가 중요했지만 플레이화면은 사실 유저가 직접적인 검색을 통해 입장하지 않기 때문에 초기 마운트시의 dynamic import ssr 설정은 꺼두었다.

이 외에도 모두가 최적화를 위해 노력하고 있다.

Mobx

기존에 나는 Redux만 경험해봤는데, 이번에 Mobx를 사용해보면서 많은 부분의 이점을 느꼈다. 프로젝트 자체도 대부분 클래스 구조로 이루어져 있었고 그와 비슷한 형태로 컴포넌트가 공유하고자하는 전역상태를 클래스 기반으로 관리하니 프로젝트와 어울리는 구조라는게 느껴졌다. 상태 클래스 안에서 getter와 setter를 전부 선언해두고 사용하여 별도의 reducer에 action을 dispatch해줄 필요없이 더 적은 코드량으로 상태를 관리하는 부분이 편하다는 생각이 들었다.

ReactQuery

ReactQuery는 정적페이지를 먼저 마이그레이션했던 프로젝트(v2)에 먼저 적용했었다. 이 때는 ReactQuery를 도입하여 별도의 가공없이 바로 사용하는 구조였기 때문에 컴포넌트에서 바로 query 및 mutation을 하도록 구현되어있었다.

하지만 이번 마이그레이션 프로젝트에서는 좀 더 명확하게 컴포넌트의 비지니스 로직을 분리할 수 있도록 각각의 컴포넌트 폴더 내 query 및 mutation 파일에서 ReactQuery를 한번 Wrapping한 커스텀훅을 가져와서 사용하도록 만들었다. 이렇게 설계하니 기존보다 이점으로 query 및 mutation에 대한 queryFn을 한번에 모아서 관리할 수 있다는 점과 로직을 별도로 분리하여 컴포넌트를 더 깔끔하고 명확하게 관리할 수 있는 점이 있었다.

아무래도 ReactQuery 자체가 스스로 데이터 캐싱 및 페칭을 잘해주지만 실시간성을 위해 유지했던 refetchOnWindowFocus 기능이 모바일 반응형 뷰에서는 windowFocus 이벤트가 발생하지 않아 한쪽에서 변경한 설정이 다른 모바일 유저가 설정 모달을 열어두고 있을 때 발생하지 않아 설정이 변경됨을 감지하는 패킷핸들러에서 refetch하도록 로직을 구현하였다.

react-hook-form & zod

내가 맡은 설정 컴포넌트에서는 ContextAPI를 적극적으로 사용하고 있다. 최상위 설정 모달을 감싸고 있는 Wrapper에서 하위 컴포넌트들에 필요할만한 것들(현재 선택된 탭, 설정 중인 스페이스의 hashId, 전역 isDirty값 등등)을 Context로 감싸 내려준다.

설정에는 두가지의 설정(스페이스 설정, 맵 설정)이 존재하고 그 설정들은 각각의 Wrapper를 갖게 되고 그 Wrapper에는 zod와 함께 생성된 스키마가 존재하게 된다. 각각의 스키마는 각자 다른 react-hook-form(이하 rhf)의 onSubmit을 갖기 때문에 최상위 Wrapper에서 유저가 선택한 탭을 기준으로 별도의 저장(Action) 버튼을 갖고 있는 Footer가 표시되도록 구현했다. 쉽게 얘기해서 스페이스 설정 중인 유저에게 표시되는 저장버튼은 스페이스 설정 스키마 form에 대한 onSubmit이 실행되고 맵 설정 중이라면 맵 설정 스키마에 대한 onSubmit이 처리된다.

rhf의 스키마 컨텍스트에 대한 isDirty는 useForm을 처음 사용할 때 props로 전달한 defaultValues를 기준으로 결정한다. 저장 버튼이 해당 form이 isDirty일 때만 활성화되도록 구현하기 위해 스페이스에 진입하여 플레이화면이 마운트될 때 해당 스페이스 및 맵이 갖고 있는 Mobx Store 설정 옵션들을 form의 초기값(defaultValues)으로 넣어주고 useEffect로 해당 스페이스에 대한 설정들을 다시 가져왔을 때 form을 reset 해주도록 처리했다.

이후에 스키마의 초기값을 async함수가 비동기적으로 리턴하게 하여 초기값을 설정하는 방법을 알아냈다. 사실 이미 스페이스와 맵의 설정값은 마운트시 갖고 있기 때문에 큰 이득이 있을 것이라는 생각이 들진 않지만 지금과 비교해보기위해 해당 방법으로 리팩터링 해볼 예정이다.

Packet을 통한 소켓통신

우리 서비스는 기본적으로 웹게임을 기반에 둔 실시간 서비스이기 때문에 서버와 패킷을 활용하여 소켓통신을 하도록 구현되어있다. 채팅과 게임은 물론이고 내가 맡은 설정의 경우 또한 그러하다.

정책적인 기획 당시 스페이스 설정의 경우 저장 이후 실시간 반영이 아닌 새로고침 이후 적용되는 것이 원칙이였고 맵 설정의 경우 저장이 되면 특정 패킷을 통해 스페이스 내 모든 유저에게 실시간으로 변경사항이 적용되어야했다. 이후 스페이스 설정 내의 몇가지 기능도 실시간 반영으로 변경되긴 했지만 기본적인 구현 방식으로는, 설정이 저장되고 api로 서버가 요청을 받게되면 저장 후 저장 및 변경되었음을 전파하는 패킷을 클라이언트로 쏴주게된다. 클라이언트 내 패킷핸들러에서 해당 패킷을 수신했다면 앱이 마운트됐을 때 행했던 해당 맵에 대한 설정을 다시 한번 set하여 모두에게 실시간으로 반영되도록 구현했다.

Typescript

사실 대부분의 JS기반의 서비스들은 이제 TS가 표준이 된만큼 중요한 부분일 것이다. 기존의 레거시 프로젝트 또한 TS로 구성되어있었지만 꽤 많은 부분이 any 타입으로 지정되어 있었다. 프로토타입으로 시장에서 검증해야한다는 이유로 많은 기능들을 빠르게 붙이기 위해 많은 부분에서 TS를 사용하는 이점을 얻지 못했고 이후 쌓이게된 코드들에는 많은 기술부채가 존재하는 상황이였다.

새로 마이그레이션을 진행하는만큼 코어부터 type-safe하게 설계하여 우리 팀은 최대한 any를 지양하기로 했다.

SCSS

SCSS는 CSS 파일로 변환되기까지 전처리 과정이 필요하지만 그만큼 큰 이점을 가져다준다고 생각한다. 최대한 각자의 컴포넌트에서 활용할 수 있도록 공통 컴포넌트를 만들어두기도 했고 그에 따른 CSS 네이밍 등 모듈화에 대한 이점이 크다고 생각한다. 이는 사실 이전에 postCSS를 사용했을 때도 있던 기능이지만 외에도 많은 이점이 있다.

SCSS 파일 내에서 네스팅을 통해 컴포넌트 구조에 따른 가독성 높은 구성을 가질 수 있고, 특히 이번 프로젝트에서는 css 또한 재사용성을 높이기 위해 mixin을 적극적으로 활용하여 사용하고 있다. mixin을 활용하면 함수와 같이 param에 defaultValue를 지정하거나 param을 지정하여 사용할 수 있다.

개인적으로 힘들었던 점

이번 마이그레이션을 진행하면서 단순히 코드이해 외의 어려움도 많았다.

  • 짧은 스프린트 기간 산정
  • 기획의 부재에 따른 커뮤니케이션 비용 증가
  • 서버개발자와의 소통

스프린트를 진행하는 기간 자체가 짧다보니 기획이 부재하는 경우가 너무 많았고 이는 프론트엔드 개발을 구현하고 있는 과정에서 구멍난 정책을 다시 잡고 가야하는 경우가 너무 많았다.

우선 마이그레이션 기간의 최대 목표는 최대한 기존 라이브의 기능을 그대로 옮겨오는 것이 주된 목표였지만, 설정쪽은 아예 기획부터 디자인이 변경되었기에 기존에 동작하던대로 옮기는대도 한계가 있었다. 그런 부분이 발견될 때마다 기획적으로 디자인적으로 소통하는데에 비용이 생각 이상으로 들어갔다.

현재 서버개발쪽에도 리소스가 현저히 부족한 상황이기 때문에 일시적인 현상이라고 생각하지만 모종의 이유로 적용된 서버쪽 변경점이 클라이언트쪽에 공유가 되지 않아 이미 기능 개발이 완료되어 qa로 넘어간 이슈가 다시 클라이언트 이슈로 재확인요청되는 경우 또한 많았다. 이미 구현된 기능의 문제점을 파악하고 변경점 또한 클라이언트쪽에서 찾아 해당 히스토리에 대해 소통해야 했기에 여기서도 꽤 많은 비용이 발생했다.

우선 프론트엔드팀과 서버팀의 밸런스를 맞추기위해 적어도 1:1 비율은 맞춰야한다고 생각한다. 예외적으로 서버리소스가 부족해서 발생하는 이슈들도 많아 모두가 이해하고 있고 이는 차차 서버쪽 팀원들이 충원되면서 자연스레 해결될 문제라고 생각한다.

개선을 위해 노력하려는 방향

모두가 이번 마이그레이션에 대한 리소스를 많이 들였던만큼 다함께 회고를 진행하게 된다면 다음 내용들을 건의해보려고 한다.

  • 이슈에 대한 기획 강화 및 기능 명세 작성
  • 스프린트 기간 산정 객관화
  • 테스트코드 도입

이번 짧지않은 스프린트를 진행하면서 부족했던 부분인만큼 기획을 강화하고 그에 따른 기능명세서가 확실해졌으면 좋겠다. 많은 시간을 기획을 정하는데 사용해버리니 막상 개발쪽 설계가 미흡해졌다. 이는 결국 기술부채로 이어지고 다른 동료들에게 안좋은 영향을 주게되어 서비스의 큰 문제가 된다는 생각이 들었다. 단순히 개발 실력을 늘리기 위함이 아닌 프로덕트 자체를 위해서도 이가 필요하다는 생각이 든다.

개발을 진행하면서 테스트코드 또한 도입해보고 싶다는 생각이 들었다. 마이그레이션을 진행하기 직전 스터디중이였던 리팩터링2를 읽으면서 테스트코드의 중요성을 느끼고 있었고 실제로 테스트 코드의 필요성을 느끼기도 했다.

단순히 빌드로 빌드에러만 잡아내는 것이 아닌 정확한 기능 명세에 따른 설계와 테스트코드가 합쳐졌을 때 진정한 힘을 발휘할 수 있다고 생각한다. 개인적인 올해 목표로 우리 프로젝트에 테스트코드를 도입하는데 크게 기여해보고 싶다는 생각을 했다. 우리도 리팩터링 책의 내용처럼 (커밋 - 빌드 - 테스트)를 할 수 있는 시기가 빨리 왔으면 좋겠다!

회고를 마치며…

개인적으로 뿌듯하면서도 아쉬운 점이 많았던 것 같다.

동료가 작성한 코드를 이해하고 내 생각을 코드로 구현하고 싶었다. 2년차가 되면서 단순히 기능 구현만이 아닌 문제가 생겼을 때 해당 문제에 대해 삽질도 해보면서 이를 해결하고 싶다는 생각도 많이 들었다.

이처럼 개인적인 욕심들을 하나하나 채우려다보니 막상 쳐내지 못하는 이슈가 쌓이는 것들을 보고 멘탈도 많이 흔들렸지만 굳건하게 정신차리고 속도감 있게 잘해냈다는 생각도 들었다.

우리팀 모두 성공적인 라이브배포를 위해 끝까지 긴장감을 늦추지 않고 잘 해내고 싶다.

젭 프론트엔드 팀 화이팅 젭팀 모두 화이팅!