avatar
Published on

NextJS 1. Page & Route

Author
  • avatar
    Name
    yceffort

์š”์ฆ˜ ๋ฆฌ์•กํŠธ๋ฅผ ์“ฐ๋Š” ๋งŽ์€ ํ”„๋กœ์ ํŠธ์—์„œ, SSR์„ ์ง€์›ํ•˜๊ธฐ ์œ„ํ•ด nextjs๋ฅผ ์“ฐ๊ณ  ์žˆ๋‹ค. ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„๋‚˜, SEO ์ง€์› ์ด์Šˆ ๋“ฑ ๋“ฑ ๋•Œ๋ฌธ์— ์•„๋ฌด๋ž˜๋„ SPA๋Š” ์š”์ฆ˜ ํŠธ๋ Œ๋“œ์—์„œ ๋งŽ์ด ๋ฐ€๋ฆฐ ๊ธฐ๋ถ„์ด๋‹ค. ๋ฌผ๋ก  razzle ์„ ์“ฐ๊ฑฐ๋‚˜ custom server ๋กœ ๋งจ ๋ฐ”๋‹ฅ์— ํ•ด๋”ฉํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์ง€๋งŒ ์—ฌ๊ธฐ์ €๊ธฐ ์ปจํผ๋Ÿฐ์Šค๋‚˜ ์ฃผ๋ณ€ ์‚ฌ๋žŒ๋“ค์˜ ๋ง์„ ๋“ค์–ด๋ณด๋ฉด nextjs๊ฐ€ ๋Œ€์„ธ์ด๊ธด ํ•œ ๊ฒƒ ๊ฐ™๋‹ค.

์ž…์‚ฌ ์ด๋ž˜๋กœ nextjs๋ฅผ ์“ฐ๋ฉด์„œ ๋ณ„ ์ƒ๊ฐ ์—†์ด ์ผ๋˜ ๊ฒƒ๋“ค์ด ๋งŽ์€๋ฐ, 9.3 ์ถœ์‹œ๋ฅผ ๊ธฐ๋…ํ•˜์—ฌ ์ด์ฐธ์— ํ•˜๋‚˜์”ฉ ์ •๋ฆฌํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค.

Table of Contents

1. Page

๊ธฐ๋ณธ์ ์œผ๋กœ, pages/ํŒŒ์ผ๋ช….js|ts|tsx ๋„ค์ด๋ฐ์œผ๋กœ ํŒŒ์ผ์„ ๋งŒ๋“ค๋ฉด /ํŒŒ์ผ๋ช… ์œผ๋กœ ๋ผ์šฐํŒ…์„ ํ•  ์ˆ˜ ์žˆ๋‹ค. pages/about.js๋กœ ํŒŒ์ผ์„ ๋งŒ๋“ค๋ฉด /about์œผ๋กœ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

๋‹ค์ด๋‚˜๋ฏน ๋ผ์šฐํŠธ์˜ ๊ฒฝ์šฐ์—๋„ ๋น„์Šทํ•˜๋‹ค. pages/๋””๋ ‰ํ„ฐ๋ฆฌ๋ช…/[id].js|ts|tsx๋กœ ์ƒ์„ฑํ•˜๊ฒŒ๋˜๋ฉด, ๋””๋ ‰ํ† ๋ฆฌ๋ช…/id๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด pages/posts/[id].tsx๋กœ ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๋ฉด, posts/1, posts/2 ์™€ ๊ฐ™์€ ์‹์œผ๋กœ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

pages/posts/[id].tsx

import React from 'react'
import { useRouter } from 'next/router'

export default function Post() {
  const router = useRouter()
  const { id } = router.query
  return <div>Post id {id}</div>
}

์ž„์˜๋กœ ์„ ์–ธํ•œ id ๋Š” ์œ„์ฒ˜๋Ÿผ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

nested routes๋„ ์œ„์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋œ๋‹ค.

2. Routing

Nextjs์—์„œ๋Š” SPA์™€ ์œ ์‚ฌํ•œ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ผ์šฐํŒ…์„ ์ง€์›ํ•œ๋‹ค. Link๋ผ๊ณ  ๋ถˆ๋ฆฌ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™œ์šฉํ•˜๋ฉด, ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ผ์šฐํŒ…์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

import Link from 'next/link'

function Home() {
  return (
    <Link href="/">
      <a>Home</a>
    </Link>
  )
}
export default Home

nextjs ๋Š” Link๋ฅผ ์ ์ ˆํ•œ a ํƒœ๊ทธ๋กœ ๋ณ€ํ™˜ํ•ด ์ค€๋‹ค.

์œ„์—์„œ ์–ธ๊ธ‰ํ•œ ๋‹ค์ด๋‚˜๋ฏน ๋ผ์šฐํŠธ์˜ ๊ฒฝ์šฐ์—๋Š”, ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด ์กฐ๊ธˆ ๋‹ค๋ฅด๋‹ค. href์™€ as๋ฅผ ์ „๋‹ฌํ•ด ์ฃผ์–ด์•ผ ํ•œ๋‹ค.

  • href: ๋””๋ ‰ํ† ๋ฆฌ ๋ช…์„ ๋„˜๊ฒจ์ฃผ๋ฉด ๋œ๋‹ค. /posts/[id]
  • as: ๋ธŒ๋ผ์šฐ์ €์— ์‹ค์ œ๋กœ ํ‘œ์‹œ๋  ์ฃผ์†Œ๋ฅผ ๋„˜๊ธด๋‹ค. /posts/1
import Link from 'next/link'

function Home() {
  return (
    <ul>
      <li>
        <Link href="/posts/[id]" as="/posts/1">
          <a>To Post</a>
        </Link>
      </li>
    </ul>
  )
}

export default Home

3. Router

nextjs์˜ ๋ผ์šฐํ„ฐ ์•ˆ์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค.

  • pathname: (String) ํ˜„์žฌ ๋ผ์šฐํŠธ
  • query: (Object) object๋กœ ํŒŒ์‹ฑํ•œ query string
  • asPath: (String) ์‹ค์ œ๋กœ ๋ธŒ๋ผ์šฐ์ €์— ํ‘œ์‹œ๋˜๊ณ  ์žˆ๋Š” path

๊ทธ๋ฆฌ๊ณ  ์•„๋ž˜์™€ ๊ฐ™์€ router api๋„ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค.

3-1. Router Api

Router.push

ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ํŠธ๋žœ์ง€์…˜์„ ๋‹ค๋ฃฐ ๋•Œ ์“ฐ๋Š” api๋‹ค.

import Router from 'next/router'
Router.push(url, as, options)
  • url: ์ด๋™ํ•  URL์„ ๋ช…์‹œํ•œ๋‹ค. ๋ณดํ†ต page๋ช…์„ ๋„ฃ๋Š”๋‹ค
  • as: ์˜ต์…”๋„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ, ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ณด์—ฌ์งˆ URL์ด๋‹ค. ์—†์œผ๋ฉด default๋กœ url์ด ๋“ค์–ด๊ฐ„๋‹ค.
  • options: ์€ shallow๋งŒ ์˜ต์…˜์œผ๋กœ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค.
    • shallow: getInitialProps๋ฅผ ์žฌ์‹คํ–‰ํ•˜์ง€ ์•Š๊ณ  ํ˜„์žฌ ํŽ˜์ด์ง€์˜ ๋ผ์šฐํŠธ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•œ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ false๋‹ค.

๋ฌด์Šจ ์†Œ๋ฆฌํ•˜๋Š”์ง€ ๋ชจ๋ฅด๊ฒ ๋‹ค. ์˜ˆ์ œ๋กœ ์•Œ์•„๋ณด์ž.

index.tsx

import React from 'react'
import { useRouter } from 'next/router'
import { NextPageContext } from 'next'

export default function Index() {
  const { push } = useRouter()

  function pushOnlyUrl() {
    push('/posts/1')
  }

  function pushWithAs() {
    push('/posts/[id]?hello=world', '/posts/1')
  }

  function shallowPush() {
    push('/?counter=1', undefined, { shallow: true })
  }

  function notShallowPush() {
    push('/?counter=1')
  }

  function pushUrl() {
    push('/about')
  }

  function pushUrlAndAs() {
    push('/about', '/about')
  }

  return (
    <>
      <ul>
        <li>
          <button onClick={() => pushOnlyUrl()}>1๋ฒˆ. Push only URL</button>
        </li>
        <li>
          <button onClick={() => pushWithAs()}>2๋ฒˆ. Push with as</button>
        </li>
        <li>
          <button onClick={() => shallowPush()}>3๋ฒˆ. shallow push</button>
        </li>
        <li>
          <button onClick={() => notShallowPush()}>
            4๋ฒˆ. not shallow push
          </button>
        </li>
        <li>
          <button onClick={() => pushUrl()}>5๋ฒˆ. push route</button>
        </li>
        <li>
          <button onClick={() => pushUrlAndAs()}>
            6๋ฒˆ. push route with as
          </button>
        </li>
      </ul>
    </>
  )
}

Index.getInitialProps = function (_: NextPageContext) {
  console.log('getInitialProps of Index')

  return {}
}

[id].tsx

import React from 'react'
import { useRouter } from 'next/router'
import { NextPageContext } from 'next'

export default function Post() {
  const router = useRouter()

  console.log('Router', JSON.stringify(router))

  const { id } = router.query
  return <div>Post id {id}</div>
}

Post.getInitialProps = function ({ req }: NextPageContext) {
  console.log('getInitialProps of Post')

  return {}
}

about.tsx

import React from 'react'
import { NextPageContext } from 'next'

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

About.getInitialProps = function (_: NextPageContext) {
  console.log('getInitialProps of about')

  return {}
}

1๋ฒˆ ๋ฒ„ํŠผ: getInitialProps๊ฐ€ ์„œ๋ฒ„์— ์ฐํžŒ๋‹ค. ์„œ๋ฒ„์‚ฌ์ด๋“œ์—์„œ ์‹คํ–‰๋˜์—ˆ์Œ์„ ์•Œ์ˆ˜๊ฐ€ ์žˆ๋‹ค. 1๋ฒˆ ๋ฒ„ํŠผ ๋™์ž‘์€ ์‚ฌ์šฉ์ž๊ฐ€ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ฃผ์†Œ๋ฅผ ์น˜๊ณ  ๋“ค์–ด์˜ค๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•˜๋‹ค.

{
  "pathname": "/posts/[id]",
  "route": "/posts/[id]",
  "query": {"id": "1"},
  "asPath": "/posts/1",
  "components": {
    "/posts/[id]": {"props": {"pageProps": {}}},
    "/_app": {}
  },
  "isFallback": false,
  "events": {}
}

2๋ฒˆ ๋ฒ„ํŠผ: getInitialProps๊ฐ€ ํด๋ผ์ด์–ธํŠธ์— ์ฐํžŒ๋‹ค. ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ์—์„œ ์‹คํ–‰๋˜์—ˆ์Œ์„ ์•Œ์ˆ˜๊ฐ€ ์žˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋˜ํ•œ url์—์„œ ๋ณด๋ƒˆ๋˜ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์ด ์‚ฌ์šฉ์ž ๋ธŒ๋ผ์šฐ์ € URL์—๋Š” ๊ฐ์ถฐ์ง„ ๊ฒƒ์„ ์•Œ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ Post ์ปดํฌ๋„ŒํŠธ์—์„œ ํ•ด๋‹น ๊ฐ’์„ ๋ฐ›์•„๋‹ค๊ฐ€ ์“ธ ์ˆ˜ ์žˆ๋‹ค.

{
  "pathname": "/posts/[id]",
  "route": "/posts/[id]",
  "query": {"hello": "world", "id": "1"},
  "asPath": "/posts/1",
  "components": {
    "/": {"props": {"pageProps": {}}},
    "/_app": {},
    "/posts/[id]": {"props": {"pageProps": {}}}
  },
  "isFallback": false,
  "events": {}
}

3๋ฒˆ ๋ฒ„ํŠผ: index์˜ getInitialProps๊ฐ€ ์‹คํ–‰๋˜๋ฉด์„œ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์ด ๋ณ€ํ–ˆ๋‹ค.

4๋ฒˆ ๋ฒ„ํŠผ: index์˜ getInitialProps๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๊ณ  ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์ด ๋ณ€ํ–ˆ๋‹ค.

5๋ฒˆ๊ณผ 6๋ฒˆ ๋ฒ„ํŠผ: ๋‹ค์ด๋‚˜๋ฏน ๋ผ์šฐํŠธ๊ฐ€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์—, ๋™์ž‘์ด ๋™์ผํ•˜๋‹ค. (getInitialProps๊ฐ€ ํด๋ผ์ด์–ธํŠธ์— ์ฐํž˜). ๊ทธ๋Ÿฌ๋‚˜ ์‚ฌ์šฉ์ž๊ฐ€ ์ฃผ์†Œ๋ฅผ ์ง์ ‘ ์น˜๊ณ  ๋“ค์–ด๊ฐ„๋‹ค๋ฉด ์„œ๋ฒ„์‚ฌ์ด๋“œ์— ์ฐํž ๊ฒƒ์ด๋‹ค.

Router.Replace

Replace๋Š” Push์™€ ๋ฐ›๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋„ ๋™์ผํ•˜์ง€๋งŒ, ๋™์ž‘๋งŒ ๋‹ค๋ฅด๋‹ค. ์ด๋ฆ„์—์„œ ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ ์ฒ˜๋Ÿผ Replace๋Š” URL์— ์ƒˆ๋กœ์šด ์Šคํƒ์„ ์Œ“์ง€ ์•Š๋Š”๋‹ค.

Router.beforePopState

๋ช‡ ๋ช‡์˜ ๊ฒฝ์šฐ (ํŠนํžˆ ์ปค์Šคํ…€ ์„œ๋ฒ„๋ฅผ ์“ฐ๋Š” ๊ฒฝ์šฐ) popsState ์š”์ฒญ์„ ๋ฐ›์•„์„œ ๋ผ์šฐํŠธ์—์„œ ์•ก์…˜์ด ์ผ์–ด๋‚˜๊ธฐ ์ „์— ๋ฌด์–ธ๊ฐ€๋ฅผ ํ•˜๊ณ  ์‹ถ์„ ์ˆ˜ ์žˆ๋‹ค.

Window ์ธํ„ฐํŽ˜์ด์Šค์˜ popstate ์ด๋ฒคํŠธ๋Š” ์‚ฌ์šฉ์ž์˜ ์„ธ์…˜ ๊ธฐ๋ก ํƒ์ƒ‰์œผ๋กœ ์ธํ•ด ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ๊ธฐ๋ก ํ•ญ๋ชฉ์ด ๋ฐ”๋€” ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

_app.tsx

function App({ Component, pageProps }: AppProps) {
  const router = useRouter()

  useEffect(() => {
    router.beforePopState(() => {
      console.log('beforePopState!!')
      return true
    })

    return () => {
      router.beforePopState(() => true)
    }
  }, [])
  return <Component {...pageProps} />
}

next์˜ routing์ด ์•„๋‹Œ, ์‚ฌ์šฉ์ž๊ฐ€ ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์ง์ ‘ ์กฐ์ž‘ํ•˜๋Š” ํ–‰์œ„ (๋’ค๋กœ๊ฐ€๊ธฐ, ์•ž์œผ๋กœ๊ฐ€๊ธฐ ๋“ฑ)๊ฐ€ ์ผ์–ด๋‚  ๊ฒฝ์šฐ ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค. ๋งŒ์•ฝ false๋ฅผ ๋ฆฌํ„ดํ•  ๊ฒฝ์šฐ, Router๋Š” popState๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๋Š”๋‹ค. (์ฃผ์†Œ๋Š” ๋ฐ”๋€Œ์ง€๋งŒ ์•„๋ฌด ์ผ์ด ์ผ์–ด๋‚˜์ง€ ์•Š๋Š”๋‹ค.)

Router.events

Router์—์„œ ์ผ์–ด๋‚˜๋Š” ๋‹ค์–‘ํ•œ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์ง€ ํ•  ์ˆ˜ ์žˆ๋‹ค.

์—ฌ๊ธฐ์„œ url์€ ๋ธŒ๋ผ์šฐ์ €์— ๋œจ๋Š” url์„ ์˜๋ฏธํ•œ๋‹ค. ๋งŒ์•ฝ as๋ฅผ ์ผ๋‹ค๋ฉด, ์—ฌ๊ธฐ์„œ url๊ฐ’์€ as ๊ฐ’์ด ๋  ๊ฒƒ์ด๋‹ค.

  • routerChangeStart(url): route๊ฐ€ ๋ณ€ํ•˜๊ธฐ ์‹œ์ž‘ํ•  ๋•Œ
  • routerChangeComplete(url): route์˜ ๋ณ€ํ™”๊ฐ€ ๋๋‚ฌ์„ ๋•Œ
  • routerChangeError(err, url): route๊ฐ€ ๋ฐ”๋€Œ๋Š” ๊ณผ์ •์—์„œ ์—๋Ÿฌ๊ฐ€ ๋‚˜๊ฑฐ๋‚˜, route ๋กœ๋”ฉ์ด ์ทจ์†Œ๋˜์—ˆ์„ ๋•Œ
    • err.cancelled: ๋„ค๋น„๊ฒŒ์ด์…˜์ด ์ทจ์†Œ๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€
  • beforeHistoryChange(url): ๋ธŒ๋ผ์šฐ์ € ํžˆ์Šคํ† ๋ฆฌ๊ฐ€ ๋ฐ”๋€Œ๊ธฐ ์ „์—
  • hashChangeStart(url): ํ•ด์‰ฌ๊ฐ’์ด ๋ณ€ํ•  ๋•Œ
  • hashChangeComplete(url): ํ•ด์‰ฌ๊ฐ’์ด ๋‹ค ๋ณ€ํ•˜๊ณ  ๋‚œ ๋’ค ์—
useEffect(() => {
  router.events.on('routeChangeStart', (as) => {
    console.log('routeChangeStart', as)
  })
}, [])