Next.js 레이아웃과 라우트 그룹: loading.tsx 적용 범위
Next.js 앱 라우터에서 한 폴더에는 이런 파일들이 함께 놓입니다.
app/tags/
├── layout.tsx
├── loading.tsx
├── error.tsx
├── page.tsx ← /tags
└── [tag]/
└── page.tsx ← /tags/reactloading.tsx 는 /tags 목록을 불러오는 동안 보여 줄 스켈레톤으로 만들었습니다. 그런데 이 파일은 /tags/react 같은 하위 페이지에도 적용됩니다. 이 블로그에서는 그 결과 없는 태그 주소가 404 가 아니라 200 을 돌려주고 있었습니다.
파일은 그 페이지에만 붙는다는 생각
파일 이름이 역할을 말해 주니, 흔히 이렇게 이해합니다. page.tsx 옆에 둔 loading.tsx 는 그 페이지의 로딩 화면이고, error.tsx 는 그 페이지의 오류 화면이다. 파일 하나가 같은 폴더의 페이지 하나에 붙는다고 보는 것입니다.
실제로 이 파일들은 페이지에 붙는 것이 아니라 세그먼트를 감싸는 컴포넌트가 됩니다. 폴더 하나가 URL 의 한 조각(세그먼트)이고, 특수 파일들은 그 세그먼트 아래 모든 것을 정해진 순서로 감쌉니다.
특수 파일이 겹치는 순서
프로젝트 구조 문서는 한 세그먼트 안의 특수 파일이 렌더링되는 계층을 이렇게 적습니다.
layout.jstemplate.jserror.js(React Error Boundary)loading.js(React Suspense 경계)not-found.js(「찾을 수 없음」을 위한 Error Boundary)page.js또는 하위layout.js
위의 파일이 아래의 파일을 감쌉니다. 그리고 문서는 이 계층이 중첩된 경로에서 재귀적으로 반복된다고 덧붙입니다. 하위 세그먼트의 컴포넌트들은 부모 세그먼트의 컴포넌트들 안에 들어갑니다.
앞의 폴더에 대입하면 /tags/react 는 이렇게 렌더링됩니다.
tags/layout
└ tags/error (경계)
└ tags/loading (Suspense 경계)
└ [tag]/pagetags/loading.tsx 가 하위 경로의 페이지까지 Suspense 로 감쌉니다. loading 문서도 같은 폴더의 page.js 와 그 아래 모든 자식을 감싼다고 적습니다. Suspense 경계가 로딩 화면을 먼저 스트리밍하기 시작하면 상태 코드를 바꿀 수 없어서, 하위 페이지의 notFound() 가 200 이 됩니다.
같은 계층에서 한 가지 더 읽을 수 있습니다. error.js 는 layout.js 안쪽에 있습니다. error 문서도 error.js 가 같은 세그먼트 위쪽의 layout.js 와 template.js 는 감싸지 않는다고 적습니다. 그래서 같은 세그먼트의 레이아웃에서 난 오류는 그 세그먼트의 error.js 가 잡지 못하고, 부모 세그먼트의 경계로 올라갑니다. 루트 레이아웃의 오류는 global-error.js 가 맡습니다.
라우트 그룹으로 감싸는 범위를 좁히기
주소는 그대로 두고 감싸는 범위만 바꾸고 싶을 때 쓰는 것이 라우트 그룹입니다. 라우트 그룹 문서는 괄호로 감싼 폴더 이름이 URL 경로에 포함되지 않는다고 설명하고, 쓰임새로 「특정 세그먼트만 레이아웃을 공유하게 하고 나머지는 빼는 것」을 꼽습니다.
앞의 문제는 목록 페이지와 그 로딩 화면을 그룹으로 한 칸 내려서 풀었습니다.
app/tags/
├── (index)/
│ ├── loading.tsx ← /tags 에만 적용
│ └── page.tsx ← 여전히 /tags
└── [tag]/
└── page.tsx ← 로딩 경계 밖(index) 는 주소에 나타나지 않으므로 /tags 는 그대로입니다. 로딩 화면은 이제 (index) 세그먼트 아래에만 있고, 형제인 [tag] 는 감싸지 않습니다.
반대 방향으로도 씁니다. 여러 경로가 같은 레이아웃을 공유하게 하고 싶은데 주소 구조는 제각각일 때입니다. 이 블로그는 전체 글 목록(/, /page/2)과 카테고리 목록(/category/React)이 같은 왼쪽 메뉴를 써야 했습니다. 둘을 (archive) 그룹으로 묶고 그 레이아웃에 메뉴를 두었습니다. 주소는 바뀌지 않았고, 글 상세 페이지처럼 메뉴가 필요 없는 경로는 그룹 밖에 남았습니다.
레이아웃이 다시 렌더링되지 않아서 생기는 제약
레이아웃을 공유하면 얻는 것이 있습니다. 레이아웃 문서에 따르면 페이지를 이동해도 레이아웃은 state 를 유지하고 다시 렌더링되지 않습니다. 메뉴의 스크롤 위치나 펼침 상태가 그대로 남고, 이동할 때 레이아웃을 다시 그리는 비용도 없습니다.
같은 성질이 제약이 됩니다. layout 레퍼런스는 레이아웃이 이동 중에 다시 렌더링되지 않으므로 현재 경로(pathname)와 검색 파라미터를 받을 수 없다고 적습니다. 받을 수 있게 하면 이동한 뒤에도 옛 값에 머물기 때문입니다.
그래서 레이아웃에서 「지금 어느 메뉴가 선택됐는가」를 표시하려면 그 부분만 클라이언트 컴포넌트로 뺍니다. 문서의 설명대로 클라이언트 컴포넌트는 이동할 때 다시 렌더링되므로 usePathname 이나 useSelectedLayoutSegments 로 최신 경로를 읽을 수 있습니다.
"use client";
export function NavLink({ href, category, children }) {
const segments = useSelectedLayoutSegments();
const active = segments[0] === "category" && decodeURIComponent(segments[1] ?? "") === category;
return <Link href={href} aria-current={active ? "page" : undefined}>{children}</Link>;
}메뉴 목록 자체는 서버 컴포넌트인 레이아웃에서 데이터를 읽어 그리고, 선택 표시만 이 작은 컴포넌트가 맡습니다. 서버 컴포넌트 글에서 경계를 말단으로 내리라고 한 것과 같은 구조입니다.
파일을 둘 폴더를 고르는 질문
제가 보기에 특수 파일을 어디에 둘지는 질문 하나로 정해집니다. 이 파일이 감싸도 되는 경로가 이 폴더 아래 전부인가.
| 파일 | 폴더 아래 전부를 감싸면 곤란한 경우 |
|---|---|
loading.tsx |
하위 페이지가 notFound() 로 404 를 돌려줘야 할 때 |
layout.tsx |
일부 하위 경로에는 그 틀이 필요 없을 때 |
error.tsx |
하위 영역마다 다른 복구 화면이 필요할 때 |
곤란하다면 그 파일과 해당 페이지만 라우트 그룹으로 한 칸 내리고, 공유가 필요하다면 여러 경로를 한 그룹으로 묶습니다. 주소를 바꾸지 않고 감싸는 범위만 조절할 수 있다는 점이 라우트 그룹의 쓸모라고 봅니다.
여기까지가 확실한 부분
특수 파일의 계층 순서와 재귀적 중첩, loading.js 가 하위 경로를 감싼다는 점, 라우트 그룹이 URL 에 포함되지 않는다는 점, 레이아웃이 이동 중에 다시 렌더링되지 않아 pathname 과 검색 파라미터를 받을 수 없다는 점은 Next.js 16 문서 기준입니다. 태그 페이지의 200 과 (index), (archive) 그룹은 2026년 9월 18일 이 블로그에 적용하고 운영에서 확인한 구조입니다.
함께 읽기
- Next.js 병렬 라우트와 인터셉트 라우트: 모달 패턴과 default.js사진 목록에서 사진을 누르면 모달로 크게 보여 주고, 같은 주소를 새 탭에서 열면 전체 페이지로 보여 주는 화면이 있습니다. 주소는 /photo/123 하나인데 보이는 모양이 둘입니다.
- Next.js 데이터 가져오기 순서: 순차와 병렬, 그리고 Suspense 위치서버 컴포넌트에서 데이터를 읽는 코드는 이렇게 생깁니다.
- Next.js 다국어 경로 설계: 쿠키와 하위 경로공개하지 않고 내부에서만 여는 제품 카탈로그 데모에 언어 전환을 붙였습니다. 한국어, 영어, 중국어, 일본어, 독일어, 프랑스어 여섯 개입니다. 고른 언어는 쿠키에 저장하고, 쿠키가 없으면 브라우저의 Accept-Language 헤더를 봅니다. 주소는 나누지 않았습니다. /products/<기종> 하나로 여섯 언어를 모…
- Next.js 라우트 핸들러와 서버 함수 구분앱 라우터에서 서버로 무언가를 보내는 방법이 둘입니다. app/api/.../route.ts 에 라우트 핸들러를 만들거나, "use server" 를 붙인 서버 함수를 부르는 것입니다. 폼 하나를 붙일 때마다 어느 쪽을 쓸지 정해야 하는데, 「둘 다 서버에서 도니 아무거나」로 두면 나중에 갈립니다.
- Next.js 이미지 최적화: next/image와 빌드 때 굽기제품 카탈로그 데모를 만들면서 이미지 처리 방식을 정해야 했습니다. 화면에 나가는 사진은 제품 사진 몇 장과 로고뿐이고, 모두 저장소에 들어 있는 파일입니다. 요청마다 달라질 것이 없습니다.