10 분 소요


1. App router vs Pages route

1.1 Next.js의 버전의 주요 분기점

Next.js를 채택할 때 주요한 의사결정은 “App router, Pages router 둘 중 어떤 router을 사용할 것인가?” 에 있다.


Pages router는 page 폴더를 기준으로 라우팅이 된다.


app router는 app 폴더 밑에 폴더명을 기반으로 자동 라우팅이 된다.


1.2 app router와 pages router의 사용

app router와 pages router 어떤 것을 사용해야 할까?

  • 공식 홈페이지에서는 app router를 사용하라고 제시하고 있다.
  • Pages Router에서는 직접 캐싱을 구현해야 했지만, App Router를 사용시 캐싱처리를 해준다.



2. 라우팅 주요 용어

  • Tree
    • 계층 구조를 시각적으로 잘 보기 위한 규칙
  • Subtree
    • tree의 한 부분
    • root부터 시작해서 leaf들에 이르기까지의 범위
  • Root
    • Tree 또는 Subtree의 첫 번째 노드
    • root layout 같은 것이 된다.

  • Leaf
    • children이 더이상 없는 node를 말한다.
  • URL Segment
    • 슬래시(/)로 분류된 URL path의 한 부분을 말한다.

  • URL Path
    • 도메인(www.sample-web.com) 이후 따라오는 전체 URL 부분을 말한다.



3. 파일(폴더) 기반 라우팅

3.1 page.tsx

  • page.tsx는 main ui가 표시될 곳이다.
  • 따라서 앞으로 page.tsx 파일을 생성하면서 코딩을 하면 된다.


3.2 static routing

React에서 Routing을 구현하기 위해 우리는 react-router-dom 패키지를 설치하고 세팅을 했지만, next.js에서는 static routing을 제공하기 때문에, 세팅을 할 필요 없이 폴더를 기반으로 실행이 가능하다.

src > app 폴더 밑에 test 폴더를 새로 만들고 그 안에 page.tsx 파일을 만들어보자

// src > app > test > page.tsx
import React from "react";

// 페이지 컴포넌트 생성시 컨벤션 : 경로에 따른 이름 + 페이지
const TestPage = () => {
  return <div>하이하이 테스트페이지!</div>;
};

export default TestPage;

브라우저에서 접근하면 폴더 기반의 라우팅이 되는 것을 확인할 수 있다!


http://localhost:3000/a/b/c 경로로 접근했을 때 page가 나오게 하려면 어떻게해야할까?

a폴더 아래 b폴더 아래 c폴더 아래 page.tsx가 있다는 의미이므로 아래와 같은 방법으로 접근하면 된다.


3.3 dynamic routing

react-router-dom에서 특정 경로가 dynamic한 parameter에 의해 계속 변할 때 아래와 같이 설정해주었다.

const Router = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
        <Route path="/works" element={<Works />} />
        <Route path="/products/:id" element={<Product />} />
      </Routes>
    </BrowserRouter>
  );
};


하지만, Next에서는 폴더 이름을 대괄호로 감싸는 방식으로 라우팅 처리를 한다.

이렇게 하게 되면, 패턴에 일치하는 모든 경로를 페이지로 연결하게 된다.

app/posts/[id]/page.tsx 파일의 경우 /posts/1 /posts/2 등 경로에 대해 동적으로 페이지를 생성하게 되는 것이다.


직접 사용해보자

// app>test>[id]>page.tsx
import React from "react";

const TestDetailPage = ({
  params,
}: {
  params: {
    id: string;
  };
}) => {
  return <div>Detail page:{params.id}</div>;
};

export default TestDetailPage;


3.4 route groups

폴더 생성시 routing에 포함이 되지 않게 하기 위해서 대괄호 ()로 감싸주면 된다. (라우팅이 아닌 단순한 분류를 하고 싶을 때 사용한다!)

import React from "react";

const AdminAboutPage = () => {
  return <div>AdminAboutPage</div>;
};

export default AdminAboutPage;
  • http://localhost:3000/admin/about -> 404 error
  • http://localhost:3000/about -> 접근 가능

⚠️ 대괄호 ()로 감싼 폴더는 route groups이 아니기 때문에 다른 경로에 about page가 존재해서는 안된다.



3.5 speciall files

Next.js에서는 Routing을 폴더 기반, 즉 Nested Folders(중첩된 폴더)로 구현을 하고 있다. 이 과정에서 어떤 라우트에 대한 특정 처리를 위해 생성되는 여러 speciall files이 존재한다.

  • 특정 처리의 종류
    • 특정 경로 하위에 있는 routing은 모두 공통 layout을 적용하고 싶을 때
    • 특정 컴포넌트가 로딩중일 때, 오류가 발생했을 때 보여주고 싶은 UI가 있을 때 등


3.5.1 layout

layout파일은 어떤 segment와 그의 자식 node에 있는 요소들이 공통적으로 적용받게 할 UI를 정의한다.

자식 node에 있는 요소들이 공통 적용을 받아야 하기 때문에 반드시 children prop이 존재해야 한다. -동일 layout 안에서 다른 경로를 계속해서 왔다갔다 할 때 re-rendering이 일어나지 않는다.


React.js를 사용했을 때는 react-router-dom으로 이렇게 구현하였다.

createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Root />}>
      <Route path="contact" element={<Contact />} />
      <Route
        path="dashboard"
        element={<Dashboard />}
        loader={({ request }) =>
          fetch("/api/dashboard.json", {
            signal: request.signal,
          })
        }
      />
      <Route element={<AuthLayout />}>
        <Route path="login" element={<Login />} loader={redirectIfUser} />
        <Route path="logout" action={logoutUser} />
      </Route>
    </Route>
  )
);


Next.js를 사용하여 layout을 구현해보자
목표 : (admin) 하위 경로의 모든 페이지들은 admin의 style 영향을 받게 하기

먼저 (admin) 하위 경로에 새롭게 sample 폴더를 만들자.

// src > app > (admin) > sample > page.tsx
import React from "react";

const SamplePage = () => {
  return <div>코리🐶</div>;
};

export default SamplePage;


그 후, 해당 폴더에 layout.tsx 파일을 만들자


Layout이 children을 포함하고 있어야만 하위에 있는 컴포넌트들을 렌더링 할 수 있다. 따라서 아래와 같이 children을 포함시켜서 공통 UI를 만들자.

// src > app > (admin) > layout.tsx
import React from "react";

const AdminLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div>
      <p>🩷</p>
      {children}
    </div>
  );
};

export default AdminLayout;


Next.js 프로젝트를 생성하고 나면 이미 root 경로에 layout.tsx 파일이 존재하는데, 아래 코드를 입력해서 공통 레이아웃이 변경되는지 확인해보자.

// src > app > layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <nav>
          <a href="/">Home</a>
          <a href="/about">About</a>
          <a href="/contact">Contact</a>
          <a href="/blog">Blog</a>
        </nav>
        {children}
      </body>
    </html>
  );
}


3.5.2 template

template 파일은 layout과 상당히 유사한 컴포넌트이다.

  • 하지만, “상태를 유지”와 “리-렌더링” 에 있어서 차이점이 존재한다.
  • 경로 전반에 걸쳐서 상태가 유지되는 레이아웃과 달리, 템플릿은 라우팅을 탐색할 때 각 하위 항목에 대해 새 인스턴스를 만든다.
  • 즉, User 입장에서 동일한 Template을 공유하는 경로 사이를 왔다갔다 할 때 DOM 요소가 다시 생성된다는 것이다.


그래서 이런 use-case가 존재한다.

  1. 템플릿을 통한 페이지 open animation
    • 페이지 간 전환 시 애니메이션을 계속해서 주고 싶을 때 layout으로 만들어놓으면, 최초 렌더링시에만 animation이 적용되고 끝나버린다. 이 때, template을 사용한다.
  2. useEffect, useState에 의존하는 기능


layout vs template

테스트페이지 → 테스트 페이지 1 → 테스트 페이지 2 를 계속 왔다갔다 해보자

// src > app > test > layout.tsx
// src > app > test > template.tsx
"use client";

import Link from "next/link";
import React from "react";

const TestTemplate = ({ children }: { children: React.ReactNode }) => {
  useEffect(() => {
    console.log("최초 렌더링 한 번만 호출합니다.");
  }, []);

  return (
    <div className="m-8 p-8 bg-white">
      <h1>테스트 페이지</h1>
      <p>테스트 경로 하위에서의 이동을 확인해봅니다.</p>
      <nav>
        <ul>
          <li>
            <Link href="/test">테스트 페이지</Link>
          </li>
          <li>
            <Link href="/test/1">테스트 페이지 1</Link>
          </li>
          <li>
            <Link href="/test/2">테스트 페이지 2</Link>
          </li>
        </ul>
      </nav>
      {children}
    </div>
  );
};

export default TestTemplate;
  • layout을 사용하는 경우 : console.log가 최초 한 번만 호출된다.



  • template을 사용하는 경우 : 페이지를 이동할 때마다 호출된다.

➡️ 결론: 특정한 이유가 있지 않는 한, layout.tsx를 사용하기!


3.5.3 not-found

404 not found 페이지를 react-router-dom에서는 다음과 같이 세팅했었다.

<Router>
  <Routes>
    <Route path={"/home"} element={<Home />} />
    <Route path={"/*"} element={<h1>404: Not Found</h1>} />
  </Routes>
</Router>


next.js에서는 우리가 별도 설정을 하지 않아도 기본 스타일이 된 not found 페이지를 제공한다.


아래와 같이 직접 스타일을 변경할 수 있다.

// src > app > not-found.tsx
import React from "react";

const NotFound = () => {
  return <div>존재하지 않는 페이지입니다.</div>;
};

export default NotFound;



4. metadata와 SEO

Next.js는 향상된 SEO를 제공하기 위해서 html head에 삽입했었던 많은 정보를 metadata는 객체 형태로 지원하고 있다.


리액트에서는 vite를 쓰던 일반 CRA를 쓰던 index.html 파일이 존재했고, 이 파일의 head 태그에 SEO(Search Engine Optimization, 검색 엔진 최적화) 향상을 위해 여러 meta, link 태그 등 메타데이터를 작성했다.


하지만 Next.js에서는 Config-based Metadata를 활용할 수 있다. 두 가지 방법이 존재한다.

① page.tsx 또는 layout.tsx 어디든지 아래의 코드를 삽입하면 적용이 된다.

export const metadata: Metadata = {
  title: "타이틀 지정!!",
  description: "This is awesome Website",
};
  • metadata in page.tsx : 해당 page.tsx 컴포넌트에만 적용
  • metadata in layout.tsx : 해당 layout의 하위 요소에 모두 적용


page.tsx 케이스는 해당 페이지만 metadata의 적용을 받는다.

// src > app > page.tsx
import { Metadata } from "next";

export const metadata: Metadata = {
  title: "타이틀 지정!!",
  description: "This is awesome Website",
};

export default function Home() {
  return <div>안녕 Next.js! 반가워!</div>;
}


layout.tsx 케이스는 해당 layout의 하위 요소에 metadata를 공통으로 적용한다.

// src > app > layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "타이틀!",
  description: "Generated by create next app",
};

export default function RootLayout({

...기존코드


② dynamic

dynamic route를 갖고있는 route에서 동적으로 변경되는 params를 기반으로 metadata를 변경하고 싶을 때 generateMetadata function을 사용한다.

import React from "react";

type Props = {
  params: {
    id: string;
  };
};

export function generateMetadata({ params }: Props) {
  return {
    title: `Detail 페이지 : ${params.id}`,
    description: `Detail 페이지 : ${params.id}`,
  };
}

const TestDetailPage = ({ params }: Props) => {
  return <div>Detail 페이지 : {params.id}</div>;
};

export default TestDetailPage;

즉, 동적으로(dynamic) 들어온 params에 대해서도 metadata를 이용할 수 있는 것이다.

자세한 내용은 여기를 확인하자



5. 페이지 이동과 관련된 기능 목록

Next.js는 <Link>라는 리액트 컴포넌트를 제공한다. 크게 두 가지 주요 역할을 한다.

① Link 태그는 prefetching 기능을 지원한다.

  • Next.js의 <Link> 컴포넌트는 뷰포트에 링크가 나타나는 순간 해당 페이지의 코드와 데이터를 미리 가져오는 프리페칭⭐ 기능을 지원한다.
    • 뷰포트가 링크에 나타나는 순간이란 사용자가 웹 페이지를 스크롤하거나 페이지를 이동하면서 해당 링크가 실제로 사용자의 화면(즉, 뷰포트 내)에 보이기 시작하는 순간을 말한다.
  • 사용자가 링크를 클릭하기 전에 데이터를 미리 로드함으로써 사용자가 링크를 클릭했을 때 거의 즉시 페이지를 볼 수 있게 한다.


② Link 태그는 route 사이에 client-side navigation을 지원한다.

  • <Link> 컴포넌트는 브라우저가 새 페이지를 로드하기 위해 서버에 요청을 보내는 대신, 클라이언트 측에서 페이지를 바꾸어 주기 때문에 페이지 전환 시 매우 빠른 사용자 경험(UX)을 제공한다.
  • 페이지의 HTML을 서버에서 다시 가져올 필요 없이, 필요한 JSON 데이터만 서버로부터 가져와서 클라이언트에서 페이지를 재구성하여 렌더링한다.
  • 결국 Link 태그는 a 태그를 만들어내기 때문에 SEO가 유리하다.

  • 변경 전 (a태그 쓰지 말것!!)
// src > app > layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Sparta Next App",
  description: "This is awesome Website",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <nav>
          <a href="/">Home</a>
          <a href="/about">About</a>
          <a href="/contact">Contact</a>
          <a href="/blog">Blog</a>
        </nav>
        {children}
      </body>
    </html>
  );
}


  • 변경 후
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Link from "next/link";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Sparta Next App",
  description: "This is awesome Website",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <nav>
          <Link href="/">Home</Link>
          <Link href="/about">About</Link>
          <Link href="/contact">Contact</Link>
          <Link href="/blog">Blog</Link>
        </nav>
        {children}
      </body>
    </html>
  );
}


5.2 useRouter

useRouter를 사용할 때는 항상 코드 최상단에 “use client”를 삽입해야 한다!⭐

  • a 태그를 알아차릴 수 없기 때문에 크롤러 입장에서는 해당 요소가 “이동을 원한다”라는 것을 알 수 없다. ⇒ SEO 불리
  • 대부분 onClick 같은 이벤트 핸들러에서 사용한다.
  • 클릭 후 로직의 순서에 따라 실행하므로, 즉시 이동이 아니다.
"use client";

import { useRouter } from "next/navigation";

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

	const handleButtonClick = () => {
		로직1();
		로직2();

		...

		router.push("/new_location");
	}

	return <button onClick={handleButtonClick}>클릭!</button>
}


"use client";

import { useRouter } from "next/navigation";
import React, { useEffect } from "react";

const TestTemplate = ({ children }: { children: React.ReactNode }) => {
  const router = useRouter();

  useEffect(() => {
    console.log("최초 렌더링 한 번만 호출합니다.");
  }, []);

  return (
    <div className="m-8 p-8 bg-black">
      <nav>
        <ul>
          <li
            onClick={() => {
              router.push("/test");
            }}
          >
            테스트 페이지
          </li>
          <li
            onClick={() => {
              router.push("/test/1");
            }}
          >
            테스트 페이지 1
          </li>
          <li
            onClick={() => {
              router.push("/test/2");
            }}
          >
            테스트 페이지 2
          </li>
        </ul>
      </nav>
      {children}
    </div>
  );
};

export default TestTemplate;


5.3 history stack

  • router.push
    • 새로운 URL을 히스토리 스택에 추가한다.
    • 사용자가 router.push로 페이지를 이동하면, 이동한 페이지의 URL이 히스토리 스택의 맨 위에 쌓인다.
    • 이후 사용자가 브라우저의 ‘뒤로 가기’ 버튼을 클릭하면, 스택에서 가장 최근에 추가된 URL로부터 이전 페이지(URL)로 돌아간다.
  • router.replace
    • 현재 URL을 히스토리 스택에서 새로운 URL로 대체한다.
    • 현재 페이지의 URL이 새로운 URL로 교체되며, ‘뒤로 가기’를 클릭했을 때 이전 페이지로 이동하지만, 교체된 페이지로는 돌아갈 수 없다.
    • 현재 페이지를 히스토리에서 완전히 대체한다.
  • router.back
    • 사용자를 히스토리 스택에서 한 단계 뒤로 이동시킨다.
    • 마치 브라우저의 ‘뒤로 가기’ 버튼을 클릭한 것과 같은 효과를 내며, 사용자를 이전에 방문했던 페이지로 돌아가게 한다.
  • router.reload
    • 현재 페이지를 새로고침한다.
    • 히스토리 스택에 영향을 미치지 않는다.
    • 페이지의 데이터를 최신 상태로 업데이트하고 싶을 때 사용할 수 있다.

⭐ 로그아웃시 이전 페이지로 이동할 필요가 없으니 replace가 효율적이다!


5.4 usePathname()를 사용한 활성화 된 링크 표시

사용자에게 현재 어떤 페이지에 있는지 나타내기 위해 활성화된 링크를 표시하는 것은 일반적인 UI 패턴이다.

  • 이를 위해 사용자의 현재 경로를 URL에서 가져와야 하는데, Next.js의 usePathname() 훅을 사용하여 현재 경로를 확인하고 활성화된 링크를 구현할 수 있다.
  • [clsx 라이브러리↗️]를 사용하여 조건부 클래스를 적용하였다.


// app/components/layouts/Header.tsx

"use client"; // 클라이언트 컴포넌트로 변환

import Link from "next/link"; // Link 컴포넌트 가져오기
import { usePathname } from "next/navigation"; // usePathname 훅 가져오기
import clsx from "clsx"; // clsx 라이브러리 가져오기

const Navbar = () => {
  const pathname = usePathname(); // 현재 경로를 pathname 변수에 할당

  return (
    <nav>
      <ul className="flex space-x-4">
        <li>
          <Link
            href="/"
            className={clsx(
              "p-2 rounded", // 기본 스타일
              { "bg-sky-100 text-blue-600": pathname === "/" } // 활성화된 링크 스타일
            )}
          ></Link>
        </li>
        <li>
          <Link
            href="/about"
            className={clsx(
              "p-2 rounded", // 기본 스타일
              { "bg-sky-100 text-blue-600": pathname === "/about" } // 활성화된 링크 스타일
            )}
          >
            소개
          </Link>
        </li>
        <li>
          <Link
            href="/contact"
            className={clsx(
              "p-2 rounded", // 기본 스타일
              { "bg-sky-100 text-blue-600": pathname === "/contact" } // 활성화된 링크 스타일
            )}
          >
            연락처
          </Link>
        </li>
      </ul>
    </nav>
  );
};

export default Navbar;


댓글남기기