next.js 에서는 Routing을 쉽게 할 수 있도록 지원하고 있다. pages 폴더에 파일시스템 방식으로 파일, 폴더를 만들면 자동으로 파일, 폴더 이름을 기반으로 Routing을 지원한다.
pages/index.js → /
pages/posts/first-post.js → /posts/first-post
만약 유저 id별로 페이지를 만들어야 한다면 routing할 파일, 폴더는 다음과 같이 생성하면 된다.
pages/blog/[slug].js → /blog/:slug (ex /blog/hello-world)
pages/[username]/settings.js → /:username/settings (ex /foo/settings)
Catch-all routes는 pages/post/[...slug].js 과 같이 파일 이름을 대괄호 안에 ...을 쓰고 key로 사용할 값을 적은 후 파일을 생성하면 된다.
pages/post/[...slug].js → /post/a, /post/a/b, /post/a/b/c 등등 (/post 는 안됨)
Optional catch all routes는 pages/post/[[...slug]].js 과 같이 파일 이름을 이중 대괄호 안에 ...을 쓰고 key로 사용할 값을 적은 후 파일을 생성하면 된다. Catch all routes와 똑같지만 이 경우 /post도 포함된다.
pages/post/[...slug].js → /post ,/post/a, /post/a/b, /post/a/b/c 등등
그렇다면 id별로 데이터는 어떻게 가져오는가? 만약 페이지에서 getStaticProps를 사용하여 데이터를 fetching
하는 경우 getStaticPaths를 이용해서 getStaticProps에 id를 넘겨 줄 수 있다.
getStaticPaths는 build time에 실행되기 때문에 오직getStaticProps를 사용해서 데이터를 가져오는 경우만 사용할 수 있다.
next.js는 dynamic routing일 경우 Pre-rendering을 getStaticPaths 함수의 key값을 이용해서 한다. 따라서 getStaticPaths 함수는 return 값으로 key값을 포함해야 Pre-rendering을 할 수 있다.
예를 들어 pages/posts/[id].js 와 같이 routing할 파일을 만들었으면
[id].js 파일에 getStaticPaths 함수는 다음과 같이 id(key) 값을 return 해야한다.
export async function getStaticPaths() {
...
return {
paths: [
{ params: { id: '1' } },
{ params: { id: '2' } }
],
fallback: ...
}
}
이 경우 build time에 /posts/1 과 /posts/2 페이지를 [id].js 페이지의 컴포넌트를 이용해서 생성한다.
이처럼 params object는 파일 이름과 같은 키 값을(여기선 id) 가지고 있어야 한다. 만약 페이지 이름이 pages/posts/[postId]/[commentId] 이면 [comment].js 의 getStaticPaths 함수는 다음과 같이 return 해야한다.
export async function getStaticPaths() {
...
return {
paths: [
{ params: { postId: '1', commentId: '1' } },
{ params: { postId: '2', commentId: '1' } }
],
fallback: ...
}
}
다른 경우로 Catch-all routes 일 때 페이지 이름이 pages/posts/[...id].js 이면 [...id].js 의 getStaticPaths 함수는 다음과 같이 return 해야한다.
export async function getStaticPaths() {
...
return {
paths: [
{ params: { id: ["a", "b", "c"] } },
{ params: { id: ["a", "b"] } },
{ params: { id: ["a"] } }
],
fallback: ...
}
}
fallback 값이 false일 경우
1. getStaticPaths가 return 한 경로의 페이지만 build 한다.
2. getStaticPaths가 반환하지 않은 모든 path에 대해서 404 페이지를 반환한다.
따라서 적은 수의 path만 프리렌더링 해야하는 경우, 새로운 페이지가 추가 될 일이 많지 않은 경우(새로운 페이지가 자주 추가 된다면 추가될 때마다 다시 빌드해줘야한다)에 사용한다.
예를 들어 pages/test/[id].js 파일에 다음과 같이 getStaticPaths함수를 작성하면
export async function getStaticPaths() {
return {
paths: [{ params: { id: "a" } }],
fallback: false,
};
}
빌드했을때 /test/a 페이지에 대해서만 Pre-rendering 한다. 그리고 build된 페이지 이외의 페이지에 대해서 접근하면 404페이지가 보여진다.
fallback 값이 true인 경우
예를 들어 pages/test/[id].js 파일에 다음과 같이 코드를 작성하면
import { useRouter } from 'next/router'
...
export async function getStaticPaths() {
return {
paths: [{ params: { id: "a" } }],
fallback: true,
};
}
export async function getStaticProps({ params }) {
const data = getDataFromAPI(params.id);
return {
props: { data },
};
}
export default function Post({ data }) {
if (router.isFallback) {
return <div>Loading</div>
}
return (
<Layout>
...
</Layout>
);
}
최초 빌드시 빌드 파일에는 /test/a 파일에 대한 build 파일만 있지만
/test/b 페이지에 접근하면 페이지에서는 Loading 이라는 화면이 뜨고 그 동안 서버에서 getStaticProps 를 통해 b에 대한 데이터를 추가적으로 가져온 후 화면에 b에 대한 페이지를 보여주게 된다. 그리고 build 파일에 /test/b 페이지에 대한 build 파일이 추가된다.
예제를 통해 보았듯이 fallback 일때의 화면이 필수적이다. 따라서 fallback 버전의 화면을 설정하지 않으면 build 과정에서 오류가 난다.
fallback: true 일때와 대부분 동일하지만, fallback 버전의 화면이 필요없다. 즉 서버에서 데이터를 추가적으로 가져오는 동안 빈화면이 보여진다.
pages/404.js 파일을 만들면 404페이지를 직접 커스텀 할 수 있다.
export default function Custom404() {
return <h1>404 - Page Not Found</h1>
}
404 에러의 경우 서버 자체는 존재하지만 서버에서 요청한 것을 찾을 수 없을 때 발생한다. 보통 HTTP에서 사용자가 요청하는 페이지나 파일을 찾을 수 없을 때 가장 많이 발생한다. 가장 자주 발생하는 원인은 페이지가 이동되거나 삭제된 경우이다.
마찬가지로 pages/500.js 파일을 만들면 500페이지를 직접 커스텀 할 수 있다.
export default function Custom500() {
return <h1>500 - Server-side error occurred</h1>
}
500 에러의 경우 서버의 동작에서 발생하는 에러 중 더 정확한 에러 코드가 아닌 경우 발생한다. 즉 예외적인 또는 예측하지 못한 에러의 경우 500 에러로 출력된다고 생각할 수 있다. 다양한 원인이 있겠지만 발생되는 원인을 간단히 살펴보면 다음과 같을 수 있다.
- 서버 통신의 Timeout 시간 지연 오류
- 서버 트래픽 과부하
- 서버 언어의 구문 에러(스크립트 문법 오류)
next.js에서 <Link>
태그를 이용하면 페이지간 이동을 할 수 있습니다.
import Link from 'next/link'
export default function FirstPost() {
return (
<>
<h1>First Post</h1>
<h2>
<Link href="/">
<a>Back to home</a>
</Link>
</h2>
</>
)
}
이러한 기능은 <a>
태그하고 유사한데, 굳이 <a>
를 쓰지않고 next.js에서 지원하는 <Link>
를 사용할까?
<Link>
vs <a>
<a>
태그의 경우 페이지를 이동할 때 network 탭을 확인해보면 필요한 파일들을 전부 새로 다 받아오면서 새로고침이 된다.
<Link>
태그의 경우 페이지를 이동할 때 이미 이전 화면에서 받아온 파일들에 추가적으로 필요한 파일들만 더 받아온다. 따라서 새로고침이 되지 않고 빠르게 화면이 전환된다.
그럼 어떻게 추가적으로 필요한 파일들만 빠르게 가져오는가?
next.js는 CSR의 장점과 SSR의 장점을 모두 가지고 있다고 앞서 배웠다. 이는 build time에 페이지 코드에서 <Link>
태그로 감싸져 있는, 즉 이 페이지에서 접근할 수 있는 페이지의 파일들을 Pre-fetching 하고 최초 페이지 최초 접속 후에는 클라이언트 측에서 화면을 바로 전환하기 때문에 추가적으로 필요한 데이터만 서버로부터 받아와 빠른 페이지 전환을 할 수 있는 것이다.
이처럼 <Link>
태그는 Client-side Navigaion이 가능하게 한다. 이를 확인해 볼 수 있는 방식이 있는데, 아래처럼 화면에 배경색을 클라이언트에서 직접 바꾼 후 <Link>
를 이용하여 페이지를 전환하면 배경색이 그대로 유지가 되는 것을 확인할 수 있다. 만약 <a>
태그처럼 이동하는 페이지의 모든 데이터를 새로 받아와 화면을 보여주는 경우라면 배경색은 유지가 되지 않을 것이다.
Link
태그를 커스텀하기 위해 다음과 같이 className을 주게 되면 적용이 되지 않을 것이다.
<Link href='/' className='styles.link'> home </Link>
className을 주기 위해서는 내부의 <a>
태그를 넣은 후 여기에 className을 줘야한다.
<Link href='/'>
<a className='styles.home'> home </a>
</Link>
push
함수처럼 URL masking 기능을 사용할 수 있는데, 방법은 <Link>
태그 as
속성에 보여지고자 하는 url을 전달하면 된다.
import Link from "next/link";
export default function FirstPost() {
return (
<>
<h1>First Post</h1>
<h2>
<Link href="/" as="/about">
<a>Back to home</a>
</Link>
</h2>
</>
);
}
Link
태그와 달리 코드 실행 중에 url
을 변경하고 싶을 때 push
함수를 사용하는데, push
함수는 useRouter
를 import하여 사용할 수 있다.
import { useRouter } from 'next/router'
기본적으로 push
함수를 사용하는 방법은 단순히 인자로 변경하고자 하는 경로를 전달하면 된다.
import { useRouter } from "next/router";
export default function Home() {
const { push } = useRouter();
const onClick = () => {
push("/about");
};
return (
<div>
<div onClick={onClick}>go to about</div>
</div>
);
}
query
를 함께 넘겨 주고 싶을 때는 객체에 pathname
, query
를 함께 적어주면 된다.
import { useRouter } from "next/router";
export default function Home() {
const { push } = useRouter();
const onClick = () => {
push({
pathname: "/about",
query: {
id: 10,
},
});
};
return (
<div>
<div onClick={onClick}>go to about</div>
</div>
);
}
push
함수의 두번째 인자로 보여지고자 하는 url을 전달할 수 있는데, 이는 쉽게 말해 push
함수 실행 후 브라우저에서 보이는 url은 첫번째 인자로 넘긴 url 혹은 pathname 대신 두번째 인자로 넘긴 url이 보여지게 되는 것이다. 이러한 URL masking
은 사용자에게 query
혹은 url
을 숨기고 싶을때 사용할 수 있다.
실제로 라우팅되는 경로는 첫번째 인자로 넘긴 경로이며 query를 전달했으면 바뀐 페이지에서 query 또한 그대로 사용할 수 있다.
import { useRouter } from "next/router";
export default function Home() {
const { push } = useRouter();
const onClick = () => {
push(
{
pathname: "/about",
query: {
id: 10,
},
},
"/hide"
);
};
return (
<div>
<div onClick={onClick}>go to about</div>
</div>
);
}
Shallow Routing을 사용하면 getServerSideProps, getStaticProps를 다시 실행하지 않고도 url을 변경할 수 있다. 즉 url은 바뀌지 않고 query값만 바뀌게 할 수 있다. push
함수의 세번째 인자로 넘기는 객체의 속성으로 shallow : true
를 지정해주면 Shallow Routing을 사용할 수 있다. 이는 tab을 눌렀을 때 이동하게 하는 로직에 사용될 수 있을 것 같다.
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