smob_logo
smob_logo
header-image
FRONTENDreact-query 도입 여정Smob 프로젝트 React-query 도입 여정2022.09.28

apollo-client는 종합적인 상태 관리 라이브러리라고 공식 문서에서 설명하고 있습니다. GraphQL 쿼리 결과를 local, normalized, in-memory cache에 저장하여 네트워크 request 최소화한다는 전략을 가지고 있으며, makeVar, useReactiveVar를 사용해서 local state을 관리하거나, useQuery, useMutation등의Hooks를 사용해서 server state까지 관리 할 수 있는 포괄적인 라이브러리라고 볼 수 있습니다.

기존 프로젝트

  1. local state 관리

    local state: 폼 입력 데이터, 모달창 상태 등 사용자가 제어하는 데이터

    local state를 관리하기 위해서 apollo에서는 Field PoliciesReactive Variables를 사용 할 수 있습니다. Field Policies에 GraphQL 서버 schema에 따른 쿼리가 아닌 local에서만 사용할 수 있는 쿼리를 정의 할 수 있습니다.

    const todosVar: ReactiveVar<Todos> = makeVar<Todos>([]);
    const userVar: ReactiveVar<User> = makeVar<User | null>(null);
    
    const QUERY_USER = gql`
      query user {
        user @client // local-only field
      }
    `;
    
    const client = new ApolloClient({
      cache: new InMemoryCache({
        typePolicies: {
          Query: {
            fields: {
              todos: {
                read() {
                  return todosVar(); // ①
                },
              },
              user: {
                read(_, { storage }) {
                  const token = globalThis?.localStorage?.getItem('token');
                  if (!token) return;
                  if (!storage.var) {
                    // ②
                    storage.var = userVar;
                    client.query({ query: QUERY_USER }).then(({ user }) => storage.var(user));
                  }
                  return storage.var();
                },
              },
            },
          },
        },
      }),
    });

    apollo-client를 recoil과 비교하면 makeVaratom, field policy는 selector라고 생각하면 쉬울것 같습니다.

    const todos = useReactiveVar(todoVar);

    위와 같이 useReactiveVar를 사용해서 상태값을 사용하거나 ①번과 같이 local-field query를 정의해서 원하는 데이터를 select해서 사용할 수도 있습니다.

    그리고 ②번과 같이 selector처럼 사용한다면 비동기 통신을 수행하고 데이터를 store하는 코드를 분리 할 수도 있지만, recoil과는 다르게 field policy가 seletor처럼 어떠한 단위로 나누어지지 않기 때문에 만약 더 많은 쿼리들이 존재한다면 마치 redux처럼 store가 비대해지는 것과 같은 문제를 발생시킬 수도 있습니다. 따라서 이 방식은 상황에 맞게 사용해야 할 필요가 있어 보이며, 이 방식은 사실 공식문서에는 나오지 않고 권장되는 방식은 아닌 것 같습니다.

    그렇다면 field policy를 잘 사용 할 수 있는 방법은 단순히 cached field customizingreadmerge function을 이용해서 원하는 값을 select하는게 최선이라 생각됩니다. 하지만 추가되는 데이터에 대해서는 merge function을 통해서 수동으로 병합시켜야 하는 것도 상당히 번거로운 작업입니다.

    그래서 요약하자면 apollo-client를 이용한 local state를 관리의 문제점은 단순한 값을 저장하고 사용하기엔 문제가 없지만, field policy를 활용해서 더 좋은 코드의 구조를 만드는데는 어려움이 있다는 점입니다.

  2. server state 관리

    server state: 서버에서 불러오는 데이터. 사용자가 제어할 수 없는 데이터

    apollo-client의 가장 큰 장점은 비대한 boilerplate가 필요없고, 여러가지 비동기 통신에 대한 상태를 관리 할 필요가 없다는 점입니다. useQuery, useMutation 등의 Hooks는 loading Handling이 쉽고, onCompleted나 onError 콜백을 통해서 간단하게 상태에 대한 UI를 처리할 수 있습니다.

    하지만 apollo-client는 cache를 다루기가 어렵습니다. 비동기 작업에 대한 결과를 자동으로 정규화해서 cache 관리할 수 있도록 하지만, 그 이후 데이터가 바뀌었거나, 최신 데이터인지 아닌지를 판별하기 위한 또 다른 추가적인 작업이 있어야 하기 때문입니다. 그리고 사용하지 않는 cache 데이터를 수동으로 garbage collect해야 하는 부분도 아쉬운 부분입니다. 실제로 저희 서비스 같은 경우 고객이 실시간으로 이용하고 있는 서비스의 기록을 보여주는 기능 또한 있기 때문에 최신 상태의 데이터를 관리해야 하는 기능을 구현하기엔 조금 아쉬운 부분이었습니다.

    또한, 프로젝트가 react18 버전으로 업그레이드 되면서 저희 프로젝트에 <Suspense>를 도입하기로 결정하였는데 apollo-client는 아직 suspense를 정식 지원하지 않고 있었습니다. 빠르게 변화하는 프론트엔드 생태계에 맞춰 라이브러리도 그에 맞게 빠르게 대응되는지도 중요한 부분이라고 생각했습니다. server-state를 관리하는 부분은 그만큼 서비스적으로나 개발적으로 중요한 부분이라고 생각되었기 때문입니다.

react-query를 도입하면서 개선된 부분

  1. apollo vs react-query

    react-query의 장점에 대해서는 이 페이지를 참고하시면 쉽게 알 수 있습니다. 그 중 기존 프로젝트와 비교했을때 가장 큰 장점은 cache를 직접 다루지 않고 react-query에 많은 부분을 위임할 수 있다는 점입니다.

    기본적으로 react-query는 설정된 staleTime을 기준으로 cache된 데이터가 오래된지를 판단합니다. 그리고 몇가지 상황에 따라 백그라운드에서 자동으로 데이터를 다시 가져옵니다. 또한, 기본적으로 5분으로 설정된 cacheTime을 기준으로 “inactive”된 데이터에 대해 garbage collection을 수행합니다.

    이러한 기능들로 기존에 제대로 관리되지 못하던 메모리나 server-state의 신뢰성에 대한 고민들을 해결 할 수 있었습니다. react-query의 cache에 대한 설명은 공식문서를 통해 더 자세히 보실 수 있습니다.

  2. 코드 구조와 colocate

    프로젝트를 진행하면서 여러가지 페이지와 컴포넌트를 어떻게 나누고 유지보수하기 좋은 구조는 어떤 것일지에 대해서 항상 고민이 있었습니다. 이번에 react-query를 도입하면서 저희는 page를 기준으로 하는colocation 방식으로 구조를 변경하였습니다.

    colocation: 코드를 사용 가능성이 있는 곳에 가깝게 위치시키는 방법입니다.

    -pages -
      users -
      hooks -
      useUser.ts -
      components -
      Component.ts -
      index.page.tsx -
      tickets -
      hooks -
      useTickets.ts -
      components -
      Component.ts -
      [id].page.tsx;

    useQuery, useMutation을 모두 custom hooks로 만들어 사용함으로써 다음과 같은 장점들을 가질 수 있었습니다.

    1. 각각의 도메인에 따른 custom hooks를 사용함으로 page component-level에서 비동기 호출을 하도록 하여 colocate 하여 구조를 단순화 시켰습니다.
    2. custom hook에서 queryKey를 관리하고, 연관된 수정이나 데이터 변환에 관련된 로직도 위임하여 관련된 수정 사항이 있을 경우 한 곳에서 수행 할 수 있도록 하였습니다.
    3. select기능이나 useMemo, useCallback을 이용해서 최적화하기에도 용이합니다.
#react-query
#react
avatar
By. 스몹이안녕하세요. 잘 부탁 드립니다.