블로그 포스트 카드를 클릭할 때마다 이상한 일이 벌어졌습니다. 상세 페이지로 이동하는 동안 전체 화면 로딩 스피너가 뜨고, 사이드바까지 같이 사라졌다가 다시 나타났습니다.
리스트는 그대로인데 레이아웃 전체가 깜빡이니, 페이지가 느리고 불안정해 보였습니다. 사용자는 “왜 사이드바까지 다시 로딩되지?”라고 느낄 수밖에 없죠.
포스트 카드 클릭→ 전체 화면 로딩 오버레이→ 사이드바 포함 레이아웃 리마운트→ 사이드바 스켈레톤 잠깐 노출→ 상세 페이지 렌더범인은 app/(public)/blog/loading.tsx였습니다.
Next.js App Router에서 loading.tsx는 해당 세그먼트의 Suspense fallback입니다. 문제는 이 파일이 사이드바를 포함한 레이아웃보다 위에 있었다는 점입니다.
blog/layout.tsx └─ Suspense (fallback = blog/loading.tsx) ← 여기가 문제 └─ (posts)/layout.tsx ← Sidebar 포함 └─ page카드를 누르면 상세 페이지가 서버에서 데이터를 기다리는 동안, Suspense가 (posts)/layout 전체를 fallback으로 갈아치웁니다. 사이드바가 같이 사라지는 이유입니다.
게다가 기존 로딩 UI는 fixed inset-0 전체 오버레이라서, 체감은 “앱이 한 번 죽었다 살아나는” 느낌이었습니다.
반대로 loading.tsx를 아예 없애면 사이드바는 유지되지만, 데이터가 올 때까지 아무 피드백 없이 리스트가 멈춰 있어 “느리다”고 느껴집니다. 둘 다 UX 문제입니다.
블로그 루트의 전체 화면 loading.tsx를 삭제했습니다. 사이드바까지 삼키는 fallback을 없애는 게 첫 단계입니다.
로딩 UI는 상세 페이지 세그먼트에만 두었습니다.
blog/(posts)/layout.tsx ← Sidebar 유지 └─ category/[slug]/post/[postSlug]/loading.tsx ← 본문만 스켈레톤 └─ category/[slug]/post/[postSlug]/page.tsx이렇게 하면 클릭 직후 URL은 바로 바뀌고, 사이드바는 그대로 둔 채 본문만 스켈레톤으로 전환됩니다. Next.js 문서에서도 동적 라우트에 loading.js가 있어야 prefetch 후 즉시 전환이 가능하다고 안내합니다.
router.push만 쓰거나 일반 <a>를 쓰면 prefetch·클라이언트 전환 이점을 놓치기 쉽습니다. 카드를 Link로 바꾸고, 클릭 즉시 opacity와 스피너로 “이동 중”임을 보여줬습니다.
const [isPending, startTransition] = useTransition();const handleNavigate = (event: React.MouseEvent<HTMLAnchorElement>) => { if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) { return; // 새 탭 열기는 기본 동작 유지 } event.preventDefault(); startTransition(() => { router.push(href); });};호버 시 router.prefetch(href)도 호출해, 실제로 누르기 전에 라우트를 미리 받아 두었습니다.
상세 페이지는 generateMetadata와 page가 같은 slug로 포스트를 각각 조회하고 있었습니다. React cache()로 감싸 같은 요청 안에서 한 번만 fetch하도록 바꿨습니다.
import { cache } from 'react';export const getPostBySlug = cache((slug: string): Promise<PostResponse> => fetcher({ url: '/api/posts/{slug}', path: { slug }, method: 'get', }),);클릭 후 흐름이 이렇게 바뀌었습니다.
포스트 카드 클릭→ 카드에 pending 스피너 (즉시)→ 사이드바 유지 + 본문 스켈레톤→ 포스트 본문 렌더사이드바가 같이 리마운트되지 않음
클릭 직후 피드백이 보여 “멈춘 것처럼” 보이지 않음
상세 데이터 fetch 중복이 줄어 서버 응답도 조금 가벼워짐
loading.tsx는 “있으면 좋은 파일”이 아니라 어디에 두느냐가 UX를 결정합니다.
공유 레이아웃(사이드바·헤더)보다 위에 두면 → 셸 전체가 깜빡입니다.
페이지 세그먼트에만 두면 → 셸은 유지하고 본문만 로딩됩니다.
아예 없으면 → 피드백이 없어 느리게 느껴집니다.
App Router에서 로딩 UX를 손볼 때는 “스피너를 예쁘게”보다 먼저, Suspense 경계가 레이아웃을 어디까지 감싸는지를 확인하는 게 맞습니다.