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.jsnext.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λ₯Ό μ°Έκ³ νμμ΅λλ€.
expressServer 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()λ₯Ό μ΄μ©νλ λ°©λ²μ΄ μμ΅λλ€.