NextJS 로 Static Site 만들기
최근 Next JS 로 Static Site 만들 일이 생겨 이를 정리 해보고자 한다.
Next JS 는 React 를 이용하여 Client Side Rendering (CSR) 와 Server Side Rendering (SSR) 를 쉽게 혼합 하여 빠른 성능 을 낼 수 있게 도와주는 프레임워크 다.
빌드 시점에 SSR 로 생성한 페이지로 First Meaningful Paint 를 앞당길 수 있으며, 이후 페이지를 이동할 때마다 Page Rendering 에 필요한 JSON_DATA를 가져와 필요한 부분만 다시 그리므로써 CSR 의 장점 또한 살릴 수 있다.
Next JS 는 기본적으로는 Node 서버를 띄워서 사용자가 진입할 때마다 페이지를 만들어낼 수 있는데, Next JS 의 export
기능으로 Static Site Generator 로 사용할 수 있다.
빌드 시점에, 미리 진입할 수 있는 Page 를 파악하고, 이를 각각의 HTML 로 만들어 낸다. 이렇게 만들어진 static 파일 (html, css, js) 들은 S3 같은 호스팅 서버에 업로드하여 사이트를 구성 할 수 있다.
간단 예제
NextJS 설치하기
// sh
npm init -y
npm install next react react-dom — save
npm install typescript @types/react @types/node — save-dev
NextJS 에는 기본적으로 react
와 react-dom
이 포함되어 있지 않아 함께 설치 해주어야 한다.
또한 typescript
를 처리할 수 있지만 포함되어 있지 않아 사용하려면, 함께 설치 해주어야 한다.
NextJS 실행하기
// package.json
"scripts": {
"start": "next",
}
package.json
의 scripts 영역에 next 명령어를 세팅 해주면 시작할 수 있다.
기본 포트는 3000 이며, 실행할 때 -p 옵션으로 포트를 변경할 수 있다.next -p ${port_number}
기본 세팅에서 npm run start
를 실행 해보면 Error: > Couldn’t find a pages directory. Please create one under the project root
에러 메시지를 만날 수 있다.
이는 page
가 세팅되지 않았기 때문인데, pages
폴더를 생성하고 page 가 될 index.tsx
파일을 세팅해 주어야 한다.
이때 확장자명은 js, jsx, ts, tsx 어느것을 사용해도 무방하다.
// /pages/index.tsx
function Index (props) {
const { message } = props;return <div>
Welcome to {message}
</div>;
};export async function getStaticProps(context) {
return {
props: {
message: `Next.js!`
}
};
};export default Index;
정상적으로 next 명령어가 실행되면, next 에서 사용할 파일들이 설정된다.
- tsconfig.json
- next-env.d.ts
이중 tsconfig.json
은 일부 옵션을 제외하고는, 값을 변경을 하더라도, next 에서 강제로 변환 해버린다.
NextJS SSG 로 빌드하기
작업물을 Static Site 로 배포 하기 위해서는 각각의 페이지와 파일들을 export
기능으로 빌드할 필요가 있다.
package.json
의 scripts 영역에 next build && next export
명령어를 세팅 해주어야 한다.
// package.json
"scripts": {
"build": "next build && next export",
}
빌드가 되면, 기본적으로 out
폴더 밑으로 정적 파일들이 생성된다.
만약 build 폴더를 바꿔야 한다면, next.config.js
에 build 폴더를 세팅해 줄 수 있다.
next.config.js
에서 distDir 값을 변경해주면 된다.
만약 html 과 asset 을 다른 서버로 관리해야 한다면, next.config.js
에서 assetPrefix 값을 변경해주면 된다.
기본적으로 파일들은 파일명을 따라가는데, 모든 html을 index.html 로 뽑아내야 한다면, next.config.js
에서 exportTrailingSlash
을 true
로 설정한다.
// next.config.js
{
"distDir": "build",
"exportTrailingSlash": true,
"assetPrefix": "https://~~~"
}
NextJS 에서 Page 추가하기
Next.js 에서는 pages
폴더 밑의 폴더와 파일들이 곧 페이지 경로가 된다.
pages/index.js
pages/about/test.js
index 는 폴더 경로를 따르며, 다른 파일명들은 경로를 만든다.
ex) pages/about/test.js => /about/test/index.html
function Index (props) {
return <div>Index</div>;
};export default Index;
페이지 파일의 default 로 반환하는 함수 혹은 객체가 각각의 page 로 생성된다.
이때 나머지 html 값들은 기본적으로 세팅되어 있는 _app.js
와 _document.js
을 참조하며, 필요하다면 커스텀 할 수 있다.
Page 에 동적 데이터 추가하기 ( getStaticProps )
보통 React 에서는 Rendering 이후에 API 로 값을 조회해서 화면을 갱신한다.
이 경우 사용자는 첫 화면으로 아무것도 없는 로딩 화면을 보게된다.
NextJS 에서는 빌드 시점에 API 값을 조회하고, 그 값을 페이지 전달하여 생성한다. 때문에, API 조회 시간을 없애, 랜더링 속도와 First Meaningful 속도를 올릴 수 있다.
NextJS 의 getStaticProps 을 사용하면 동적으로 데이터를 가져와, 이 값을 정제하여, 페이지에 전달해 줄 수 있다.
브라우저가 아닌 Node 상에서 API 를 조회해야 하기 때문에, fetch 는 쓸수 없고, node-fetch
를 써야한다.
빌드 시점에 1번 만 데이터를 가져와 페이지에 전달하기 때문에, 값이 계속 변화하는 API 에는 클라이언트에서 한번 더 조회할 필요가 있다.
import fetch from ‘node-fetch’;function Index ({ data }) {
return <div>
Dog API
<img src={data.message} />
</div>;
};export async function getStaticProps(context) {
const res = await fetch(
'https://dog.ceo/api/breeds/image/random'
);const data = await res.json();return {
props: { data }
};
};
getStaticProps
에서는 파일값도 조회하여 전달할 수 있다. 하지만, 파일을 직접 전달할 수는 없고, 내용을 object 나 string 형태로 전달해야 한다.
빌드된 html 을 확인하면, <script id="__NEXT_DATA__" type="application/json">{…}</script>
형태로 값을 전달하는 것을 볼 수 있다.
파일을 조회하는 것도 Node 에서 하기 때문에, fs
, path
를 사용할 수 있다.
보통 Node 에서는 현재 경로를 __dirname
로 가져올 수 있지만, NextJS 에서는 별도의 process.cwd()
를 사용해야 한다.
import fs from 'fs';
import path from 'path';export async function getStaticProps(context) {
const file = path.join(process.cwd(), '/src/asset/data.txt');
const content = fs.readFileSync(file, 'utf8');return {
props: { content }
};
};
NextJS 에서 Page 를 동적으로 추가하기
Next JS 에서는 데이터에 따라 페이지를 동적으로 만들 수 있다.
pages/post/[id].js
pages/posts/[…slug].js
[id].js
는 id 기반의 동적 페이지를 만들 수 있다.
/post/1
/post/user
[…slug].js
는 경로까지 동적으로 만들 수 있다.
/posts/name/1
/posts/1234/5678
이때 파일명은 자유롭게 사용이 가능하며, 폴더 또한 위 규칙을 따른다.
Static Site 에 동적 Page 추가하기 ( getStaticPaths )
Static Site 를 만들 경우에는 빌드시점에 동적 경로를 만들어 내야한다.
getStaticPaths 에 생성할 paths 를 세팅 해주면, 빌드시점에 각 페이지들을 동적으로 생성한다.
getStaticPaths 동적 경로를 만드는 방법은 여러 방법이 존재한다.
pages/a/user/[id].tsx
[`/a/user/100`]
pages/b/[name]/[id].tsx
[ { params: { “name”: “user”, “id”: “100” } } ]
pages/c/[…path].tsx
[ { params: { “path”: [“user”, “100”] } } ]
경로를 String 이나 Params 객체 를 배열로 전달한다.
export async function getStaticPaths() {
const paths1 = ['/user/100'];const paths2 = [
{ params: { 'name': 'user', 'id': '100' } }
];const paths3 = [
{ params: { 'path': ['user', '100'] } }
];return {
paths: paths1 || paths2 || paths3,
fallback: false
}
}
이때 fallback 값은 paths 페이지가 없을 경우 처리방법을 의미한다.
Static Site 에서는 페이지 처리를 호스팅 서버에서 하기 때문에, Dev Mode 에서만 의미가 있다.
Page 에서 동적 경로 값 가져오기
getStaticPaths
에서 설정한 페이지 값을 빌드 시점에, getStaticProps(context)
로 경로 값을 가져올 수 있다.{ params: {'id': '100'} }
페이지에서는 next/router
의 useRouter
hook 으로 경로 값을 가져 올 수 있다.
// pages/post/[id].js
import { useRouter } from 'next/router';const Index = ({data}) => {
const router = useRouter();{
'pathname': '/post/[id]',
'query': { 'id': '100' },
'asPath': '/post/100',
'components': { '/post/[id]': { 'props': {}}}
}const { id } = router.query;return <div>
This Page is ${id}
</div>
}export async function getStaticProps (context) {
console.log(context);
// {"params":{"id": "100"}}return { props: {} };
}export async function getStaticPaths() {
const paths = ['/post/100'];
return { paths, fallback: false }
}
NextJS 에서 Page 이동하기
NextJS 에서는 처음에는 SSR 로 만들어진 페이지를 제공하고, 페이지가 이동할 때는 JSON 으로 필요한 데이터만 변경하여, Rendering 하는 방식을 쉽게 적용할 수 있다.
이를 가능하게 하는 것이 NextJS 의 Link 다.
Link 단독으로는 사용할 수 없고, href
를 전달할, child element
가 존재해야 한다.
import Link from 'next/link';<Link href='/post/[id]' as='/post/100'>
<a>Link</a>
</Link>
- href :
/post/[id]
처럼 매칭될 페이지 이름 이 반드시 필요하다. - as :
/post/100
처럼 이동할 경로가 필요하다.
a tag 를 사용하면, 페이지 전환으로 일어나지만, Link 으로 Wrapping 하면, Preload 를 할 수 있다.
Link 로 참조하는 경로는 미리 JSON 데이터를 호출해 두며, 페이지 전환이 일어날때 빠르게 참조할 수 있다.
만약 잘못되거나 없는 경로를 매핑한다면, NextJS 에서 계속 존재 여부를 확인하게 된다.
만약 a tag 대신 다른 컴포넌트를 만들어서 써야한다면, passHref 값을 설정할 수 있다.
이때 href 와 onClick 을 전달해주어야 preload 가 가능하며, 페이지 전환이 일어나지 않는다.
만약 이동할때 스크롤 위치를 유지시키고 싶다면, scroll
를 설정할 수 있다.
const MyButton = React.forwardRef(
({ href, onClick }, ref) => {
return (
<a href={href}
onClick={onClick}
ref={ref}
>Click Me</a>
);
}
);function MyLink() {
return (
<Link href='/post/[id]'
as={{ pathname: '/post/100' }}
scroll={false}
>
<MyButton />
</Link>
)
}
만약 Link 를 쓰고 싶지 않다면, Router 나 Location 객체를 사용할 수 있다.
import Router from 'next/router';<div onClick={() => Router.push('/post/100'}>
Click
</div>
Page 이동 감지하기
페이지 변경이 일어나지 않고 페이지가 이동처리가 되면, GA 로 PV 를 잡기 어려워 진다.
이럴때, Router.events.on
을 이용해서 경로 변경을 감지 할 수 있다.
- routeChangeStart : 경로 변경 시작
- beforeHistoryChange : 브라우저 location 변경전
- routeChangeComplete : 경로 변경 완료
이는 Link 로 이동할때만 발생하며, 다른 방법으로 페이지에 접근할때는 발생하지 않는다.
import Router from 'next/router';Router.events.on('routeChangeStart', console.log);
Router.events.on('beforeHistoryChange', console.log);
Router.events.on('routeChangeComplete', console.log);
```
NextJS 에서 Layout 구성하기
NextJS 에서 HTML 이 생성될때는 기본적으로 세팅되어 있는 _app.js
와 _document.js
을 참조한다.
만약 Head 영역에 title 같은 정보를 추가해야 한다면 next/head
를 이용할 수 있다.
해당 영역에 title, meta, style 같은 정보를 넣을 수 있으며, 만약 중복값이 들어간다면 가장 마지막 값만이 적용된다.
import Head from 'next/head'function Index() {
return (
<div>
<Head>
<title>title1</title>
<meta name=”title” content=”title1" key=”title” />
</Head>
<Head>
<title>title2</title>
<meta name=”title” content=”title2" key=”title” />
</Head>
<p>Hello world!</p>
</div>
)
}
만약 Head 에 GA 같은 정보를 심어야 할려고 할때 <script>
를 넣을수 있다.
하지만 보안상 script 는 직접 넣을 수 없다. 때문에, React 에서 제공하는 dangerouslySetInnerHTML
를 이용해야 한다.
<script
dangerouslySetInnerHTML={{
__html: ``,
}}
/>
_app.js
각 페이지별로 같은 정보를 넣어 주는건 여간 귀찮은일이 아니다.
NextJS 에서 _app.js 를 쓰면 공통 Layout 을 만들 수 있다.
pages/_app.js
형태로 존재해야 한다.
import Head from 'next/head';
import '../css/global.scss';export default function MyApp({ Component, pageProps }) {
return <>
<Head>
<meta httpEquiv="Content-type" content="text/html; charset=utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="width=device-width" />
<style>{"* {font-family: ‘’;}"}</style>
</Head>
<Component {…pageProps} />
</>
}export default MyApp;
공통적으로 사용하는 meta 정보나 global css 를 세팅할 수 있다.
_document.js
_app.js 만으로도 Layout 을 구현하는데, 문제가 없지만 만약 html 이나 body 속성을 추가해야 한다면, _document.js 이 필요하다.
pages/_document.js
형태로 존재해야 한다.
import Document, {Html, Head, Main, NextScript} from 'next/document';export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { …initialProps }
}render() {
return (
<Html lang="ko-KR">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
styled-components
같은 스타일 라이브러리를 사용하려면 아래와 같은 세팅이 필요하다.
https://github.com/zeit/next.js/tree/master/examples/with-styled-components
그외 NextJS 삽질기
window 에러
NextJS 는 기본적으로 Node 상에서 동작하기 때문에, window 객체를 바로 쓰면 에러가 발생한다.
때문에, window 를 쓰려면 process.browser 로 브라우저 상태인지 확인해야 한다.
useEffect(() => {
if(process.browser) {
//…
}
}, []);
public asset
NextJS 에서는 모든 asset 을 public 폴더로 참조할 수 있다.
// public/asset/image.png
<img src='/asset/image.png' />
하지만 import image 'asset/image.png';
형태로 많이 사용하다 보니, 해당 방법이 불편하였다.
next-images
를 사용하면 해당 방법을 사용할 수 있었다.
https://github.com/twopluszero/next-images
const withImages = require('next-images');
const config = {
//…
};module.exports = withImages(config);
styled-components 에서 did not match
NextJS에서 styled-components
를 사용할수 있다.
styled-components
는 classNames
를 자동으로 생성하다 보니, 빌드시점과 렌더링 시점의 className
이 다를 때 에러가 노출되고, style
이 먹지 않았다.
Warning: Text content did not match. Server:
주로 해당 에러가 발생되는 경우는 styled(Component)
같이 styled 를 재사용할 때 발생하였다.
이경우 mixin 를 사용하면, 해결되었다.
const mixinStyles = css`
color: white;
`;const StyleComponent = styled.div`
${mixinStyles};
`;