기존에 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-post
pages/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' }
}