[Next.js] #4 Routing
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의 한 부분을 말한다.
- 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가 존재한다.
- 템플릿을 통한 페이지 open animation
- 페이지 간 전환 시 애니메이션을 계속해서 주고 싶을 때 layout으로 만들어놓으면, 최초 렌더링시에만 animation이 적용되고 끝나버린다. 이 때, template을 사용한다.
- 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. 페이지 이동과 관련된 기능 목록
5.1 Link
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;
댓글남기기