본문 바로가기

dev-log

20230728

이직 후 한달차 개발자의 짧은 회고

개발

- '리액트' 라는 라이브러리는 참 대단하다. '크로스 플랫폼'을 대응할 수 있는 추상화된 라이브러리로서 최고인 듯 하다. 그러나 리액트 '네이티브'는 어렵다. 다른 '플랫폼'에서 같은 '리액트' 코드를 100% 같이 쓸 수 있는 것은 아니다. 그나마 비즈니스 로직과 상태 관리에 대한 내 관점이 뚜렷하고 그 관점이 협업을 하면서 생산성에 도움이 되었기 때문에 '참을 만 하다.' 한 달간 기존 코드베이스에서 유지보수가 아니라 코드 해석이 상당히 어려운 플로우를 리팩토링한 성과와 교통정리를 열심히 했다. 이 성과는 앞으로 고객 분들이 찾아와 주실 때 성능과 반응으로 체감이 될 것 같아 기대하고 있다. 내가 세웠던 리팩토링의 기준은 2가지이다. <1> 리액트를 어느정도 알고있는 개발자가 이해하기 쉬운 수준으로 간결하게 짤 것. <2> 도메인에 종속되는 값과 그렇지 않은 값을 분리하여 컴포넌트를 설계할 것. <1>은 나와 CTO님이 하나의 코드베이스로 웹과 네이티브 모두를 다루어야 하는 기술적 챌린지에 도전을 하는 과정인지라 ReactDOM에 익숙한 사람에게 이해에 무리가 없어야 한다는 추가적인 기준을 세웠다.

 

어려웠던 점: 익숙하지 않은 네이티브 환경의 네비게이션

- 우리 팀은 꽤나 멋지게 추상화된 라이브러리 하나를 사용하고 있다. 추상화된 네비게이션으로 NextJS 프로덕트와 ReactNative 프로덕트 모두를 대응하기 위해서 네이티브 네비게이션을 PATH으로 적용하여야만 한다. 여기까지는 괜찮다. 딥링킹 지원도 자연스럽게 되고, 리소스를 표현하는 방법은 웹개발을 했던 사람에게 더 친숙하니까. 그런데 네이티브 환경에서는 어떤 기준으로 웹의 페이지와 같은 스크린을 그리는 것일까? 나는  네이티브 개발 경험이 없는 터라 이 문제에 대해서 Worst Practice가 무엇인지 모른다. 

- 어떤 페이지스러운 스크린 A가있다고 치자. A는 그 전에 있는 스크린 B의 CTA같은 버튼을 눌러서 마운트가 된다. A는 위에 헤더가 있으며 헤더의 Back 버튼을 누르면 A에서 B로 바뀌어야 한다. 그리고 A에서의 인터렉션 결과로 B에 특정한 값을 전달해서 B 화면을 업데이트를 시키거나 비즈니스로직을 조작해야한다.

- A를 구현하는 방법은 크게 페이지화를 시키거나 뷰 레이어 위에 겹치는 컴포넌트로 만들 수 있다. 둘 중에 정답은 없지만 나는 리팩토링을 할 때 전자에서 후자로 바꾸었다. 그 이유는 다음과 같다. <1> 기획단에서 네이티브 네비게이션에 대해 반드시 적용해야 한다는 요구사항이 없었다. <2> A에서 B로 넘어갈 때 업데이트되는 값으로 비즈니스로직을 전개하기 위해서는 불필요한 useEffect을 써야만 한다. A와 똑같은 파일을 비즈니스 로직의 입맛에 맞춰서 중복해서 생성하여 로직을 관리하는 것보다 useEffect으로 로직을 관리하는게 더 싼 부채다. 그렇지만 useEffect으로 비즈니스 로직을 업데이트하는 것 자체가 불필요하지 않는가? A는 이벤트의 결과로 마운트되고 A 내부 이벤트의 결과로 언마운트된다면, 이 컴포넌트를 마운트하기 위해 필요한 값과 로직의 결과를 받을 수 있는 함수만 순수하게 전달해주면 된다고 생각한다. 물론 후자로 바꿀 경우 <1> 네이티브 네비게이션에 달려있는 Header를 똑같이 구현해야 하는 무시무시한 트레이드 오프가 있을 것이다. 뷰의 일관성이 조금 안맞는다고 그게 뭐 대수냐 생각할 수 있지만 디자이너의 입장에서는 그게 또 아니니까. 그래서 지금 하고 있는 일에 대해서 디자이너에게 최대한 말씀을 드렸고, 다행히 같이 봐주신 덕분에 '완벽하지는 않아도 비슷한' AppBar를 만들 수 있었다. 웹에서도 동일한 스펙으로 돌아가기 때문에 괜찮다고 생각한다 ㅎㅎ(꼭 그렇지는 않겠다. tabIndex와 관련된 이슈가 있을 수 있겠구나.) 아. AppBar와 같은 디자인 시스템을 이제 적용해야겠네.

- 내가 리팩토링을 세운 기준과 별개로 다른 프로덕트는 어떤 기준으로 네비게이션이 되는 컴포넌트를 세우는 걸까? 네이티브를 빡세게 다루는 팀에 속해서 이를 경험해보고 싶다.

 

리액트를 어느정도 아는 개발자가 이해하기 쉬운 흐름을 만드려면

- ContainerComponent와 PresentationalComponent의 관계를 다시 한 번 생각해보게 된다. 모든 컴포넌트를 저 두 개로 완벽하게 양분화할 수는 없겠지만, 로직을 담아놓은 어떤 큰 싱글턴 객체를 만들고 그 객체가 조작하는 값을 담은 Store을 만들고 두 개를 프로바이더로 나눠서 값과 로직을 분리하는 작업은 짱이다. 한동안 Flux를 안쓰고 (사실 비즈니스 로직을 만드는 일이 별로 없었다..) 있었다 보니까 오랜만에 다시 한 번 이 패턴의 간결함이 주는 위대함에 감탄을 했었다 ㅎㅎ

 

백엔드와 프론트엔드의 개발자는 서로 신뢰하되 코드는 방어적으로 짜는게 좋다

- 위에서 말한 Store는 어떤 Form의 Payload으로 전달될 수도 있다. 이 Store에 담긴 값은 정말 많은 경우에 따라 다이나믹하게 바뀔 수 있다. 더 좋은 UX가 있을 수록 DX가 깨진다(GOOD UX OFTEN NECESSITATES BAD DX)는 말을 들어본 적이 있는가? SuperTypeSafetyLibrary가 모든 UX를 반영할 수는 없다. 결국 어디에선가 타협을 해야하는 지점이 반드시 들어온다. 리팩토링을 하면서 그 지점에 대한 기준을 세워볼 수 있었다. 뭐 비록 Form에 한정되는 것이긴 하지만.. 타입이 깨져보여도 전반적인 플로우의 흐름은 더 단단해지는 경험을 했었다.

- 그 기준이 뭐냐면, FormState는 넓게, Payload는 미친듯이 좁게 가져가는 것이다. 물론 처음부터 좁게 가져가면 Best다. 그러나 마땅한 초기값을 정할 수 없다면 필연적으로 Nullable 또는 Nullish 또는 빈 스트링과 같은 값을 집어넣게 된다. 이 초기값에 대한 기준은 백엔드와 원만하게 커뮤니케이션을 하여 정할 수 있다.

  <1> 특정 상황에 따라 Optional하게 보내야한다. 즉, T | undefined는 undefined가 괜찮은 경험을 주었다.

  <2> 특정 상황에 따라 Null을 보내야 한다. 즉, T | null은 null이 괜찮은 경험을 주었다.

  <3> 특정 상황에 따라 Optional 또는 Null을 보내야한다. 즉 T | undefined | null은 반드시 커뮤니케이션으로 잡고 가여야만 한다. Nullish는 최대한 지양하는 것이 좋겠다.

- 페이로드의 타입이 너무나도 복잡한 나머지 이것저것 타입을 신경쓰느라 워킹데이 5일 중 3일 이상을 설계에만 신경을 쓴다고 하면 다음과 같은 방법을 적용해보면 좋을 것 같다.

  <1> 일단 최대한 FormState을 넓게 가져간다.

  <2> zod와 같은 런타임 밸리데이션 라이브러리를 가져간다.

  <3> 스크린에서 필요한 값들을 <2>에서 만든 스키마를 적용해서 TypeSafety하게 파싱한다.

  <4> 비즈니스 플로우에 따라 달라지는 페이로드를 일련의 단위별로 묶어서 Transform한다. 이때 반드시 스키마로 런타임 밸리데이션을 체크한다. Transform하는 로직에는 반드시 주석을 달자. 어차피 any-script에 가까운 다이나믹한 폼 데이터라면 주석이라도 달아서 비즈니스 플로우를 빠르게 이해하도록 넛지를 주는게 중요하다. 

  <5> Client에 흘려 보낸다.

 

- 이 방법은 FormState가 다이나믹 할 때 쓰기에 적합하다고 생각한다. RHF와 같은 라이브러리랑은 잘 맞지도 않는다. 근데 이러한 UX의 경우 RHF가 더 맞지 않을 수 있다. 타입스크립트의 Limitation 때문에 모든 케이스를 정적으로 만들 수는 없기 때문이다. 뭐.. 런타임 타입 체크가 가능한 언어를 사용하면 이런 문제는 애초에 날라가지 않을까 싶다(?)