useSuspenseQuery가 무한으로 요청을 즐긴다.

Jemin·2023년 11월 21일
4

트러블슈팅

목록 보기
8/11
post-thumbnail

서론

선언형 컴포넌트로 수정하는 작업을 하면서 useQueryuseSuspenseQuery로 함수를 변경했을 때 무한 요청을 보내는 문제가 발생했다.

기존의 코드를 좀 살펴봤는데 기존 코드부터가 문제가 있었다.

// 검색 컴포넌트
const SearchForm = () => {
    const { refetch } = useGetSearchCenters();

    return (
        <div>
        ...
        </div>
    );
};

export default SearchForm;

// 검색 결과 컴포넌트
const SearchResult = () => {
    const { data } = useGetSearchCenters();

    return (
        <>
            {data &&
                data.data.data.map((center) => (
                    <CenterItem key={center.id} center={center} />
                ))}
        </>
    );
};

export default SearchResult;

이렇게 필요한 컴포넌트마다 useQuery를 호출하면 데이터를 가져와서 사용할 수 있겠다. 라는 1차원적인 생각으로 코드를 작성했던 모양이다.

useGetSearchCenter 훅은 다음과 같은데 검색 완료시 조회를 하기 때문에 enabled 옵션을 주었고 사용자가 입력한 입력값은 전역 상태로 관리하고 있다.

export const useGetSearchCenters = () => {
    const { page, perPage } = useSearchStateStore((state) => ({
        page: state.page,
        perPage: state.perPage,
    }));
  
    const { data, refetch } = useQuery({
        queryKey: ["getSearchCenters"],
        queryFn: () => centerAPI.getSearchCenters(page, perPage),
        enabled: false,
    });

    return {
        data,
        refetch,
    };
};

잘못된 코드임에도 잘 동작하는 이유는 그저 같은 호출을 2번하는게 전부이기 때문인데 어째서 useSuspenseQuery로 변경하면 무한으로 요청을 보내는지 궁금해졌다.

Suspense의 이해

근본적인 문제를 보기 위해 좀 더 상위 컴포넌트의 시점에서 보자.

const Home = () => {
    return (
        <section>
        	<SearchForm /> // 비동기 작업 호출 컴포넌트
            <QueryErrorResetBoundary>
                {({ reset }) => (
                    <ErrorBoundary onReset={reset} fallbackRender={FallbackUI}>
                        <Suspense fallback={<Loading />}>
                            <SearchResult />
                        </Suspense>
                    </ErrorBoundary>
                )}
            </QueryErrorResetBoundary>
        </section>
    );
};

export default Home;

분명 SearchForm컴포넌트에서 useSuspenseQuery를 호출하는데 렌더링되는 부분은 SearchResult컴포넌트이기에 로딩이 필요한 SearchResult컴포넌트만 Suspense컴포넌트 안에 넣어뒀다.

Suspense의 동작 원리는 다음과 같다.

  1. Suspense를 사용하려면 Suspense컴포넌트를 사용하여 Suspense 경계를 설정한다.

  2. 코드 스플리팅이나 데이터 로딩과 같은 비동기 작업이 시작되면 React는 Suspense 경계를 만날 때까지 계속 진행한다.

  3. Suspense 경계에 진입하면 React는 fallback prop으로 지정된 로딩 중인 UI를 렌더링한다.

  4. 비동기 작업이 완료되면 React는 Suspense 경계 내에서 작업이 완료됐을을 감지하고 로딩 중인 UI 대신에 실제 컴포넌트나 데이터를 렌더링한다.

즉, SearchForm컴포넌트의 Suspense 경계가 없기 때문에 상위 컴포넌트에 영향을 주고, 해당 컴포넌트가 다시 렌더링되어 또 다시 비동기 작업이 시작되는 상황이다.

이제 정확한 원인을 알았으니 변경하는 작업에 들어가보자.

해결

SearchForm 컴포넌트에서 refetch 함수를 사용하기 위해 useQuery를 호출하는데 이를 처리할 Suspense컴포넌트가 없기에 발생하는 문제라 비동기 호출을 다른 방식을 사용하기로 했다.

const SearchForm = () => {
    const queryClient = useQueryClient();

    const handleSearch = () => {
        queryClient.fetchQuery({ queryKey: [queryKeys.getSearchCenters] });
    };

    return (
        <div>
        ...
        </div>
    );
};

export default SearchForm;

Query Client에서 Query Key를 사용해 다른 컴포넌트에서 fetch를 호출하는 방식으로 변경했다. 이제 useQuery함수 자체를 호출하는 것은 아니기 때문에 Suspense로 인한 문제가 발생하지 않을 것이다.

추가적인 삽질

기존에는 useQueryenabled 옵션을 false로 주었기 때문에 검색 버튼을 누르기 전에 아무것도 렌더링되지 않았다.

// 기존의 useQuery 함수
const { data } = useQuery({
        queryKey: [queryKeys.getSearchCenters],
        queryFn: () => centerAPI.getSearchCenters(page, perPage),
  		enabled: false,
    });

useSuspenseQuery를 적용하고도 같은 동작을 기대했지만, 컴포넌트가 마운트된 이후 바로 비동기 작업이 시작되었다.

// useSuspenseQuery로 변경
const { data } = useSuspenseQuery({
        queryKey: [queryKeys.getSearchCenters],
        queryFn: () => centerAPI.getSearchCenters(page, perPage),
    });

queryFn에 넘겨지는 파라미터가 상태로 관리되면서 발생하는 문제인줄 알고 계속 원인을 찾았지만 매번 그렇듯이 답은 공식문서에 있었다.

useSuspenseQuery함수에서는 enabled 옵션을 지원하지 않는다.
아무래도 Suspense에 대한 이해도가 부족함과 동시에 공식문서를 제대로 읽어보지 않은 나를 탓할 수 밖에 없었다.

마무리

공식문서를 잘 읽어보자.

profile
경험은 일어난 무엇이 아니라, 그 일어난 일로 무엇을 하느냐이다.

2개의 댓글

comment-user-thumbnail
2024년 2월 20일

안녕하세요. 지나가다 보고 댓글 남깁니다. 해당 동작은 서버 사이드에서 react-query에서 쿼리 클라이언트를 찾지 못해 발생하는 버그 같습니다. 관련된 이슈 링크도 남깁니다. https://github.com/TanStack/query/issues/6116

1개의 답글