혼자 적어보는 노트

[Next.js] SSR + ReactQuery 적용하기 본문

NextJS

[Next.js] SSR + ReactQuery 적용하기

jinist 2022. 11. 12. 03:26

 

 

Next.js를 사용하여 프로젝트를 진행중이었는데 SSR로 작성된 페이지로 진입 시 csr보다 1초정도 느리게 페이지가 이동되는 현상이 있었다. (이전 프로젝트에서도 마찬가지)

 

이 부분을 개선하고 싶어서 여러 방면으로 알아보았지만 마땅한 해결 방법을 찾지 못했고 계속 신경쓰이던 부분이었는데,이번에 reactQuery를 테스트삼아 도입을 하며 조금은 개선을 하게 되었다. (아직 이후 발견하는 문제를 찾지는 못함)

 


 

📝 과정

1. 질문 목록을 클릭하면 해당 질문의 카테고리 정보와 함께 상세 페이지([id].tsx)로 이동한다.

2. 상세 페이지에서는 상세정보를 요청을 하고(SSR) 응답받은 데이터를 페이지에 내려준다.

3. 2번에서 받은 상세 정보에는 이전,다음 질문에 대한 (prevId, nextId)가 존재하며 해당 값이 담긴 버튼을 통해 페이지를 이동한다.

 

 원하는 결과

1. 한번 들어갔던 상세 페이지를 다시 들어갈 경우에 캐싱된 데이터로 바로 보여주기

2. 다음 질문에 대한 데이터를 prefetch하여 다음 페이지 이동 시 바로 보여주기

 

 

Dehydrate

 

SSR에서 reactquery를 사용할 때 initialData를 사용하는 방법도 있었지만 dehydrate가 조금 더 효율적이라고 해서

dehydrate방식을 사용하기로 했다. 

 

 

[_app.tsx]

import type { AppProps } from 'next/app';

import { Hydrate, QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from '~/react-query/queryClient';
import type { DehydratedState } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

function MyApp({ Component, pageProps }: AppProps<{ dehydratedState: DehydratedState }>) {
  return (
 
      <QueryClientProvider client={queryClient}>
        <Hydrate state={pageProps.dehydratedState}>
            <Component {...pageProps} />
          <ReactQueryDevtools initialIsOpen={false} />
        </Hydrate>
      </QueryClientProvider>

  );
}

export default MyApp;

 

 

[[id].tsx]

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60,
    },
  },
});

export const getServerSideProps = async (context: NextPageContext) => {
  const { id, mainCategory, subCategory } = context.query;
  const questionId = Number(id);
  
  //.. 생략
  
  try {
    const query = { mainCategory, subCategory };

    await queryClient.prefetchQuery([QUERY_KEY.questionDetail, questionId, query], () =>
      questionApi.getDetail(questionId, query),
    );

    return {
      props: {
        questionId,
        query,
        dehydratedState: dehydrate(queryClient),
      },
    };
  } catch (e) {
    return {
      notFound: true,
    };
  }
};


const QuestionDetail = ({ questionId, query }: QuestionDetailProps) => {
  const { detailData } = useQuestionDetail(questionId, query);
  
 // 생략 
}

 

여기서 캐시된 데이터를 사용하려면 queryClient를 getServerSideProps 함수의 바깥에 작성해주어야 한다.

❗ 내부에 작성하게되면 다시 해당 페이지에 접근 시 캐시된 데이터를 사용하지 않음!

 

사실 이 부분에서 조금 쩔쩔맸는데 새벽에 프로젝트 팀원분이 찾아주셨다! 👍🙇‍♀️

[참고 링크: React-Query를 Next.js와 함께 사용해보자]

 

 

이렇게 하면 질문 목록페이지에서 상세정보페이지(ssr)에 접근 시 처음에는 서버에 응답을 받는 시간이 소요되지만

다른 페이지로 이동했다가 다시 접근 시 서버 요청/응답을 기다리지 않고 캐시된 데이터를 불러온다.

(지정한 staletime까지)

 

 

Prefetch 적용하기

 

위처럼 캐시된 데이터를 사용하는 것을 이전/다음페이지(SSR)로 이동할 때도 응용할 수 있을거라 생각해서 미리 다음 페이지를 받아오도록 prefetch를 해보았지만 페이지가 넘어가는 순간! 이상하게도 동일한 데이터를 server에서 다시 받으면서 딜레이가 생기길래 CSR로 처리해보기로 했다. 

 

[참고 링크: React Query v4 + SSR in Next JS]

 

 

[이전/다음페이지 버튼]

const QuestionMoveButtons = ({ categoryQuery, nextId, prevId }: QuestionMoveButtonProps) => {
  const router = useRouter();

  const onMovePrev = () => {
    router.push({ pathname: `/question/${prevId}`, query: { ...categoryQuery } }, undefined, {
      shallow: true,
    });
  };

  const onMoveNext = () => {
    router.push({ pathname: `/question/${nextId}`, query: { ...categoryQuery } }, undefined, {
      shallow: true,
    });
  };

새로고침 or URL 변경 시에는 SSR로 데이터를 받아오고 이후 페이지를 이동할 때는 serverSideProps를 거치지 않도록 shallow 옵션을 사용했다.

 

 

URL은 변경되지만 데이터는 그대로이기 때문에 페이지가 변경됨에 따라 데이터도 변경될 수 있도록 처리를 해주어야했다.

 

 

[[id].tsx]

const QuestionDetail = ({ questionId: defaultId, query }: QuestionDetailProps) => {
  const [questionId, setQuestionId] = useState(defaultId);
  const { detailData } = useQuestionDetail(questionId, query);
  const router = useRouter();
  const queryClient = useQueryClient();

  useEffect(() => {
    const { id } = router.query;
    const numberId = Number(id);

    /* CSR 이동 체크 */
    if (numberId !== questionId) {
      setQuestionId(numberId);
    }
  }, [router.query.id]);

  useEffect(() => {
    /* 다음 페이지 데이터 미리 받아오기 */
    if (detailData && detailData.nextId) {
      queryClient.prefetchQuery([QUERY_KEY.questionDetail, detailData.nextId, query], () =>
        questionApi.getDetail(detailData.nextId, query),
      );
    }
  }, [detailData?.nextId]);
  
  
  
  // 생략
  }

 

페이지가 이동된다면 url의 id는 변경되지만 questionId는 변경되지 않기에 두 값을 비교하여 페이지가 이동된 것을 체크하고,  서버에서 받은 detailData의 nextId가 변경될 때 다음페이지의 데이터를 미리 받아오도록 했다.

 

+ 댓글 또한 위와 비슷한 방식으로 questionId의 값에 따라 새로 업데이트 해주면 된다!

 

 

 

✅결과

왼쪽: 적용 전 / 오른쪽: 적용 후

 

 

 

 

 

 

 

 

 

Comments