- Published on
NextJS 1. Page & Route
- Author
- 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 stringasPath
: (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)
})
}, [])