KimMinJun
Coding Note
KimMinJun
전체 방문자
오늘
어제
  • 분류 전체보기 (507) N
    • CS (1)
    • Web (29) N
      • Vanilla JS (13)
      • TS (2)
      • React (7)
      • Next.js (5) N
      • ETC (1)
    • Docker (14)
    • Git (5)
    • ALGORITHM (11)
      • 정렬 (6)
      • 최단경로 (1)
      • 자료구조 (1)
      • 슬라이딩 윈도우 (1)
      • etc (2)
    • PS (432)
      • 백준 (187)
      • Programmers (105)
      • CodeUp (21)
      • STL (3)
      • 제코베 JS 100제 (50)
      • SWEA (0)
      • LeetCode (65)
    • IT (1)
    • React 공식문서 (번역, 공부) (11)
      • Quick Start (2)
      • Installation (0)
      • Describing the UI (9)
      • Adding Interactivity (0)
      • Managing State (0)
      • Escape Hatches (0)
    • Next.js 공식문서 (번역, 공부) (3)
      • Getting Started (2)
      • Building Your Application (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록
  • 관리

공지사항

인기 글

태그

  • recursion
  • 정렬
  • 다이나믹 프로그래밍
  • 그래프
  • Level 0
  • 제코베 JS 100제
  • tree
  • string
  • 문자열
  • codeup
  • programmers
  • Level 2
  • 수학
  • C++
  • C
  • Level1
  • Level 1
  • 백준
  • LeetCode
  • js

최근 댓글

최근 글

hELLO · Designed By 정상우.
KimMinJun

Coding Note

Web/Next.js

Next.js 15 / File system conventions - layout.js(ts)

2025. 8. 12. 23:41

개요

Next.js의 'File system convention'은 프로젝트 폴더와 파일의 구조만으로

라우팅 및 주요 기능이 자동으로 결정되는 방식이다.

특히 Next.js 15.4.6 App Router 기준으로 20개가 넘는 컨벤션들이 있는데,

공식문서를 보고 하나씩 정리해보려 한다.

 

먼저, Next.js를 설치하게 되면 기본적으로 세팅이 되있기도 한 `layout.js`이다.

`layout` 파일은 레이아웃을 정의하기 위해 사용된다.

모든 페이지에서 공통적으로 사용되는 레이아웃은 가장 상위인 `app` 디렉터리에 만든다.

 

Reference

Props

`children` (필수)

레이아웃 컴포넌트는 `children` prop을 반드시 가져야 한다.

렌더링 되는동안, `children`은 레이아웃이 감싸고 있는 경로에 대한 자식들로 채워진다.

이 자식들은 보통 자식 레이아웃이나, 컴포넌트로 채워지지만,

loading.js나 error.js 같은 특별한 파일 일수도 있다.

 

`params` (선택)

`params`는 동적 라우팅 파라미터 객체를 담은 Promise이다.

 

만약, 경로가 `app/dashboard/[team]/layout.js`이고,

URL이 `/dashboard/1`이라면,

`params`는 `Promise<{ team: '1' }>`이 된다.

 

`params`는 Promise이므로 `async/await` 또는 React의 `use` 함수를 사용해 값을 접근해야 한다.

 

Root Layout

`app` 디렉터리는 반드시 루트 레이아웃이 있어야 한다.

일반적으로, `app/layout.js`가 루트 레이아웃이다.

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>{children}</body>
    </html>
  )
}

 

루트 레이아웃은 반드시 `<html>`과 `<body>` 태그를 정의해야 한다.

그리고, 메타데이터는 직접 작성하지 말고, Metatdata API를 사용해야 한다.

import './globals.css';

export const metadata = {
  title: '제목',
  description: '설명',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

 

라우트 그룹을 사용하면 여러 개의 루트 레이아웃을 만들 수 있다.

다만, 서로 다른 루트 레이아웃 간 이동은 전체 페이지 새로고침을 발생시킨다.

예를 들어, `/cart`(app/(shop)/layout.js)에서

`blog`(app/(marketing)/layout.js)로 이동하면 새로고침이 발생한다.

 

또한, 루트 레이아웃은 동적 세그먼트 하위에 있을 수도 있다.

예를 들어, `app/[lang]/layout.js`를 국제화(i8n)에 사용 가능하다.

 

주의 사항

Request Object

  • 네비게이션 시 레이아웃은 캐싱되어 불필요한 서버 요청이 발생하지 않게 한다.
  • 레이아웃은 다시 렌더링되지 않으므로 성능이 향상된다.
  • 따라서 레이아웃에서는 원본 Request 객체에 접근할 수 없다.
  • 대신 Server Component 및 서버 함수에서 `headers`와 `cookies` API를 사용해야 한다.
import { cookies } from 'next/headers'
 
export default async function Layout({ children }) {
  const cookieStore = await cookies()
  const theme = cookieStore.get('theme')
  return '...'
}

 

Query Params

  • 레이아웃은 네비게이션 시 리렌더링되지 않으므로 쿼리 파라미터가 갱신되지 않는다.
  • 만약 최신 쿼리 값을 얻고 싶다면,
    • 페이지 컴포넌트에서 `searchParams` prop을 사용하거나,
    • Client Component에서 `useSearchParams` 훅을 사용해야 한다.
'use client'
 
import { useSearchParams } from 'next/navigation'
 
export default function Search() {
  const searchParams = useSearchParams()
 
  const search = searchParams.get('search')
 
  return '...'
}
import Search from '@/app/ui/search'
 
export default function Layout({ children }) {
  return (
    <>
      <Search />
      {children}
    </>
  )
}

 

Pathname

  • 레이아웃은 네비게이션 시 리렌더링되지 않으므로 최신 pathname에 접근할 수 없다.
  • 최신 경로를 사용하려면 Client Component에서 `usePathname`훅을 사용해야 한다.
'use client'
 
import { usePathname } from 'next/navigation'
 
// Simplified breadcrumbs logic
export default function Breadcrumbs() {
  const pathname = usePathname()
  const segments = pathname.split('/')
 
  return (
    <nav>
      {segments.map((segment, index) => (
        <span key={index}>
          {' > '}
          {segment}
        </span>
      ))}
    </nav>
  )
}
import { Breadcrumbs } from '@/app/ui/Breadcrumbs'
 
export default function Layout({ children }) {
  return (
    <>
      <Breadcrumbs />
      <main>{children}</main>
    </>
  )
}

 

Fetching Data

  • 레이아웃은 `children`에 데이터를 전달할 수 없다.
  • 하지만, 동일한 데이터를 여러 라우트에서 가져와야 할 경우에는
    • React의 `cache`를 사용해 중복을 제거하거나,
    • Next.js의 `fetch`를 사용해 자동으로 요청을 중복을 제거할 수 있다.

아래와 같은 유저 데이터를 받아오는 함수가 있다고 할 때,

export async function getUser(id: string) {
  const res = await fetch(`https://.../users/${id}`)
  return res.json()
}

 

layout은 기본적으로 서버 컴포넌트이기 때문에 직접 fetch 또는 다른 데이터 조회 코드를 직접 호출할 수 있다.

// app/dashboard/layout.tsx
import { getUser } from '@/app/lib/data'
import { UserName } from '@/app/ui/user-name'
 
export default async function Layout({ children }) {
  const user = await getUser('1')
 
  return (
    <>
      <nav>
        {/* ... */}
        <UserName user={user.name} />
      </nav>
      {children}
    </>
  )
}

 

위의 layout에 children에 들어가는 page 컴포넌트에서 다시 한번 똑같은 데이터를 요청해도,

fetch를 사용할 경우, 같은 요청은 캐싱되어있기 때문에 성능 이슈가 없다.

// app/dashboard/page.tsx
import { getUser } from '@/app/lib/data'
import { UserName } from '@/app/ui/user-name'
 
export default async function Page() {
  const user = await getUser('1')
 
  return (
    <div>
      <h1>Welcome {user.name}</h1>
    </div>
  )
}

 

Accessing child segments

  • 레이아웃 컴포넌트는 자신을 감싸는 segment는 알 수 있어도, 자식 segment 정보까지는 알 수 없다.
  • 하위 segment를 사용하려면 Client Component에서
    `useSelectedLayoutSegment` 또는
    `useSelectedLayoutSegments` 훅을 사용해야 한다.

아래 코드에서 `useSelectedLayoutSegment()` 훅은

URL 경로에서 상위 레이아웃 바로 아래 레벨의 활성화된 경로를 문자열로 반환한다.

예를 들어 URL이 `/blog/hello-world`라면,

부모인 `blog` 바로 아래 segment인 `hello-world`를 반환한다.

// app/ui/nav-link.tsx
'use client'
 
import Link from 'next/link'
import { useSelectedLayoutSegment } from 'next/navigation'
 
export default function NavLink({
  slug,
  children,
}: {
  slug: string
  children: React.ReactNode
}) {
  const segment = useSelectedLayoutSegment()
  const isActive = slug === segment
 
  return (
    <Link
      href={`/blog/${slug}`}
      // Change style depending on whether the link is active
      style={{ fontWeight: isActive ? 'bold' : 'normal' }}
    >
      {children}
    </Link>
  )
}

 

따라서 만약에 아래 코드에서 `post.slug`가 'hello-world'라면,

`/blog/hello-world`로 접속했을 때, 위의 NavLink의 isActive가 true가 되면서,

bold로 스타일링 된다.

// app/blog/layout.tsx
import { NavLink } from './nav-link'
import getPosts from './get-posts'
 
export default async function Layout({
  children,
}: {
  children: React.ReactNode
}) {
  const featuredPosts = await getPosts()
  return (
    <div>
      {featuredPosts.map((post) => (
        <div key={post.id}>
          <NavLink slug={post.slug}>{post.title}</NavLink>
        </div>
      ))}
      <div>{children}</div>
    </div>
  )
}

 

저작자표시 (새창열림)

'Web > Next.js' 카테고리의 다른 글

Next.js 15 / Image Component  (2) 2025.08.13
Next.js 15 / CSS Modules  (1) 2025.08.13
Next.js 15 + Supabase로 Kakao 로그인 구현하기 - 2  (1) 2025.08.11
Next.js 15 + Supabase로 Kakao 로그인 구현하기 - 1  (3) 2025.08.08
    'Web/Next.js' 카테고리의 다른 글
    • Next.js 15 / Image Component
    • Next.js 15 / CSS Modules
    • Next.js 15 + Supabase로 Kakao 로그인 구현하기 - 2
    • Next.js 15 + Supabase로 Kakao 로그인 구현하기 - 1
    KimMinJun
    KimMinJun

    티스토리툴바