Next.js

Lead Software Engineer @ProtoPie

Microsoft MVP

TypeScript Korea User Group Organizer

Marktube (Youtube)

Mark Lee

이 웅재

Next.js Setup

npx create-next-app
~/fds17th 
➜ npx create-next-app
npx: 1개의 패키지를 1.307초만에 설치했습니다.
✔ What is your project named? … start-with-create-next-app
Creating a new Next.js app in /Users/mark/fds17th/start-with-create-next-app.

Installing react, react-dom, and next using npm...

...

Initialized a git repository.

Success! Created start-with-create-next-app at /Users/mark/fds17th/start-with-create-next-app
Inside that directory, you can run several commands:

  npm run dev
    Starts the development server.

  npm run build
    Builds the app for production.

  npm start
    Runs the built app in production mode.

We suggest that you begin by typing:

  cd start-with-create-next-app
  npm run dev
{
  "name": "start-with-create-next-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "10.0.5",
    "react": "17.0.1",
    "react-dom": "17.0.1"
  }
}

package.json

next dev

next build && next start

next build && next start

next build && next export && npx serve out

next build && next export && npx serve out

mkdir start-with-manual-app

cd start-with-manual-app

npm init -y
npm i next react react-dom
{
  "name": "start-with-manual-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "next": "^10.0.5",
    "react": "^17.0.1",
    "react-dom": "^17.0.1"
  }
}

package.json

Dev

  • Runs next dev which starts Next.js in development mode

Build

  • Runs next build which builds the application for production usage

Start

  • Runs next start which starts a Next.js production server

Static HTML Export

  • next export allows you to export your app to static HTML,
    which can be run standalone without the need of a Node.js server.

Pages

export default function Index() {
  return (
    <div>
      <h1>Index</h1>
    </div>
  );
}

pages/index.js

import React from "react";

export default class Index extends React.Component {
  render() {
    return (
      <div>
        <h1>Index</h1>
      </div>
    );
  }
}

pages/index.js

Routing

Next.js has a file-system based router

Index routes

 

  • pages/index.js → /

  • pages/blog/index.js → /blog

 

Nested routes

  • pages/blog/first-post.js → /blog/first-post

  • pages/dashboard/settings/username.js → /dashboard/settings/username

 

Dynamic route segments

  • pages/blog/[slug].js → /blog/:slug (/blog/hello-world)

  • pages/[username]/settings.js → /:username/settings (/foo/settings)

  • pages/post/[...all].js → /post/* (/post/2020/id/title)

Dynamic Routes

http://localhost:3000/book/39

pages/book/[id].js

import { useRouter } from "next/router";

const Book = () => {
  const router = useRouter();
  const { id } = router.query;

  return (
    <div>
      <h1>Book: {id}</h1>
    </div>
  );
};

export default Book;

pages/book/[id].js

import { useRouter } from "next/router";

const Book = () => {
  const router = useRouter();

  return (
    <div>
      <h1>Book: {JSON.stringify(router.query)}</h1>
    </div>
  );
};

export default Book;

pages/book/[id]/[comment].js

import { useRouter } from "next/router";

const Book = () => {
  const router = useRouter();

  return (
    <div>
      <h1>Book: {JSON.stringify(router.query)}</h1>
    </div>
  );
};

export default Book;

pages/book/[...slug].js

import { useRouter } from "next/router";

const Book = () => {
  const router = useRouter();

  return (
    <div>
      <h1>Book: {JSON.stringify(router.query)}</h1>
    </div>
  );
};

export default Book;

// /book => 404
// /book/a => {"slug":["a"]}
// /book/a/b => {"slug":["a","b"]}

pages/book/[[...slug]].js

import { useRouter } from "next/router";

const Book = () => {
  const router = useRouter();

  return (
    <div>
      <h1>Book: {JSON.stringify(router.query)}</h1>
    </div>
  );
};

export default Book;

// /book => {}
// /book/a => {"slug":["a"]}
// /book/a/b => {"slug":["a","b"]}

Link

pages/about.js (X)

const About = () => {
  return (
    <div>
      <h1>About</h1>
      <p>
        <a href="/">index</a>
      </p>
    </div>
  );
};

export default About;

pages/about.js (O)

import Link from "next/link";

const About = () => {
  return (
    <div>
      <h1>About</h1>
      <p>
        <Link href="/">
          <a>index</a>
        </Link>
      </p>
    </div>
  );
};

export default About;

pages/about.js - encodeURIComponent

import Link from "next/link";

const About = () => {
  return (
    <div>
      <h1>About</h1>
      <p>
        <Link href={`/book/${encodeURIComponent("slug")}`}>
          <a>index</a>
        </Link>
      </p>
    </div>
  );
};

export default About;

pages/about.js - object

import Link from "next/link";

const About = () => {
  return (
    <div>
      <h1>About</h1>
      <p>
        <Link
          href={{
            pathname: '/book/[slug]',
            query: { slug: "slug" },
          }}
        >
          <a>index</a>
        </Link>
      </p>
    </div>
  );
};

export default About;

pages/about.js - replace

import Link from "next/link";

const About = () => {
  return (
    <div>
      <h1>About</h1>
      <p>
        <Link href="/" replace>
          <a>index</a>
        </Link>
      </p>
    </div>
  );
};

export default About;

pages/about.js - scroll

import Link from "next/link";

const About = () => {
  return (
    <div>
      <h1>About</h1>
      <p>
        <Link href="/" scroll={false}>
          <a>index</a>
        </Link>
      </p>
    </div>
  );
};

export default About;

// 링크의 기본 동작은 페이지 상단으로 스크롤하는 것입니다.
// 정의 된 해시가 있으면 일반 <a> 태그와 같이 특정 id 로 스크롤됩니다.
// 위로 스크롤을 방지하려면 / hash scroll = {false}를 Link에 추가 할 수 있습니다.

Router

useRouter > withRouter

withRouter

import Link from "next/link";
import { withRouter } from "next/router";

const About = ({ router }) => {
  return (
    <div>
      <h1>About</h1>
      <p>
        <a href="/" onClick={click}>
          index
        </a>
      </p>
    </div>
  );

  function click(e) {
    e.preventDefault();
    router.push(e.target.href);
  }
};

export default withRouter(About);

useRouter

import Link from "next/link";
import { withRouter } from "next/router";

const About = () => {
  const router = useRouter();
  return (
    <div>
      <h1>About</h1>
      <p>
        <a href="/" onClick={click}>
          index
        </a>
      </p>
    </div>
  );

  function click(e) {
    e.preventDefault();
    router.push(e.target.href);
  }
};

export default About;

Static File Serving

pages/login.js

import Image from "next/image";

const Login = () => (
  <div>
    <Image src="/bg_signin.png" width="400" height="534" />
  </div>
);

export default Login;

Styles

  • css

  • css in node_modules

  • css module

  • sass

  • sass module

  • less

  • css-in-js

CSS - pages/_app.js

import "@styles/index.css";

const MyApp = ({ Component, props }) => {
  return <Component {...props} />;
};

export default MyApp;

CSS in node_modules - pages/_app.js

import 'antd/dist/antd.css';
import "@styles/index.css";

const MyApp = ({ Component, props }) => {
  return <Component {...props} />;
};

export default MyApp;

CSS Module - pages/_app.js

import styles from "@styles/index.module.css";

const Index = () => {
  console.log(styles);

  return (
    <div>...</div>
  );
};

export default Index;
npm install scss

Tip - next.config.js

const path = require('path')

module.exports = {
  sassOptions: {
    includePaths: [path.join(__dirname, 'styles')],
  },
}

css-in-js - HelloWorld.js

function HelloWorld() {
  return (
    <div>
      Hello world
      <p>scoped!</p>
      <style jsx>{`
        p {
          color: blue;
        }
        div {
          background: red;
        }
        @media (max-width: 600px) {
          div {
            background: blue;
          }
        }
      `}</style>
      <style global jsx>{`
        body {
          background: black;
        }
      `}</style>
    </div>
  )
}

export default HelloWorld

nextjs + tailwindcss

npm install tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
~/fds17th/app-without-cna is 📦 v1.0.0 via ⬢ v14.15.1 took 8s 
➜ npx tailwindcss init -p

  
   tailwindcss 2.0.2
  
   ✅ Created Tailwind config file: tailwind.config.js
   ✅ Created PostCSS config file: postcss.config.js

tailwind.config.js

module.exports = {
  purge: ["./pages/**/*.js", "./components/**/*.js"],
  darkMode: "media", // or 'media' or 'class'
  theme: {
    extend: {
      colors: {
        "accent-1": "orange",
      },
    },
  },
  variants: {
    extend: {},
  },
  plugins: [],
};

postcss.config.js

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

styles/index.css

@tailwind base;

/* Write your own custom base styles here */

/* Start purging... */
@tailwind components;
/* Stop purging. */

html,
body {
  @apply bg-gray-50 dark:bg-gray-900;
}

/* Write your own custom component styles here */
.btn-blue {
  @apply px-4 py-2 font-bold text-white bg-blue-500 rounded;
}

/* Start purging... */
@tailwind utilities;
/* Stop purging. */

/* Your own custom utilities */

pages/_app.js

import "@styles/index.css";

const MyApp = ({ Component, props }) => {
  return <Component {...props} />;
};

export default MyApp;

pages/index.js

import { useRouter } from "next/router";
import Link from "next/link";

const Index = () => {
  const router = useRouter();

  function click(e) {
    e.preventDefault();
    router.push(e.target.href);
  }
  
  return (
    <div>
      <nav>
        <ul className="flex items-center justify-between p-8">
          <Link href="/">
            <a className="text-blue-500 no-underline text-accent-1 dark:text-blue-300">
              Home
            </a>
          </Link>
          <ul className="flex items-center justify-between space-x-4">
            <li>
              <a
                className="no-underline btn-blue"
                href="/about"
                onClick={click}
              >
                Blog
              </a>
            </li>
            <li>
              <a
                className="no-underline btn-blue"
                href="/about"
                onClick={click}
              >
                About
              </a>
            </li>
          </ul>
        </ul>
      </nav>
      <div className="py-20">
        <h1 className="text-5xl text-center text-gray-700 dark:text-gray-100">
          Index
        </h1>
      </div>
    </div>
  );
};

export default Index;

Environment Variables

next.config.js

module.exports = {
  env: {
    customKey: 'my-value',
  },
}

Code

function Page() {
  return <h1>The value of customKey is: {process.env.customKey}</h1>
}

export default Page

// <h1>The value of customKey is: {'my-value'}</h1>

next.config.js

const dotenv = require('dotenv');

dotenv.config();

module.exports = {
  env: {
    customKey: process.env.customKey,
  },
}

.env

customKey=my-value

Custom App

Custom Document

Custom Error Page

pages/_app.js

const About = () => {
  return (
    <div>
      <h1>About</h1>
      <p>
        <a href="/">index</a>
      </p>
    </div>
  );
};

export default About;

15. Next.js

By Woongjae Lee

15. Next.js

Fast Campus Frontend Developer School 17th

  • 1,892