[Next.js] Next.js 따라하기: 블로그

donguraemi·2023년 8월 25일
0

넥스트JS

목록 보기
4/5
post-thumbnail

해당 프로젝트는 Next.js 튜토리얼을 따라만든 것입니다. 튜토리얼의 예시코드는 여기에서 확인하실 수 있습니다.

example
튜토리얼을 따라 만들면 위와 같은 결과물을 얻을 수 있다. 헤더에는 작성자의 프로필이 나타나고 아래에 블로그 포스팅 리스트가 나열된다. 포스팅을 클릭하여 포스팅 내용을 보는 것도 가능하다.


프로젝트 구조

├── components
│   ├── date.js
│   ├── layout.js
│   └── layout.module.css
├── lib
│   └── posts.js
├── pages
│   ├── api
│   │	└── hello.js
│   └── posts
│   │	└── [id].js
│   ├── _app.js
│   └── index.js
├── posts
│   ├── pre-rendering.md
│   └── ssg-ssr.md
├── public
│   └── images
│   	└── profile.jpg
└── styles
    ├── global.css
    └── utils.module.css

위의 파일들은 해당 프로젝트에서 편집하는 파일들이다. 각 파일들의 쓰임은 다음과 같다.

  • components : 웹 페이지에 사용되는 컴포넌트 관련 파일 저장
  • lib/posts.js : 블로그 포스팅을 불러오는 SSG 메서드 저장
  • pages
    • api : api 저장
    • posts : 특정 블로그 포스트를 클릭하면 실행되는 url로 동적 경로를 지원
  • posts : 블로그 포스팅 내용 저장
  • public : 이미지 같은 정적 자산 저장
  • styles : css 파일 저장

_app.jsglobal.css

모든 페이지에 공통으로 같은 CSS 파일을 적용하고 싶다면 어떻게 해야할까? 페이지마다 CSS 파일을 가져와서 사용할 수도 있지만 페이지가 많아질수록 이는 매우 귀찮은 작업이다.

_app.js는 어플리케이션의 모든 페이지를 감싸는 최상위 리액트 컴포넌트다. 여기에 글로벌 css 파일을 추가하면 모든 페이지에 같은 css가 적용된다.

import '../styles/global.css';

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

layout.js

post result
위 사진은 메인 페이지에서 첫번째 포스트를 클릭하면 출력되는 화면이다. 메인 페이지와 마찬가지로 작성자의 프로필이 상단에 출력된다. 공통적으로 사용되는 부분을 컴포넌트로 분리하고 페이지에서 해당 컴포넌트를 가져오는 방식으로 구현하면 코드 중복을 줄이고 일관성 있게 코드를 작성할 수 있다.

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

const name = 'Chae Hyungwon';
export const siteTitle = 'Next.js Sample Website';

export default function Layout({ children, home }) {
  return (
    <div className={styles.container}>
      <Head>...</Head>
      <header className={styles.header}>
        {home ? (
          <>
            <Image
              priority
              src="/images/profile.jpg"
              className={utilStyles.borderCircle}
              height={144}
              width={144}
              alt="Chae Hyungwon"
            />
            <h1 className={utilStyles.heading2Xl}>{name}</h1>
          </>
        ) : (
          <>
            <Link href="/">
              <Image
                priority
                src="/images/profile.jpg"
                className={utilStyles.borderCircle}
                height={108}
                width={108}
                alt="ChaeHyungwon"
              />
            </Link>
            <h2 className={utilStyles.headingLg}>
              <Link href="/" className={utilStyles.colorInherit}>
                {name}
              </Link>
            </h2>
          </>
        )}
      </header>
      <main>{children}</main>
      {!home && (
        <div className={styles.backToHome}>
          <Link href="/">← Back to home</Link>
        </div>
      )}
    </div>
  );
}

화면에 출력되지 않는 <Head>의 내용은 생략하고 본문만 가져왔다. layout.jsLayout 컴포넌트는 childrenhome을 전달받는다.

  • children : 페이지의 메인 내용
  • home : 메인 페이지 여부

children<main> 태그에 감싸져 출력되는 메인 콘텐츠다. home은 메인 페이지의 유무를 나타내는데 home이 전달되는 경우, 메인 페이지로 간주한다.

{home ? (
  <>
    <Image
      priority
      src="/images/profile.jpg"
      className={utilStyles.borderCircle}
      height={144}
      width={144}
      alt='profile'
    />
    <h1 className={utilStyles.heading2Xl}>{name}</h1>
  </>
  ) : (
  <>
    <Link href="/">
      <Image
        priority
        src="/images/profile.jpg"
        className={utilStyles.borderCircle}
        height={108}
        width={108}
        alt='profile'
      />
    </Link>
    <h2 className={utilStyles.headingLg}>
      <Link href="/" className={utilStyles.colorInherit}>{name}</Link>
    </h2>
  </>
)}

첫번째 삼항 연산자를 해석해보자. 메인 페이지인 경우, 프로필이 144 x 144 크기로 출력된다. 메인 페이지가 아닌 경우, 108 x 108 크기로 출력되며 클릭 시 메인 페이지로 이동한다.

{!home && (
  <div className={styles.backToHome}>
    <Link href="/">← Back to home</Link>
  </div>
)}

두번째 삼항 연산자를 보자. 메인 페이지가 아닌 경우, 메인 페이지로 이동하는 링크가 표시된다.


index.js : 블로그 메인 화면 만들기

앞에서 만든 Layout을 활용하여 블로그 메인 화면을 만들어보자.

import Head from 'next/head';
import Link from 'next/link';
import Layout, { siteTitle } from '../components/layout';
import Date from '../components/date';
import { getSortedPostsData } from '../lib/posts';
import utilStyles from '../styles/utils.module.css';

export default function Home({ allPostsData }) {
  return (
    <Layout home>
      <Head>
        <title>{siteTitle}</title>
      </Head>
      <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}`}>{title}</Link>
              <br />
              <small className={utilStyles.lightText}>
                <Date dateString={date} />
              </small>
            </li>
          ))}
        </ul>
      </section>
    </Layout>
  );
}

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

Home 컴포넌트는 allPostsData를 전달받는다. allPostsData는 블로그의 모든 포스팅 정보다. 데이터를 전달받으면 리스트 형식으로 출력한다.


posts/[id].js : 동적 경로 만들기

벨로그의 포스팅은 velog.io/@[벨로그 프로필]/[포스팅 제목] 형태의 url을 갖는다. 벨로그처럼 외부 데이터를 활용하여 동적으로 경로를 생성해보자.

posts/[포스팅 id] 형식의 url을 사용하려면 pages의 하위 디렉토리에 posts 디렉토리를 만들고 그 밑에 [id].js 파일을 만든다.

import Head from 'next/head';
import Layout from '../../components/layout';
import Date from '../../components/date';
import { getAllPostIds, getPostData } from '../../lib/posts';
import utilStyles from '../../styles/utils.module.css';

export default function Post({ postData }) {
    return (
    <Layout>
        <Head>
            <title>{postData.title}</title>
        </Head>
        <article>
            <h1 className={utilStyles.headingX1}>{postData.title}</h1>
            <div className={utilStyles.lightText}>
                <Date dateString={postData.date} />
            </div>
            <div dangerouslySetInnerHTML={{__html: postData.contentHtml}} />
        </article>
    </Layout>
    );
}

export async function getStaticPaths() {
    const paths = getAllPostIds();
    return {
        paths,
        fallback: false
    }
}

export async function getStaticProps({ params }) {
    const postData = await getPostData(params.id);
    return {
        props: {
            postData
        }
    }
}

Home 컴포넌트와 마찬가지로 Layout 컴포넌트를 가져와 사용한다. Post 컴포넌트는 특정 포스팅의 정보인 postData를 전달받는다.

getStaticPaths는 사전 렌더링할 경로를 결정한다. getStaticPathspathsgetStaticProps로 전달한다.


lib/posts.js : 포스팅 데이터 조회하기

앞에서 getStaticPropsgetStaticPaths 메서드가 나왔는데 이에 대한 설명이 없었다. 여기서 getStatic...에 대해 알아보자.

getStatic...은 SSG 방식으로 데이터를 렌더링 한다. SSG는 빌드 시에 데이터를 렌더링하기 때문에 자주 수정되지 않는 데이터를 조회할 때 사용하면 좋다.

lib/pages는 다양한 모듈을 사용한다. fspath 모듈의 경우 내장되어 있지만 gray-matterremark는 설치가 필요하다.

npm install gray-matter
npm install remark
  • gray-matter : 문자열이나 파일의 front-matter를 파싱하는 모듈이다. 마크다운 파일로 블로그를 만들 경우 거의 필수로 사용된다.
  • remark : 마크다운 입력값을 파싱하여 마크다운을 출력한다. remark-html은 마크다운을 HTML로 컴파일한다.
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';

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

export function getSortedPostsData() {
    const fileNames = fs.readdirSync(postsDirectory);
    const allPostsData = fileNames.map(fileName => {
        const id = fileName.replace(/\.md$/, '');
        const fullPath = path.join(postsDirectory, fileName);
        const fileContents = fs.readFileSync(fullPath, 'utf8');

        const matterResult = matter(fileContents);
        return {
            id,
            ...matterResult.data
        };
    });
    return allPostsData.sort((a, b) => { return a.data - b.data; });
}

export function getAllPostIds() {
    const fileNames = fs.readdirSync(postsDirectory);
    return fileNames.map(fileName => {
        return {
          params: {
            id: fileName.replace(/\.md$/, '')
        }
    }
  });
}
  
export async function getPostData(id) {
    const fullPath = path.join(postsDirectory, `${id}.md`);
    const fileContents = fs.readFileSync(fullPath, 'utf8');
  
    const matterResult = matter(fileContents);
    const processedContent = await remark().use(html).process(matterResult.content);
    const contentHtml = processedContent.toString();
    return {
      id,
      contentHtml,
      ...matterResult.data
    };
}
  • getSortedPostsData : (in 메인 페이지) 블로그의 전체 포스팅 조회
  • getAllPostIds : (in 포스트 페이지) 블로그의 전체 포스팅 아이디값 조회
  • getPostData : (in 포스트 페이지) id에 해당하는 포스트 데이터 조회

Fallback을 사용하는 이유

export async function getStaticPaths() {
    const paths = getAllPostIds();
    return {
        paths,
        fallback: false
    }
}

getStaticPathspaths와 함께 fallback: false를 반환한다. fallback은 뭘까?

fallback은 어떤 기능이 제대로 동작되지 않았을 때 대처하는 기능이나 동작을 의미한다. fallback은 다음 값을 가질 수 있다.

  • false : 유효하지 않은 경로라면 404 page 반환

  • true : getStaticProps의 기능을 다음과 같이 변경

    • getStaticPaths로부터 반환된 경로들이 빌드 시에 getStaticProps에 의해 HTML로 렌더링 된다.
    • 빌드 시에 생성된 경로가 아니라면, 404 페이지를 반환하는 대신 페이지의 fallback 버전을 보여준다.
    • 백그라운드에서 요청된 경로에 대해 getStaticProps 함수를 이용하여 HTML과 JSON 파일을 생성한다.
    • 백그라운드 작업이 끝나면 요청된 경로에 해당하는 JSON 파일을 받아 새롭게 렌더링 한다.
    • 새롭게 생성된 페이지를 빌드 시에 렌더링된 페이지 리스트에 추가한다. 이후 같은 경로로 온 요청에 대해 해당 페이지를 재사용한다.
  • blocking : true와 비슷하게 동작하지만, 빌드 시에 생성된 경로가 아니라면 fallback 상태를 보여주지 않고 SSR처럼 동작한다.


date-fns : 날짜 조작하기

date-fns는 날짜 형식을 조작하는 라이브러리다. 내장된 라이브러리가 아니기 때문에 별도로 설치해야 한다.

npm install date-fns
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>
    );
}

API 만들기

API는 Application Programming Interface의 줄임말로, 두 어플리케이션 간 서비스 계약으로 볼 수 있다. 요청과 응답을 사용하여 두 어플리케이션이 서로 통신하는 방법을 정의한다.

pages/api의 모든 하위 파일은 /api/*로 매핑되어 api의 엔드포인트로 간주된다. 아주 간단한 api를 만들어보자.

/*
	/api/hello
*/
export default function handler(req, res) {
    res.status(200).json({text: 'Hello'});
}

Vercel를 이용하여 배포하기

Next.js를 배포하는 가장 간단한 방법은 Next.js 제작자가 개발한 Vercel 플랫폼을 이용하는 것이다. Vercel은 정적 및 하이브리드 어플리케이션을 위한 서버리스 플랫폼이다.

Vercel을 사용하기 위해서는 Vercel 계정을 만들어야 한다. Continue with Github를 선택하여 회원 가입을 진행했다.

다음으로 소스코드를 올린 레포지토리를 가져와야 한다. 레포지토리를 성공적으로 가져오면 configure project가 뜬다. 이는 수정하지 않고 기본값을 사용해도 된다. deploy 버튼을 누르면 다음과 같이 성공적으로 배포된 것을 확인할 수 있다. https://next-js-practice-gcpl8h6wc-kimyu0218.vercel.app/
deploy

0개의 댓글