nextjs app router 구조에서 페이지네이션 구현 중 다음 페이지 버튼을 눌러도 로딩 스켈레톤이 표시되지 않는 문제 발생.
첫 화면 로드시나 url을 직접 변경했을 때만 loading 컴포넌트가 렌더링 됨
구글링 해보니까 유사한 문제를 겪고 있는 유저들 다수 발견
https://www.reddit.com/r/nextjs/comments/1404ekm/loadingjs_for_searchparams/
https://github.com/vercel/next.js/issues/49297#issuecomment-1568557317
Next.js의 App Router는 기본적으로 URL의 경로(segment)가 변경될 때만 loading.tsx 파일을 호출해서 로딩 컴포넌트 표시
내가 구현한 페이지네이션은 쿼리 파라미터만 변경하는 방식이었음
/antipatterns?page=1 → /antipatterns?page=2
Nextjs는 쿼리 파라미터만 변경된 경우에는 같은 경로로 인식해서 loading.tsx가 호출되지 않았던 것
Next.js는 파일 시스템 기반 라우팅을 사용해서 pages 디렉토리 안에 있는 파일 이름이 곧 경로가 되고 따라서 아래 세 개의 경로 모두 /products를 가리킨다고 인식함
/antipatterns?tag=ux
/antipatterns?id=123
/antipatterns?page=1
key props를 가진 Suspense 컴포넌트를 사용하고, Suspense 안의 자식 컴포넌트를 비동기 처리하여 Promise를 던진다.
1. app/antipatterns/loading.ts 삭제
2. key Props를 가진 Suspense 컴포넌트 사용
3. Suspense 자식 컴포넌트로 Promise를 던지는 서버 컴포넌트 사용
// 수정 후
// app/antipatterns/page.tsx
<Suspense key={page} fallback={<ArticlePreviewSkeleton />}>
<List page={page} />
</Suspense>
중요한 점은 getAntipatterns()를 suspend할 수 있도록 page.tsx에서 await하지 말고 서버 컴포넌트 내부에서 await 해야함. 즉, 데이터를 바로 await 하지 말고 List.tsx 같은 서버 컴포넌트로 분리해 처리해야함.
Suspense는 React에서 비동기 렌더링을 제어하기 위한 컴포넌트
내부에서 Promise를 던지는 컴포넌트를 감싸면, 해당 Promise가 해결되기 전까지 fallback으로 지정한 로딩 UI가 표시됨
Suspense는 자식 컴포넌트가 JSX 대신 Promise를 던지면(suspend), 해당 렌더링을 일시 중단함
이 상태에서는 fallback으로 지정한 로딩 UI만 먼저 렌더링됨
Promise가 해결되면 React가 자동으로 다시 렌더링을 시도하고, 실제 UI가 표시
- Suspense fallback을 다시 띄우려면 Suspense에 key를 반드시 넣어서 page가 변경될 때 재마운트 시켜야 함.
- 자식 컴포넌트가 반드시 Promise를 던져야 한다는 것.
데이터를 await 해서 미리 받아온 경우에는 Suspense가 작동하지 않음
서버 컴포넌트에서는 async function 안에서 await를 사용할 경우 React가 내부적으로 자동 suspend 처리
따라서 데이터를 가져오는 코드는 반드시 Suspense 내부의 서버 컴포넌트 안에서 실행해야 함