Frontend/Next.js

[Next.js] 1. Next.js 프로젝트와 Page Router 기본

ayeongjin 2025. 2. 1. 01:54

1. Page Router란?

  • pages/ 폴더 기반으로 자동 라우팅이 설정됩니다.
  • pages/ 내부의 파일명과 경로가 곧 URL 경로가 됩니다.
  • App Router (app/) 방식과 공존할 수 있으며, 프로젝트에 따라 선택적으로 사용할 수 있습니다.

Next.js 13 이후

  • app/ 폴더를 사용하면 App Router (서버 컴포넌트 기반) 방식
  • pages/ 폴더를 사용하면 Page Router (기존 방식) 유지 가능
  • 프로젝트 요구사항에 따라 app/과 pages/를 함께 사용할 수도 있음

 


 

2. Next.js 프로젝트 시작하기

 

1) 프로젝트 생성

npx create-next-app@14 my-next-app
cd my-next-app

npx: 최신 버전의 Node 패키지를 설치 없이 실행할 수 있도록 도와주는 도구
create-next-app: 새로운 Next.js 프로젝트를 생성
@14: Next.js 14 버전 지정

 

2) 프로젝트 실행

개발자 모드로 실행하려면:

npm run dev

프로덕션 모드로 실행하려면:

npm run build
npm run start

애플리케이션은 http://localhost:3000에서 실행됩니다.

 


 

3. Next.js 프로젝트 구조와 라우팅

 

1) 프로젝트 구조

my-next-app/
├── public/          # 정적 파일(이미지, 아이콘 등)
├── src/             # 소스 코드 (Next.js 13 이후 권장 구조)
│   ├── app/         # App Router (Next.js 13 이상)
│   ├── components/  # 재사용 가능한 컴포넌트
│   ├── pages/       # Page Router 방식
│   ├── styles/      # 스타일 파일
│   ├── api/         # API 라우트
├── .gitignore       # Git 무시할 파일 설정
├── package.json     # 프로젝트 설정 및 의존성 관리
├── next.config.js   # Next.js 설정 파일
├── README.md        # 프로젝트 설명

 

2) 페이지 기반 라우팅

Next.js에서는 pages/ 폴더 안의 파일이 자동으로 라우팅됩니다.

 

파일 경로 라우팅 경로
pages/index.tsx /
pages/book/index.tsx /book
pages/book/[id].tsx /book/:id (동적 라우트)
pages/book/[...id].tsx /book/:id, /book/:id/:id2, /book/:id/:id2/:id3 (다중 필수 동적 라우트)
pages/book/[[...id]].tsx /book, /book/:id, /book/:id/:id2 (다중 선택적 동적 라우트)

 


 

4. 주요 파일

 

1) _document.tsx

Next.js에서 _document.tsx 파일을 사용하여 HTML 문서의 기본 구조를 설정할 수 있습니다.

import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html lang="ko">
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

 

2) _app.tsx

  • Next.js에서 _app.tsx는 모든 페이지의 진입점 역할을 합니다. 즉, 모든 페이지가 렌더링될 때 _app.tsx가 먼저 실행됩니다.
  • 모든 페이지의 공통 레이아웃을 설정하는 역할을 합니다.
import GlobalLayout from "@/components/global-layout";
import "@/styles/globals.css";
import { NextPage } from "next";
import type { AppProps } from "next/app";
import { ReactNode } from "react";

type NextPageWithLayout = NextPage & {
  getLayout?: (page: ReactNode) => ReactNode;
};

export default function App({ Component, pageProps }: AppProps & { Component: NextPageWithLayout }) {
  const getLayout = Component.getLayout ?? ((page) => <GlobalLayout>{page}</GlobalLayout>);
  return getLayout(<Component {...pageProps} />);
}
  • 매개변수: { Component, pageProps }
    • Component: 현재 렌더링되는 페이지 컴포넌트 (pages/ 폴더 안의 파일)
    • pageProps: 각 페이지에서 전달받는 props (데이터 패칭 결과 등)
AppProps & { Component: NextPageWithLayout }
export default function App({ Component, pageProps }: AppProps & { Component: NextPageWithLayout }) {

✅ Component는 NextPageWithLayout 타입을 가지므로, getLayout을 선택적으로 포함할 수 있음.

✅ getLayout을 활용하면 페이지마다 다른 레이아웃을 적용할 수 있습니다.

 


 

5. 레이아웃 적용

 

1) getLayout 동작 방식

const getLayout = Component.getLayout ?? ((page: ReactNode) => page);
  • Component.getLayout이 존재하면 사용
  • 없으면 기본적으로 ((page: ReactNode) => page)를 사용하여 페이지 그대로 반환
const getLayout = Component.getLayout ?? ((page: ReactNode) => page);

이렇게 하면 모든 페이지에서 getLayout을 필수로 지정하지 않아도 되고, 기본값을 적용할 수 있음.

 

2) return 부분: <GlobalLayout> 적용

return <GlobalLayout>{getLayout(<Component {...pageProps} />)}</GlobalLayout>;
  • getLayout(<Component {...pageProps} />)
    • Component(현재 페이지)를 getLayout 함수에 전달
    • getLayout이 설정된 경우, 해당 레이아웃으로 감싸짐
    • 없으면 ((page) => page)가 기본값이므로 그대로 반환됨

 

✅ GlobalLayout을 사용하는 페이지 (/)

// pages/index.tsx
import GlobalLayout from "@/components/global-layout";
import { ReactNode } from "react";

export default function Home() {
  return <h1>홈 페이지</h1>;
}

// getLayout을 정의하여 GlobalLayout 적용
Home.getLayout = (page: ReactNode) => <GlobalLayout>{page}</GlobalLayout>;
  • getLayout이 존재하므로:이 부분은 <GlobalLayout><Component {...pageProps} /></GlobalLayout>이 됨.
  • tsx 복사편집 getLayout(<Component {...pageProps} />)
  • 그리고 _app.tsx에서 또 <GlobalLayout>을 감싸고 있으므로 중첩됨 😵‍💫⏩ 결과적으로 <GlobalLayout>이 두 번 적용됨! 🚨
  • tsx 복사편집 return <GlobalLayout>{getLayout(<Component {...pageProps} />)}</GlobalLayout>;

 

3) GlobalLayout 중첩 문제 해결

GlobalLayout을 무조건 감싸면 getLayout이 적용된 페이지에서도 중첩될 수 있다.

이를 해결하려면 getLayout이 없을 때만 GlobalLayout을 적용하도록 수정해야 한다.

 

✅ 수정된 _app.tsx

export default function App({ Component, pageProps }: AppProps & { Component: NextPageWithLayout }) {
  const getLayout = Component.getLayout ?? ((page) => <GlobalLayout>{page}</GlobalLayout>);
  return getLayout(<Component {...pageProps} />);
}

이제 getLayout이 없을 때만 기본적으로 GlobalLayout을 적용한다.

 

결과

  • / 페이지는 GlobalLayout을 직접 적용하므로 _app.tsx에서 다시 감쌀 필요 없음.
  • /search 페이지 같은 경우 SearchableLayout을 적용하면 GlobalLayout 없이 동작.

 

4) getLayout을 적용한 페이지 예제

 

(1) / 페이지 (기본 홈)

// pages/index.tsx
import GlobalLayout from "@/components/global-layout";
import { ReactNode } from "react";

export default function Home() {
  return <h1>홈 페이지</h1>;
}

// GlobalLayout 적용
Home.getLayout = (page: ReactNode) => <GlobalLayout>{page}</GlobalLayout>;

✅ getLayout이 설정되었으므로 _app.tsx에서 중복 적용되지 않음.

 

(2) /search 페이지 (검색)

// pages/search/index.tsx
import SearchableLayout from "@/components/searchable-layout";
import { ReactNode } from "react";

export default function Page() {
  return <h1>검색 페이지</h1>;
}

// SearchableLayout 적용
Page.getLayout = (page: ReactNode) => <SearchableLayout>{page}</SearchableLayout>;

✅ _app.tsx에서는 GlobalLayout을 적용하지 않으므로 SearchableLayout만 적용됨

 


 

6. 스타일 적용법

CSS Module 사용 예제

// index.tsx
import style from "./index.module.css";

export default function Home() {
  return (
    <>
      <h1 className={style.h1}>인덱스</h1>
      <h2 className={style.h2}>H2</h2>
    </>
  );
}
/* index.module.css */

.h1 {
  color: red;
}
.h2 {
  color: blue;
}

 


 

7. 페이지 이동 방법

 

1) Link 컴포넌트 사용

<Link href="/">홈으로 이동</Link>

 

2) router.push() 사용 (프로그래매틱 라우팅)

import { useRouter } from "next/router";

export default function Page() {
  const router = useRouter();

  return <button onClick={() => router.push("/test")}>Go to Test</button>;
}

✅ router.push()를 사용하면 프로그래매틱한 라우팅이 가능합니다.

 

3) 프리페칭 (Prefetching)

Next.js에서는 기본적으로 Link 컴포넌트가 보이는 경우, 해당 페이지를 미리 로드합니다.

<Link href="/about" prefetch>About 페이지로 이동</Link>

프리페칭을 비활성화하려면:

<Link href="/about" prefetch={false}>About 페이지로 이동</Link>

프리페칭을 활용하면 페이지 전환 속도를 빠르게 만들 수 있습니다.

 


 

8. 404 오류 페이지

404 페이지는 기본적으로 pages/404.tsx 파일을 생성하여 설정할 수 있습니다.

// 404.tsx
export default function Page() {
  return <div>존재하지 않는 페이지입니다.</div>;
}

✅ next.config.js에서 trailingSlash: true를 설정하면 /404/ 경로를 명확히 처리할 수 있습니다.

module.exports = {
  trailingSlash: true,
};