NextJS 로 Static Site 만들기

박성룡 ( Andrew park )
18 min readMay 10, 2020

--

최근 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 에는 기본적으로 reactreact-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 에서 사용할 파일들이 설정된다.

  1. tsconfig.json
  2. 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 에서 exportTrailingSlashtrue 로 설정한다.

// 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 동적 경로를 만드는 방법은 여러 방법이 존재한다.

  1. pages/a/user/[id].tsx
    [`/a/user/100`]
  2. pages/b/[name]/[id].tsx
    [ { params: { “name”: “user”, “id”: “100” } } ]
  3. 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/routeruseRouter 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 값을 설정할 수 있다.

이때 hrefonClick 을 전달해주어야 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 를 쓰고 싶지 않다면, RouterLocation 객체를 사용할 수 있다.

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-componentsclassNames 를 자동으로 생성하다 보니, 빌드시점과 렌더링 시점의 className 이 다를 때 에러가 노출되고, style 이 먹지 않았다.

Warning: Text content did not match. Server:

주로 해당 에러가 발생되는 경우는 styled(Component) 같이 styled 를 재사용할 때 발생하였다.

이경우 mixin 를 사용하면, 해결되었다.

const mixinStyles = css`
color: white;
`;
const StyleComponent = styled.div`
${mixinStyles};
`;

--

--