Trouble shooting 일지 - 광고 플랫폼 app 편 #1-1

이유미·2022년 9월 28일
0

github repository
https://github.com/ymStudyLog/ad-platform-dashboard-ym

< react 전역 상태란 >

일반적으로 react에서 데이터는 부모로부터 props를 통해 자식까지 순차적으로 폭포처럼 아래로 전달된다. 그래서 컴포넌트들이 많이 나누어질수록 prop-drilling 현상이 일어날 수 밖에 없다. (참고한 사이트 : https://edykim.com/ko/post/prop-drilling/)

prop-drilling은 props(property) 내리꽂기라고 하는데 말그대로 react 컴포넌트 트리에서 데이터(event handler나 state같은...)를 원하는 위치까지 내리꽂아 전달하는 과정을 말한다.

이렇게 되면 여러 컴포넌트에 데이터가 전달되는데, 전혀 관련이 없는 컴포넌트가 데이터 이동 길목으로 사용 될 경우 가독성이 떨어지고, 코드 리팩토링이 힘들어진다.

이런 문제들을 해결하기 위해 생긴 것이 전역 상태 관리 개념이다. state를 모아둔 컨테이너 역할을 하는 것이 store이다. 이렇게 되면 어떤 컴포넌트에서도 store에 접근해서 필요한 데이터를 가져다가 사용할 수 있게 된다. prop-drilling이 필요하지 않게 되는 것이다.

< recoil 사용은 적절한 선택이었나? >

(Context API 와 redux, recoil의 기본 개념은 알고 있기 때문에 에러 원인과 관련이 있을 것 같은 내용을 집중적으로 공부했다)

일단 전역 상태 관리 라이브러리는 사용하려는 상황에 맞게 골라 사용해야 하는데 보통 아래와 같다.

  1. Context API를 사용해야 할 때
  • 단순히 prop-drilling을 피하는 것이 목적일 경우
  • react 컴포넌트 트리 안에서 전역 데이터를 공유해야 할 때
  1. redux를 사용해야 할 때
  • UI(레이아웃)와 분리하여 복잡한 상태 관리 로직을 다루어야 할 때
  • redux의 미들웨어 기능이 필요할 때
  • 비동기 작업으로 인해 액션 전달시 추가로 다른 값을 주거나 다른 작업이 필요할 때
  • 거대한 코드베이스를 여러 사람이 작업할 때

-> (2022.10.19) redux 공식문서를 바탕으로 업데이트 :
아예 공식문서에서 스스로의 필요가 아닌 이상 절대로 그냥(혹은 누군가 사용해야 된다고 했다고) redux를 사용하지 말라고 언급했다. 아래 3가지는 redux를 사용하는 간단한 예시이다.
-app을 사용하면서 상당한 양의 데이터가 계속 변경 될 경우(꽤 많은 양의 데이터를 의미하는 듯)
-내가 관리하는 state를 변경하는 방법이 딱 하나의 절대적인 방법만 존재하기를 원할 경우(reducer의 필요성)
-최상위 컴포넌트에서 모든 상태를 관리하려니 힘들 경우(state가 너무 많거나, 컴포넌트 단계가 깊어지거나 등등)

여기까지만 보면 간단한 프로젝트를 구현할 때 전역 상태 관리가 필요할 경우에는 Context API를 사용하는 것이 적합하다는 것을 알 수 있다. => 간단한 프로젝트라면 전역 상태 관리가 필요하지 않다.
하지만 이것만 보고 '이번 프로젝트에 Context API를 사용했어야 하나' 라는 생각을 하기 전에 추가로 고려해야하는 중요한 부분이 있다.
Context를 사용하면 컴포넌트 재사용이 어려워진다. 단순히 prop-drilling을 대체하기 위함이라면 Context 사용보다는 컴포넌트 합성이 더 간단한 해결책일 수 있다. 또한 특정 컴포넌트 단위에서 상태로 관리될 수 있는지도 먼저 확실히 해야한다.(전역 상태 관리를 하기 전에 이 방법이 꼭 필요한지 확실하게 짚어야 한다는 뜻) 이 과정 없이 무분별하게 전역 상태 관리 라이브러리를 적용하면, 본인이 마주친 에러들처럼 원인을 알 수 없는 에러를 발생시킬 수도 있다.

  1. 그렇다면 recoil은 언제 사용해야 할까?
    recoil이 왜 등장했는지를 알면 예상하기 쉽다.

    prop-drilling이 일어날 수 밖에 없는 react의 특징 때문에 react 프로젝트의 대부분은 MVC 아키텍쳐 구조로 설계하고 있었다. 하지만 프로젝트 규모가 커지면 상태도 많아지기 때문에 MVC 패턴으로는 단순한 상태 변화도 예측가능한 범위를 벗어나 관리가 힘들어지게 되었다.(ft. facebook)

    이것을 해결하기 위해 새로 등장한 것이 Flux 아키텍쳐이다. Flux 아키텍처의 특징은 데이터의 단방향 흐름인데, 이 Flux 아키텍쳐를 바탕으로 만든게 redux이다.

    redux 라이브러리로 상태 관리 흐름이 단방향으로 이루어져서 사이드 이펙트가 없어지게 되었지만, 단순한 전역 상태 관리에도 초기 세팅에 많은 코드가 필요하고 코드 작성이 어렵다는 단점이 있다. 또한 react에 최적화된 라이브러리가 아니었다고 한다. https://dev.to/g_abud/why-i-quit-redux-1knl

    그렇게 탄생한게 recoil이다. 페이스북은 recoil이 react를 위한 상태관리 라이브러리라고 했다. recoil은 react의 hooks와 비슷하여 redux보다 사용하기 쉬워졌다.

  2. atom과 selector

    • atom : recoil의 기본, redux의 store와 유사한 개념이다.
      atom() 메서드는 변경가능한 recoil state object를 반환하고, unique키에 각각 지정된 state value가 저장되어 모여있다고 생각하면 된다.
      redux에서 state는 읽기 전용 값(중요한 redux의 특징 중 하나임) 이기 때문에 상태 변경시 리듀서로 새로운 상태를 생성해야 하지만, recoil의 state는 그 자체로 읽고 변경이 가능 하다. useState와 사용방법이 유사한 useRecoilState로 그것이 가능함. (값을 읽기만 가능한 useRecoilValue, 변경만 가능한 useSetRecoilState, state를 default로 리셋하는 useResetRecoilState도 있어서 목적에 맞게 골라 사용하면 됨)

    • selector : 다른 selector나 atom의 데이터를 가져다가 다른 데이터를 만들어 리턴하는 역할. 비동기 로직을 처리할 수 있으나 읽기 전용 값이라 외부에서 구독하여 상태를 변경할 수 없다.
      다른 selector나 atom의 값을 get 해와서 이 값을 바탕으로 다른 state를 만들고, 이렇게 파생된 state를 다른 selector나 atom에 set으로 저장하는 방식으로 동작한다.
      = 비동기 작업을 하고 싶을 때 selector를 사용하면 된다.

    (여기에서 react의 Suspense가 등장한다. recoil의 selector로 비동기 처리를 하고 있을 때 렌더링할 데이터가 도달하기 이전인 loading 상태를 보여줄 UI가 없을 경우 Suspense가 이것을 지원해주기 때문이다. 이 Suspense가 본격적으로 등장한 것은 react v18부터 이기 때문에 react v18의 업데이트 내용에 대한 것도 공부가 필요해 보인다.)

  3. redux vs recoil

개인적인 의견은 좀 더 클린하고 일관성 있는 코드를 작성하기 위해서 선언형 프로그래밍을 지향해야 되고 => 그러려면 UI와 비즈니스 로직을 분리해야 하고 => react에서는 이 비즈니스 로직을 react hooks의 형태를 따라 작성하는 것이 좋다고 생각한다. 그래서 recoil이 좀 더 react와 사용하기에 적합하다고 생각한다.

결국 recoil 라이브러리 선택은 옳았는가?에 대한 본인의 생각은 옳지 않았다. 단순한 토이 프로젝트와 다름없는 작은 규모의 app이기 때문에 recoil 사용법을 익혔다는데에 의의를 둬야 할 것 같다.

< 광고 플랫폼 app 수정 방향 >

기존에는 data fetching이 recoil의 selector()에서도 이루어지고, A,B 두 페이지에서도 중복으로 이루어지고 있다.
동일한 작업이 반복되는 것도 일차적으로 잘못됐지만, 결국 에러의 근본적인 원인은 recoil을 사용해서 비동기 작업을 하는 로직을 전부 잘못 짰기 때문이다. 그래서 recoil에 대해 다시 공부해보니 '내가 전역으로 관리하려고 했던 state 값이 반드시 전역 관리가 필요한가?'에 대한 고민을 해보지 않았다는 문제가 추가되었다.

원인을 파악했으니
1) 먼저 RecoilRoot 컴포넌트와 recoil을 사용할 컴포넌트 사이에 react의 Suspense 컴포넌트를 넣어서 비동기 작업 응답이 올때까지 보여줄 컴포넌트를 지정해주고, (여기에 추가로 쭉 다른 프로젝트에서도 활용 가능한 나만의 커스텀 로딩 컴포넌트를 만든다)

2) 기존 store에 있는 state가 반드시 전역관리가 필요한 것인지 먼저 고민해 본 후,
=> 기존 store에는 database에서 각각의 데이터를 통째로 get request로 불러와서 전역 상태에 저장해뒀다. 하지만 app의 구성 상 드롭다운 메뉴에서 선택된 기간 or 광고 상태에 따른 일부 데이터만이 필요하기 때문에 전체 data를 불러와서 전역 상태에 저장할 필요가 없다. 전부 필요없는 로직이기 때문에 삭제한다.

3) 전역 상태 관리가 필요한 것만 남기고 다른 state는 컴포넌트끼리 props로 넘겨주는 방식으로 바꾸는 것을 중심으로 수정했다.
=> 그렇다면 드롭다운에서 사용자에 의해 선택된 데이터가 중요한데, 사용자 입장에서 고민해보니 프로그램을 종료해도 이전에 보던 내용이 기억되었다가 그래도 로드되는게 편해보였다. 그래서 이 선택된 드롭다운 데이터를 localStorage에 저장하기로 결정했다.
=> dashboard의 드롭다운 아이템을 만들 때 database에 저장된 data 중 가장 오래된 날짜, 가장 최근 날짜를 가져와서 이 사이를 7일씩 나눠야했다. 그냥 dashboard(A 페이지) 컴포넌트에서 이 데이터를 작업해도 되지만, 그러면 페이지를 이동할 때마다 이 값들이 날아가고 다시 불러와지는 작업이 반복되기 때문에 프로그램이 처음 실행될때 한번만 그 값을 가져오면 된다고 생각했다. 그래서 data 중 가장 오래된 날짜, 가장 최근 날짜 두개를 전역 상태로 관리하기로 정했다.

위 계획대로 수정하고 나니 나머지 로직 수정은 어렵지 않았다. 드롭다운 아이템을 만드는 로직을 따로 custom hooks으로 만들고, 각 페이지에서 localStorage 값을 확인하게끔 만들었는데 localStorage 확인하는 부분에서 또다시 에러가 생겼다. 다음 글에서 이어쓰겠다.

profile
신입 프론트엔드 개발자 구직중

0개의 댓글