티스토리 뷰

 

 

 

문제 발견


최근에 Next.js로 개발 중, 흥미로운 문제를 마주했다.

 

개발 모드에서 새로고침을 하지 않으면 데이터가 로드되지 않는 현상이었다. 처음에는 새고로침으로 문제가 해결되었기 때문에 큰 문제가 아니라고 생각했지만, 매번 yarn dev를 실행할 때 마다 새로고침을 해야 하는 번거로움이 있었다.

 

 

에러 발생


새로고침 없이 페이지 로딩을 기다려보니 다음과 같은 에러가 발생했다.

ChunkLoadError: Loading chunk app/layout failed.(timeout: http://localhost:3000/_next/static/chunks/app/layout.js)

 

이 오류를 봤을 때 app/layout.tsx 파일에 문제가 있을 것이라 생각해 해당 컴포넌트를 살펴보았다.

 

 

원인 분석하기: 1. CDN로 폰트로딩


처음에는 폰트 로딩이 문제의 원인이라고 생각했다.

//  app/layout.tsx

import TanstackQueryProvider from '@/utils/tanstack-query-provider';
import DynamicHeader from '@/components/header/dynamic-header';
import MicrosoftClarity from '@/metrics/microsoft-clarity';
import '../styles/globals.css';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko">
      <body>
        <div
          className="mx-auto w-[390px]"
          style={{ '--layout-width': '390px' } as React.CSSProperties}
        >
          <TanstackQueryProvider>
            <DynamicHeader />
            {children}
          </TanstackQueryProvider>
          <MicrosoftClarity />
        </div>
      </body>
    </html>
  );
}

 

 

global.css에서 CDN을 통해 Pretendard 글꼴을 불러오고 있었는데, CDN의 네트워크 지연이나 연결 문제가 layout.tsx 청크의 로딩을 지연시킬 수 있다고 생각했기 때문이다.

// global.css

@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-dynamic-subset.min.css');
@tailwind base;
@tailwind components;
@tailwind utilities;
...

 

 

이 문제를 해결하기 위해 CDN 대신 폰트 파일을 직접 다운로드해서 사용해봤지만, 문제는 해결되지 않았다.

 

 

비록 원래의 문제는 해결하지 못했지만, 이 과정에서 폰트 로딩과 관련된 중요한 사실을 알게 되었다.

1. CDN으로 폰트를 불러오는 것 보다 직접 다운로드해서 사용하는 것이 사용자 경험 측면에서 더 유리하다.

CDN으로 폰트를 불러올 경우, 클라이언트에서 custom 폰트를 다운받기 전까지는 운영체제에서 사용가능한 fallback font를 사용하게 된다.

따라서 custom 폰트가 로드되기 전, 후에 폰트 사이즈 크기차이로 인해서 CLS(Cumulative Layout Shift)가 발생해서 사용자 경험이 떨어진다.

2. Next.js의 next/font를 사용하면 빌드 타임에 폰트를 다운로드하고, fallback 폰트가 사용되는 동안 css의 size-adjust 속성으로 레이아웃 시프트를 방지할 수 있다.

 

 

원인 분석하기: 2. TanStack Query Provider


다음으로는 TanstackQueryProvider를 의심했다. 하지만 Provider는 React Query 공식 문서의 내용을 그대로 따랐기 때문에 문제가 없을 거라고 생각했다.

 

그러나 문서를 다시 꼼꼼히 읽어보니 중요한 부분을 놓쳤다는 것을 알게되었다. 바로 서버 컴포넌트에서는 hydration API를 사용해야 한다는 점이었다.

 

 

 

서버 컴포넌트에서의 데이터 Prefetch


Next.js app router를 사용하면서 한 가지 간과했던 점이 있었는데 바로 모든 컴포넌트가 서버 컴포넌트로 작동한다는 것이다. 서버 컴포넌트에서는 클라이언트 API를 사용할 수 없기 때문에 Tanstack Query 같은 클라이언트 라이브러리를 사용하면 에러가 발생한다.

 

만약 서버 컴포넌트에서 데이터 페칭을 하고 싶다면 서버에서 먼저 데이터를 가져온 뒤 이 데이터를 클라이언트로 전송해주어야 한다.

 

아래는 위 내용을 코드로 나타낸 것이다.

// RootPage (page.tsx)

import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import { findAllPostsList } from '@/api/generated/endpoints/post/post';
import MainPageClient from '@/components/pages/main-page/main-page-client';

export default async function Page() {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: findAllPostsList,
  });

  const dehydratedState = dehydrate(queryClient);

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

 

먼저 new QueryClient로 새 QueryClient를 생성해주어야 한다. 여기서 의아했던 점은 provider.tsx에서 isServer가 true인 경우 새 QueryClient가 반환되도록 했는데, 여기서도 새로 생성해야 하는 것이다.

// provider.tsx

'use client';

import { ReactNode } from 'react';
import { isServer, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

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

let browserQueryClient: QueryClient | undefined = undefined;

const getQueryClient = () => {
  if (isServer) {
    return makeQueryClient();
  }

  if (!browserQueryClient) {
    browserQueryClient = makeQueryClient();
  }

  return browserQueryClient;
};

interface TanstackQueryProvidersProps {
  children: ReactNode;
}

const TanstackQueryProvider = ({ children }: TanstackQueryProvidersProps) => {
  const queryClient = getQueryClient();

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
};

export default TanstackQueryProvider;

1. isServer true일 때 queryclient객체를 생성하지 않으면 skeleton 표시가 안된다?

 

 

https://claude.ai/chat/17244a1b-d157-4a1f-8a5c-366af847d9e3

 


 

참고

https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#final-words

https://dev.to/algoorgoal/nextjs-tailwindcsse-pretendard-ponteu-jeogyonghagi-1g87

https://www.reddit.com/r/nextjs/comments/192jhue/trying_nextjs_with_the_app_router_for_the_first/

https://github.com/vercel/next.js/issues/56484

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함