기본 적으로 Next.js는 모든 페이지를 사전랜더링 합니다.
생성된 각 HTML은 해당 페이지에 필요한 최소한의 JavaScript코드와 연결되고 페이지가 완전히 로드 되면 나머지 JavaScript코드가 실행되며 페이지가 완전히 상호작용 하도록합니다.
사전 랜더링의 형태는 크게 두가지로 정적랜더링/동적랜더링이 있습니다.
정적 랜더링은 우리가 웹을 처음 배울때 HTML을 작성하여 웹을 꾸며주었던 때로 돌아갑니다.
next build
시 우리가 jsx(tsx)로 작성하였던 코드가 HTML 정적파일로 생성됩니다.
정적페이지는 우리가 next.js를 쓰는 큰 이유중 하나입니다.
Next에선 데이터가 없을 경우 간단하지만 데이터를 외부에서 가져오는 페이지를 작성 할 경우 일정한 디자인 패턴을 따릅니다.
🔔 정적랜더링은 사전랜더링시 보여지게 되는 기본 랜더링입니다. (HTML)
function About() {
return <div>About</div>
}
export default About
외부에서 데이터를 가져올 필요가 없다면 위 처럼 평범하게 작성해주면 됩니다.
일부 페이지는 사전 랜더링을 위한 외부 데이터를 가져와야 할 때가 있다.
크게 다음과 같은 경우가 있거나 한 페이지의 여러 경우가 있을 수 있을 것입니다.
🔔 ReactApp과 다른 점은 사전 랜더링시 데이터를 가져와 함께 보여줄 수 있다는 점입니다.
예: 블로그 페이지는 서버에서 블로그 게시물 목록을 가져와야 할 수 있다.
import type { GetStaticProps, InferGetStaticPropsType } from "next";
type Post = {
title: string;
}
export const getStaticProps: GetStaticProps = async (context) => {
const response = await fetch("https://example.com");
const post: Post = await response.json();
return {
props: {
post
}
}
}
const function Blog = ({ post }: InferGetStaticPropsType<typeof getStaticProps>) => {
// { post: Post }로 타입 지정됨
return (
<div>{post.title}</div>
)
}
export default Blog;
위 사전 렌더링 컴포넌트에서 데이터를 가져오기 위해선 NextJS는 동일한 파일에서 getStaticProps
함수를 export async
로 호출합니다.
getStaticProps
는 컴포넌트 빌드시 호출된 데이터를 props에 매핑하여 전달합니다.build
시 DOM생성전에 딱 한번만 호출 됨으로 속도가 빠르고 SEO에 유리합니다.🔔
getStaticProps
다음과 같은 상황에서 사용 하는걸 추천합니다.
- 페이지에 필요한 데이터가 빌드 시에 사용 가능할 때
- 데이터를 headless CMS에서 가져올 때
- 모든 사용자에게 같은 데이터를 보여줄 때
- SEO를 위해서 속도 빠른 페이지가 필요할 때
- Node api(path, fs 등)을 사용해야 할 때
동적 라우팅을 사용할 때, 어떤 페이지를 미리 Static으로 빌드할 지 정하고 싶을 땐,
getStaticPath
를 사용합니다.
import type { GetStaticPaths } from "next";
export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: [
{ params: {} }
],
// 빌드 시에 해당 페이지들을 static으로 생성
fallback: true | false | 'blocking'
// fallback을 리턴해야 함
}
// 예시
return {
paths: [
{ params: { id: "1" }},
{ params: { id: "2" }}
]
// pages/posts/[id].tsx라고 가정
// pages/posts/1과 pages/posts/2를 static으로 생성
}
// 예시 2
return {
paths: [
{
params: {
id: "1",
title: "first post"
}
},
{
params: {
id: "2",
title: "second post"
}
}
]
// pages/posts/[id]/[title].tsx라고 가정
// pages/posts/1/first post와 pages/posts/2/second post/를 static으로 생성
}
}
fallback: false
1. getStaticPaths에서 리턴하지 않은 페이지는 모두 404로 연결
fallback: true
1. 먼저 사용자에게 fallback 페이지를 보여줌
2. 서버에서 정적 페이지를 생성함
3. 해당 페이지를 사용자에게 보여줌
4. 다음부터 해당 페이지로 접속하는 사용자에게는 만들어진 정적 페이지를 보여줌
⭐ 많은 정적 페이지를 생성해야 하지만 빌드 시간이 너무 오래 걸릴 때 사용
fallback:block
1. 사용자에게 SSR 한 정적 페이지를 보여줌
2. 다음부터 해당 페이지로 접속하는 사용자에게는 SSR된 정적 페이지를 보여줌
위의 예제는 수동으로 정적페이지가 될 경로를 지정해줬습니다.
허나 데이터가 1억개가 있고 더 늘어날 가능성도 있으며 각 데이터의 디테일 페이지를 정적으로 만들어야 된다고 생각해 봅시다. 위 방식대로라면 정말 끔찍한 작업이 될 것입니다.
NextJS에서 다음처럼 동적 경로를 가지는 정적페이지를 간편하게 만들어 줄 수 있습니다.
export const getStaticPaths: GetStaticPaths = async () => {
const test: any[] = (
await (
await fetch(
`some_api`
)
).json()
).results;
const paths = test.map((item) => ({ params: { id: item.id } }));
return { paths, fallback: "blocking" };
};
export const getStaticProps: GetStaticProps = async (context) => {
const movies: any[] = (
await (
await fetch(
`some_api`
)
).json()
).results;
return {
props: { test: movies },
//핵심은 이녀석 입니다.
//빌드 후 10초 마다 페이지에 요청이 들어오면 페이지를 재생성하여 업데이트 해줍니다.
//페이지 업데이트가 실패하더라도 이전 정적페이지는 유지 됩니다
revalidate: 10
};
};
💥 정적 랜더링에서 외부 데이터는 화면과 함께 보여줘야 할 때 사용합니다.
그 외의 외부 데이터는 CSR방식으로 구성하는게 좋을 수 있습니다.
동적랜더링은 React app에서 자주 사용하던 형태입니다.
useEffect
를 통해 다음과 같이 많이 사용 하셨을 겁니다.
useEffect(() => {
(async () => {
const { results } = await (await fetch(`/example.com`)).json();
})();
}, []);
특정 이벤트가 있을 때 외부 데이터를 가져오는게 아닌 위 처럼 컴포넌트가 did mount되는
즉 페이지가 요청 될 때 서버사이드로 외부 데이터를 가져 올 수 있습니다.
페이지가 요청 될 때마다 필요한 외부 데이터가 있을 경우 다음과 같이 getServerSideProps
를 이용합니다. 페이지가 요청 될 때 마다 (새로고침등..) props 매핑합니다.
export default function Home({
test,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<div className={styles.container}>
{test?.map((item: any) => (
<div key={item.id}>{item.title}</div>
))}
</div>
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const test: any = (
await (
await fetch(
`some_api.com`
)
).json()
).results;
return { props: { test } };
};
getServerSideProps
로 가져온 데이터도 SEO가 적용되는 정적데이터로써 기능을 합니다.
외부데이터가 랜더링 되는 시점 때문에 동적랜더링에 넣은 것이니 헷갈리지 마세요!
⭐ 정적랜더링과 동적랜더링을 적재적소에 쓰는 것이 next.js의 핵심입니다.
리액트에서는 대부분 react-router-dom을 이용해 라우트를 구현 했을 텐데
NextJS도 똑같이 react-router-dom을 사용합니다. 허나 구현 하는 방식이 정해져 있습니다.
색인 경로
index이름을 가진 파일은 루트경로로써 자동 라우팅됩니다.
pages/index.tsx→/
pages/blog/index.tsx→/blog
중첩 경로
중첩할 경로 형태로 폴더 구조를 만들면 같은 방식으로 자동 라우팅됩니다.
pages/blog/first-post.js→/blog/first-post
pages/dashboard/settings/username.js→/dashboard/settings/username
동적 경로
동적 경로를 사용 할려면, 대괄호 구문을 사용할 수 있습니다. 이를 통해 정의된 매개변수를 일치시킬 수 있습니다.
pages/blog/[slug].js→ /blog/:slug( /blog/hello-world)
pages/[username]/settings.js→ /:username/settings( /foo/settings)
pages/post/[...all].js→ /post/*( /post/2020/id/title)
⭐ 페이지 경로 우선순위
1. 정적경로 ex) pages/home.tsx
2. 동적경로 ex) pages/[id].tsx
3. 동적배열경로 ex) pages/[...id].tsx
Link컴포넌트를 이용해 정적 경로간 연결을 구현 할 수 있습니다.
import Link from 'next/link'
function Home() {
return (
<ul>
<li>
<Link href="/">Home</Link>
</li>
<li>
<Link href="/about">About Us</Link>
</li>
<li>
<Link href="/blog/hello-world">Blog Post</Link>
</li>
</ul>
)
}
export default Home
각각은 경로는 pages 폴더의 구조를 매핑합니다.
동적 경로 또한 Link
컴포넌트로 구현이 가능합니다.
import Link from 'next/link'
function Posts({ posts }) {
return (
//방법1
<ul>
{posts.map((post) => (
<li key={post.id}>
//문자 호환성을 위해 encodeURIComponent를 사용함.
<Link href={`/blog/${encodeURIComponent(post.slug)}`}>
{post.title}
</Link>
</li>
))}
</ul>
//방법2 (추천)
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link
href={{
pathname: '/blog/[slug]',
query: { slug: post.slug },
}}
>
{post.title}
</Link>
</li>
))}
</ul>
)
}
export default Posts
NextJS에선 useRouter
를 이용하여 동적 경로의 params에 접근 할 수 있습니다.
pages/post/[pid].tsx
import { useRouter } from 'next/router'
const Post = () => {
const router = useRouter()
const { pid } = router.query
return <p>Post: {pid}</p>
}
export default Post
위와 같은 형태로 접근 해서 사용이 가능합니다.
prams는 물론 query개체도 useRouter()
의 query
객체로 접근이 가능합니다.
ex) /post/abc
query
객체엔 다음과 같이 존재 할 것입니다.
{ "pid": "abc" }
ex) /post/abc?foo=bar
query
객체엔 다음과 같이 존재 할 것입니다.
{ "foo": "bar", "pid": "abc" }
push는 클라이언트 측 페이지 이동을 담당하며, 히스토리 스택에 기록이 쌓입니다.
replace는 클라이언트 측 페이지 이동을 담당하며, 히스토리 스택에 기록이 쌓이지 않습니다.
router.push(url, as, options) | router.replace(url, as, option)
getStaticProps
,getServerSideProps
를 실행하지 않고 페이지 업데이트 (default:false)빠른 페이지 전환을 위해 정적 페이지를 미리 가져와 전환하는 것
router.prefetch(url, as, options)
💥 정적페이지를 미리 가져오기 때문에 배포 전용 기능입니다.
라우터가 동작하기전 특정 작업을 실행하고 싶을때 사용합니다.
router.beforePopState({url, as, options})
import { useEffect } from 'react'
import { useRouter } from 'next/router'
export default function Page() {
const router = useRouter()
useEffect(() => {
router.beforePopState(({ url, as, options }) => {
// 다음 두 경로만 허용 하고 싶을 때
if (as !== '/' && as !== '/other') {
// Have SSR render bad routes as a 404.
window.location.href = as
return false
}
return true
})
}, [])
return <p>Welcome to the page</p>
}
뒤로가기 이벤트를 발생시킵니다. window.history.back()
과 같습니다.
router.back();
앞으로 가기 이벤트를 발생시킵니다. window.history.forward()
와 같습니다.
router.forward()
현재 URL을 다시 로드합니다. window.history.reload()
와 같습니다.
router.reload();
next/router로 이벤트를 감지해서 특정 이벤트가 발생하면 함수를 실행합니다.
1. routeChangeStart
routeChangeStart(url, { shallow })
경로가 변경되기 시작할때 발생
2. routeChangeComplete
routeChangeComplete(url, { shallow })
경로가 완전히 변경되면 발생
3. routeChangeError
routeChangeError(url, { shallow })
경로 변경시 오류가 발생하거나 경로 전환 취소시 발생 (err.cancelled - 탐색이 취소되었는지 여부)
4. beforeHistoryChange
beforeHistoryChange(url, { shallow })
브라우저의 history를 변경하기 전에 발생
5. hashChangeStart
hashChangeStart(url, { shallow })
해시는 변경되지만 페이지는 변경되지 않을때 발생
6. hashChangeComplete
hashChangeComplete(url, { shallow })
해시가 변경되었지만 페이지는 변경되지 않을때 발생
useEffect(() => {
const handleRouteChangeError = (err, url) => {
if (err.cancelled) {
console.log(`Route to ${url} was cancelled!`)
}
}
router.events.on('routeChangeError', handleRouteChangeError)
// 컴포넌트가 해체될 때 이벤트를 해제해줍니다.
return () => {
router.events.off('routeChangeError', handleRouteChangeError)
}
}, [])