
기존에 SPA 방식에서는 route설정을 별도로 라이브러리(react-router, vue-router) 설치를 통해 설정이 필요했다. Next.js에서 라우터 설정을 하는 방식을 정리해보려고 한다.
js, jsx,ts,tsx로 부터 export된 리액트 컴포넌트들은 각 페이지가 되며 route와 관련되어 있다.
만약 pages/about.jsx 로 파일을 만들었다면 /about path로 접근이 가능하다.
export default function About() {
return <div>About</div>
}
파일명이 index라면 부모의 directory명을 따라 가게 된다.
pages/index.js → /pages/blog/index.js → /blog중첩된 파일들은 중첩된 라우트를 제공한다. 파일 구조에 따라 중첩된 라우트 구조를 가지게 된다.
pages/blog/first-post.js → /blog/first-postpages/dashboard/settings/username.js → /dashboard/settings/username리엑트로 페이지 구조를 짜게되면 공통인 레이아웃들이 생기게 되고 그렇게 되면 재사용 되는 컴포넌트들이 많아진다. navigation, footer와 같은 공통 컴포넌트들이 매번 페이지에 들어가게 된다. 이럴때 layout을 제공해준다.
import Navbar from './navbar'
import Footer from './footer'
export default function Layout({ children }) {
return (
<>
<Navbar />
<main>{children}</main>
<Footer />
</>
)
}
예시
Custom App에 하나의 레이아웃을 사용할때 재사용되는 레이아웃 컴포넌트를 감싸 다음과 같이 사용할 수 있다.
import Layout from '../components/layout'
export default function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
페이지당 레이아웃을 사용할 경우엔 getLayout 속성을 사용하여 적용할 수 있다.
import Layout from '../components/layout'
import NestedLayout from '../components/nested-layout'
export default function Page() {
return (
/** Your content */
)
}
Page.getLayout = function getLayout(page) {
return (
<Layout>
<NestedLayout>{page}</NestedLayout>
</Layout>
)
}
export default function MyApp({ Component, pageProps }) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout || ((page) => page)
return getLayout(<Component {...pageProps} />)
}
페이지와 페이지간의 이동시에 상태(입력 값, 스크롤 위치 등)를 유지하려고 한다. 이 레이아웃 패턴은 리액트 구성 요소 트리가 유지되므로 상태를 지속 가능하게 해준다.
// pages/index.tsx
import type { ReactElement } from 'react'
import Layout from '../components/layout'
import NestedLayout from '../components/nested-layout'
import type { NextPageWithLayout } from './_app'
const Page: NextPageWithLayout = () => {
return <p>hello world</p>
}
Page.getLayout = function getLayout(page: ReactElement) {
return (
<Layout>
<NestedLayout>{page}</NestedLayout>
</Layout>
)
}
export default Page
// pages/_app.tsx
import type { ReactElement, ReactNode } from 'react'
import type { NextPage } from 'next'
import type { AppProps } from 'next/app'
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
}
export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page)
return getLayout(<Component {...pageProps} />)
}
Next에서는 NextPageWithLayout 타입을 제공해주고,AppProps 를 활용해 custom app에서 레이아웃의 타입을 override해줘서 사용해야 한다.
레이아웃 안에서는 client-side에서 사용하는 useEffect나 useSWR을 활용하여 data를 fetch할 수있다. 레이아웃은 Page가 아니기 때문에 getStaticProps 나 getServerSideProps를 사용할 수 없다.
import useSWR from 'swr'
import Navbar from './navbar'
import Footer from './footer'
export default function Layout({ children }) {
const { data, error } = useSWR('/api/navigation', fetcher)
if (error) return <div>Failed to load</div>
if (!data) return <div>Loading...</div>
return (
<>
<Navbar links={data.links} />
<main>{children}</main>
<Footer />
</>
)
}
정확한 segment 이름을 모르고 있고 동적인 데이터로 경로를 만들경우 요청시/빌드시 prerender를 통해 동적 segment를 만들 수 있다.
동적 세그먼트는 대괄호로 묶어 사용이 가능하다.
ex. [id] [slug] 처럼 대괄호로 묶어서 사용이 가능하다.
import { useRouter } from 'next/router'
export default function Page() {
const router = useRouter()
return <p>Post: {router.query.slug}</p>
}
| Route | Example URL | params |
|---|---|---|
| pages/blog/[slug].js | /blog/a | { slug: 'a' } |
| pages/blog/[slug].js | /blog/b | { slug: 'b' } |
| pages/blog/[slug].js | /blog/c | { slug: 'c' } |
뒤의 모든 후속 세그먼트를 담을 수 있는 방법도 있다.
대괄호 사이에 '...' 을 입력하면 된다.
| Route | Example URL | params |
|---|---|---|
| pages/blog/[...slug].js | /blog/a | { slug: ['a'] } |
| pages/blog/[...slug].js | /blog/b | { slug: ['a', 'b'] } |
| pages/blog/[...slug].js | /blog/c | { slug: ['a', 'b', 'c'] } |
아무것도 입력을 안하는 것도 받고 싶다면 Optional을 사용하면 되고 이때는 대괄호를 두개로 감싸면 된다.
| Route | Example URL | params |
|---|---|---|
| pages/blog/[[...slug]].js | /blog | { } |
| pages/blog/[[...slug]].js | /blog/a | { slug: ['a'] } |
| pages/blog/[[...slug]].js | /blog/b | { slug: ['a', 'b'] } |
| pages/blog/[[...slug]].js | /blog/c | { slug: ['a', 'b', 'c'] } |
Next.js 라우터는 SPA와 유사하게 페이지간의 client-side 이동을 할 수 있다.
import Link from 'next/link'
function Home() {
return (
<ul>
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/about">About Us</Link>
</li>
<li>
<Link href="/blog/hello-world">Blog Post</Link>
</li>
</ul>
)
}
export default Home
viewport내(스크롤해서 보이거나, 초기에 보이는 것)에 있는 것들은 SSG를 사용한다면 prefetch한다.
그리고 동적인 path도 사용할 수 있고 query도 넘겨줄 수 있다.
import Link from 'next/link'
function Posts({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${encodeURIComponent(post.slug)}`}>
{post.title}
</Link>
</li>
))}
</ul>
)
}
export default Posts
import Link from 'next/link'
function Posts({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link
href={{
pathname: '/blog/[slug]',
query: { slug: post.slug },
}}
>
{post.title}
</Link>
</li>
))}
</ul>
)
}
export default Posts
Link 컴포넌트를 사용하지 않고 client-side 라우터 이동을 하고 싶을때 next/router에 있는 useRouter를 사용하면 된다.
import { useRouter } from 'next/router'
export default function ReadMore() {
const router = useRouter()
return (
<button onClick={() => router.push('/about')}>
Click here to read more
</button>
)
}
얕은 라우팅은 getServerSideProps,getStaticProps,getInitialProps가 포함되어 있으면 데이터 가져오지 않고도 URL을 바꿀 수 있게 해준다.
import { useEffect } from 'react'
import { useRouter } from 'next/router'
// Current URL is '/'
function Page() {
const router = useRouter()
useEffect(() => {
// Always do navigations after the first render
router.push('/?counter=10', undefined, { shallow: true })
}, [])
useEffect(() => {
// The counter changed!
}, [router.query.counter])
}
export default Page
이렇게 되면 라우터는 /?counter=10으로 변경되었지만, 페이지는 교체되지 않는다.
주의할 점
얕은 라우팅은 현재 페이지의 URL 변경에만 동작한다. 예를 들어 다른 페이지로 가는 다음 코드를 실행 할떄
router.push('/?counter=10', '/about?counter=10', { shallow: true })
새 페이지이기 때문에 현재 페이지를 unload하고 새로운 페이지를 load 한뒤 데이터를 가져오기를 기다릴 것이다. 미들웨어가 동적으로 재작성할 수 있고 얕은 라우팅을 통해 건너띈 데이터 가져오기는 클라이언트 측에서 알 수가 없다.
Next.js는 App 컴포넌트를 사용하여 페이지를 초기화한다. 이를 재정의하고 페이지 초기화를 제어할 수 있으며 다음을 수행할 수 있다.
import type { AppProps } from 'next/app'
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
Component props는 활성화된 페이지이며 라우터 이동을 할 때 Component는 새로운 페이지로 바뀔 것이다. 그러므로 Component로 보내는 어떠한 props든 페이지에서 받게 된다.
pageProps는 데이터 가져오는 메소드(getStaticProps,getStaticPath 등)로 페이지에 preload하는데 쓰이는 초기 props이다.(초기값은 빈 객체)
getInitialProps 사용하기getStaticProps를 사용하지 않는 페이지에 Automatic Static Optimization
가 비활성화 되게 된다.
Next.js에서는 이 패턴을 추천하지 않는다. 대신에 App router를 점진적으로 적용하는 것을 추천한다고 한다.
import App, { AppContext, AppInitialProps, AppProps } from 'next/app'
type AppOwnProps = { example: string }
export default function MyApp({
Component,
pageProps,
example,
}: AppProps & AppOwnProps) {
return (
<>
<p>Data: {example}</p>
<Component {...pageProps} />
</>
)
}
MyApp.getInitialProps = async (
context: AppContext
): Promise<AppOwnProps & AppInitialProps> => {
const ctx = await App.getInitialProps(context)
return { ...ctx, example: 'data' }
}