1. 라우팅 그룹
app(root segment)내의 서브트리 구조내에서
개발자의 관리 편의성을 높히기 위해 디렉터리를 그룹화하여 다룰 수 있는 기능을 의미합니다.
(docs) 세그먼트처럼 "(", ")"로 감싼 상위 디렉터리는
✘ localhost:3000/(doc)/guides로
표시되지 않고
✔︎ localhost:3000/guides로
표시됩니다.
즉, (docs)디렉터리는 url path에 영향을 미치지않고
개발 프로젝트의 디렉터리 구조 내에서 그룹의 이름표 역할을 해주어
(docs)는 도큐먼츠를 의미하니 문서들을 모아둔 그룹이라고 볼 수 있겠네요.
해당 그룹의 용도나 기능 등을 바로 파악할 수 있도록 도와주는 기능입니다.
위 그림의 예시에선 마케팅과 숍을 나누어 관리하는 그룹이군요.
1. 라우팅 그룹 연습
최상위 경로에 있는 /tailwind.config.js의 내용입니다.
const { fontFamily } = require("tailwindcss/defaultTheme")
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{ts,tsx}",
],
darkMode: ["class"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
},
}
app/(docs)guides/layout.tsx와 app/(docs)/guides/page.tsx
// app/(docs)/guides/page.tsx
const Page = () => {
return (
<div className="py-8">
<h1 className="text-3xl font-bold mb-4">
How to Use Next.js 13 App Directory Catch-All Routes
</h1>
<p className="text-lg">
Next.js 13 introduced a new feature called "app directory catch-all
routes" that allows you to create dynamic routes based on the file
system in the `pages` directory. Here's how you can use it:
</p>
<h2 className="text-2xl font-bold mt-8 mb-4">
Step 1: Create the App Directory
</h2>
<p className="text-lg">
In your Next.js project, create a new directory called `app` in the root
directory. This is where you'll define your app-specific pages and
catch-all routes.
</p>
<h2 className="text-2xl font-bold mt-8 mb-4">
Step 2: Define the Catch-All Route
</h2>
<p className="text-lg">
Inside the `app` directory, create a file named `[...slug].js` (or
`[...slug].tsx` if you're using TypeScript). This file will act as the
catch-all route and handle all dynamic routes under the `app` directory.
</p>
<p className="text-lg">
Here's an example of how you can define the catch-all route file:
</p>
<pre className="bg-gray-200 p-4 rounded-lg mt-4">
<code className="text-sm font-mono text-gray-800">
{`// pages/app/[...slug].js\n\n`}
{`import React from "react";\n\n`}
{`const AppPage = ({ query }) => {\n`}
{` // Access the dynamic route parameters from the query object\n`}
{` const { slug } = query;\n\n`}
{` return (\n`}
{` <div>\n`}
{` <h1>{slug.join("/")}</h1>\n`}
{` {/* Render your app-specific content here */}\n`}
{` </div>\n`}
{` );\n`}
{`};\n\n`}
{`export default AppPage;\n`}
</code>
</pre>
<h2 className="text-2xl font-bold mt-8 mb-4">
Step 3: Accessing the Dynamic Route Parameters
</h2>
<p className="text-lg">
Inside the catch-all route file, you can access the dynamic route
parameters by destructuring the `query` object passed as a prop to the
page component. In the example above, we're accessing the `slug`
parameter, which will be an array of path segments. You can use it to
render your app-specific content dynamically based on the route.
</p>
<h2 className="text-2xl font-bold mt-8 mb-4">
Step 4: Add Content and Functionality
</h2>
<p className="text-lg">
You can now add your app-specific content and functionality within the
catch-all route component. This can include fetching data, rendering
components, and handling user interactions.
</p>
<h2 className="text-2xl font-bold mt-8 mb-4">Step 5: Test and Deploy</h2>
<p className="text-lg">
Once you've defined your catch-all route and added your app-specific
content, you can test it locally by running your Next.js app. You can
navigate to different dynamic routes under the `app` directory and see
the content rendered dynamically.
</p>
<p className="text-lg">
When you're ready, you can deploy your Next.js app with the catch-all
routes to a hosting provider of your choice. Make sure your hosting
provider supports Next.js 13 or later.
</p>
<p className="text-lg mt-8">
That's it! You've successfully set up and used Next![](https://velog.velcdn.com/images/jay/post/39d8edd1-42ca-4341-bb9d-9bd56f12354f/image.png)
.js 13 app directory
catch-all routes. You can now create dynamic routes under the `app`
directory and render app-specific content based on those routes.
</p>
</div>
)
}
export default Page
app/(docs)guides/layout.tsx와 app/(docs)/guides/layout.tsx
// app/(docs)guides/layout.tsx
import { ReactNode } from "react"
type LayoutProps = {
children: ReactNode
}
const Layout = ({ children }: LayoutProps) => {
return (
<div className="flex flex-col min-h-screen">
<header className="bg-primary py-4">
<div className="container mx-auto">
{/* Your header content goes here */}
</div>
</header>
<main className="flex-grow container mx-auto">{children}</main>
<footer className="bg-primary py-4">
<div className="container mx-auto">
{/* Your footer content goes here */}
</div>
</footer>
</div>
)
}
export default Layout
파일 생성 후 localhost:3000/guides로 접속해보면,
(docs) 하위 폴더에 생겼음에도 불구하고
url path에는 라우팅 그룹이 노출되지 않음을 알 수 있습니다.
2. 페이지 네비게이션
import Link from 'next/link';
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>;
}
Link 기능을 통해 지정한 경로로 이동시키는 기능입니다.
1. 페이지 네비게이션 Link
app/layout.tsx 아래와 같이 작성하여 Link 기능을 구현해봅니다.
import { Inter } from "next/font/google"
import Link from "next/link"
import "./globals.css"
const inter = Inter({ subsets: ["latin"] })
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
}
const links: Array<{ href: string; key: string }> = [
{
href: "/",
key: "home",
},
{
href: "/dashboard",
key: "dashboard",
},
{
href: "/guides",
key: "guides",
},
{
href: "/login",
key: "login",
},
]
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
{/* <div style={{ color: "orange" }}>/app/layout.tsx 적용</div> */}
<header>
<div>
<ul className="flex bg-slate-500">
{links.map(({ href, key }) => (
<li className="flex-auto">
<Link href={href} key={key}>
{key}
</Link>
</li>
))}
</ul>
</div>
</header>
{children}
</body>
</html>
)
}
app/page.tsx를 아래와 같이 작성하여 Link 기능을 구현해봅니다.
export default function Home() {
return <div>home page</div>
}
각 파일을 수정후 / url path로 이동하니 아래와 같이 헤더가 생겼습니다.
현재 디렉터리의 구조는 다음과 같습니다.
Link 컴포넌트에 클릭 이벤트를 발생시키지 않고 호버링만 했는데도
해당 페이지에서 사용하는 파일을 미리 다운로드 받아올 수 있습니다.
필수적으로 알아야 할 api는 아닙니다만
현재 우리 페이지는 루트 레이아웃 파일을 직접 보지 않는 한 어느 링크 태그가 동작한 상태인지 알기 어렵습니다.
현재 어떤 링크를 사용하고 있는지 tailwind와 usePathname을 통해 표기해보겠습니다.
usePathname은 현재 접속중인 url path의 이름을 알려주는 훅입니다.
app 디렉토리 하위에 ui 디렉토리를 만들었습니다.
우리가 사용하는 클라이언트 컴포넌트는 ui 디렉토리에 생성할 것입니다.
url path로 사용할 것이 아니고 클라이언트 컴포넌트를 저장할 디렉토리로 사용할 것이기 때문에
세그먼트라고 부르지 않고 디렉토리라고 부릅니다.
app/ui/Navigation.tsx 파일을 생성합니다.
"use client"
import Link from "next/link"
import { usePathname } from "next/navigation"
type NavigationProps = {
links: Array<{ href: string; key: string }>
}
export const Navigation = ({ links }: NavigationProps) => {
const pathname = usePathname()
return (
<ul className="flex bg-slate-500">
{links.map(({ href, key }) => {
const isActive = pathname == href
return (
<li
className={`flex-auto ${isActive ? "text-blue-600" : "text-white"}`}
>
<Link href={href} key={key}>
{key}
</Link>
</li>
)
})}
</ul>
)
}
여기서 🔥생소한 키워드🔥가 등장했습니다.
"use client";는 next.js에게 이 컴포넌트가 서버 컴포넌트가 아니라 클라이언트 컴포넌트라는 것을 알려줍니다.
use*로 시작하는 리엑트 훅은 이 클라이언트 컴포넌트 내부에서만 사용되어야 하며 서버 컴포넌트에서는 실행시킬 수 없습니다.
usePathname을 통해 현재 pathname을 확인하고 방문한 페이지의 url path가 동일할 때 링크의 색상을 다르게 해주었습니다.
그리고 기존에 링크 컴포넌트들이 있던 root/layout.tsx에서는 위 클라이언트 컴포넌트를 가져다가 사용하는 코드로 수정하겠습니다.
주석친 부분은 Navigation.tsx로 옮겨간 부분입니다.
...
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
console.log("rerender root layout")
return (
<html lang="en">
<body className={inter.className}>
<header>
<div>
<Navigation links={links} />
{/* <ul className="flex bg-slate-500">
{links.map(({ href, key }) => (
<li className="flex-auto">
<Link href={href} key={key}>
{key}
</Link>
</li>
))}
</ul> */}
</div>
</header>
{children}
</body>
</html>
)
}
2. 페이지 네비게이션 useRouter()
앞서 설명했듯이 useRouter는 리엑트 훅이기 떄문에 클라이언트 컴포넌트에서만 사용이 가능합니다.
useRouter는 push나 refresh와 같은 api를 사용할 수 있습니다.
Link 태그가 선언적인 방법으로 페이지간 네비게이션을 가능하게 한다면,
useRouter훅을 이용해서는 버튼 클릭 뿐만이 아닌 특정한 상황에서도 페이지 네비게이션을 사용할 수 있게 합니다.
router.push
programmatic한 방법으로 네비게이션을 할 수 있다는 이야기는,
Link 태그가 선언적인 방법으로 Link 태그 자체만을 이용해 네비게이션을 수행해야 한다면,
useRouter는 router.push를 이용해 필요한 태그 혹은 상황에 맞춰 네비게이션 로직을 적용시킬 수 있다는 말입니다.
다음과 같이 app/ui 하위에 LinkButton.tsx를 만들고 네비게이션 기능을 제공하는 LinkButton을 만들어보겠습니다.
// app/ui/LinkButton.tsx
"use client"
import { useRouter } from "next/navigation"
export default function LinkButton({ to }: { to: string }) {
const router = useRouter()
return (
<button type="button" onClick={() => router.push(to)}>
Dashboard
</button>
)
}
router.replace
클라이언트 사이드 네비게이션을 이용한 라이브러리인 react-router-dom을 사용해보신 분들이라면 useRouter훅에 익숙하실 겁니다.
이러한 useRouter 훅은 내부적으로 history api를 사용하는데 replace는 browser history stack을 쌓지 않아 네비게이션 후 뒤로가기를 눌러도 이전 페이지로 이동되지 않습니다.
router.refresh
현재 라우트를 기준으로 페이지를 새로고침합니다. data도 다시 받아오고 서버컴포넌트도 다시 리렌더링 합니다.
실습
앞서 만들었던 LinkButton을 refresh, replace 할 수 있게 변경한 다음 push, replace, refresh가 어떻게 다른지 테스트 해보겠습니다.
"use client"
import { useRouter } from "next/navigation"
type LinkButtonProps = {
to: string
replace?: boolean
refresh?: boolean
children: React.ReactNode
}
export default function LinkButton({
to,
replace,
refresh,
children,
}: LinkButtonProps) {
const router = useRouter()
const navigate = replace
? router.replace
: refresh
? router.refresh
: router.push
return (
<button
type="button"
onClick={() => navigate(to)}
className="flex-auto m-3"
>
{children}
</button>
)
}
replace, fresh를 prop로 받습니다.
Push
replace