React + Next.js + TypeScript = ❀️

Building and deploying SSR React with NextJS

nextjsssrreactexpresstypescript

2019-03-24


Next.js λž€?

Next.js λŠ” Webpack λ“± 섀정없이 μ„œλ²„ μ‚¬μ΄λ“œ λ Œλ”λ§, λΌμš°νŒ…, μ½”λ“œ μŠ€ν”Œλ¦¬νŒ… λ“± λ‹€μ–‘ν•œ κΈ°λŠ₯을 μ œκ³΅ν•˜λŠ” React ν”„λ ˆμž„μ›Œν¬μž…λ‹ˆλ‹€. ν•„μš”ν•  κ²½μš°μ—λŠ” next.config.js μ•ˆμ—μ„œ Webpack μ„€μ • 등을 μ˜€λ²„λΌμ΄λ”©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

πŸ‘‰ μ„œλ²„ μ‚¬μ΄λ“œ λ Œλ”λ§(Sever Side Rendering, SSR) : 기쑴의 Single Page App μ—μ„œλŠ” μ΄ˆκΈ°μ— λ‘œλ“œλ˜λŠ” μžλ°”μŠ€ν¬λ¦½νŠΈ μ½”λ“œκ°€ html 을 그리게 λ©λ‹ˆλ‹€. SSR 은 μ„œλ²„μ—μ„œ html 을 κ·Έλ €μ„œ λ¦¬ν„΄ν•©λ‹ˆλ‹€. 검색 μ—”μ§„μ—μ„œ html 크둀링이 κ°€λŠ₯ν•˜κΈ° λ•Œλ¬Έμ— SEO 에 μœ λ¦¬ν•˜κ³ , μ΄ˆκΈ°μ— μžλ°”μŠ€ν¬λ¦½νŠΈ νŒŒμΌμ„ λΆˆλŸ¬μ„œ λ Œλ”λ§ν•˜λŠ” μ‹œκ°„λ„ 단좕할 수 μžˆμŠ΅λ‹ˆλ‹€.

이 ν¬μŠ€νŒ…μ—μ„œλŠ” Next.js 둜 React ν”„λ‘œμ νŠΈλ₯Ό κ΅¬μ„±ν•˜κ³ , TypeScript 와 κ΄€λ ¨λœ 섀정을 ν•˜λŠ” 방법에 λŒ€ν•΄μ„œ λ‹€λ£Ήλ‹ˆλ‹€.


1. Setup

1. Install packages

$ yarn add react @types/react next @types/next
$ yarn add typescript @zeit/next-typescript

2. Add .babelrc

ν”„λ‘œμ νŠΈ 루트 κ²½λ‘œμ— .babelrcλ₯Ό μΆ”κ°€ν•˜κ³  μ•„λž˜μ˜ 섀정을 μΆ”κ°€ν•©λ‹ˆλ‹€. TypeScript λ₯Ό μ‚¬μš©ν•˜κΈ° μœ„ν•΄μ„œ @zeit/next-typescript/babelκ°€ ν•„μš”ν•©λ‹ˆλ‹€.

{
  "presets": ["next/babel", "@zeit/next-typescript/babel"]
}

3. Create 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;
  }
);

4. Create tsconfig.json

// tsconfig.json
{
  "compileOnSave": false,
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "jsx": "react",
    "allowJs": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "lib": ["es6", "dom"],
  },
}

2. Components

1. Create 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;

2. Add scripts

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에 μΆ”κ°€ν•©λ‹ˆλ‹€.

3. Using CSS, styled-components

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"]

3. Routing

Next.js μ—μ„œ μžλ™μœΌλ‘œ λ§Œλ“€μ–΄μ£ΌλŠ” file system λΌμš°νŒ…μ€ ν΄λΌμ΄μ–ΈνŠΈ μ‚¬μ΄λ“œμ—μ„œλ§Œ μ§€μ›λ©λ‹ˆλ‹€. ν”„λ‘œμ νŠΈκ°€ λ‘œλ”©λœ ν›„ λ‚΄λΆ€μ˜ 링크λ₯Ό ν†΅ν•΄μ„œ λΌμš°νŒ…μ΄ λ˜μ—ˆμ„ λ•Œλ§Œ νŽ˜μ΄μ§€μ— μ ‘κ·Όν•  수 있으며, μ£Όμ†Œμ°½μ—μ„œ 직접 url 을 μž…λ ₯ν•˜λ©΄ μ ‘κ·Όν•  수 μ—†μŠ΅λ‹ˆλ‹€.

이 λ¬Έμ œλŠ” Custom server API 에 라우트λ₯Ό λ“±λ‘ν•˜κ³  μ„œλΉ™ν•˜λ„λ‘ ν•¨μœΌλ‘œμ¨ ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ•„λž˜μ˜ λ‚΄μš©μ€ Learn Next.js - create a custom serverλ₯Ό μ°Έκ³ ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

1. Install express

Server API λ₯Ό λ§Œλ“€κΈ° μœ„ν•΄ express λ₯Ό μ„€μΉ˜ν•©λ‹ˆλ‹€. 그리고 μ•„λž˜μ™€ 같이 server.js νŒŒμΌμ„ λ§Œλ“€μ–΄μ€λ‹ˆλ‹€.

$ yarn add express

2. Create 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);
  });

3. Edit scripts

// package.json
"scripts": {
  "dev": "node server.js",
  "start": "NODE_ENV=production node server.js",
  "build": "next build"
}

4. Add routes

λ§Œμ•½ /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);
});

5. Client side routing

ν΄λΌμ΄μ–ΈνŠΈ μ‚¬μ΄λ“œ λΌμš°νŒ…μ€ μ•„λž˜μ²˜λŸΌ 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}`);

4. Data fetching

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()λ₯Ό μ΄μš©ν•˜λŠ” 방법이 μžˆμŠ΅λ‹ˆλ‹€.