본문 바로가기

layout 컴포넌트

codeConnection 2024. 8. 1.

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의 역할을 하는 것이다.

댓글