Building and deploying SSR React with NextJS
2019-03-24
Next.js λ Webpack λ± μ€μ μμ΄ μλ² μ¬μ΄λ λ λλ§, λΌμ°ν
, μ½λ μ€ν리ν
λ± λ€μν κΈ°λ₯μ μ 곡νλ React νλ μμν¬μ
λλ€. νμν κ²½μ°μλ next.config.js
μμμ Webpack μ€μ λ±μ μ€λ²λΌμ΄λ©ν μ μμ΅λλ€.
π μλ² μ¬μ΄λ λ λλ§(Sever Side Rendering, SSR) : κΈ°μ‘΄μ Single Page App μμλ μ΄κΈ°μ λ‘λλλ μλ°μ€ν¬λ¦½νΈ μ½λκ° html μ κ·Έλ¦¬κ² λ©λλ€. SSR μ μλ²μμ html μ κ·Έλ €μ 리ν΄ν©λλ€. κ²μ μμ§μμ html ν¬λ‘€λ§μ΄ κ°λ₯νκΈ° λλ¬Έμ SEO μ μ 리νκ³ , μ΄κΈ°μ μλ°μ€ν¬λ¦½νΈ νμΌμ λΆλ¬μ λ λλ§νλ μκ°λ λ¨μΆν μ μμ΅λλ€.
μ΄ ν¬μ€ν μμλ Next.js λ‘ React νλ‘μ νΈλ₯Ό ꡬμ±νκ³ , TypeScript μ κ΄λ ¨λ μ€μ μ νλ λ°©λ²μ λν΄μ λ€λ£Ήλλ€.
$ yarn add react @types/react next @types/next
$ yarn add typescript @zeit/next-typescript
.babelrc
νλ‘μ νΈ λ£¨νΈ κ²½λ‘μ .babelrc
λ₯Ό μΆκ°νκ³ μλμ μ€μ μ μΆκ°ν©λλ€. TypeScript λ₯Ό μ¬μ©νκΈ° μν΄μ @zeit/next-typescript/babel
κ° νμν©λλ€.
{
"presets": ["next/babel", "@zeit/next-typescript/babel"]
}
next.config.js
next.config.js
μμμλ webpack
hook μ μ¬μ©νμ¬ webpack μ€μ μ ν΄μ£Όλ κ²λ κ°λ₯ν©λλ€. next-typescript
νλ¬κ·ΈμΈμ μ¬μ©νκΈ° μν΄μ μλμ κ°μ΄ export ν©λλ€.
// next.config.js
const withTypescript = require("@zeit/next-typescript");
module.exports = withTypescript(
webpack: (config) => {
return config;
}
);
tsconfig.json
// tsconfig.json
{
"compileOnSave": false,
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"lib": ["es6", "dom"],
},
}
pages/
Next.js λ κΈ°λ³Έμ μΌλ‘ file system routing μ μ§μν©λλ€. λ¨Όμ μλμ κ°μ΄ index νμΌμ λ§λ€μ΄μ€λλ€.
pages/
κ²½λ‘μlol.tsx
νμΌμ μμ±νλ©΄/lol
λΌλ νμ΄μ§κ° μμ±λκ³ url λ‘ μ κ·Όν μ μμ΅λλ€.
$ mkdir pages
$ touch pages/index.tsx
// pages/index.tsx
import * as React from "react";
const Index: React.FunctionComponent = () => {
return (
<div>
<p>hello world</p>
</div>
);
};
export default Index;
package.json
μ μλμ κ°μ΄ next μ λΉλ/κ°λ° νκ²½ μ€ν¬λ¦½νΈλ₯Ό μΆκ°ν©λλ€. yarn dev
λ₯Ό μ€ννκ³ http://localhost:3000/
μ μ μνλ©΄ 'hello world'λ₯Ό νμΈν μ μμ΅λλ€.
// package.json
"scripts": {
"dev": "next",
"start": "next start",
"build": "next build"
}
Next.js κ° λΉλλ κ²°κ³Όλ¬Όμ΄ λ΄κΈ°λ
.next
λ₯Ό.gitignore
μ μΆκ°ν©λλ€.
CSS λ₯Ό μ¬μ©νκΈ° μν΄μλ @zeit/next-css
λ₯Ό μ€μΉνκ³ μλμ κ°μ΄ next.config.js
μ μΆκ°ν©λλ€.
// next.config.js
const withTypescript = require("@zeit/next-typescript");
const withCss = require("@zeit/next-css");
module.exports = withTypescript(
withCss({
webpack: config => {
return config;
}
})
);
styled-component μ κ°μ css-in-js λ₯Ό μ¬μ©νκΈ° μν΄μλ μλ² μ¬μ΄λ λ λλ§μ μ§μνλ babel νλ¬κ·ΈμΈμ μ€μΉν΄μ€λλ€. κ·Έλ¦¬κ³ .babelrc
νμΌμλ μ€μ μ μΆκ°ν©λλ€.
μ΄ νλ¬κ·ΈμΈμ΄ μμ κ²½μ°, ν΄λΌμ΄μΈνΈ μ¬μ΄λμμ μμ±ν
className
ν΄μκ°κ³Ό μλ² μ¬μ΄λμμ μμ±ν κ°μ΄ μΌμΉνμ§ μμ μ€λ₯κ° λ°μν©λλ€.
$ yarn add babel-plugin-styled-components -D
// .babelrc
"plugins": ["babel-plugin-styled-components"]
Next.js μμ μλμΌλ‘ λ§λ€μ΄μ£Όλ file system λΌμ°ν μ ν΄λΌμ΄μΈνΈ μ¬μ΄λμμλ§ μ§μλ©λλ€. νλ‘μ νΈκ° λ‘λ©λ ν λ΄λΆμ λ§ν¬λ₯Ό ν΅ν΄μ λΌμ°ν μ΄ λμμ λλ§ νμ΄μ§μ μ κ·Όν μ μμΌλ©°, μ£Όμμ°½μμ μ§μ url μ μ λ ₯νλ©΄ μ κ·Όν μ μμ΅λλ€.
μ΄ λ¬Έμ λ Custom server API μ λΌμ°νΈλ₯Ό λ±λ‘νκ³ μλΉνλλ‘ ν¨μΌλ‘μ¨ ν΄κ²°ν μ μμ΅λλ€. μλμ λ΄μ©μ Learn Next.js - create a custom serverλ₯Ό μ°Έκ³ νμμ΅λλ€.
express
Server API λ₯Ό λ§λ€κΈ° μν΄ express λ₯Ό μ€μΉν©λλ€. κ·Έλ¦¬κ³ μλμ κ°μ΄ server.js
νμΌμ λ§λ€μ΄μ€λλ€.
$ yarn add express
server.js
// server.js
const express = require("express");
const next = require("next");
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();
app
.prepare()
.then(() => {
const server = express();
server.get("*", (req, res) => {
return handle(req, res);
});
server.listen(3000, err => {
if (err) throw err;
console.log("> Ready on http://localhost:3000");
});
})
.catch(ex => {
console.error(ex.stack);
process.exit(1);
});
// package.json
"scripts": {
"dev": "node server.js",
"start": "NODE_ENV=production node server.js",
"build": "next build"
}
λ§μ½ /map
κ²½λ‘μ pages/map.tsx
μ»΄ν¬λνΈλ₯Ό 보μ¬μ£Όκ³ μΆλ€λ©΄ μλμ κ°μ΄ μ½λλ₯Ό μΆκ°ν©λλ€. /map/:name
κ³Ό κ°μ query parameter λ μλμ κ°μ΄ μΆκ°ν μ μμ΅λλ€.
server.get("/map", (req, res) => {
const actualPage = "/map";
app.render(req, res, actualPage);
});
server.get("/map/:name", (req, res) => {
const actualPage = "/map";
const queryParams = { name: req.params.name };
app.render(req, res, actualPage, queryParams);
});
ν΄λΌμ΄μΈνΈ μ¬μ΄λ λΌμ°ν
μ μλμ²λΌ next/link
λ₯Ό μ¬μ©ν©λλ€. Link
μ»΄ν¬λνΈμ children μΌλ‘ <a>
νκ·Έλ₯Ό μ¬μ©ν΄μΌν©λλ€.
import Link from "next/link";
<Link href="/map">
<a>Map</a>
</Link>;
Router.push()
λ₯Ό μ¬μ©ν΄μ κ²½λ‘λ₯Ό μ΄λνλ κ²λ κ°λ₯ν©λλ€.
Router.push(`/map/${res.data.title}`);
Next.js μμλ getInitialProps
λ₯Ό ν΅ν΄ asPath
, query
λ±μ λ³μμ μ κ·Όν μ μμ΅λλ€. μ΄ static ν¨μλ μ΅μ΄ μλ² μ¬μ΄λ λ λλ§ μ λλ ν΄λΌμ΄μΈνΈ λΌμ°ν
μΌλ‘ μ κ·Ό μ νλ²μ©λ§ νΈμΆλ©λλ€.
import * as React from "react";
import Axios from "axios";
import { NextContext } from "next";
import { API_URL } from "../store";
class MapPage extends React.Component<Props, {}> {
static async getInitialProps({ query }: NextContext) {
const res = await Axios.get(`${API_URL}/places/?map=${query.name}`);
return { places: res.data.places };
}
render() {
return <Map places={this.props.places} />;
}
}
export default MapPage;
getInitialProps
μμμλ μλ² μ¬μ΄λκΈ° λλ¬Έμconsole.log()
λ₯Ό μ°μΌλ©΄ κ²°κ³Όκ°μ λΈλΌμ°μ κ° μλλΌ ν°λ―Έλμ λνλκ² λ©λλ€.
Next.js μμ
API_URL
κ°μ νκ²½λ³μλ₯Ό μ¬μ©νλ λ°©λ²μdotenv
λλ Next.js μμ μ§μνλgetConfig()
λ₯Ό μ΄μ©νλ λ°©λ²μ΄ μμ΅λλ€.