노트

loading 경계와 레이아웃 끌어올리기

Loading UI and Shared Layouts

프런트엔드#nextjs#performance · 연결된 개념 5개

쉽게 말하면

loading 경계는 공사 중인 가게 앞에 세우는 가림막 같은 거예요. 가림막은 page만 가리고 layout은 안 가리니까, 기다릴 필요 없는 탭바를 layout으로 옮기면 처음부터 진짜 모습으로 보여요.

비유가 깨지는 곳 layout은 이동해도 다시 렌더되지 않아 현재 경로를 받지 못해요. 그래서 활성 탭은 클라이언트에서 usePathname으로 정하고, layout이 cookies 같은 런타임 데이터를 읽으면 loading이 덮지 못해요.

Next.js App Router에서 loading.tsx는 같은 세그먼트의 page와 그 아래(하위 layout 포함)를 Suspense로 감싸지만, 같은 세그먼트의 layout은 감싸지 않는다. 그래서 데이터를 기다릴 필요가 없는 공통 UI(탭바, 필터 헤더)를 page에서 layout으로 끌어올리면 스켈레톤에 덮이지 않고 처음부터 실제 모습으로 보인다.

app/shop/
  layout.tsx    ← 탭바: loading 바깥, 이동해도 유지
  loading.tsx   ← 스켈레톤: page 자리만 덮음
  page.tsx      ← 느린 데이터
  • 탭바가 page 안에 있으면 라우트에 들어오거나 탭을 바꿀 때마다 "탭바 스켈레톤 → 실제 라벨"로 깜빡인다. 라벨이 고정이거나 URL만으로 정해지는 UI라면 기다릴 이유가 없다
  • layout은 이동해도 다시 마운트되지 않으니 탭 사이를 오갈 때 공통 UI가 그대로 남고, loading이 덮는 영역도 콘텐츠로 줄어든다
  • 스켈레톤이 아니라 실제 텍스트를 fallback으로 그리면 교체가 눈에 덜 띈다

공유할 라우트만 묶기: 라우트 그룹

괄호로 감싼 폴더 (main)은 URL에 나타나지 않는 라우트 그룹(Route Group)이다. 같은 탭바를 쓰는 라우트들만 app/(main)/ 아래에 두고 app/(main)/layout.tsx에서 한 번 그리면 된다. 목록에는 필요하고 상세에는 필요 없는 헤더라면 app/products/(list)/layout.tsx처럼 목록만 그룹으로 감싸 형제 [id]에는 주입되지 않게 한다. 서로 다른 그룹이 같은 URL로 풀리면 에러다.

주의할 점

  • layout은 이동 때 다시 렌더되지 않는다. 그래서 layout은 현재 경로나 쿼리를 받지 못한다. 활성 탭 표시는 클라이언트 컴포넌트에서 usePathname()으로 판단한다. 쿼리스트링이 필요 없다면 useSearchParams()를 피한다. 미리 렌더되는 라우트에서 CSR bailout을 부른다
  • 경로 매칭은 세그먼트 경계를 지킨다. pathname.startsWith('/best')는 나중에 생긴 /best-of-week도 잡는다. pathname === '/best' || pathname.startsWith('/best/')로 쓴다
  • layout이 런타임 데이터를 읽으면 loading이 그 부분을 덮지 못한다. cookies(), headers(), 캐시되지 않은 fetch가 layout에 있으면 Cache Components가 꺼져 있을 때는 layout이 끝날 때까지 이동이 막히고, 켜져 있을 때는 그 접근을 따로 <Suspense>로 감싸야 하며 그렇지 않으면 빌드 에러로 알려 준다. 끌어올린 UI가 이런 데이터를 쓰면 캐시하거나("use cache"와 Cache Components) 자체 경계를 둔다
  • loading 경계로 스트리밍이 이미 시작된 뒤 page에서 redirect()하면 응답 헤더가 나간 뒤라 상태 코드로 보낼 수 없고 스트림 안에서 이동시킨다. 그 사이 layout과 스켈레톤이 잠깐 보일 수 있으니, 리다이렉트 판단은 경계보다 먼저(첫 await 전) 한다

클라이언트 이동은 현재 라우트와 목적지가 공유하는 layout 아래만 다시 렌더하므로, 공유 layout보다 위에 있는 Suspense fallback은 이동 중에 쓰이지 않는다. 이동이 즉시 보이는지는 공유 layout 아래의 경계와 캐시에 달렸다(부분 사전 렌더링(PPR)의 instant 검증).

Next.js 16.3 문서 기준이다(2026). 스트리밍 원리는 스트리밍 SSR, 정적 셸에 무엇이 들어가는지는 부분 사전 렌더링(PPR).

출처: Next.js - loading.js · Next.js - Route Groups · Next.js - layout.js: Pathname · Next.js - Ensuring instant navigations

연결된 개념

이 노트를 가리키는 문서

아직 없습니다.

뜻이 가까운 노트

  • Pages Router에서 App Router로

    App Router는 Next.js 13에서 도입된 app/ 디렉터리 기반 라우터로, 서버 컴포넌트·중첩 레이아웃(Nested Layouts)·스트리밍을 기본으로 한다. pages/ 기반의 Pages Router에서 옮길 때 바뀌는 생각을 정리한다.

  • Activity와 숨겨진 라우트 보존

    Activity는 React 19.2의 컴포넌트로, UI를 언마운트하지 않고 display: none으로 숨기면서 state와 DOM(Document Object Model)을 보존한다. 숨기는 동안 Effect는 정리(cleanup)된다. 예전 이름은 Offscreen이다.

  • 코드 스플리팅과 동적 임포트

    코드 스플리팅(Code Splitting)은 하나의 큰 JavaScript 번들을 여러 청크(chunk)로 나누는 빌드 기법이고, 동적 임포트(Dynamic Import, import())는 그 청크를 실제로 필요해질 때 받아오는 방법이다. 나누는 것은 "어떻게", 불러오는 시점은 "언제"의 문제다.

  • 'use client'와 클라이언트 경계

    'use client'는 파일 맨 위에 적어 "여기서부터 클라이언트 번들에 들어간다"는 경계를 선언하는 지시어(Directive)다. 경계 파일과 그 파일이 import하는 모듈은 모두 클라이언트 컴포넌트가 된다. state, Effect, 이벤트 핸들러, 브라우저 API(Application Programming Interface)가 필요한 곳에 쓴다.

  • 커넥션 드레이닝과 무중단 재시작

    배포 중 서버를 재시작하는 몇 초 동안 로드밸런서가 그 서버로 요청을 보내면 502가 난다. 먼저 로드밸런서에서 빼고(드레이닝), 진행 중인 요청을 마친 뒤 재시작하고, 준비되면 다시 넣는다.

보기 옵션