layout 컴포넌트
React.js에서 App 컴포넌트에서 했던 일을 Next.js에서는 layout 컴포넌트가 해주고 있다고 생각하면 된다.
프로젝트를 처음 생성하고 나면 src/app 폴더 하위에서부터 layout.tsx 컴포넌트가 만들어져 있을 건데,
이곳에서 해당 레이아웃 컴포넌트가 있는 폴더 내부의 모든 파일들 (자식 요소들, children)에 대해서 공통된 레이아웃을 설정할 수 있다.
예를 들어 아래처럼 모든 페이지에 둥둥 떠있는 내비게이션 헤더를 만들 수도 있다.
import Link from 'next/link';
export default function Layout({ children }) {
return (
<div>
<nav>
<Link href="/home">홈</Link> ㅣ <Link href="/login">로그인</Link>
</nav>
<div>{children}</div>
</div>
);
}
위 사진을 보면 페이지는 두 개가 있음을 알 수 있다.
1) /dashboard
2) /dashboard/settings
그런데 layout 파일은 하나다.
settings 폴더 안에도 레이아웃 파일을 만들 수 있는데, 이렇게 되면 /dashboard/layout.js 파일과 /dashboard/settings/layout.js 파일이 중첩되어 사용된다.
즉 위 사진 상황에서는 dashboard 라우트 내에 존재하는 layout 파일이 두 개의 페이지에 함께 공유된다는 것을 알 수 있다.
만약 중첩된 레이아웃(Nesting Layout)을 만든다면 아래와 같은 구성을 하고 있을 것이다.
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return <section>{children}</section>
}
그림을 보면 좀 더 명확히 이해가 되는데, app 폴더에 바로 위치한 루트 레이아웃에서 헤더를 만들고, 대시보드 폴더에 있는 레이아웃에서 사이드바를 만들었다면, 앱 폴더 내에 있는 라우터에서 어디를 가도 헤더는 무조건 떠 있는데 /dashboard로 접근하면 헤더와 사이드바가 동시에 보이는 것이다.
즉 대시보드에 있는 레이아웃은 /dashboard라는 라우트에 대한 독립적인 레이아웃이면서 상위 레이아웃과 중첩시킬 수 있는 레이아웃이 된다.
이걸 코드로 표현하면 아래와 같을 것이다.
// app/layout.js
export default function RootLayout({ children }) {
return (
<html>
<head />
<body>
<header>Header</header>
<main>{children}</main>
<footer>Footer</footer>
</body>
</html>
)
}
// app/dashboard/layout.js
export default function DashboardLayout({ children }) {
return (
<div>
<aside>Sidebar</aside>
<section>{children}</section>
</div>
)
}
Root Layout(필수)
이렇게 앱 라우트 마다 별도로 설정하는 레이아웃 말고, 루트 단에서도 레이아웃이 있다. app 폴더에 바로 위치한 layout.js를 말하는데, 이 파일은 필수 파일이고 리액트에서는 app.tsx가 하던 역할을 한다. (확장자를 계속 혼용해서 이야기 하고 있는데, 공식 문서에서 js라고 나와 있어서 그런 것이고 실제로 사용하시는 확장자로 보시면 된다.)
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{/* Layout UI */}
<main>{children}</main>
</body>
</html>
)
}
루트 레이아웃은 위와 같이 구성되어 있다. children은 app 폴더 내에 존재하는 모든 페이지를 말한다.
그리고 이 루트 레이아웃에서는 페이지의 전체적인 메타데이터를 설정할 수도 있다.
그렇다면 페이지 마다 고유한 메타데이터도 동적으로 설정할 수 있나? ===>>> 있다.
그 외 레이아웃 컴포넌트의 특징
- 레이아웃 컴포넌트는 .js .jsx .tsx 파일 확장자 모두 지원한다.
- 루트 레이아웃에만 <html>과 <body>를 포함시킬 수 있다.
- 루트 레이아웃 파일은 앱 전체의 공통 레이아웃을 설정하기 때문에 가능하다.
- 그 외 하위 레이아웃 파일은 특정 라우트에 대한 라우트만 적용하기 때문에 <HTML> 태그와 <BODY> 태그는 사용할 수 없다. 오로지 상위 레이아웃 파일과 중첩된 레이아웃을 만들기 위해서만 사용된다. 여기에는 이 태그를 사용하지 않아도 루트 레이아웃의 <main> 태그 내에서 렌더링 된다.
- 이런 방식은 SEO 최적화에도 유리하다.
// app/layout.js
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<title>My App</title>
</head>
<body>
<header>Header</header>
<main>{children}</main>
<footer>Footer</footer>
</body>
</html>
);
}
// app/dashboard/layout.js
export default function DashboardLayout({ children }) {
return (
<div className="dashboard-layout">
<aside>Sidebar</aside>
<section>{children}</section>
</div>
);
}
- 레이아웃 컴포넌트는 별도 설정하지 않으면 기본으로 서버 컴포넌트로 설정된다. 하지만 클라이언트 컴포넌트로도 만들 수 있다. 'use client' 지시어를 명시하면 된다.
- 레이아웃 컴포넌트에서도 데이터 페칭이 가능하다. 레이아웃 컴포넌트의 영향을 받는 모든 페이지에서 필요한 데이터가 있다면 레이아웃 컴포넌트에서 데이터 페칭을 하면 모든 페이지에서 공통적으로 필요한 데이터를 미리 로드할 수 있다.
// app/layout.js
import React from 'react';
// 가정: 데이터 페칭 함수
async function fetchGlobalData() {
const response = await fetch('https://api.example.com/global');
return response.json();
}
export default async function RootLayout({ children }) {
const globalData = await fetchGlobalData();
return (
<html lang="en">
<head>
<title>My App</title>
</head>
<body>
<header>Header</header>
<main>
<GlobalDataContext.Provider value={globalData}>
{children}
</GlobalDataContext.Provider>
</main>
<footer>Footer</footer>
</body>
</html>
);
}
// 전역 데이터를 위한 Context 생성
const GlobalDataContext = React.createContext(null);
export function useGlobalData() {
return React.useContext(GlobalDataContext);
}
- 레이아웃 컴포넌트에서 데이터 페칭을 하는 경우, 페이지 컴포넌트에서 동일한 데이터 페칭을 하더라도 리액트가 이를 중복된 요청으로 생각하고 중복 요청을 제거하기 때문에 성능에 영향을 끼치지 않는다. 이를 Dedupe라고 한다.
// app/dashboard/layout.js
import React from 'react';
// 가정: 데이터 페칭 함수
async function fetchDashboardData() {
const response = await fetch('https://api.example.com/dashboard');
return response.json();
}
export default async function DashboardLayout({ children }) {
const dashboardData = await fetchDashboardData();
return (
<div className="dashboard-layout">
<aside>Sidebar</aside>
<section>
<h1>Dashboard Layout</h1>
<p>Dashboard Data: {JSON.stringify(dashboardData)}</p>
{children}
</section>
</div>
);
}
// app/dashboard/page.js
import React from 'react';
// 가정: 데이터 페칭 함수
async function fetchDashboardData() {
const response = await fetch('https://api.example.com/dashboard');
return response.json();
}
export default async function DashboardPage() {
const dashboardData = await fetchDashboardData();
return (
<div>
<h1>Dashboard Page</h1>
<p>Dashboard Data: {JSON.stringify(dashboardData)}</p>
</div>
);
}
- Route Groups를 사용하면 특정 경로 세그먼트를 공통 레이아웃에 포함하거나 제외 시킬 수 있다.
- 라우트 그룹은 (관심사) 형태로 폴더명을 지어주면 된다.
- 아래 폴더 구조를 보면 (marketing)과 (dashboard)라는 라우트 그룹이 존재하는데, 각각 라우트 그룹 안에 있는 최상위 레이아웃 파일이 해당 라우트 그룹 내에 있는 파일들에만 공통적으로 영향을 줄 수 있다. 예를 들어 /app/(marketing)/layout.ts 파일은 home과 about 라우트에만 레이아웃을 공유한다. dashboard는 그 layout만 공유한다.
app/
(marketing)/
layout.js
home/
page.js
about/
page.js
(dashboard)/
layout.js
stats/
page.js
settings/
page.js
- 참고로 app 폴더 안에 있는 루트 레이아웃 파일이 페이지 라우터 방식에서의 _app.tsx, _document.tsx의 역할을 하는 것이다.
'Programing > Next.js' 카테고리의 다른 글
detail page 동적 라우팅 기본 패턴 (0) | 2024.08.06 |
---|---|
<Image> 컴포넌트 (0) | 2024.08.05 |
<Link> 컴포넌트 (0) | 2024.07.31 |
개발 초기 아이템 리스트 스켈레톤 로더 구현하기 (0) | 2024.07.31 |
Next.js의 App Router와 Page Router의 차이 (0) | 2024.07.30 |
댓글