Next.js 블로그 만들기 - (1) React-Markdown

shorecrab·2022년 6월 14일
10

처음부터 블로그를 전부 만드려는 계획은 있었는데 카카오 인턴에 합격하고 비는 시간 동안 내친김에 만들어보기로 했다. 이전하려는 이유는 velog가 불편하다거나 한 것이 아니라 내가 세세하게 조절할 수 있는 블로그를 만들고 싶었기 때문이다.
그리고 블로그를 제작에 대한 글을 블로그에 올리는 것도 재밌을 것 같아서 올려보려고 한다. 블로그 제작의 모든 과정을 담을 수 있으면 좋겠지만, 개발하면서 이전에 잘 해보지 않았던 것들이나 난이도가 조금 있는 것들에 대해서만 글을 작성할 것이다.

시작

최대한 간단하게 MVP(Minimum Value Product, 최소 기능 결과)를 뽑아내서 velog와 병행해서 사용하기로 했다. 이후에 마크다운 에디터, 인증 등을 추가하고 UI를 개선하면 완전 이전을 하려고 한다. 구성은 다음과 같이 준비하려고 한다. 최대한 간단하고 빠르게 구축할 수 있도록 준비했다.

오늘은 우선 DB도 사용하지 않고 파일 시스템 만으로 마크다운 렌더링을 시도해 볼 것이다.

프로젝트 시작은 Next.js + Typescript + StyledComponent + ESLint + StyleLint + Prettier + Husky를 사용해 개인적으로 만든 보일러 플레이트에서 시작했다. (보일러 플레이트 관련 포스트도 올리도록 하겠다.) 거기에 Storybook을 추가해서 컴포넌트 개발을 편하게 할 수 있도록 했다.

정적 페이지 생성

Next.js의 getStaticPropsgetStaticPaths를 활용해서 마크다운에 대한 정적 페이지를 생성할 수 있도록 개발을 시작했다. 원래는 빌드타임에 한 번만 정적 페이지를 생성할 때 사용하는 getStaticProps 만 있었던 것 같은데, Next 버전이 올라가면서 getStaticPaths 가 추가된 것 같다. getStaticPaths를 사용하면 새로운 정적 페이지에 접근할 때 페이지를 런타임에 생성할 수 있도록 할 수 있다. 생성된 페이지는 저장되어서 이후의 접근에는 새롭게 생성하지 않는다. (getStaticProps에는 revalidate를 prop과 같이 넘겨서 이전에 방문한 페이지더라도 다시 생성하는 기능이 있는데, 여기서는 아직 사용하지 않았다.)

아래 코드를 통해서 파일 시스템에서 마크다운 파일을 읽고 새로운 정적 페이지를 생성해주고 있다.

// pages/posts/[id]/index.tsx
export async function getStaticProps(context: GetStaticPropsContext) {
  try {
    const post = fs
      .readFileSync(`public/posts/${context.params?.id}.md`)
      .toString();

    return {
      props: { post },
    };
  } catch (err) {
    console.error(err);
    return {
      props: {},
      notFound: true,
    };
  }
}

export async function getStaticPaths() {
  try {
    const posts = fs
      .readdirSync('public/posts')
      .map(file => file.split('.')[0]);

    const paths = posts
      .filter(file => file.match(/\.md$/))
      .map(post => ({
        params: {
          id: post,
        },
      }));

    return { paths, fallback: 'blocking' };
  } catch (err) {
    console.error(err);
  }
}

getStaticProps 를 잘 보면, 마크다운 파일을 읽어서 props.post로 넘겨주고 있는 것을 확인할 수 있다. 해당 포스트는 React-Markdown 패키지를 통해서 React 엘리먼트로 렌더링되도록 했다.

마크다운 변환

React-Markdown을 활용해서 마크다운을 변환하는 컴포넌트를 생성했다. 처음 해보는 작업이어서 자료를 찾는데 시간이 조금 걸렸다. React-Markdown은 내부적으로 remark 패키지를 활용하고 아래처럼 순차적인 처리를 통해서 React 엘리먼트로 뽑아낸다.

                                                           react-markdown
         +----------------------------------------------------------------------------------------------------------------+
         |                                                                                                                |
         |  +----------+        +----------------+        +---------------+       +----------------+       +------------+ |
         |  |          |        |                |        |               |       |                |       |            | |
markdown-+->+  remark  +-mdast->+ remark plugins +-mdast->+ remark-rehype +-hast->+ rehype plugins +-hast->+ components +-+->react elements
         |  |          |        |                |        |               |       |                |       |            | |
         |  +----------+        +----------------+        +---------------+       +----------------+       +------------+ |
         |                                                                                                                |
         +----------------------------------------------------------------------------------------------------------------+

복잡한 내부 구현에 비해 사용법은 아주 간단하다. <ReactMarkdown>으로 마크다운을 감싸주기만 하면 된다.
아래 코드는 Syntax Highlight까지 적용한 모습이다. Syntax Highlight을 적용해야 velog에서 처럼 코드에 색깔을 입혀줄 수 있다.
하이라이팅을 위해서는 주로 hl.js나 prism 같은 패키지가 있어서 이것들을 사용한다고 들었는데, 여기서는 remark로 마크다운을 내가 직접 뽑아서 하이라이팅을 할 것이 아니라 React-Markdown과 같이 쓸 수 있는 SyntaxHighlighter 패키지를 사용했다. SyntaxHighlighter를 적용한 코드 역시 React-Markdown 소개 페이지에서 알려주고 있어서 비교적 쉽게 따라할 수 있었다.

// stories/components/markdown-view/index.tsx
interface MarkdownViewProps {
  post: string;
}

const MarkdownView = ({ post }: MarkdownViewProps) => {
  return (
    <ReactMarkdown
      components={{
        code({ inline, className, children, ...props }) {
          const match = /language-(\w+)/.exec(className || '');
          return !inline && match ? (
            <SyntaxHighlighter
              language={match[1]}
              PreTag="div"
              {...props}
              style={docco}
            >
              {String(children).replace(/\n$/, '')}
            </SyntaxHighlighter>
          ) : (
            <code className={className} {...props}>
              {children}
            </code>
          );
        },
      }}
    >
      {post}
    </ReactMarkdown>
  );
};

렌더링

// pages/posts/[id]/index.tsx
const Post = ({ post }: PostProps) => {
  return (
    <Layout>
      <div className="markdown-body">
        <MarkdownView post={post} />
      </div>
    </Layout>
  );
};

처음에 보여준 코드와 같은 파일에 있다.
바로 위에서 만든 MarkdownView를 사용해서 페이지를 렌더링한다. <div>를 잘 보면 className="markdown-body"라는 속성이 보인다. 이것은 Github 마크다운의 스타일을 따라한 css를 적용하기 위해서 설정한 것이다.

결과

위 그림처럼 마크다운 렌더링에 성공했다! Syntax Highlighting도 잘 적용된 모습이고 github style 역시 적용되어 있다. 간단하지만 생각보다 복잡한 과정을 통해서 마크다운 렌더링을 할 수 있었다.
다음에는 DB 연동 과정에 대해서 글을 쓸 것이다. 간단하게 ORM도 쓰지 않고 SQLite를 사용해서 파일 메타데이터를 등록하고 이를 바탕으로 getStaticPaths 에서 새로운 페이지를 만들 수 있도록 할 것이다.

profile
주니어 프론트엔드 개발자!

0개의 댓글