본문 바로가기
SPA/Next

[Next] Data Fetching에 대해 알아보기 (4) - useQuery

by J4J 2023. 3. 5.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 Data Fetching의 마지막인 useQuery에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

이전 글

 

[Next] Data Fetching에 대해 알아보기 (1) - CSR / SSR

[Next] Data Fetching에 대해 알아보기 (2) - SSG / Dynamic Routing

[Next] Data Fetching에 대해 알아보기 (3) - ISR

 

 

 

들어가기에 앞서

 

Next 공식문서에 따르면 Next의 Data Fetching의 종류는 다음과 같이 있습니다.

 

  • SSR
  • SSG
  • CSR
  • Dynamic Routes
  • ISR

 

 

 

공식문서에서 소개하고 있는 위의 Data Fetching들은 이전 글들에서 모두 다룬 상태입니다.

 

그리고 이번 글은 useQuery와 관련되어서 글을 적어보려고 합니다.

 

useQuery가 무엇인지에 대해서는 [React] React Query에 대한 소개와 사용 환경 설정를 참고해 주시길 바라며, 이번 내용은 Next의 Data Fetching과 같이 useQuery에도 SEO를 위해 prerendering 되는 방법을 공유해보고자 합니다.

 

 

 

useQuery 공식문서에 따르면 useQuery의 Data Fetching을 위해 SSR과 SSG의 사용을 추천하며 적용 방법으로는 다음과 같이 있습니다.

 

  • Initial Data
  • Hydration

 

 

반응형

 

 

useQuery - Initial Data

 

Inital Data는 기존 Data Fetching 방식을 사용하여 데이터를 가져온 뒤 useQuery의 initial Data에 초기 값으로 넣어주는 방식입니다.

 

간단한 예시로 다음과 같은 코드를 작성해볼 수 있습니다.

 

import axios, { AxiosResponse } from 'axios';
import { GetServerSideProps } from 'next';
import { useQuery } from 'react-query';

export const getServerSideProps: GetServerSideProps = async (context) => {
    try {
        const res = await axios.get('http://localhost:8080/product/no');

        return {
            props: {
                res: res.data,
            },
        };
    } catch (error) {
        return {
            notFound: true,
        };
    }
};

interface Props {
    res: AxiosResponse<any, any>;
}

const InitialData = (props: Props) => {
    const queryProductNo = () => {
        const res = useQuery(
            'productNo',
            async () => {
                const res = await axios.get('http://localhost:8080/product/no');
                return res.data;
            },
            {
                initialData: props.res,
            },
        );

        if (res.isLoading) return <div>Loading...</div>;
        if (res.data) {
            const productNos: number[] = res.data as unknown as number[];
            return (
                <div>
                    {productNos.map((productNo) => (
                        <div key={productNo}>
                            <h2>데이터 Fetch로 가져온 상품번호 : {productNo}</h2>
                        </div>
                    ))}
                </div>
            );
        }
    };
    return <>{queryProductNo()}</>;
};

export default InitialData;

 

 

 

코드를 작성한 뒤 서버를 실행해보면 다음과 같이 prerendering이 되어 Preview에 fetching 된 데이터들이 보이는 것을 확인할 수 있습니다.

 

initla data preview

 

 

 

 

또한 Response에서 실행된 html의 코드를 확인해보면 props에 fetching 된 결과로 만들어진 데이터들이 적재되어 사용되고 있는 것도 확인할 수 있습니다.

 

initial data response

 

 

 

useQuery - Hydration

 

useQuery에서는 hydration을 적용하기 위해 prefetchQuery를 제공해주고 있습니다.

 

hydration을 사용하게 되면 prefetchQuery를 통해 fetching된 데이터를 캐싱한 뒤 dehydrate를 해놓고 props를 통해 페이지에 전달하여 우리가 원하는 곳에 사용할 수 있게 도와줍니다.

 

hydration은 다음과 같이 사용해볼 수 있습니다.

 

 

 

[ 1. _app.tsx에 hydrate 등록 ]

 

import type { AppProps } from 'next/app';
import { useState } from 'react';
import { DehydratedState, Hydrate, QueryClient, QueryClientProvider } from 'react-query';

const App = ({ Component, pageProps }: AppProps<{ dehydratedState: DehydratedState }>) => {
    /**
     * queryClient
     */
    const [queryClient] = useState(() => new QueryClient());

    return (
        <QueryClientProvider client={queryClient}>
            {/* hydration을 사용하기 위해 hydrate 등록 필수 */}
            <Hydrate state={pageProps.dehydratedState}>
                <Component {...pageProps} />
            </Hydrate>
        </QueryClientProvider>
    );
};

export default App;

 

 

 

[ 2. hydrate 코드 작성 ]

 

import axios from 'axios';
import { GetServerSideProps } from 'next';
import { dehydrate, QueryClient, useQuery } from 'react-query';

export const getServerSideProps: GetServerSideProps = async (context) => {
    const queryClient = new QueryClient();

    try {
        // prefetchQuery에 await 필수
        await queryClient.prefetchQuery('productNo', async () => {
            const res = await axios.get('http://localhost:8080/product/no');
            return res.data;
        });

        return {
            props: {
                dehydratedState: dehydrate(queryClient),
            },
        };
    } catch (error) {
        return {
            notFound: true,
        };
    }
};

const Hydration = () => {
    const queryProductNo = () => {
        const res = useQuery('productNo', async () => {
            const res = await axios.get('http://localhost:8080/product/no');
            return res.data;
        });

        if (res.isLoading) return <div>Loading...</div>;
        if (res.data) {
            const productNos: number[] = res.data as unknown as number[];
            return (
                <div>
                    {productNos.map((productNo) => (
                        <div key={productNo}>
                            <h2>데이터 Fetch로 가져온 상품번호 : {productNo}</h2>
                        </div>
                    ))}
                </div>
            );
        }
    };
    return <>{queryProductNo()}</>;
};

export default Hydration;

 

 

 

 

코드를 작성한 뒤 서버를 실행해보면 initial data와 유사한 결과가 나오는 것을 확인해 볼 수 있습니다.

 

hydration preview

 

hydration response

 

 

 

먼저 Preview는 initial data와 동일하게 나오는 것을 볼 수 있습니다.

 

하지만 Response를 보면 initial data와 달리 dehydratedState에 데이터가 담겨 있는 것을 확인해 볼 수 있습니다.

 

단순히 props로 데이터를 넘긴것이 아니라 dehydrate를 해놨기 때문에 이전과 다른 props 값을 확인하게 되는 것입니다.

 

 

 

 

useInfiniteQuery

 

useInfiniteQuery도 useQuery와 유사하게 사용해 볼 수 있습니다.

 

차이점이라고 한다면 useQuery는 prefetchQuery를 사용했지만 useInfiniteQuery는 prefetchInfiniteQuery를 사용한다는 것입니다.

 

hydration을 예시로 하여 다음과 같은 코드를 작성해볼 수 있습니다.

 

import axios from 'axios';
import { GetServerSideProps } from 'next';
import { useEffect, useRef } from 'react';
import { dehydrate, QueryClient, useInfiniteQuery } from 'react-query';

export const getServerSideProps: GetServerSideProps = async (context) => {
    const queryClient = new QueryClient();

    try {
        // prefetchInfiniteQuery에 await 필수
        await queryClient.prefetchInfiniteQuery('productNo', async () => {
            const res = await axios.get('http://localhost:8080/product/no');
            return res.data;
        });

        return {
            props: {
                // infiniteQuery를 사용하거나 Promise.all을 사용할 경우 JSON처리 필수
                dehydratedState: JSON.parse(JSON.stringify(dehydrate(queryClient))),
            },
        };
    } catch (error) {
        return {
            notFound: true,
        };
    }
};

const InfiniteHydration = () => {
    // ref
    const observerRef = useRef<IntersectionObserver>();
    const boxRef = useRef<HTMLDivElement>(null);

    const infiniteQueryProductNo = () => {
        const res = useInfiniteQuery(
            'productNo',
            async ({ pageParam = 0 }) => {
                const res = await axios.get('http://localhost:8080/product/no');
                return res.data;
            },
            {
                getNextPageParam: (lastPage, allPages) => {
                    // 다음 페이지 요청에 사용될 pageParam값 return 하기
                    return true;
                },
            },
        );

        // IntersectionObserver 설정
        const intersectionObserver = (entries: IntersectionObserverEntry[], io: IntersectionObserver) => {
            entries.forEach((entry) => {
                if (entry.isIntersecting) {
                    // 관찰하고 있는 entry가 화면에 보여지는 경우
                    io.unobserve(entry.target); // entry 관찰 해제
                    res.fetchNextPage(); // 다음 페이지 데이터 요청
                }
            });
        };

        // useEffect
        useEffect(() => {
            if (observerRef.current) {
                // 기존에 IntersectionObserver이 있을 경우
                observerRef.current.disconnect(); // 연결 해제
            }

            observerRef.current = new IntersectionObserver(intersectionObserver); // IntersectionObserver 새롭게 정의
            boxRef.current && observerRef.current.observe(boxRef.current); // boxRef 관찰 시작
        }, [res]); // res값이 변경될때마다 실행

        if (res.isLoading) return <div>Loading...</div>;
        if (res.data) {
            return (
                <div>
                    {res.data.pages.map((page, pageIndex) => {
                        const productNos: number[] = page;

                        return productNos.map((productNo, productNoIndex) => (
                            // 가장 마지막에 있는 Box를 boxRef로 등록
                            <div
                                key={`${productNo}/${pageIndex}`}
                                ref={
                                    productNos.length * pageIndex + productNoIndex ===
                                    res.data.pages.length * productNos.length - 1
                                        ? boxRef
                                        : null
                                }
                            >
                                <h2>데이터 Fetch로 가져온 상품번호 : {productNo}</h2>
                            </div>
                        ));
                    })}
                </div>
            );
        }
    };
    return <>{infiniteQueryProductNo()}</>;
};

export default InfiniteHydration;

 

 

 

 

코드를 작성한 뒤 서버를 실행해보면 다음과 같이 prerendering이 된 결과물을 확인할 수 있으며 infiniteQuery도 정상적으로 적용되어 infinite scroll이 사용되고 있는 것을 확인할 수 있습니다.

 

infiniteQuery preview

 

 

 

 

 

 

 

이상으로 Data Fetching의 마지막인 useQuery에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글