[Next.js] Routing
1. Next.js 설치
공식홈페이지의 내용을 참조하여 설치하자 클릭
npx create-next-app@latest
그 후, 아래와 같이 세팅하기!
파일 구조!
2. Next.js 특징
2.1 layout, page 컴포넌트
페이지를 구성하는 layout, page 컴포넌트의 역할을 살펴보자.
- layout.tsx
- 리액트 컴포넌트이다.
- children을 가지고 있다.
- page.tsx
- 리액트 컴포넌트이다.
src > app > page.tsx 파일을 다음과 같이 수정해보자
export default function Home() {
return <div>안녕 Next.js! 반가워!</div>;
}
그 후 아래 명령어로 실행하기!
yarn dev
page.tsx 파일이 UI를 핸들링 하는 주 파일이라는 것을 알 수 있다.
2.2 package.json 파일
package.json 파일의 scripts > dev / build / start를 살펴보자
- dev(npm run dev) : 개발자가 개발하는 중 사용하게 될 방법이다.
- build(npm run build) : production 레벨로 배포하기 전 필요한 빌드 작업 과정을 실행하기 위한 방법이다.
- start(npm run start) : 만들어진 build 파일을 이용하여 실행시키는 방법이다.
따라서, build하지 않고 start를 하는 경우 실행될 수 없다.
3. 라우팅 이해하기
3.1 주요 용어
- 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.2 파일(폴더) 기반 라우팅
3.2.1 page.tsx
- page.tsx는 main ui가 표시될 곳이다.
- 따라서 앞으로 page.tsx 파일을 생성하면서 코딩을 하면 된다.
3.2.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.2.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.2.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.3 speciall files
Next.js에서는 Routing을 폴더 기반, 즉 Nested Folders(중첩된 폴더)로 구현을 하고 있다. 이 과정에서 어떤 라우트에 대한 특정 처리를 위해 생성되는 여러 speciall files이 존재한다.
- 특정 처리의 종류
- 특정 경로 하위에 있는 routing은 모두 공통 layout을 적용하고 싶을 때
- 특정 컴포넌트가 로딩중일 때, 오류가 발생했을 때 보여주고 싶은 UI가 있을 때 등
3.3.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.3.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.3.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;
3.3.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를 이용할 수 있는 것이다.
자세한 내용은 여기를 확인하자
4. 페이지 이동과 관련된 기능 목록
4.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>
);
}
4.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;
4.2.1 history stack
- router.push
- 새로운 URL을 히스토리 스택에 추가한다.
- 사용자가 router.push로 페이지를 이동하면, 이동한 페이지의 URL이 히스토리 스택의 맨 위에 쌓인다.
- 이후 사용자가 브라우저의 ‘뒤로 가기’ 버튼을 클릭하면, 스택에서 가장 최근에 추가된 URL로부터 이전 페이지(URL)로 돌아간다.
- router.replace
- 현재 URL을 히스토리 스택에서 새로운 URL로 대체한다.
- 현재 페이지의 URL이 새로운 URL로 교체되며, ‘뒤로 가기’를 클릭했을 때 이전 페이지로 이동하지만, 교체된 페이지로는 돌아갈 수 없다.
- 현재 페이지를 히스토리에서 완전히 대체한다.
- router.back
- 사용자를 히스토리 스택에서 한 단계 뒤로 이동시킨다.
- 마치 브라우저의 ‘뒤로 가기’ 버튼을 클릭한 것과 같은 효과를 내며, 사용자를 이전에 방문했던 페이지로 돌아가게 한다.
- router.reload
- 현재 페이지를 새로고침한다.
- 히스토리 스택에 영향을 미치지 않는다.
- 페이지의 데이터를 최신 상태로 업데이트하고 싶을 때 사용할 수 있다.
⭐ 로그아웃시 이전 페이지로 이동할 필요가 없으니 replace가 효율적이다!
댓글남기기