티스토리 뷰
[Next.js] ChunkLoadError: Loading chunk app/layout failed. (timeout: http://localhost:3000/_next/static/chunks/app/layout.js)
무화과(Fig) 2024. 10. 4. 19:00
문제 발견
최근에 Next.js로 개발 중, 흥미로운 문제를 마주했다.
개발 모드에서 새로고침을 하지 않으면 데이터가 로드되지 않는 현상이었다. 처음에는 새고로침으로 문제가 해결되었기 때문에 큰 문제가 아니라고 생각했지만, 매번 yarn dev를 실행할 때 마다 새로고침을 해야 하는 번거로움이 있었다.
또 이 문제의 원인을 알고 있어야 이후에 같은 상황이 발생하지 않을 것이라고 생각해 원인을 찾아보기로 했다.
ChunkLoadError: Loading chunk app/layout failed.(timeout: http://localhost:3000/_next/static/chunks/app/layout.js)
새로고침 없이 페이지 로딩을 기다려보니 다음과 같은 에러가 발생했다.
ChunkLoadError: Loading chunk app/layout failed.
이 오류를 봤을 때, 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를 사용할 때는 Provider 설정 외에도 React Query의 상태를 서버에서 클라이언트로 제대로 전달하기 위한 추가적인 설정이 필요하다.
따라서 서버 컴포넌트에서 필요한 데이터를 prefetch 한 뒤, 이를 클라이언트 컴포넌트로 전달하는 방식으로 코드를 수정했다. 이렇게 수정했더니, 초기 렌더링 시에도 새로고침 없이 데이터를 바로 받아올 수 있게 되었다.
// 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>
);
}
Next.js 의 버전 문제?
처음에 해당 에러를 구글링 했을 때는, 에러가 발생하는 이유가 Next.js 의 버전 때문이라고 생각했다. 이유는 Next.js 레포지토리의 이슈로 해당 문제를 제기한 사람이 있었는데, 여러명이 13.5.4로 다운그레이드를 해서 문제를 해결했다는 답글을 보았기 때문이다.
.next 폴더를 지우고 yarn dev로 서버를 재실행하면 문제가 해결된다는 말도 있어서 진행해보았는데, 처음 한 번 문제가 해결됐다가 이후에는 계속 문제가 지속되었다.
이후에 해당 에러에 대해 더 찾아보던 중, Reddit에서 비슷한 경험을 한 개발자의 글을 발견했다. 이 개발자는 Redux를 Next.js의 app router와 함께 사용하려다 유사한 문제를 마주했고, 나와 마찬가지로 ReduxStore를 클라이언트 컴포넌트로만든 뒤 Provider로 전체 앱을 감싸는 형태로 사용하고 있었다.
나는 이 글에 대한 하나의 답변을 보고, 지금 발생하는 문제의 원인이 무엇인지 파악하게 되었다.
Are you absolutely sure you actually need the redux store with Next.js? ... I'm guessing redux needs to be mounted in a client component, and not the layout directly
결론적으로 TanstackQueryProvider는 클라이언트 컴포넌트에서 mount 되어야 하는데, 서버 컴포넌트인 layout.tsx에서 실행하려고 하니 서버 컴포넌트와 클라이언트 컴포넌트 사이의 불일치 때문에 에러가 발생한 것이 이유였던 것 같다.
이번 에러를 해결하기 위해 Next.js의 app router와 서버 컴포넌트, 클라이언트 컴포넌트의 개념에 대해 더 잘 알게 된 것 같다.
특히 TanStack Query와 같은 라이브러리를 사용할 때 서버 컴포넌트에서의 데이터 prefetching과 클라이언트로의 데이터 전달 과정을 제대로 구성해야 한다는 점을 알게 되었다👍
참고
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/
'Client > Next.js' 카테고리의 다른 글
[Next.js] Next.js 프로젝트를 Vercel에 배포하기: 커스텀 도메인 설정부터 Production 배포까지 (0) | 2024.09.22 |
---|---|
[Next.js] Invalid JSON (trailing comma, dangling comma, terminal comma) (0) | 2024.06.07 |
[Next.js] 외부 이미지 사이즈 지정하기 (0) | 2024.06.05 |
[Next.js] 'next'은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는배치 파일이 아닙니다. (0) | 2024.06.04 |
[Next.js] Next.js 개념 정리 (0) | 2024.06.03 |
- Total
- Today
- Yesterday
- 비제어 컴포넌트
- js
- 프론트엔드
- 비동기
- currentTarget
- Next.js
- 제어 컴포넌트
- GitHub
- 리액트
- Target
- 코드잇 스프린트
- rest parameter
- tanstackquery
- innerhtml
- html
- 스프린트프론트엔드6기
- 동기
- react
- CSS
- 중급 프로젝트
- 객체
- hydrationboundary
- 취업까지달린다
- 배열
- map
- javascript
- 유사배열객체
- Git
- arguments
- 코드잇스프린트
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |