July 03, 2020
본 글은 공식 문서 기준으로 작성되었습니다.
React는 CSR이기에 SEO가 안된다는 치명적인 단점이 있다. 물론 cra eject로 설정하여 적용할 수 있지만, 번거롭기 때문에 더 편리하게 사용할 수 있는 Next.js(이하 Next)가 나왔다.
Next가 나오면서 SSR가 되며, 더 빠르게 페이지를 불러오기 위해 코드 스프릿도 지원한다.
해당 글은 next 9.4.4 버전으로 작성되었습니다.
$ npx create-next-app 폴더명
$ npm install next react react-dom정상적으로 설치 되었다면 package.json에 아래와 같은 scripts가 있는 걸 확인할 수 있다.
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},react와 달리 생성 후 아무것도 존재하지 않는다. 그래서 폴더부터 하나씩 만들어야 한다.
pages 폴더에 index.js를 만들어보자.
폴더 구조
node_modules/
pages
└─ index.js
package.json
packgae-lock.jsonindex.js
function HomePage() {
return <div>Welcome to Next.js!</div>
}
export default HomePage그 후, npm run dev로 서버를 돌리면 http://localhost:3000 에서 서버가 열린다.
$ npm run dev
ready - started server on http://localhost:3000
event - compiled successfully
event - build page: /next/dist/pages/_error
wait - compiling...
event - compiled successfully
event - build page: /
wait - compiling...
event - compiled successfullynpm run dev을 하면 위의 코드가 CLI창에 뜨면서 .next라는 폴더가 생기는 것을 볼 수 있다.
이를 통해 next는 자동으로 컴파일과 빌드(웹팩과 바벨로)를 진행하며 /에 페이지를 정적으로 페이지를 만드는 것을 확인할 수 있다.
React와 달리 Next는 static한 페이지를 만들며, pages 폴더 안에 있는 js 파일이 하나의 URL처럼 작동된다.
그렇다면 about 페이지를 만들어보자.
node_modules/
pages
├─ index.js
└─ about.js
package.json
packgae-lock.jsonpages/about.js
function About() {
return <div>About</div>
}
export default About그리고 pages/index.js에 Link를 추가해주자.
import Link from 'next/link'
function HomePage() {
return (
<div>
Welcome to Next.js!
<br />
<Link href="/about">About</Link>
</div>
)
}
export default HomePage
그러면 위와 같은 화면이 렌더되며, About을 누르면 localhost:3000/about에 생성한 about.js 화면이 표시된다.
여기까지 와서 나는 Next가 어떻게 SSR이 가능한지 궁금해졌다.
그러기 위해서는 Next의 구동 순서에 대해 알아야한다.
Next는 _app.js와 _document.js가 제일 처음에 실행된다. 두 파일 모두 pages 폴더 안에 있어야 한다.
우리가 맨 처음에 프로젝트를 생성할 때 없는 파일이지만, Next 자체에서 제공하는 로직으로 실행된다. 따라서 프로젝트 입맛에 맞게 만들기 위해서는 커스터마이징을 해야 하는데 바로 이 두개의 파일에서 진행된다.
두 파일 모두 Server only file로 클라이언트 단에서 사용하는 함수(ex. addEventlistner, window 등)를 사용하면 안된다.
최초로 실행되는 파일로, Client에서 띄워지는 전체 컴포넌트의 레이아웃이라 이해하면 된다. 공통 레이아웃으로 최초에 실행되어 내부에 들어갈 컴포넌트들을 실행한다.
import React from 'react'
import App from 'next/app'
function App {
render() {
const { Component, ...other } = this.props
return <Component {...other} />
}
}
export default App여기서 Component란 props로 받은 페이지들을 뜻한다.
그 다음에 _document.js가 실행되는데, 이는 _app.js에서 구성한 HTML이 어떤 형태로 들어갈지 구성해주는 것이다.
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocumentReact에서 프로젝트를 진행하면 렌더링 후에 componentDidMount나 useEffect()로 데이터를 불러와야 한다. 하지만 Next에서는 getInitialProps를 통해 데이터를 미리 불러와 한 번에 렌더링이 가능하다. 미리 데이터를 불러옴으로 속도가 빨라지며, 코드 상의 처리가 깔끔해진다.
Next 9.3 이후로는 getStaticProps나 getServerSideProps 사용을 권장한다. 아래에서 더 자세히 설명.
만약 어디에서나 공통된 데이터가 필요하다면 _app.js에 getInitialProps를 붙이면 되고, 각기 다른 데이터가 필요하다면 페이지마다 getInitialProps를 붙이면 된다.
각 페이지마다 getInitialProps를 붙이는 방법은 아래와 같다.
function Page({ stars }) {
return <div>Next stars: {stars}</div>
}
Page.getInitialProps = async ctx => {
const res = await fetch('https://api.github.com/repos/vercel/next.js')
const json = await res.json()
return { stars: json.stargazers_count }
}
export default Page주의:
getInitialProps으로 리턴되는 객체는 Date, Map, Set으로 사용되는 것이 아닌 순수 객체여야 한다.getInitialProps은 자식 컴포넌트에서 사용할 수 없으며, 오로지 default export 컴포넌트에서만 사용할 수 있다.Context Object
getInitialProps는 context라는 단일 인자를 받는데, 설정하지 않는다면 기본값으로 설정된다.
pathname - pages 폴더 안에 있는 현재 Routequery - 객체로 이루어진 쿼리스트링, ex. /category?id=phone에서 {id: ‘phone’}asPath - query를 포함한 String의 실제 경로, ex. /category?id=phone 전체 경로req - HTTP request object (server only)res - HTTP response object (server only)err - Error object if any error is encountered during the renderingNext 9.3에서는 getInitialProps보다 getStaticProps와 getServerSideProps 사용을 권장한다.
간단히 먼저 소개하자면,
getStaticProps (Static Generation): 빌드(build)할 때 데이터를 불러옴getStaticPaths (Static Generation): 데이터에 기반하여 pre-render때 특정한 동적 라우팅 구현getServerSideProps (Server-side Rendering): 요청(request)아 있을 때 데이터를 불러옴어떤 페이지에서 getStaticProps 함수를 async로 export하면, getStaticProps에서 리턴되는 props를 가지고 페이지를 pre-render 한다.
export async function getStaticProps(context) {
return {
props: {}, // 컴포넌트로 넘어갈 props
}
}context에 몇 가지 매개변수가 존재한다.
params: 페이지의 동적 라우팅에 사용되는 라우트 매개변수를 지닌다. 페이지 이름이 [id].js라면 params는 {id: ...}로 보인다.preview: true일 때 preview 모드가 된다.previewData: setPreviewData로 설정한 preview data를 지닌다.기본적인 틀은 아래와 같다.
// getStaticProps()에 의해 build 시간에 게시물이 채워진다
function Blog({ posts }) {
return (
<ul>
{posts.map(post => (
<li>{post.title}</li>
))}
</ul>
)
}
// 아래 함수는 서버 단에서 build 시간에 호출된다.
// 클라이언트 단에서 호출되지 않으므로, 직접 데이터베이스 쿼리에도 접근 가능하다.
export async function getStaticProps() {
const res = await fetch('https://.../posts')
const posts = await res.json()
// By returning { props: posts }을 리턴하여 Blog 컴포넌트는 build 시간에 'posts'를 props로 받는다.
return {
props: {
posts,
},
}
}
export default BloggetStaticProps를 사용해야 될 때:
동적 라우팅이 필요하다면 getStaticPaths로 경로 리스트를 정의해야하고, HTML에 build 시간에 렌더되어야 한다.
Next는 pre-render에서 정적으로 getStaticPaths에서 호출하는 경로들을 가져올 것이다.
export async function getStaticPaths() {
return {
paths: [
{ params: { ... } }
],
fallback: true or false
};
}paths는 동적 라우팅 경로를 pre-render한다. (ex. pages/posts/[id].js)params는 페이지 이름에 사용되는 매개변수와 일치해야 한다.
만약 페이지 이름이 pages/posts/[postId]/[commentId]라면, params는 postId와 commentId를 포함해야 한다.pages/[...slug]와 같이 모든 경로를 사용한다면, params는 slug가 담긴 배열이어야 한다.false라면 getStaticPaths로 리턴되지 않은 것은 모두 404 페이지가 뜰 것이다.true라면 getStaticPaths로 리턴되는 것은 build 시간에 HTML이 렌더될 것이다.return {
paths: [
{ params: { id: '1' } },
{ params: { id: '2' } }
],
fallback: ...
}어떤 페이지에서 getServerSideProps 함수를 async로 export하면, Next는 각 요청(request)마다 리턴되는 데이터를 getServerSideProps로 pre-render한다.
export async function getServerSideProps(context) {
return {
props: {}, // 컴포넌트로 넘어갈 props
}
}Context Object
getServerSideProps의 context는 getStaticProps의 것과 비슷한다.
params - 만약 해당 페이지가 동적 라우팅이 사용된다면 params는 라우팅 매개변수를 지닌다. 페이지 이름이 [id].js라면 params는 {id: ...}req - HTTP request objectres - HTTP response objectquery - 쿼리스트링preview: true일 때 preview 모드가 된다.previewData: setPreviewData로 설정한 preview data를 지닌다.function Page({ data }) {
// 렌더 데이터...
}
// 매 요청마다 호출된다
export async function getServerSideProps() {
// 외부 API에서 데이터 호출
const res = await fetch(`https://.../data`)
const data = await res.json()
// 페이지에 props로 데이터 보내기
return { props: { data } }
}
export default PagegetServerSideProps를 사용해야 될 때:
getStaticProps보다 느리다.참고