[이마고웍스] Recoil을 사용한 전역 상태 관리 통합

Ji-Heon Park·2023년 6월 29일
4

Imagoworks

목록 보기
2/10

이마고웍스의 클라우드 서비스는 치아정보 파일을 업로드 하고, 웹에서 치아 3d 파일을 확인할 수 있는 클라우드 서비스입니다. 클라우드팀에서 프론트엔드 최적화 및 코드 리팩토링 업무를 진행하며 상태 관리툴을 Recoil로 통합 및 전환하는 업무를 하였습니다.

Recoil이란?

Recoil lets you create a data-flow graph that flows from atoms (shared state) through selectors (pure functions) and down into your React components.

Recoil은 페이스북에서 만든 상태관리 라이브러리로 useState를 사용하는 것만큼 사용이 간단하고 상태관리를 효과적으로 할 수 있게 도와줍니다.

모노레포 서비스에 Recoil 전환 배경

이전에는 여러 사람 or 팀이 작업하면서 ContextAPI, Redux, Recoil 여러 상태관리 툴이 사용되었습니다. 또는 React Query로 fetch한 데이터를 다시 리덕스나 리코일로 불필요하게 감싸지는 곳도 있었습니다. 이로 인해 코드의 복잡도가 올라가 디버깅이 어려워지고 서버와 클라이언트의 상태 간 Source of Truth 문제가 있습니다.

이를 방지하기위해 서버의 데이터는 리액트 쿼리로, 온전한 클라이언트의 데이터는 Recoil로 통합 및 분리하였습니다.

Redux

Redux는 기본적인 store 구성을 위해 많은 보일러 플레이트와 장황한 코드를 작성해야 합니다. 또한 비동기 데이터 처리 또는 계산된 값 캐시와 같은 중요한 기능은 라이브러리의 기능이 아니며, 이를 해결하기 위해 또 다른 라이브러리를 사용해야 합니다.

ContextAPI

My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It's also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It's not ready to be used as a replacement for all Flux-like state propagation. -페이스북 엔지니어 Sebastian Markbage

React가 가진 상태 공유 솔루션인 Context API는 반복적이고 복잡한 업데이트에 사용할 경우 비효율적입니다. Context API는 데이터 서브셋을 대상으로 변경을 감지하고 업데이트할 수 없기 때문입니다. Provider 하위의 모든 consumer들은 Provider 속성이 변경될 때마다 다시 렌더링됩니다. memoization을 통해 일부 문제를 해결할 수 있지만, 근본적인 해결 방법이 아니며 한계가 있습니다.

Why Recoil ?

Recoil의 경우 기존 React와 비슷한 사용법으로 러닝커브가 낮고 코드가 단순합니다. 이미 hook을 사용하고 있는 사람들에게 익숙합니다. 또한 컴포넌트가 사용하는 데이터 조각만 사용할 수 있고, 계산된 selector를 선언할 수 있으며, 비동기 데이터 흐름을 위한 내장 솔루션까지 제공됩니다.

무엇보다 Facebook에서 발표한 React에 최적화된 React 전용 상태 관리 라이브러리 이기에 React와 개발 방향성이 같습니다. 동시성 모드(Concurrent Mode)를 비롯한 다른 새로운 React의 기능들과의 호환 가능성도 가집니다.

Concurrent Mode (동시성 모드)
흐름이 여러개가 존재하는 경우를 의미합니다. 리액트에서 알아서 렌더링 동작의 우선순위를 정하여 적절한 때에 렌더링을 해주는 것입니다.

전환 과정

모노레포로 이루어진 프로젝트를 전환의 시작점으로 하였습니다. 커스텀 훅이나 기능별 라이브러리 이외에도 공통적으로 사용되는 상태도 libs 폴더에서 관리하기로 했습니다. 아래 사진과 같이 Cloud Desktop과 Cloud Mobile이 같은 states 폴더에 의존하도록 합니다.

공식 데브툴의 부재

리덕스의 경우 개발자 도구를 사용하면 현재 스토어의 상태를 조회 할 수 있고 지금까지 어떤 액션들이 디스패치 되었는지, 그리고 액션에 따라 상태가 어떻게 변화했는지 확인 할 수 있는 반면에 Recoil은 공식적인 데브툴이 없습니다.

디버깅을 용이하게 하기 위해 useRecoilSnapshot을 사용하여 임시방편으로 상태의 스냅샷을 체크할 수 있는 컴포넌트를 만들었습니다:

import { useEffect } from 'react';
import { useRecoilSnapshot } from 'recoil';

const DebugObserver = () => {
  const snapshot = useRecoilSnapshot();

  useEffect(() => {
    if (process.env.NODE_ENV === 'development') {
      for (const node of snapshot.getNodes_UNSTABLE({ isModified: true })) {
        console.debug('[🛸Recoil]', node.key, snapshot.getLoadable(node).contents);
      }
    }
  }, [snapshot]);

  return null;
};

export default DebugObserver;

코드 측면의 설계

리코일은 상태 변경 로직을 언제 어디서든 쉽게 가져다 쓸 수 있는 것이 큰 장점입니다. 하지만 높은 자유도는 여러 사람이 협업하는 환경에서 단점이 될 수 있습니다.

리덕스의 경우 상태 변경 함수(리듀서)를 함께 적어 초기 상태와 상태 변경 로직을 한눈에 파악 가능합니다. 하지만 recoil에서는 atom으로 상태를 정의하고 해당 상태를 변경해주는 것은 변경이 필요한 부분에서 setRecoilState를 이용해서 언제든 변경할 수 있습니다.

As-Is는 위에서 말한 쉽게 찾아볼 수 있는 Recoil의 개념도 입니다. 상태의 직접적인 접근과 변경을 지양하고 중간 레이어인 hooks를 거치도록 레이어를 설계하였습니다. 전역상태의 무분별한 변경으로 인해 디버깅과 예측이 힘들어지는 것을 방지하고, 상태 변경 로직을 한 곳에 모아두기 위함입니다.

폴더 구조는 다음과 같습니다:

src
├── bottomSnackbar
│   └── atoms.ts
│   └── selectors.ts
│   └── useBottomSnackbar.ts
│
└── globalDialogue
    └── atoms.ts
    └── selectors.ts
    └── useGlobalDialogueStates.ts
    ...

진행 상황 및 후기

본 글의 내용을 요약한 초안을 작성하여 담당자분들께 피드백을 받은 후 작업을 시작하였습니다.

전역 상태의 리팩토링이기에 어떤 버그나 사이드 이펙트를 마주할지 몰라 부분적으로 전환 작업을 하고 있습니다. 다른 프론트엔드 팀원분과 함께하여 현재 60%정도 전환을 할 수 있었습니다.

아직 큰 이슈나 장애가 없었고 성능 역시 별다른 문제가 없음을 확인했습니다. Recoil로 전환을 하며 동일한 성능을 유지함과 동시에 눈에 띄게 적어진 코드량과 낮은 러닝커브 덕분에 생산성을 올려주는것만 으로 충분한 이점이 있었다고 생각합니다.

모노레포에서의 전환이 성공적으로 완료되면 추후 Back Office, landing page 등 다른 레포에도 이와 같은 전환 및 통합 작업을 할 예정입니다.

profile
Frontend Developer | 기록되지 않은 것은 기억되지 않는다

0개의 댓글

관련 채용 정보