yceffort

NextJS 1. Page & Route

Published on March 11, 2020

요즘 리액트를 쓰는 많은 프로젝트에서, SSR을 지원하기 위해 nextjs를 쓰고 있다. 초기 로딩 속도나, SEO 지원 이슈 등 등 때문에 아무래도 SPA는 요즘 트렌드에서 많이 밀린 기분이다. 물론 razzle 을 쓰거나 custom server 로 맨 바닥에 해딩하는 방법도 있지만 여기저기 컨퍼런스나 주변 사람들의 말을 들어보면 nextjs가 대세이긴 한 것 같다.

입사 이래로 nextjs를 쓰면서 별 생각 없이 썼던 것들이 많은데, 9.3 출시를 기념하여 이참에 하나씩 정리해보려고 한다.

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 태그로 변환해 준다.

위에서 언급한 다이나믹 라우트의 경우에는, 처리하는 방식이 조금 다르다. hrefas를 전달해 주어야 한다.

  • 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)
  })
}, [])