본문 바로가기
Web Study/Next.js

Next.js로 SNS x.com 클론코딩하기 - 8.5

by 쿠리의일상 2024. 2. 8.

전 게시글에서 마쳤어야 했는데 너무 긴 내용을 담은 것 같고 애매하게 섹션 3의 1강의만 남아서 .5 게시글을 작성하게 되었습니다. 다음 섹션부턴 백엔드 개발자와 협업한다는 전제하로 들어가기 때문에 이번 글은 가볍게 확인하기 좋은 것 같습니다~

 

넥스트 로딩과 에러

자동적으로 넥스트는 page.tsx 가 로딩 중일 땐 loading.tsx 를 보여주고, 에러가 발생했을 땐 error.tsx 를 보여주게 설계되어 있습니다. 이 기능은 기본적으로 리액트의 와 를 활용한 것으로 귀찮았던 과정을 줄일 수 있게 되었습니다.

https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming

 

Routing: Loading UI and Streaming | Next.js

Built on top of Suspense, Loading UI allows you to create a fallback for specific route segments, and automatically stream content as it becomes ready.

nextjs.org

공식 페이지에서도 보여주듯 loading.tsx 의 내부는 위처럼 구현되었다고 합니다. 

이렇게 넥스트 상 계층구조를 미리 구현하여 따로 개발자가 구현하지 않아도 되게끔 파일 구조로 변경시킨 것입니다.

 

loading.tsx

그렇다면 넥스트에서 로딩 페이지가 보여지는 경우는 언제인가?

  • 넥스트의 서버 컴포넌트에서 데이터 패칭하는 경우 -> 로딩O
  • 리액트 Lazy가 적용되어 있는 경우 -> 로딩O
  • 리액트 18에서 나온 'use' 훅의 경우 -> 로딩O
    • 해당 use 훅은 Promise 를 넣어줄 수 있으며, Promise를 넣으면 resolve 되기 전까지 기다려주게 됨
    • 또한 use 훅은 컨텍스트도 넣어줄 수 있는데, useContext와 유사하다고 함

 

가장 먼저 접속하는 /home 페이지에서 로딩창이 뜨지 않는 이유는 서버 사이드 렌더링 특성상 가장 처음 보여지는 페이지를 미리 읽어오기 때문이고, /explore 이나 다른 페이지에서 가장 먼저 접속해서 로딩 컴포넌트를 만들어준 페이지로 이동하면 로딩창이 뜨게 되는 것입니다.

 

다만 이 로딩창은 /home 페이지의 로딩을 의미하는 것으로, Post 쪽 로딩을 위해선 저번 글에서 구현했던 무한 스크롤링 부분에서 사용해준 useQuery 훅에서 제공하는 isPendingisLoading 을 활용해서 직접 구현해줘야 합니다.

  • isPending
    • 맨 처음 어떤 데이터도 없을 때, 대기 중인 경우 true
  • isLoading
    • isFetching && isPending
    • 처음 데이터를 가져올 때 true가 됨
  • isFetching
    • 데이터를 불러오게 될 때 true
    • queryFn이 호출될 때마다 true가 됨

 

if (isPending) {
    return (
      <div style={{ display: "flex", justifyContent: "center" }}>
        <svg
          className={styles.loader}
          height="100%"
          viewBox="0 0 32 32"
          width={40}
        >
          <circle
            cx="16"
            cy="16"
            fill="none"
            r="14"
            strokeWidth="4"
            style={{ stroke: "rgb(29, 155, 240)", opacity: 0.2 }}
          ></circle>
          <circle
            cx="16"
            cy="16"
            fill="none"
            r="14"
            strokeWidth="4"
            style={{
              stroke: "rgb(29, 155, 240)",
              strokeDasharray: 80,
              strokeDashoffset: 60,
            }}
          ></circle>
        </svg>
      </div>
    );
  }

위처럼 useQuery 에서 isPending 중일 때 로딩창을 설정할 수 있습니다.

 

다만 주의할 점은 prefetchInfiniteQuery 를 사용하여 서버에서 가져온 정보들은 로딩창을 구현할 수 없습니다. 해당 함수는 애초에 로딩되지 않게끔 미리 서버에서 정보를 가져오는 기능을 하기 때문입니다.

즉 서버 사이드 렌더링이 필요한 경우, 로딩창은 포기해야하는 것입니다. 검색엔진 최적화를 위해 SSR를 활용하는 경우가 많은데, 로딩 화면을 가져가 버리면 의미가 없어지기 때문입니다. 이 경우 메타데이터 등에 넣어주는 방식도 있으니 고려해봐야 합니다.

 

헷갈릴 법한데 정리하자면,

  • 서버 컴포넌트에서 로딩/에러를 구현하려면 -> loading.tsx / error.tsx
  • 클라이언트 컴포넌트에서 로딩/에러를 구현하려면 -> useQuery 등의 리액트 쿼리의 훅에서 지원하는 isPending / isError 값을 활용해주기

 

 

다만 useSuspenseQuery/useSuspenseInfiniteQuery 를 사용하기 위해 직접 최적화를 위한 Suspense 를 구현해줄 수 있습니다. 그렇게 되면, 클라이언트 측에 isPending 값을 활용하여 매번 로딩창을 구현하지 않아도 되게끔 처리가 가능해집니다. 위의 두 훅은 기존의 useQuery, useInfiniteQuery의 Suspense 가 감싸진 기능을 추가한 것 뿐이므로 사용법은 동일합니다.

import TabDecider from "@/app/(afterLogin)/home/_component/TabDecider";
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from "@tanstack/react-query";
import getPostRecommend from "@/app/(afterLogin)/home/_lib/getPostRecommends";

export default async function TabDeciderSuspense() {
  const queryClient = new QueryClient();
  await queryClient.prefetchInfiniteQuery({
    queryKey: ["posts", "recommends"],
    queryFn: getPostRecommend,
    initialPageParam: 0,
  });
  const dehydratedState = dehydrate(queryClient);

  return (
    <HydrationBoundary state={dehydratedState}>
      <TabDecider />
    </HydrationBoundary>
  );
}

리액트 쿼리의 HydrationBoundary 가 필요한 영역이 불필요한 부분을 제외하고 좁아지게 되었습니다.

export default async function Home() {
  return (
    <main className={style.main}>
      <TabProvider>
        <Tab />
        <PostForm />
        <Suspense fallback={<Loading />}>
          <TabDeciderSuspense />
        </Suspense>
      </TabProvider>
    </main>
  );
}

위처럼 미리 보여줘도 상관없는 Tab과 PostForm은 Suspense 외부에 두고, TabDeciderSuspense 컴포넌트를 따로 구현하고, 그 위에 Suspense 로 감싸줌을 알 수 있습니다. 이제 로딩이 될 때 fallback에 담긴 컴포넌트가 보여질 것입니다.