본문 바로가기
Frontend/Next.js

[Next.js] 튜토리얼 블로그

by 디스코비스킷 2024. 1. 17.
반응형

메인 페이지 UI 만들기(마크다운 파일 생성)

튜토리얼 블로그

폴더 생성 후
npx create-next-app ./ --typescript
넥스트+타입스크립트 템플릿으로 설치


기본 화면

메인페이지 UI만들기

layout.tsx에서 favicon이나 head안의 데이터를 수정할 수 있다.
page.tsx에서는 메인페이지의 UI를 만들 수 있다.

md파일안에 포스트 생성하기

블로그 튜토리얼 공식문서
서버가 없기때문에
여기의 블로그 데이터를 md(mark down)파일로 만들어서 사용할것이다.

posts 폴더 생성


각 포스트별로 md파일을 만든다.

마크다운 파일을 데이터로 추출하기

markdown 형식의 데이터를 일반 글로 전환하는 함수가 필요.

post에 사용할 함수 생성

여러 모듈을 사용하면서 파일 읽고 replace해서 데이터로 추출하는 작업 필요.
date를 내림차순으로 정렬(sort)

  • fs: fs 모듈은 파일 시스템과 관련된 작업을 수행하는 데 사용되는 모듈이다. fs 모듈을 사용하여 파일을 읽고 쓰고, 디렉토리를 만들고 삭제하고, 파일의 권한을 변경할 수 있다.
  • path: path 모듈은 파일 경로를 처리하는 데 사용되는 모듈이다. path 모듈을 사용하여 파일 경로를 조작하거나 파일 경로의 일부를 추출할 수 있다.
  • matter(gray-matter): matter 모듈은 Markdown 파일을 처리하는 데 사용되는 모듈이다. matter 모듈을 사용하여 Markdown 파일을 HTML로 변환할 수 있다. md파일을 convert해줌.

lib/posts.ts

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

// posts directory 가져옴
const postsDirectory = path.join(process.cwd(), 'posts');

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

    const allPostsData = fileNames.map(fileName => {
        // fileName을 id로 
        const id = fileName.replace(/\.md$/, ''); 

        const fullPath = path.join(postsDirectory, fileName);
        const fileContents = fs.readFileSync(fullPath, 'utf-8');

        const matterResult = matter(fileContents);

        return {
            id, 
              ...matterResult.data as {date: string; title: string},
        }
    })

    return allPostsData.sort((a, b) => {
        if(a.date < b.date) {
            return 1
        } else {
            return -1
        }
    })

}

getStaticProps를 이용한 포스트 리스트 나열

getStaticProps로 allPostData를 가져와서 리스트 Map

실수로 Next.js 14버전 이상으로 설치를 해서
pages.tsx에서 작업하던 내용을 pages 폴더에 MainPage컴포넌트를 생성해 작성했다. 라우팅을 따로 해주어야하는데 서버에서 렌더되기전에 라우팅처리를 어떻게 하는지 몰라서 아직 두는중... 일단 컴포넌트만 작성.
나중에 라우팅 처리를 할 것이다..

import { getSortedPostsData } from "@/lib/posts";
import { GetStaticProps } from "next";
import Image from "next/image";

interface allPostDataProps {
  allPostsData: {
    date: string;
    title: string;
    id: string;
  }[];
}

const MainPage = ({ allPostsData }: allPostDataProps) => {
  console.log("test", allPostsData)
  console.log("test222")
  return (
    <main className="flex min-h-screen flex-col items-center p-24 gap-5">
      <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
        <p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto  lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
          블로그입니다.
        </p>
      </div>

      <div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1] text-2xl">
        JNGMNJ
      </div>
      <section>
        <div className="text-center">
          <p>[JNGMNJ Introduction]</p>
          <p>(This is a website)</p>
        </div>
      </section>
      <section className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
        {allPostsData.map(({ id, title, date }) => (
          <a
            href=""
            className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
            target="_blank"
            rel="noopener noreferrer"
          >
            <h2 className={`mb-3 text-2xl font-semibold`}>
              {title}
              <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
                -&gt;
              </span>
            </h2>
            <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>{id}</p>
            <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>{date}</p>
          </a>
        ))}
      </section>
    </main>
  );
};

export default MainPage;

export const getStaticProps: GetStaticProps = async () => {
  const allPostsData = getSortedPostsData();
  return {
    props: {
      allPostsData,
    },
  };
};

포스트 자세히 보기 페이지로 이동(file system 기반의 라우팅)

파일기반 네비게이션 기능

리액트에서는 route를 위해서 react-router라는 라이브러리를 사용하지만,
Next.js에서는 페이지 개념을 기반으로 구축된 파일 시스템 기반 라우터가 있다.

파일이 페이지 디렉토리에 추가되면 자동으로 경로를 사용할 수 있다.
페이지 디렉토리 내의 파일은 가장 일반적인 패턴을 정의하는 데 사용할 수 있다.

파일 생성 예시

pages/index.js 👉 "/" 경로
pages/blog/index.js 👉 "/blog" 경로
pages/blog/first-post.js 👉 "/blog/first-post"
pages/blog/[slug].js 👉 "/blog/:slug(/blog/hellow-world)"
pages/[username]/settings.js 👉 "/:username/settings"

포스트 파일 생성

posts/[id].tsx

  import React from 'react'

  const Post = () => {
    return (
      <div>[id]</div>
    )
  }

  export default Post

Link 함수를 이용한 페이지 이동

포스트리스트 map()안에서 Link걸어준다.
리액트와 다르게 href 속성을 사용.

<Link href={/posts/${id}}>

포스트 데이터를 가져와서 보여주기(remark)

Post데이터를 가져와야 하는 경로 목록을 가져오기

pages/posts/[id].tsx

 export const getStaticPaths: GetStaticPaths = async () => {
    const paths = getAllPostIds();
    return {
      paths,
      fallback: false // getStaticPaths로 리턴되지 않는 것은 모두 404page
    }
  }

전달받은 아이디를 이용해서 해당 포스트의 데이터 가져오기

remark와 remark-html을 이용해서 markdown을 html 변환
npm install remark remark-html --save
/lib/posts.ts

export async function getPostData(id: string) {
    // matter를 이용해서 html string 변환
    const fullPath = path.join(postsDirectory, `${id}.md`)
    const fileContents = fs.readFileSync(fullPath, 'utf-8')

    const matterResult = matter(fileContents);
    const processedContent = await remark().use(remarkHtml).process(matterResult.content);
    const contentHtml = processedContent.toString();

    return {
        id,
        contentHtml,
        ...(matterResult.data as {date: string; title: string;})
    }
}

/pages/[id].tsx

import { getAllPostIds, getPostData } from '@/lib/posts'
import { GetStaticPaths, GetStaticProps } from 'next'
import React from 'react'

  interface postDataProps {
    postData: {
      title: string;
      contentHtml: string;
      date: string;
    }
  }
  const Post = ({ postData }: postDataProps) => {
    return (
      <div>
        <div>
          <title>{postData.title}</title>
        </div>
        <article>
          <h1>{postData.title}</h1>
          <div>{postData.date}</div>
          <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
        </article>
      </div>
    );
  };

  export default Post

  export const getStaticPaths: GetStaticPaths = async () => {
    const paths = getAllPostIds();
    return {
      paths,
      fallback: false // getStaticPaths로 리턴되지 않는 것은 모두 404page
    }
  }

  export const getStaticProps: GetStaticProps = async ({ params }) => {
    const postData = await getPostData(params.id as string);

    return {
      props: {
        postData
      }
    }
  }

반응형

최근댓글

최근글

© Copyright 2023 jngmnj