React query와 context api

정지현·2024년 5월 20일

React

목록 보기
4/8
post-thumbnail

저번까지 React-query가 왜 좋은지, 그리고 지금 실험용 코드들을 올리는 Next14 App router에서 React-query를 써도 괜찮은지에 대해서 알아 보았었다. 이제 사용만 하면 괜찮은데, 여기서 재발견하고 싶은 것은 React-query가 하나의 컴포넌트 단을 얼마나 편하게 사용할 수 있게 하는지이다.

1. React query는 간편한다.

import { useState, useEffect } from 'react';
import { PokeDataType } from '../types';

export default function useQuery(url: string) {
  const [data, setData] = useState<PokeDataType>(
    Object.create(null) as PokeDataType,
  );
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const abortControl = new AbortController();

    const handleFetch = async () => {
      setData(Object.create(null) as PokeDataType);
      setIsLoading(true);
      setError(null);

      try {
        const res = await fetch(url, { signal: abortControl.signal });

        if (res.ok === false) {
          throw new Error(`A network error occurred.`);
        }

        setTimeout(async () => {
          const json = await res.json();
          setData(json);
          setIsLoading(false);
        }, 1000);
      } catch (e: any) {
        if (e.message.includes('abort')) {
          return;
        }
        setError(e.message);
        setIsLoading(false);
      }
    };

    handleFetch();

    return () => {
      if (abortControl) {
        console.log('abort', abortControl.signal, url);
        abortControl.abort();
      }
    };
  }, [url]);

  return { data, isLoading, error };
}

요 코드가 React query를 사용했을때 얼마나 더 간편해질까? 하는 것이다.

import { useQuery } from '@tanstack/react-query';
import { PokeDataType } from '../types';

const fetchPokemonById = async (id: number, signal: AbortSignal) => {
  const result = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`, {
    signal,
  });
  const json = await result.json();
  return json;
};

// I can see how many codes I saved using React query! Compared with useQuery hook. And this data can be used by anywhere from my app.
export const usePokemon = (id: number) =>
  useQuery<PokeDataType, Error>({
    queryKey: ['pokemon', id],
    queryFn: ({ signal }) => fetchPokemonById(id, signal),
  });

똑같은 형태를 라이브러리 하나를 썼을 뿐이지만, 아주 간편해지는 것을 확인할 수 있다.
무엇보다 여기에는 캐싱 기능과 각종 추가 유틸리티 펑션들이 있으니 감안해야할 것이 굉장히 줄어든다!

다만 한가지 맹점이 있는데,

2. 하지만 생각해봐야할 문제가 있다.

'use client';

import dynamic from 'next/dynamic';
import { useState } from 'react';
import './styles/index.css';
import { usePokemon } from './quries';

const ButtonGroup = dynamic(() => import('./components/ButtonGroup'), {
  ssr: false,
});
const PokemonCard = dynamic(() => import('./components/PokemonCard'), {
  ssr: false,
});

export default function UseWhyReactQuery() {
  // But using react query emerges another problem. I need to lift up my loading and error handler to handle data type check.
  // good time to consider using context.
  const [id, setId] = useState<number>(1);
  const { data: pokemon, isLoading, error } = usePokemon(id);

  return (
    <main className="main">
      <section className="card-container absolute-center">
        <PokemonCard error={error} isLoading={isLoading} data={pokemon} />
        <ButtonGroup handleSetId={setId} />
      </section>
    </main>
  );
}

해당 코드는 id 스테이트를 다른 컴포넌트에서도 쓸 목적으로 작성된 것이다.
로딩과 에러 케이스를 그리는 것은 아래 PokemonCard인데, 이 상황에 대해서 타입스크립트는 pokemon 데이터에 대해서 undefined가 잡히지 않았다고 에러를 준다!

그냥 type 을 Type | undefined로 바꾸면 되지 않을까?

아니면 id를 로딩과 에러 핸들링을 위로 올리는것이 좋을까? 하지만 SOLID하지 않게 되는건 아닐까?

글로벌 스토어를 만드는건 어떨까? 하지만 id가 글로벌하게 써야할 정도로 상위에 존재하는 값일까?

여기에 대한 대답은 개발자 한명한명이 다를 것이라고 생각한다. 하지만 여기서는 지금까지 생각하지 못했던 방식으로 한번 풀어보고 싶다.

3. React-query의 개발자 TKDodo는 어떻게 생각할까?

이러한 문제들에 대해서 TKDodo는 대부분의 경우 TS가 맞다고 하며 이러한 문제는 Implicit dependency라고 정의한다. 개발자의 머리 속에는 이 코드는 문제가 없지만, 그것이 앞으로는 그렇지 않을 수 있다는 것이다.

  1. 나 자신은 코드가 문제가 없다는 것을 알 수 있다. 하지만 같은 동료가 보기에는 문제가 될 수 있다. 또한 3개월 뒤의 내가 코드를 다시 보면 또 문제가 될 수 있다.

  2. 가장 위험한 것은 이 경우에 대해 잘 알지 못하는 상황에서 코드의 다른 부분이 수정되는 경우 문제가 될 수 있다는 것이다.
    예를 들어서 PokeCard 컴포넌트를 실수로 다른 곳에서 부른다고 생각해 보자. 캐싱된 데이터가 있으면 괜찮겠지만, 그게 아니라면 충분히 문제가 될 수 있지 않을까?

3-1. 확실하지 않은(implicit) 케이스를 확실하게(explicit) 만들자.

React Context는, 전에 확인한 바 대로 State를 컨트롤하는데 좋은 기능이 아니다. 하위 컴포넌트를 모두 리렌더 시킨다는 것을 확인한 바 있다.

여기서는 context를 zustand로 갈아끼우면서 같은 코드량에 엄청난 성능 향상을 얻었다고 게시한 글을 봐도 확인할 수 있다.

React Context는 Dependency injection에 유용한 기능이다. 그렇기때문에, id와 쿼리에서 제공하는 데이터를 컨텍스트로 모두 참조할 수 있게 한다면 그것이 최선일 수 있다.

3-2. 실제 컨텍스트로 만들어 보자.

import React, { createContext, useContext } from 'react';
import { PokeDataType } from '../types';
import { usePokemon } from '../queries';
import PokeCardLoading from '../components/PokeCardLoading';
import PokeCardError from '../components/PokeCardError';

type PokemonContextValue = PokeDataType;

const PokeContext = createContext<PokemonContextValue | null>(null);

export const usePokemonContext = () => {
  const context = useContext(PokeContext);
  if (!context) {
    throw new Error(
      'usePokemonContext must be used within a PokemonContextProvider',
    );
  }
  return context;
};

export const PokemonContextProvider = ({
  id,
  children,
}: {
  id: number;
  children: React.ReactNode;
}) => {
  const usePokemonQuery = usePokemon(id);

  if (usePokemonQuery.isSuccess) {
    return (
      <PokeContext.Provider value={usePokemonQuery.data}>
        {children}
      </PokeContext.Provider>
    );
  }

  if (usePokemonQuery.isError) {
    return <PokeCardError error={usePokemonQuery.error} />;
  }

  return <PokeCardLoading />;
};

우선 가장 좋은점은 react-query가 내뱉는 리턴 밸류에는 항상 | undefined 타입이 따라오는데, 이것을 제거할 수 있다는 것이 가장 큰 장점이다.

다만, 내가 생각하는 단점은 확장성에 있어서 문제가 될 수 있다.

지금은 get만 구현되어 있으니까, crud를 모두 만들어서 컨텍스트 내부에서 참조한다고 생각해 보자.

하지만, 전에 알아 보았듯이 컨텍스트의 프로바이더는 render 시 계속해서 새로운 값으로 갱신되어 모든 자식의 rerender를 유발한다. 여기서는 useMemo를 써서 데이터가 명확한 레퍼런스를 받을 경우 바뀔 수 있게 권고하지만...

해당 코드에서는 로딩 스테이트, 에러 스테이트 시 해당 훅을 사용하지 않기 때문에, 리액트 훅의 기본 사용 원칙에 어긋나게 된다.

그렇다면 상위에 훅을 선언해보면 어떨까?

그렇다. 애초에 조건문으로 데이터가 valid한지를 체크하기 때문에 다시 react-query의 데이터는 | undefined를 가지게 된다!

3-3. 작성자분의 말씀.

Nothing is without drawbacks, and neither is this technique. Specifically, it might create network waterfalls, because your component tree will stop rendering (it "suspends") at the Provider, so child components won't be rendered and can't fire off network requests, even if they are unrelated.

I'd mostly consider this approach for data that is mandatory for my sub-tree: User information is a good example because we might not know what to render anyway without that data.

위 코드는 실제로 데이터가 성공적으로 페칭되지 않을때까지 실제 children을 렌더하지 않기 때문에, 우리가 구글 개발자 도구에서 보곤 하는 network waterfalls 현상을 일으킬 수 있다.

자신이 사용한다면 메인이 아니라 sub-tree에서 사용할 것이다. 유저 정보가 좋은 예시로 실제 데이터를 받기 전에는 무엇을 그릴 지 알 수 없기 때문이다.

안타깝지만, 역시 모든 상황을 대응할 수 있는 코드라는 건 아직까지 찾지 못했다. 하지만 여기에 대해 생각해보는 것만으로도 교육적이었다고 생각한다.

참고한 URL:

https://tkdodo.eu/blog/react-query-and-react-context
profile
Can an old dog learn new tricks?

0개의 댓글