Jaeseo's Information Security Story

[Next.js, React SSR] - 튜토리얼(1) (feat. 공식 튜토리얼 따라하기) 본문

Coding and Linux Study/React

[Next.js, React SSR] - 튜토리얼(1) (feat. 공식 튜토리얼 따라하기)

Jaeseokim 2020. 5. 1. 16:54

⭐주의⭐ 지식이 매우 부족한 상태에서 아무렇게나 적어둔 글입니다.  잘못된 정보가 포함될 수 있습니다. 잘못 된 부분은 댓글로 알려주세요!

Next.js란?

Next.js 는 React의 SSR(Server Side Rendering)를 도와주는 프레임 워크입니다.

기본 React는 SSR과 CSR 두 가지 모두 지원을 하지만 보통 기본적으로 SPA(Single Page Applciation)으로 CSR(Client Side Rednering)를 사용합니다.

이때 CSR과 SSR에 대한 장점과 단점이 나타나게 되는데 CSR기반은 초기에 한번만 로딩 되고 나면 JS를 이용하여 모든 Dom를 구현하기 때문에 사용자가 페이지 전환이 유연하게 이루어 진다는 것을 느끼게 됩니다. 단점은 사이트 규모가 커지게 되면 초기에 로딩 하게 되는 JS 파일의 크기가 늘어나 초기 로딩 시간이 늘어나 사용자 이탈이 생길 수 있습니다. 또한 구글과 같은 크롤링 봇은 JS가 실행된 이후의 결과 값을 크롤링 하지만 모든 검색엔진의 크롤링 봇이 JS 스크립트를 지원하지 않기 때문에 SEO(Search Engine Optimization) 검색엔진 최적화에서 문제가 발생할 수 있습니다.

그에 비해 SSR은 서버에서 렌더링을 끝나고 완성된 HTML 문서를 전송하기 때문에 SEO 최적화에는 도움이 됩니다. 단점은 URL 이동시 하얀색 화면의 깜빡임 등 화면 전환에 대해 부드럽지 않은 인상을 남기게 됩니다.

React의 유연한 SSR를 돕기 위해 나온 것이 바로 Next.js 입니다. 

Next.js 공식 튜토리얼 따라하기 (간단한 blog app 만들기!)

공식 사이트의 https://nextjs.org/learn/basics/create-nextjs-app 문서를 참조하여 작성하였습니다.

Create a Next.js App

일단 Next.js 프로젝트를 만들어 봅니다.

npx create-next-app mynextblog

npx가 없으신 분은 npm install --global npx 명령어를 통해서 설치 해주세요.

일단 Default starter app을 선택하여 기본 템플릿으로 사용합니다.

설치가 완료가 되면 아래의 구조와 3가지 script가 설정 되어 있는 것을 볼 수 있습니다.

이제 npm run dev 명령을 통해 실행을 시켜 봅니다.

http://localhost:3000

이제 Next.js 의 페이지 처리에 대해 알아 보겠습니다.

Next.js는 pages 경로 밑의 파일들이 path에 연결이 되어 있는데 예를 들어 아래와 같이 연결이 됩니다.

pages/index.js --> /

pages/posts/first-post.js --> /posts/first-post

이제 실제로 파일 만들어 보고 실제로 연결이 되는지 확인 합니다.

pages/posts/first-post.js 에 아래의 내용으로 작성합니다.

export default function FirstPost() {
  return <h1>First Post</h1>
}

http://localhost:3000/posts/first-post

Navigate Between Pages

이제 Next.js에서 페이지 이동에 대해 처리를 해봅니다.

Next.js에서는 Link 컴포넌트를 이용하여 이동에 대해 처리를 합니다.

<Link href="/"> 컴포넌트는 기존 HTML의 <a herf="/"> 와 비슷한 구조로 사용합니다.

pages/posts/first-post.js 를 아래의 내용으로 수정합니다.

import Link from 'next/link'

export default function FirstPost() {
  return (
    <>
      <h1>First Post</h1>
      <h2>
        <Link href="/">
          <a>Back to home</a>
        </Link>
      </h2>
    </>
  )
}

pages/index.js 를 아래의 내용으로 수정합니다.

import Link from "next/link";

export default function Home() {
  return (
    <>
      <h1>
        Learn{" "}
        <Link href="https://nextjs.org/">
          <a>Next.js!</a>
        </Link>
      </h1>
      <h2>
        <Link href="/posts/first-post">
          <a>My First Post!</a>
        </Link>
      </h2>
    </>
  );
}

여기서 Next.js의 장점에 대해 볼 수 있게 되는데 Next.js의 문서에서 설명하기를 코드에 대해 자동으로 분할 처리를 하므로 각 페이지는 해당 페이지에 대해 필요한 정보만 로딩을 한다고 설명을 합니다.

즉 페이지가 렌더링 될 때 에는 다른 페이지의 코드가 로드되지 않으므로 수백만의 페이지를 추가하더라도 빠르고 즉각적으로 반응이 가능하게 됩니다.

Assets, Metadata, and CSS

static assets 처리

Next.js는 기본적으로 정적 assets에 대해 처리 기능도 제공을 합니다.

public 경로에 있는 파일은 "/" 밑으로 매칭이 되어 사용이 가능합니다.

보통 robots.txt 와 같은 파일과 정적인 파일(이미지 파일, JS 파일)에 대해 사용할 때 자주 사용됩니다.

<img src="/vercel.svg" alt="logo" />

Metadata 처리

각 페이지 별 MetaData 에 대해 처리를 하고 싶다면 import Head from 'next/head' Head 컴포넌트를 이용하여 해결이 가능합니다.

index.js , /posts/first-post.js 에 아래의 내용을 수정하여 추가 합니다.

<Head>
    <title>Main Page</title>
</Head>

&&

<Head>
    <title>First Post</title>
</Head>

 

CSS Styling 처리

기본적으로 CSS 처리에 대해서는 아래와 같이 사용할 수 있습니다.

이때 사용되는 sytle 방식은 기본적으로 해당 컴포넌트에만 적용이 됨으로 리소스에 대해 최적화가 가능해 집니다.

<style jsx>{`
  …
`}</style>

하지만 위 방식으로 사용하게 되면 자식 컴포넌트가 같은 스타일을 사용할 때에는 다시 정의 해야 하기 때문에 자식에게도 동일 하게 사용 할 때에는 아래의 방식으로 사용합니다.

<style jsx global>{`
  …
`}</style>

또한 css-modules 방식도 지원을 하고 있어서 아래와 같은 형태로 사용도 가능합니다.

이때 css파일의 이름은 무조건 .module.css 으로 끝나야 합니다.

index.module.css

.title {
  text-align: center;
  transition: 100ms ease-in background;
}
.title:hover {
  background: #ccc;
}

index.js

import Link from "next/link";
import Head from "next/head";
import styles from "./index.module.css"

export default function Home() {
  return (
    <>
      <Head>
        <title>Main Page</title>
      </Head>
      <h1 className={styles.title}>
        Learn{" "}
        <Link href="https://nextjs.org/">
          <a>Next.js!</a>
        </Link>
      </h1>
      <h2>
        <Link href="/posts/first-post">
          <a>My First Post!</a>
        </Link>
      </h2>
    </>
  );
}

또한 .css .sass 파일들을 직접 Import 하여 사용하는 것도 가능합니다.

이번에는 전역 css 설정에 대해 알아보겠습니다.

Next.js에는 최상위 컴포넌트가 존재 하는데 이것을 이용하여 페이지 간 탐색할 때 상태가 유지 가능합니다.

아래와 같이 /pages/_app.js 파일을 만들어 봅니다.

export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />
}

그리고 서버를 재 시작 합니다.

/styles/global.css 파일을 작성합니다.

html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
    Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
  line-height: 1.6;
  font-size: 18px;
}

* {
  box-sizing: border-box;
}

a {
  color: #0070f3;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

img {
  max-width: 100%;
  display: block;
}

그리고 이제 _app.js에 import 시킵니다.

import "../styles/global.css"

위와 같이 정상적으로 적용이 된 모습을 볼 수 있습니다.

이제 한번 위의 내용을 가지고 프로필 화면을 디자인 해봅니다.

Profile Layout 만들기

만들기 전에 준비물로 프로필 이미지로 사용될 사진이 필요합니다. 저는 Github의 프로필 사진을 사용하였습니다.

사진을 /public/images/사진파일명 위치로 준비 시켜둡니다.

그 다음 Layout 컴포넌트를 만들겠습니다.

/components/Layout

import Head from 'next/head'
import styles from './layout.module.css'
import utilStyles from '../styles/utils.module.css'
import Link from 'next/link'

const name = 'Jaeseo Kim'
const profileSrc = '/images/profile.jpeg'
export const siteTitle = 'My First Next.js Blog'

export default function Layout({ children, home }) {
  return (
    <div className={styles.container}>
      <Head>
        <link rel="icon" href="/favicon.ico" />
        <meta
          name="description"
          content="Learn how to build a personal website using Next.js"
        />
        <meta
          property="og:image"
          content={`https://og-image.now.sh/${encodeURI(
            siteTitle
          )}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.zeit.co%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
        />
        <meta name="og:title" content={siteTitle} />
        <meta name="twitter:card" content="summary_large_image" />
      </Head>
      <header className={styles.header}>
        {home ? (
          <>
            <img
              src={profileSrc}
              className={`${styles.headerHomeImage} ${utilStyles.borderCircle}`}
              alt={name}
            />
            <h1 className={utilStyles.heading2Xl}>{name}</h1>
          </>
        ) : (
          <>
            <Link href="/">
              <a>
                <img
                  src={profileSrc}
                  className={`${styles.headerImage} ${utilStyles.borderCircle}`}
                  alt={name}
                />
              </a>
            </Link>
            <h2 className={utilStyles.headingLg}>
              <Link href="/">
                <a className={utilStyles.colorInherit}>{name}</a>
              </Link>
            </h2>
          </>
        )}
      </header>
      <main>{children}</main>
      {!home && (
        <div className={styles.backToHome}>
          <Link href="/">
            <a>← Back to home</a>
          </Link>
        </div>
      )}
    </div>
  )
}

/styles/utils.module.css

.heading2Xl {
  font-size: 2.5rem;
  line-height: 1.2;
  font-weight: 800;
  letter-spacing: -0.05rem;
  margin: 1rem 0;
}

.headingXl {
  font-size: 2rem;
  line-height: 1.3;
  font-weight: 800;
  letter-spacing: -0.05rem;
  margin: 1rem 0;
}

.headingLg {
  font-size: 1.5rem;
  line-height: 1.4;
  margin: 1rem 0;
}

.headingMd {
  font-size: 1.2rem;
  line-height: 1.5;
}

.borderCircle {
  border-radius: 9999px;
}

.colorInherit {
  color: inherit;
}

.padding1px {
  padding-top: 1px;
}

.list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.listItem {
  margin: 0 0 1.25rem;
}

.lightText {
  color: #999;
}

/components/layout.module.css

.container {
  max-width: 36rem;
  padding: 0 1rem;
  margin: 3rem auto 6rem;
}

.header {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.headerImage {
  width: 6rem;
  height: 6rem;
}

.headerHomeImage {
  width: 8rem;
  height: 8rem;
}

.backToHome {
  margin: 3rem 0 0;
}

이제 만들어진 Layout 컴포넌트를 적용 시켜 봅니다.

/pages/index.js

import Link from "next/link";
import Head from "next/head";
import Layout from "../components/Layout"

export default function Home() {
  return (
    <>
      <Layout home>
        <Head>
          <title>Main Page</title>
        </Head>
        <h1>
          Learn{" "}
          <Link href="https://nextjs.org/">
            <a>Next.js!</a>
          </Link>
        </h1>
        <h2>
          <Link href="/posts/first-post">
            <a>My First Post!</a>
          </Link>
        </h2>
      </Layout>
    </>
  );
}

/pages/posts/first-post.js

import Head from "next/head";
import Layout from "../../components/Layout";

export default function FirstPost() {
  return (
    <>
      <Layout>
        <Head>
          <title>First Post</title>
        </Head>
        <h1>First Post</h1>
      </Layout>
    </>
  );
}

 

⭐Pre-rendering and Data Fetching⭐

가장 중요하다고 생각이 되는 파트 중 하나 입니다.

Next.js 를 쓰게 되는 가장 중요한 이유 중 하나인 Pre-rendering 을 손쉽게 가능하게 해주게 되고. 또한 Data Fetching 한 내용을 바탕으로 Pre-rendering 이 가능하게 됩니다.

이 차이에 대해 알아볼 수 있는 방법이 있는데 JavaScript 기능을 끄고 접근을 해보면 차이에 대해 알 수 있게 됩니다.

자바 스크립트 기능을 끄고 접근을 하자 CSS와 다른 javascript 이벤트에 대해서는 작동을 하지 않지만 미리 컨텐츠는 검색에 사용될 만큼 완성되어 오는 모습을 볼 수 있습니다.

그에 비해 예전에 만든 mask-map에 대해 접근을 해보면 하얀색 화면만 출력 되는 것을 볼 수 있습니다.

그 이유는 기존 React는 Script를 이용하여 아무것도 없는 상태에서 JS를 이용하여 Dom을 그리는 방식을 채택 하고 있기 때문에 생기게 된다. 하지만 Next.js 는 React를 사용하지만 컨텐츠에 대해 HTML를 일부 완성하여 전송 하기 때문에 SEO 최적화에 대해 강점을 가지게 됩니다.

이제 Blog글을 md으로 작성을 하고 파일을 읽어 Pre-rendering 하도록 합니다.

/posts/ 경로에 원하는 파일 이름과 함께 아래의 형태로 글을 작성합니다.

---
title: 'Introduce'
date: '2020-04-30'
---

**정보보안 전문가를 꿈꾸고 있는 학생입니다!**

**IT, Security Study, Coding, Write UP Etc...** 

- github : [jaeseokim](https://github.com/JaeSeoKim)
- blog : [tistory](http://jaeseokim.tistory.com/)
- **Kshield Jr 3기 정보보호 관리진단** 수료
- Kshield Jr 3기 **KISA 원장 인증상** 수상
- 서울특별시 창의아이디어 경진대회 은상 수상 ( **서울 시장상** )
- [**TeamMODU**](http://modusecurity.xyz/) 소속

gray-matter 를 이용하여 이 내용을 읽어 들이겠습니다.

npm install gray-matter

/lib/posts.js 파일을 만들어 아래와 같이 작성합니다.

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const postsDirectory = path.join(process.cwd(), 'posts')

export function getSortedPostsData() {
  // Get file names under /posts
  const fileNames = fs.readdirSync(postsDirectory)
  const allPostsData = fileNames.map(fileName => {
    // Remove ".md" from file name to get id
    const id = fileName.replace(/\.md$/, '')

    // Read markdown file as string
    const fullPath = path.join(postsDirectory, fileName)
    const fileContents = fs.readFileSync(fullPath, 'utf8')

    // Use gray-matter to parse the post metadata section
    const matterResult = matter(fileContents)

    // Combine the data with the id
    return {
      id,
      ...matterResult.data
    }
  })
  // Sort posts by date
  return allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1
    } else {
      return -1
    }
  })
}

/pages/index.jsimport { getSortedPostsData } from '../lib/posts' 를 추가 해줍니다.

그리고 getStaticProps 함수를 이용하여 데이터를 Fetch 하고 Props으로 전달합니다.

export async function getStaticProps() {
  const allPostsData = getSortedPostsData()
  return {
    props: {
      allPostsData
    }
  }
}

이제 index.js를 아래의 내용으로 수정을 하게 되면 작성한 글의 title, id, date 이 보이는 모습을 볼 수 있습니다.

export default function Home({ allPostsData }) {
  return (
    <>
      <Layout home>
        <Head>…</Head>
        <section className={utilStyles.headingMd}>
          정보보안 전문가를 꿈꾸고 있는 학생입니다!
        </section>
        <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
          <h2 className={utilStyles.headingLg}>Blog</h2>
          <ul className={utilStyles.list}>
            {allPostsData.map(({ id, date, title }) => (
              <li className={utilStyles.listItem} key={id}>
                {title}
                <br />
                {id}
                <br />
                {date}
              </li>
            ))}
          </ul>
        </section>
      </Layout>
    </>
  );
}

만약 SSR를 사용하여 자주 변화되는 데이터에 대해 반응을 해야 한다면 getStaticProps 대신 getServerSideProps 을 사용하면 됩니다.

Next.js 를 만든 Vercel팀에서 SWR 이라는 훅을 만들어서 제공을 하는데 Data Fetching 할 때 사용하는 훅으로 보입니다. 이것은 나중에 공부하여 작성해보도록 하겠습니다.

Dynamic Routes

Next.js에서는 동적으로 변화하는 Route에 대해서는 [id].js 와 같은 형태를 통해 표현이 가능합니다.

/pages/posts/[id].js 파일을 만들어 봅니다.

import Layout from "../../components/Layout";

export default function Posts({ postData }) {
  return (
    <div>
      <Layout>
      ...
      </Layout>
    </div>
  );
}

이제 getStaticPaths 를 이용하여 접근이 가능한 path를 지정합니다.

그 전에 작성한 파일들의 이름 즉 id를 가져오는 함수를 만듭니다.

/lib/posts.js

export function getAllPostIds() {
  const fileNames = fs.readdirSync(postsDirectory)

  // Returns an array that looks like this:
  // [
  //   {
  //     params: {
  //       id: 'ssg-ssr'
  //     }
  //   },
  //   {
  //     params: {
  //       id: 'pre-rendering'
  //     }
  //   }
  // ]
  return fileNames.map(fileName => {
    return {
      params: {
        id: fileName.replace(/\.md$/, '')
      }
    }
  })
}
export async function getStaticPaths() {
  const paths = getAllPostIds();
  return {
    paths,
    fallback: false,
  };
}

이제 접근 허용에 대해서 설정을 하였으니 getStaticProps 를 사용하여 해당 id의 데이터만 가져오도록 처리를 합니다.

/lib/posts.js 아래의 함수를 추가 합니다.

export function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')

  // Use gray-matter to parse the post metadata section
  const matterResult = matter(fileContents)

  // Combine the data with the id
  return {
    id,
    ...matterResult.data
  }
}
export async function getStaticProps({ params }) {
  const postData = getPostData(params.id);
  return {
    props: {
      postData,
    },
  };
}

위와 같이 가져온 데이터를 props로 전달합니다.

그리고 간단하게 데이터를 출력해봅니다.

export default function Posts({ postData }) {
  return (
    <div>
      <Head>
        <title>{postData.title}</title>
      </Head>
      <Layout>
        {postData.title}
        <br />
        {postData.id}
        <br />
        {postData.date}
      </Layout>
    </div>
  );
}

접근을 해보면 이렇게 정상적으로 보이는 모습을 볼 수 있습니다. 또 존재 하지 않는 id에 대해 접근을 하면 404 에러도 출력 하는 모습을 볼 수 있습니다.

이제 md를 Parsing하여 화면에 출력도 하겠습니다.

npm install remark remark-html 명령어를 통해 라이브러리를 설치합니다.

그리고 posts.js 의 getPostData 함수를 아래와 같이 수정을 합니다.

export async function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')

  // Use gray-matter to parse the post metadata section
  const matterResult = matter(fileContents)

  // Use remark to convert markdown into HTML string
  const processedContent = await remark()
    .use(html)
    .process(matterResult.content)
  const contentHtml = processedContent.toString()

  // Combine the data with the id and contentHtml
  return {
    id,
    contentHtml,
    ...matterResult.data
  }
}

그리고 [id].js 파일에 <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} /> 를 추가하고 const postData = await getPostData(params.id); 부분에 await 를 추가 합니다.

이제 이렇게 정상적으로 내용도 보이는 모습을 볼 수 있습니다.

이제 내부 디자인과 Link 처리를 하겠습니다.

npm install date-fns 를 설치하여 date에 대해 표시하는 컴포넌트를 제작 합니다.

/components/Date

import { parseISO, format } from 'date-fns'

export default function Date({ dateString }) {
  const date = parseISO(dateString)
  return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
}

[id].js 파일을 수정합니다.

<Layout>
      <Head>
        <title>{postData.title}</title>
      </Head>
      <article>
        <h1 className={utilStyles.headingXl}>{postData.title}</h1>
        <div className={utilStyles.lightText}>
          <Date dateString={postData.date} />
        </div>
        <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
      </article>
    </Layout>

index.js 도 수정 합니다.

<>
      <Layout home>
        <Head>…</Head>
        <section className={utilStyles.headingMd}>
          <strong>정보보안 전문가를 꿈꾸고 있는 학생입니다!</strong>
        </section>
        <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
          <h2 className={utilStyles.headingLg}>Blog</h2>
          <ul className={utilStyles.list}>
            {allPostsData.map(({ id, date, title }) => (
              <li className={utilStyles.listItem} key={id}>
                <Link href="/posts/[id]" as={`/posts/${id}`}>
                  <a>{title}</a>
                </Link>
                <br />
                <small className={utilStyles.lightText}>
                  <Date dateString={date} />
                </small>
              </li>
            ))}
          </ul>
        </section>
      </Layout>
    </>

디자인 처리도 하게 되면 위와 같이 간단한 블로그가 완성 됩니다.

API Routes

추가적으로 API Routes 기능이 Next.js 에서 제공을 하는데

/pages/api/ 경로의 파일은 아래의 형태를 가지게 되어 Server에서 처리가 필요한 작업을 수행이 가능합니다.

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

export default (req, res) => {
  res.statusCode = 200
  res.json({ name: 'John Doe' })
}

여기서 외부 DB와 연결하여 작성한 글을 DB에 업로드 과 같은 작업을 수행 가능합니다.

이때 getStaticProps , getStaticPaths 내부에서는 라우트 처리가 안됩니다. 이유는 이 함수들은 서버에서 처리가 되는 코드들로 클라이언트의 번들 스크립트에 포함이 되지 않기 때문에 직접 처리를 원하는 작업을 넣는 방법을 처리를 해야 합니다.

Deploying Next.js App

이제 Next.js 를 개발한 Vercel 에서 제공하는 Deploy 기능을 사용합니다.

github 나 사용하는 git repo에 업로드 후 https://vercel.com 에 들어가 로그인을 하고 Import Project 를 실행하여 Deploy를 진행 하여 줍니다.

deploy가 성공적으로 끝나게 되면 도메인으로 만든 Next.js 앱에 접근이 가능하게 됩니다.

https://first-nextjs-blog.jaeseokim.now.sh/

Comments