NEXT.js 알아보기 - Routing

데브현·2023년 10월 5일
1

NEXT.js를 알아보자

목록 보기
3/3

기존에 SPA 방식에서는 route설정을 별도로 라이브러리(react-router, vue-router) 설치를 통해 설정이 필요했다. Next.js에서 라우터 설정을 하는 방식을 정리해보려고 한다.

Pages and Layouts

js, jsx,ts,tsx로 부터 export된 리액트 컴포넌트들은 각 페이지가 되며 route와 관련되어 있다.

만약 pages/about.jsx 로 파일을 만들었다면 /about path로 접근이 가능하다.

export default function About() {
  return <div>About</div>
}

Index routes

파일명이 index라면 부모의 directory명을 따라 가게 된다.

  • pages/index.js/
  • pages/blog/index.js/blog

Nested routes

중첩된 파일들은 중첩된 라우트를 제공한다. 파일 구조에 따라 중첩된 라우트 구조를 가지게 된다.

  • pages/blog/first-post.js/blog/first-post
  • pages/dashboard/settings/username.js/dashboard/settings/username

Layout pattern

리엑트로 페이지 구조를 짜게되면 공통인 레이아웃들이 생기게 되고 그렇게 되면 재사용 되는 컴포넌트들이 많아진다. 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해줘서 사용해야 한다.

Data Fetching

레이아웃 안에서는 client-side에서 사용하는 useEffect나 useSWR을 활용하여 data를 fetch할 수있다. 레이아웃은 Page가 아니기 때문에 getStaticPropsgetServerSideProps를 사용할 수 없다.

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 />
    </>
  )
}

Dynamic Routes

정확한 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>
}
RouteExample URLparams
pages/blog/[slug].js/blog/a{ slug: 'a' }
pages/blog/[slug].js/blog/b{ slug: 'b' }
pages/blog/[slug].js/blog/c{ slug: 'c' }

Catch-all Segments/Optional Catch-all Segments

뒤의 모든 후속 세그먼트를 담을 수 있는 방법도 있다.
대괄호 사이에 '...' 을 입력하면 된다.

RouteExample URLparams
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을 사용하면 되고 이때는 대괄호를 두개로 감싸면 된다.

RouteExample URLparams
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'] }

Linking and Navigating

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한다.

동적 경로(Dynamic Path)

그리고 동적인 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>
  )
}

얕은 라우팅(Shallow Routing)

얕은 라우팅은 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 한뒤 데이터를 가져오기를 기다릴 것이다. 미들웨어가 동적으로 재작성할 수 있고 얕은 라우팅을 통해 건너띈 데이터 가져오기는 클라이언트 측에서 알 수가 없다.

Custom App

Next.js는 App 컴포넌트를 사용하여 페이지를 초기화한다. 이를 재정의하고 페이지 초기화를 제어할 수 있으며 다음을 수행할 수 있다.

  • 페이지 변경 사이에 공유 레이아웃 만들기
  • 페이지에 추가 데이터 삽입
  • 전역 CSS 추가

사용법

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' }
}
profile
하다보면 안되는 것이 없다고 생각하는 3년차 프론트엔드 개발자입니다.

0개의 댓글