Next.js의 Routing

김명주·2023년 5월 1일
0

리액트에서는 react-router-dom 라이브러리를 통해 페이지간 라우팅을 구현할 수 있었다.

그러나 넥스트(Next.js) 에서는 pages 폴더 안에 컴포넌트를 생성하면 자동으로 경로가 설정된다.

Next.js의 Router는 file-system 기반이다. pages/ 와 src/pages/ 가 동시에 존재한다면 pages/가 우선순위를 갖는다. pages 폴더가 존재한다면 src 폴더 내의 내용물은 무시된다고 생각하면 편하다.

Nested routes

Next.js의 Router는 file-system 기반이다. 폴더가 여러가지 생기면 그만큼 더 많은 depth가 생겨나게 된다.

ex) pages/products/first-item.js -> /products/first-item, pages/settings/my-info.js -> /settings/my-info 등

하지만 우리가 프로젝트를 만들다 보면 경로를 수정해야 할 경우도 생기는데 이럴 경우 절대 경로를 설정할 필요가 있다.

그럴 경우에 jsconifg.json 폴더를 만들고 아래처럼 작성해주면 src가 루트가 되었기 때문에 변경할때마다 상대경로를 지정하지 않아도 된다.

{"compilerOptions": {"baseUrl": "src"}}

만약 pages/products나 pages/settings에 접근하고 싶다면? 그 루트에 index.js가 존재해야 접근이 가능하다. 그렇지 않다면 우리가 만들어둔 file-system에만 접근이 가능하다.

slug

slug는 다양한 위계의 Dynamic Routing을 제공한다.
slug는 대괄호를 사용하여 특정 값을 넣으면 그 값의 wild card처럼 동작한다.
예를들어 다음과 같은 방식이다.

  • pages/category/[slug].js => /category/:slug (ex. /category/food)
import Layout from 'components/Layout'
import SubLayout from 'components/SubLayout'
import styles from '/styles/Home.module.css'

export default function CategorySlug() {
  return (
    <>
      <h1 className={styles.title}>CategorySlug</h1>
    </>
  )
}

CategorySlug.getLayout = function getLayout(page) {
  return (
    <Layout>
      <SubLayout>{page}</SubLayout>
    </Layout>
  )
}
  • pages/[username]/info.js => /:username/:info (ex. /jimmy/info)
import Layout from 'components/Layout'
import SubLayout from 'components/SubLayout'
import styles from '/styles/Home.module.css'

export default function UsernameInfo() {
  return (
    <>
      <h1 className={styles.title}>UsernameInfo</h1>
    </>
  )
}

UsernameInfo.getLayout = function getLayout(page) {
  return (
    <Layout>
      <SubLayout>{page}</SubLayout>
    </Layout>
  )
}

그렇다면 만약 /category/info로 이동하면 무엇이 출력될까? 답은 CategorySlug가 출력된다. 이렇게 slug로 매핑한 값은 명시해둔 값이 우선시된다.

...slug?

만약 slug의 depth를 여러 depth로 사용하고 싶다면 아래처럼 사용하면 된다.

  • pages/cart/[...slug].js => /cart/* (ex. /cart/2022/06/24)

다만 이렇게 작성한 slug의 depth는 무한히 갈 수 있다.

그렇다면 [slug]를 어떻게 활용할 수 있을까? 실제로 슬러그 값이 주어지면 그 값에 따라 각기 다른걸 보여줘야 우리가 의도하는 페이지를 만들 수 있을 것이다.

useRouter()

useRouter 훅을 이용하면 하나의 slug를 가지고 다양한 페이지의 컨텐츠를 보여줄 수 있다.

// http://localhost:3000/category/info
import Layout from 'components/Layout'
import SubLayout from 'components/SubLayout'
import styles from '/styles/Home.module.css'
import { useRouter } from 'next/router'

export default function CategorySlug() {
  const router = useRouter();
  const {slug} = router.query
 
  return (
    <>
      <h1 className={styles.title}>{slug}</h1>
    </>
  )
}

CategorySlug.getLayout = function getLayout(page) {
  return (
    <Layout>
      <SubLayout>{page}</SubLayout>
    </Layout>
  )
}

그렇다면 여러개의 query도 받을 수 있을까?

// http://localhost:3000/category/info?from=here&age=123

import Layout from 'components/Layout'
import SubLayout from 'components/SubLayout'
import styles from '/styles/Home.module.css'
import { useRouter } from 'next/router'

export default function CategorySlug() {
  const router = useRouter();
  const {slug, from, age} = router.query
  console.log(slug, from, age)
  // info here 123
  return (
    <>
      <h1 className={styles.title}>{slug} {from} {age}</h1>
    </>
  )
}

CategorySlug.getLayout = function getLayout(page) {
  return (
    <Layout>
      <SubLayout>{page}</SubLayout>
    </Layout>
  )
}

저렇게 여러개의 query도 받을 수 있다.
만약 query에 slug가 있다면, page 구조 안에 있는 slug가 우선시 되어서 뒤에 있는 slug는 무시된다.
왜냐면 page 구조 안에 있는 slug는 file-system에서 우선적으로 잡아놓은 것이기 때문이다.
또한 query가 두가지라면 두가지 모두 넘어온다. (ex. /category/info?from=here&from=home => eventhome)

그렇다면 다중 slug는?

만약 pages/[username]/[info] 같은 형태의 다중 slug에서는 어떻게 될까?

// http://localhost:3000/jimmy/name

import Layout from 'components/Layout'
import SubLayout from 'components/SubLayout'
import styles from '/styles/Home.module.css'
import { useRouter } from 'next/router'

export default function UsernameInfo() {
  const router = useRouter();
  const {username, info} = router.query

  console.log(username, info) // jimmy name
  return (
    <>
      <h1 className={styles.title}>{username}'s {info}</h1>
		{/* jimmy's name */}
    </>
  )
}

UsernameInfo.getLayout = function getLayout(page) {
  return (
    <Layout>
      <SubLayout>{page}</SubLayout>
    </Layout>
  )
}

pages/cart/[...slug]의 경우에는 배열로 받아진다.

// http://localhost:3000/cart/01/02/03

import Layout from 'components/Layout'
import SubLayout from 'components/SubLayout'
import styles from '/styles/Home.module.css'
import { useRouter } from 'next/router'
export default function CartDateSlug() {
  const router = useRouter()
  const { slug } = router.query
  console.log(slug)
  //  ['01', '02', '03']
  return (
    <>
      <h1 className={styles.title}>CartDateSlug {JSON.stringify(slug)}</h1>
      {/*  ['01', '02', '03'] */}
    </>
  )
}

CartDateSlug.getLayout = function getLayout(page) {
  return (
    <Layout>
      <SubLayout>{page}</SubLayout>
    </Layout>
  )
}

Optional slug

위의 예제에서 cart 뒤에 아무것도 주지 않으면 404 에러가 발생한다. 왜냐면 [...slug]에 아무런 값도 없기 때문이다.
하지만 index를 따로 만들지 않고 slug 값이 없더라도 404 에러를 커버할 수 있다.

폴더 [...slug].js를 대괄호로 한번 더 감싸주면 된다.

  • pages/cart/[[...slug]].js
// http://localhost:3000/cart

import Layout from 'components/Layout'
import SubLayout from 'components/SubLayout'
import styles from '/styles/Home.module.css'
import { useRouter } from 'next/router'
export default function CartDateSlug() {
  const router = useRouter()
  const { slug } = router.query
  console.log(slug)
  //  undefined
  return (
    <>
      <h1 className={styles.title}>CartDateSlug {JSON.stringify(slug)}</h1>
    </>
  )
}

CartDateSlug.getLayout = function getLayout(page) {
  return (
    <Layout>
      <SubLayout>{page}</SubLayout>
    </Layout>
  )
}

이렇게 slug값이 존재하지 않아도 기본 페이지를 보여줄 수 있다.

Routing의 방법

대표적으로 두가지가 있다.
1. Link를 이용하는 방법

import Layout from 'components/Layout'
import SubLayout from 'components/SubLayout'
import styles from '/styles/Home.module.css'
import Link from 'next/link'
import { useRouter } from 'next/router'
export default function CartDateSlug() {
  const router = useRouter()
  const { slug } = router.query
  return (
    <>
      <h1 className={styles.title}>CartDateSlug {JSON.stringify(slug)}</h1>
      <Link href = "/cart/2022/06/05">202265</Link>
    </>
  )
}

CartDateSlug.getLayout = function getLayout(page) {
  return (
    <Layout>
      <SubLayout>{page}</SubLayout>
    </Layout>
  )
}
  1. router.push를 이용하는 방법
import Layout from 'components/Layout'
import SubLayout from 'components/SubLayout'
import styles from '/styles/Home.module.css'
import Link from 'next/link'
import { useRouter } from 'next/router'
export default function CartDateSlug() {
  const router = useRouter()
  const { slug } = router.query
  return (
    <>
      <h1 className={styles.title}>CartDateSlug {JSON.stringify(slug)}</h1>
      <Link href = "/cart/2022/06/05">202265</Link>
      <br/>
      <button onClick={() => router.push("/cart/2022/06/24")}>2022624일로</button>
    </>
  )
}

CartDateSlug.getLayout = function getLayout(page) {
  return (
    <Layout>
      <SubLayout>{page}</SubLayout>
    </Layout>
  )
}

Shallow Routing

Shallow Routing이란 getServerSideProps/getStaticProps등을 다시 실행시키지 않고 현재 상태를 잃지 않고 url을 바꾸는 방법이다.

url을 바꾸는 3가지 방식

  1. location.replace("url") => 로컬 state가 유지되지 않고 리렌더링이 일어남
  2. router.push(url) => 로컬 state가 유지되고 data fetching이 일어남
  3. router.push(url, as, {shallow:true}) => 로컬 state는 유지되지만, data fetching은 일어나지 않는다.
    • 주의할 점은 동일한 url에서 query만 바뀌어야지만 작동한다.
    • 존재하지 않는 페이지로 url을 설정하면 404 에러가 발생
import Layout from 'components/Layout'
import SubLayout from 'components/SubLayout'
import styles from '/styles/Home.module.css'
import { useState } from 'react'
import { useRouter } from 'next/router'


export async function getServerSideProps() {
  console.log('서버에서 데이터 보내는중..')
  return {
    props: { },
  }
}
export default function Info() {
  const router = useRouter()
  const [clicked, setClicked] = useState(false)
  const { status = 'initial' } = router.query
  return (
    <>
      <h1 className={styles.title}>Info</h1>
      <h1 className={styles.title}>clicked {String(clicked)}</h1>
      <h1 className={styles.title}>status {status}</h1>
      <button
        onClick={() => {
          alert('edit')
          setClicked(true)
          location.replace('/settings/my/info?status=editing') -> 로컬 state 유지 안되고 리렌더링이 일어난다. 페이지를 아예 새로 로드하는것과 마찬가지
        }}
      >
        edit(replace)
      </button>
      <br/>
      <button
        onClick={() => {
          alert('edit')
          setClicked(true)
          router.push('/settings/my/info?status=editing') -> 로컬 state는 유지되지만, data fetching이 다시 일어난다.
        }}
      >
        edit(push)
      </button>
      <br/>
      <button
        onClick={() => {
          alert('edit')
          setClicked(true)
          router.push('/settings/my/info?status=editing', undefined, {shallow:true}) -> 로컬 state도 유지되고, date fetching도 일어나지 않는다.
        }}
      >
        edit(shallow)
      </button>
    </>
  )
}

Info.getLayout = function getLayout(page) {
  return (
    <Layout>
      <SubLayout>{page}</SubLayout>
    </Layout>
  )
}
profile
개발자를 향해 달리는 사람

0개의 댓글