Next.js - Next.js로 Blog 만들기

lbr·2022년 9월 12일
0

What is Next.js?

https://nextjs.org/

Next.js 는 react 프레임워크입니다.
versel 이라는 회사에서 만들었습니다.

Next.js의 가장 큰 특징이자 장점은 개발자가 서버사이드렌더링(SSR)에 대한 고려를 직접할 필요가 없다는 점입니다.

첫번째 방식으로 서버사이드렌더링(SSR)을 통해서 서버에서 content가 모두 만들어진 다음에 내려오는 방식으로 사용할 수 있고,
두번째 방식으로 정적사이트를 만들 수 있습니다.

Zero Config
특별한 설정없이 매우 쉽게 사용할 수 있기 때문에 많은 개발자들이 선호하고 있습니다. 실제 production으로 사용해도 무리가 없을 정도로 제공이 됩니다.

Hybrid : SSG and SSR

SSG : static side generation
SSR : server side rendering

이 2개를 하이브리드로 사용할 수 있습니다.

File-system Routing

라우팅이 react routing과는 다르게 파일 시스템 라우팅을 사용하고 있습니다. 처음에는 어색할 수 있지만 매우 편합니다.
파일의 경로가 곧 라우팅이 되기 때문에, 직관적으로 라우팅을 설정할 수 있습니다.

TypeScript Support

타입스크립트가 기본적으로 서포트가 되기 때문에 쉽게 타입스크립트를 적용해서 사용할 수 있습니다.

API Routes

SSR으로 프로젝트를 진행할 경우 API Routes도 제공합니다.

기타

react는 클라이언트이지만, 클라이언트 사이드 개발에다가 서버사이드의 개발도 추가되어있기 때문에 어떻게 보면 풀스택 프로젝트라고 볼 수도 있습니다.

CSS와 CSSmodule도 제공되고 있습니다.
react를 이용해서 static한 사이트를 만들거나 SSR을 지원하는 프로젝트를 만들고 싶을 때, 매우 유용하고 가장 인기있는 프레임워크입니다.

Next.js 프로젝트 만들기

Next.js를 만드는 방법은 2가지가 있습니다.

  1. Manual Setup
  2. create-next-app

결국에는 create-next-app 으로 만든 결과물이 Manual Setup과 같게 됩니다.

그래서 먼저 Manual Setup 을 먼저 만들어 보고, create-next-app 으로 만들어보겠습니다.

Manual Setup으로 프로젝트 만들기

설치

mkdir Manual-Setup-my-blog
cd Manual-Setup-my-blog
npm init -y
npm i next react react-dom
code .

package.json 설정

  "scripts": {
    "dev" : "next dev",
    "build" : "next build",
    "start" : "next start"
  },

디렉토리 생성

root경로에 pages 폴더 생성.
pages 폴더 안에 index.js 테스트 컴포넌트 만들고, 개발 서버 실행.

실행

npm run dev

페이지가 정상 출력된다면 프로젝트가 잘 생성된 것입니다.
생성된 프로젝트는 npm run build 명령어를 통해서 build할 수 있습니다. 빌드를 하기 전에는 npm start 를 할 수 없습니다.

create-next-app으로 프로젝트 만들기

설치

npx create-next-app create-next-app-my-blog
cd create-next-app-my-blog
code .

yarn이 설치되어있다면 yarn 프로젝트로 생성됩니다.
만약 yarn으로 진행하고 싶지 않다면,
방금 yarn으로 만들어진 프로젝트를 삭제하고 yarn도 삭제한 뒤,
다시 위 설치과정을 진행합니다.

rm -rf create-next-app-my-blog
npm uninstall yarn -g

실행

npm run dev

라우팅 설정하기

next js는 기본적으로 파일 이름을 통해서 라우팅을 설정하기 때문에 static한 경로를 설정하는 것은 매우 쉽고, 다이나믹한 라우팅을 설정하는 것은 next js 에서 제공하는 규칙을 따라서 진행하면 되기 때문에 쉽게 적용할 수 있습니다.

pages/index.js -> /
pages/blog/index.js -> /blog
pages/blog/first-post.js -> /blog/first-post
pages/dashboard/settings/username.js -> /dashboard/settings/username

주소 맨 끝에 /를 덧붙여도 next js에서 default로 자동으로 없애줍니다.
만약 트레일링 slash가 필요하다면 config 파일을 만들어서 세팅을 true로 바꿔주면 됩니다.

dynamic routing

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)

예시

  1. http://localhost:3000/blog/hello
    [slug].js 가 출력됩니다.
  2. http://localhost:3000/blog/first-post
    first-post가 출력됩니다.

다이나믹 라우팅과 스태틱 라우팅이 같이 있을 경우에는 스태틱 라우팅이 우선합니다.

  1. http://localhost:3000/123test123/hello
    404에러
  2. http://localhost:3000/123test123/settings
    settings가 출력됩니다.
  3. http://localhost:3000/123test123/settings/test12345
    404에러

  1. http://localhost:3000/post
    404에러
  2. http://localhost:3000/post/dsfsdf
    [...all].js가 출력됩니다.
  3. http://localhost:3000/post/dsfsdf/werewrw
    [...all].js가 출력됩니다.

[...all]을 사용하면 index.js 만 제외하고 어떤 경로도 해당 파일을 출력하게 됩니다.

다이나믹하게 들어온 값들을 우리가 컴포넌트에서 이용해야지 더 의미있게 사용할 수 있게 됩니다.

다이나믹 라우팅으로 들어온 값 이용하기

next js에서 제공하는 router에 hook을 이용해서 얻어올 수 있게 도와줍니다.

import { useRouter } from "next/router";

export default function UsernameSettings() {
  const router = useRouter();

  const { username } = router.query;

  return (
    <div>
      <h1>{username}/Settings</h1>
    </div>
  );
}

Sanity 연결하고 데이터 가져오기

  • getStaticProps (Static Generation)
    Fetch data at build time.
  • getStaticPaths (Static Generation)
    Specify dynamic routes to pre-render pages based on data.
  • getServerSideProps (Server-side Rendering)
    Fetch data on each request.

Static Generation : 미리 모든 데이터를 준비한 다음에 그것을 정적페이지로 만들어서 그 정적페이지들을 파일 단위로 serve하는 방식

Server-side Rendering : 해당 페이지를 바로 서버에서 생산해서 내려주는 방식입니다.

Server-side Rendering 방식은 node 서버이고, Static Generation는 node 서버가 아니어도 상관없는 방식입니다.

그래서 Server-side Rendering 방식은 요청을 받았을 때 데이터를 생산해냅니다.

하지만 Static Generation 방식은 Server-side Rendering 과는 다르게 요청할 때 처리하는 것이 아니라 빌드할 때 결정이 됩니다.

  • getStaticProps : 빌드할 때 어떤 데이터를 가져와서 페이지로 만들건지 처리하는 부분이 getStaticProps입니다.

  • getStaticPaths : 다이나믹한 라우팅을 사용했을 때 어떤 path를 생산해야할지 정해주는 기준.

우리가 만드려는 블로그는 Server-side Rendering 이 아닌 Static Generation 으로 만들기로 했기 때문에 getStaticPropsgetStaticPaths 를 적절히 활용해야 우리의 블로그가 문제없이 빌드타임에 페이지를 생산해 낼 것입니다.

blog home page 생성하기 (getStaticProps)

우리의 blog home page는 다이나믹 라우팅이 아니기 때문에 getStaticPaths 는 필요없고, getStaticProps 만 있으면 됩니다.

getStaticProps 를 활용해서 blog home의 데이터를 받아서 컴포넌트에 주입해 주도록 하겠습니다.

import styles from "../styles/Home.module.css";

export default function Home({ hello }) {
  return (
    <div className={styles.container}>
      <h1>Blog Home {hello}</h1>
    </div>
  );
}

export function getStaticProps() {
  // sanity 로부터 데이터를 가져온다.
  return {
    props: {
      hello: "world",
    },
  };
}

실행하면 getStaticProps에서 리턴한 값이 Home 컴포넌트의 props로 들어가는 것을 확인할 수 있습니다.

우리는 getStaticProps 에서 sanity로 부터 진짜 데이터를 가져올 것입니다.

@sanity/client 설치

sanity에서 데이터를 가져오기 위해서 sanity에 연결해서 query를 실행해서 가져올 수 있는 sanity에서 제공하고 있는 클라이언트 library를 설치해야합니다.

설치

npm i @sanity/client

사용하기

import sanityClient from "@sanity/client";

// ...

export function getStaticProps() {
  // sanity 로부터 데이터를 가져온다.
  sanityClient({
    dataset: 'production', // 우리가 설정한 dataset인 production
    projectId: '2zbqhksc', // 각각의 sanity project마다 가지고 있는 고유의 값. sanity.io에 들어가서 확인할 수 있습니다.
    useCdn: process.env.NODE_ENV === 'production',
  });

dataset : 우리가 설정한 dataset인 production
projectId : 각의 sanity project마다 가지고 있는 고유의 값. sanity.io에 들어가서 확인할 수 있습니다. https://www.sanity.io/
useCdn : true 혹은 false를 입력합니다. 보통 process.env.NODE_ENV === 'production' 를 사용합니다.

export async function getStaticProps() {
  // sanity 로부터 데이터를 가져온다.
  const client = sanityClient({
    dataset: "production", // 우리가 설정한 dataset인 production
    projectId: "2zbqhksc", // 각각의 sanity project마다 가지고 있는 고유의 값. sanity.io에 들어가서 확인할 수 있습니다.
    useCdn: process.env.NODE_ENV === "production",
  });

  const home = await client.fetch(
    `*[_type == 'home'][0]{'mainPostUrl' : mainPost -> slug.current}`
  );

  const posts = await client.fetch(`
  *[_type == 'post']{
    title, 
    subtitle, 
    createdAt, 
    'content' : content[]{
      ..., 
      ...select{_type == 'imageGallery' => {'images' : images[]{..., 'url' : asset -> url}}}
    },
    'slug' : slug.current,
    'thumbnail' : {
      'alt' : thumbnail.alt,
      'imageUrl' : thumbnail.asset -> url
    },
    'author' : author -> {
      name,
      role,
      'image' : image.asset -> url
    },
    'tag' : tag -> {
      title,
      'slug' : slug.current
    }
  }
  `);

  return {
    props: {
      home,
      posts,
    },
  };
}

sanity에서 비동기로 query를 보내 데이터를 가져옵니다.

post page 생성하기(getStaticPaths)

post 페이지는 다이나믹라우팅이기 때문에 getStaticPaths 를 사용해야 합니다.

import sanityClient from "@sanity/client";

export default function PostAll({ slug }) {
  // const router = useRouter();

  // const { slug } = router.query;

  return (
    <div>
      <h1>Post : {slug}</h1>
    </div>
  );
}

export async function getStaticPaths() {
  // sanity 로부터 데이터를 가져온다.
  const client = sanityClient({
    dataset: "production", // 우리가 설정한 dataset인 production
    projectId: "2zbqhksc", // 각각의 sanity project마다 가지고 있는 고유의 값. sanity.io에 들어가서 확인할 수 있습니다.
    useCdn: process.env.NODE_ENV === "production",
  });

  const posts = await client.fetch(`
  *[_type == 'post']{
    title, 
    subtitle, 
    createdAt, 
    'content' : content[]{
      ..., 
      ...select{_type == 'imageGallery' => {'images' : images[]{..., 'url' : asset -> url}}}
    },
    'slug' : slug.current,
    'thumbnail' : {
      'alt' : thumbnail.alt,
      'imageUrl' : thumbnail.asset -> url
    },
    'author' : author -> {
      name,
      role,
      'image' : image.asset -> url
    },
    'tag' : tag -> {
      title,
      'slug' : slug.current
    }
  }
  `);

  const paths = posts.map((post) => ({
    params: {
      slug: post.slug,
    },
  }));

  return {
    // paths: [{params: {slug: 'my-blog-test'}}], // 배열의 item 하나하나가 static한 페이지가 만들어집니다. // 여기서는 post의 갯수만큼 넣어주어야 합니다.
    paths,
    fallback: false, // true 혹은 false. true : 라우팅은 맞지만 paths에 들어있지 않은 경우에는 404로 가지 않고 처리하려합니다. false : 반대로 paths에 없는 곳은 404로 나오게 됩니다.
  };
}

// 위의 처리로 paths의 item 만큼의 static 페이지가 생깁니다.
// 이제 만들어진 static 페이지에서 데이터를 가져다가 컴포넌트에 넣어주는 부분을 만들기 위해서
// getStaticPaths 만든 params 부분이 getStaticProps의 prop으로 들어오게됩니다.
export function getStaticProps({ params }) {
  const { slug } = params;

  return {
    props: {
      slug,
    },
  };
  // 여기서 나온 props는 PostAll 컴포넌트의 props로 들어갑니다.
}


// getStaticPaths 사용할 때 주의점.
// [slug].js 를 다이나믹 라우팅으로 가져오고 있기 때문에 params에는 slug라는 이름을 사용해야합니다.

getStaticPaths 만든 params 부분이 getStaticPropsprop으로 들어오게됩니다.

getStaticPaths 사용할 때 주의점 : [slug].js 를 다이나믹 라우팅으로 가져오고 있기 때문에 params에는 slug라는 이름을 사용해야합니다.

여기까지 url parh처리가 끝났으니 이제 그 url에 맞는 post 데이터도 출력하게 해보겠습니다.

각각의 path에 맞는 post 페이지 만들기

export async function getStaticPaths() {
  // sanity 로부터 데이터를 가져온다.
  const client = sanityClient({
    dataset: "production",
    projectId: "2zbqhksc",
    useCdn: process.env.NODE_ENV === "production",
  });

  const posts = await client.fetch(`
  *[_type == 'post']{
    title, 
    subtitle, 
    createdAt, 
    'content' : content[]{
      ..., 
      ...select{_type == 'imageGallery' => {'images' : images[]{..., 'url' : asset -> url}}}
    },
    'slug' : slug.current,
    'thumbnail' : {
      'alt' : thumbnail.alt,
      'imageUrl' : thumbnail.asset -> url
    },
    'author' : author -> {
      name,
      role,
      'image' : image.asset -> url
    },
    'tag' : tag -> {
      title,
      'slug' : slug.current
    }
  }
  `);

  const paths = posts.map((post) => ({
    params: {
      slug: post.slug,
    },
  }));

  return {
    // paths: [{params: {slug: 'my-blog-test'}}], // 배열의 item 하나하나가 static한 페이지가 만들어집니다. // 여기서는 post의 갯수만큼 넣어주어야 합니다.
    paths,
    fallback: false, // true 혹은 false. true : 라우팅은 맞지만 paths에 들어있지 않은 경우에는 404로 가지 않고 처리하려합니다. false : 반대로 paths에 없는 곳은 404로 나오게 됩니다.
  };
}

// 위의 처리로 paths의 item 만큼의 static 페이지가 생깁니다.
// 이제 만들어진 static 페이지에서 데이터를 가져다가 컴포넌트에 넣어주는 부분을 만들기 위해서
// getStaticPaths 만든 params 부분이 getStaticProps의 prop으로 들어오게됩니다.
export async function getStaticProps({ params }) {
  const { slug } = params;

  // sanity 로부터 데이터를 가져온다.
  const client = sanityClient({
    dataset: "production",
    projectId: "2zbqhksc",
    useCdn: process.env.NODE_ENV === "production",
  });

  const posts = await client.fetch(`
  *[_type == 'post']{
    title, 
    subtitle, 
    createdAt, 
    'content' : content[]{
      ..., 
      ...select{_type == 'imageGallery' => {'images' : images[]{..., 'url' : asset -> url}}}
    },
    'slug' : slug.current,
    'thumbnail' : {
      'alt' : thumbnail.alt,
      'imageUrl' : thumbnail.asset -> url
    },
    'author' : author -> {
      name,
      role,
      'image' : image.asset -> url
    },
    'tag' : tag -> {
      title,
      'slug' : slug.current
    }
  }
  `);

  const post = posts.find((p) => p.slug === slug);
  console.log(post);
  return {
    props: {
      slug,
      post,
    },
  };
  // 여기서 나온 props는 PostAll 컴포넌트의 props로 들어갑니다.
}

중복되는 코드가 많으니 정리해줍니다.

코드 정리

services 폴더와 SanityService.js 를 새로 만들어 sanity와 관련된 로직을 모아둡니다.

SanityService.js

import sanityClient from "@sanity/client";

export default class SanityService {
  _client = sanityClient({
    dataset: "production",
    projectId: "2zbqhksc",
    useCdn: process.env.NODE_ENV === "production",
  });

  async getHome() {
    return await this._client.fetch(
      `*[_type == 'home'][0]{'mainPostUrl' : mainPost -> slug.current}`
    );
  }

  async getPosts() {
    return await this._client.fetch(`
    *[_type == 'post']{
      title, 
      subtitle, 
      createdAt, 
      'content' : content[]{
        ..., 
        ...select{_type == 'imageGallery' => {'images' : images[]{..., 'url' : asset -> url}}}
      },
      'slug' : slug.current,
      'thumbnail' : {
        'alt' : thumbnail.alt,
        'imageUrl' : thumbnail.asset -> url
      },
      'author' : author -> {
        name,
        role,
        'image' : image.asset -> url
      },
      'tag' : tag -> {
        title,
        'slug' : slug.current
      }
    }
    `);
  }
}

스타일 작업 (1) - Blog Home

ant design을 사용하여 디자인을 해보겠습니다.

ant-design 설치

https://ant.design/

npm

npm i antd @ant-design/icons

yarn

yarn add antd @ant-design/icons

CRA 에서는 ant다지인의 css를 index.js에 추가했지만,
next.js에서는 pages폴더 아래 _app.js 파일을 이용합니다.

_app.js

import 'antd/dist/antd.css';
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

import로 antd을 추가합니다. Component 컴포넌트는 next js에서 각각의 모든 컴포넌트를 의미합니다. 여기에 import로 antd을 추가하면, 모든 컴포넌트에 해당 antd을 import 한 것과 같습니다.

document에 link를 추가해서 default로 사용하고 싶을 때에는 우리는 pages폴더 아래에 _document파일을 추가해서 사용할 수 있습니다.

https://nextjs.org/docs/advanced-features/custom-document

예를 들어 html 문서의 <head> 태그 사이에 무언가를 추가하고 싶을 때에는 지금처럼 custom document를 만들어서 추가해 줄 수 있습니다.

pages > _document.js

import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html>
      <Head>
        <link
          href="https://fonts.googleapis.com/css2?family=Roboto&display=swap"
          rel="stylesheet"
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

마지막으로 styles > globals.css 에서 Roboto를 지정해줍니다.

style 설정은 끝났습니다.
이제 antd을 사용하여 page와 component를 코딩하고 꾸미면됩니다.

dayjs 설치

https://day.js.org/

출력되는 시간값의 포맷에 변경을 주기위해 dayjs를 설치합니다.

npm

npm i dayjs

yarn

yarn add dayjs

스타일 작업 (2) - Post

해당 페이지에 들어가서 post의 detail한 정보를 출력하는 부분은 sanity에서 제공하는 block-content-to-react library를 이용하여 만들어보겠습니다.

block-content-to-react 라이브러리 설치

npm

npm i @sanity/block-content-to-react

yarn

yarn add @sanity/block-content-to-react

간단한 사용법

import BlockContent from "@sanity/block-content-to-react";
// ...
export default function BlogPostDetail({ blocks }) {
  return (
    <Row>
      <Col span={24}>
        <BlockContent
          blocks={blocks}
          projectId="2zbqhksc"
          dataset="production"
          serializers={serializers}
        />
      </Col>
    </Row>
  );
}

react-syntax-highlighter 설치

코드출력을 예쁘게 만들어줍니다.

npm

npm i react-syntax-highlighter

yarn

yarn add react-syntax-highlighter

데모 사이트 : https://react-syntax-highlighter.github.io/react-syntax-highlighter/demo/

간단한 사용법

import SyntaxHighlighter from "react-syntax-highlighter";
import { srcery } from "react-syntax-highlighter/dist/esm/styles/prism";

// ...

const serializers = {
  types: {
    code: ({ node }) => {
      const { code } = node;
      return (
        <SyntaxHighlighter language="javascript" style={srcery}>
          {code}
        </SyntaxHighlighter>
      );
    },

// ...

next.config.js

https://nextjs.org/docs/api-reference/next.config.js/introduction

root에서 next.config.js 를 생성합니다.

옵션

trailingSlash: false // false : url경로 뒤에 붙은 `/`를 자동으로 없애주게됩니다. true이면 없애도 다시 자동으로 붙여주게됩니다.
env: {
  SANITY_PROJECT_ID: 'dkfjl39348' // 이렇게 env에 지정해놓으면 어디서든지 전역으로 접근하여 해당 값을 사용할 수 있게 됩니다. 가져오는 예 : process.env.SANITY_PROJECT_ID
}

Next.js 배포 이해하기

배포하는 방식에는 크게 2가지가 있습니다.
1. 서버사이드 방식
2. static side generator를 이용하는 방식

현재 우리가 실습한 사이트는 SSG 방식입니다.

배포방법

  1. npm run build

빌드가 완료된 다음에는 2가지 선택을 할 수 있습니다.

1-1. npm start : production모드로 빌드된 프로젝트가 실행됩니다. 이렇게 해서 나온 페이지는 SSR이고, node.js 앱으로 실행된 것입니다.

npm start로 프로젝트를 실행하면 한가지 좋은 점이 있습니다.
pages안에 있는 api를 호출할 수 있게 됩니다.
참고설명 : Next.js API route support: https://nextjs.org/docs/api-routes/introduction

pages > api > hello.js

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

export default function handler(req, res) {
  res.status(200).json({ name: 'John Doe' })
}

url 주소로 /api/hello로 접근하면 설정한 json값 { name: 'John Doe' }이 출력됩니다. 이 기능은 static side generation에서는 활용할 수 없는 기능입니다. 이것은 node서버가 있기 때문에 node 서버가 request를 받아서 응답을 response 해주기 때문에 이렇게 사용할수 있습니다.

이번에는 static side generator 기능을 이용해보겠습니다.

1-2. next export: 빌드된 결과물이 static page로 export 되게됩니다. build 후 export 될 수 있도록 package.json 파일을 수정하겠습니다. script에서 "build": "next build && next export"로 수정합니다. 그 후 다시 npm run build 를 실행합니다.

out이라는 폴더가 새로 생깁니다. static 사이트로 만들어진 폴더입니다. out이라는 폴더를 파일서버로 실행하면 static한 사이트로 활용할 수 있게 됩니다.

npx serve out 을 실행합니다. 위에서 했던 hello로 request를 요청하면 아까와는 다르게 json데이터를 가져오기 못하고 404에러가 나옵니다.

out 폴더안의 post에 들어가면 html파일이 나옵니다. 이 파일들을 그대로 배포하면 서버성능과 관계없이, 트레픽에 문제없이 우리들의 블로그 사이트를 운영할 수 있게 됩니다.

0개의 댓글