개발 블로그에서 마크다운을 다루는 방법
2022. 06. 19.마크다운?
마크다운은 일반 텍스트 기반의 경량 마크업 언어다. 일반 텍스트로 서식이 있는 문서를 작성하는 데 사용되며, 일반 마크업 언어에 비해 문법이 쉽고 간단한 것이 특징이다. 위키백과
# 이처럼 #이 하나 붙으면 h1 태그로 렌더함
- list도 만들 수 있고
`Code도 표현할 수 있다.`
```ts
const HelloBlog = () => "Hello Blog";
```
이처럼 #이 하나 붙으면 h1 태그로 렌더함
- list도 만들 수 있고
Code도 표현할 수 있다.
const HelloBlog = () => "Hello Blog";
블로그 글을 쓸 때 매번 <h1>제목</h1>
<code>const a = 1</code>
이렇게 작성한다면 나는 글도 써야하고 마크업도 신경써야 할 것이다.
이 때 마크다운 형식으로 포스팅을 작성한다면
- 앞서 말한 것처럼 글감에만 집중할 수 있다.
- 마크다운 형식을 사용하는 다른 곳에 사용하기 용이하다.(대표적으로 GitHub Issue, Pull Request 등)
- 몇가지 스타일을 커스텀해두면 그 다음에는 디자인을 신경 쓰지 않아도 글을 작성하는 것이 수월하다.
본론으로 돌아와서, 나의 블로그 제작 계획은 이러했다.
/posts/
에 포스팅.md
형식으로 작성하고, 웹페이지를 렌더할 때(정확히는 빌드타임) 해당 파일을 읽어서 html로 변환하여 보여주는 것이다. 즉 내가 포스팅을 작성하기 위해서는 마크다운 파일을 /posts/
아래에 작성만 하면 되는 것이다.
우선 렌더하는 곳 /posts/[postid]
에서 getStaticPaths
/getStaticProp
를 통해 마크다운 파일을 읽도록 했다. 이 함수들은 빌드타임에 실행되고, 빌드 후에는 다시 실행되지 않는 함수이다. (즉 유저가 접속할 때 마다 실행되는 것이 아니고 빌드 시에 호출한 getStaticXXX()의 결과물을 보여줄 것이다.) next 도큐먼트
export const getStaticPaths: GetStaticPaths = async () => {
const dir = path.join(process.cwd(), "posts");
const fileNames = fs.readdirSync(dir);
const paths = fileNames.map((fileName) => ({
params: { fileName: postUtils.getFileNameBase(fileName) },
}));
return {
paths,
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
if (params?.fileName == null) {
throw new Error("fileName을 찾을 수 없습니다.");
}
const dir = path.join(process.cwd(), "posts");
const fileName = `${dir}/${params.fileName}.md`;
const content = fs.readFileSync(fileName, "utf8");
const { attributes, body } = postUtils.parseFrontMatter(content);
const safeAttributes = postUtils.safeParseAttributes(attributes, {
date: dateUtils.formatDate(fs.statSync(fileName).birthtime),
});
return {
props: {
attributes: safeAttributes,
body,
},
};
};
이 때 포스트의 메타데이터를 주입해주기 위해 frontMatterParser
를 사용했는데, 파일 맨 위에 선엄함으로써 이 포스트의 title, description, date 등의 정보들을 정의할 수 있다. yaml 파일 형식에서 많이 사용하는 듯 하다. 나는 예전에 jekyll을 이용한 깃허브 블로그를 만들 때 front matter를 사용했던 기억이 떠올라 같은 방식을 선택했다.
---
title: "블로그 만들기(1) - 마크다운 이 녀석"
description: "개발 블로그에 기본이라고 할 수 있는 마크다운 파서와 Syntax highlight 삽질을 들여다보자"
date: "2022-06-19"
---
이 포스팅의 FrontMatter
작업을 요약하자면
- 마크다운 파일을 불러와서
- front matter를 분리한 본문(body)를 획득했다.
남은 작업은 아래의 3번 4번 정도인데..
- markdown을 html로 바꾸어 렌더하기
- syntax highlighting을 통해 고도화하기
Markdown 문법의 텍스트를 html로 바꾸어주는 라이브러리는 꽤나 많은데, 가장 유명하고 커뮤니티가 큰 라이브러리는 unifiedjs 이다. unified는 remark
(마크다운) / rehype
(HTML), retext
(자연어) 등등을 지원하는데, 블로그 포스팅을 위해서는 remark와 rehype만 있으면 된다.
추가적으로 Code Syntax를 하이라이팅 해주고 싶어서 rehype-prism-plus
라는 라이브러리도 추가했다.
import rehypePrism from "rehype-prism-plus";
import rehypeStringify from "rehype-stringify/lib";
import { remark } from "remark";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
function MarkdownRenderer({ markdown }: { markdown: string }) {
const htmlText = remark()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStringify, { allowDangerousHtml: true })
.use(rehypePrism)
.processSync(markdown)
.toString();
return <MarkdownRenderBlock dangerouslySetInnerHTML={{ __html: htmlText }} />;
}
const MarkdownRenderBlock = styled.div`
line-height: 3rem;
blockquote {
box-shadow: ${createNeumorphismBoxShadow(1, 2, { inset: true })};
margin: 0;
padding: 5px;
border-left: 0.25em solid ${neumorphismTheme.secondary};
& > * {
margin: 0;
}
}
p > code {
box-shadow: ${createNeumorphismBoxShadow(1, 2, { inset: true })};
padding: 5px;
margin-left: 3px;
margin-right: 3px;
}
:not(pre) > code[class*="language-"],
pre {
background: ${theme.palette.primary.light};
box-shadow: ${createNeumorphismBoxShadow(3, 5, { inset: true })};
border: none;
padding: 15px;
}
// 여기 아래부터 커스텀(prism-themes)
/**
* VS theme by Andrew Lock (https://andrewlock.net)
* Inspired by Visual Studio syntax coloring
*/
code[class*="language-"],
pre[class*="language-"] {
color: #393a34;
font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier,
monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
font-size: 0.9em;
line-height: 1.2em;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre > code[class*="language-"] {
font-size: 1em;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
background: #c1def1;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
background: #c1def1;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border: 1px solid #dddddd;
background: none;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: 0.2em;
padding-top: 1px;
padding-bottom: 1px;
background: #f8f8f8;
border: 1px solid #dddddd;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #008000;
font-style: italic;
}
.token.namespace {
opacity: 0.7;
}
.token.string {
color: #a31515;
}
.token.punctuation,
.token.operator {
color: #393a34; /* no highlight */
}
.token.url,
.token.symbol,
.token.number,
.token.boolean,
.token.variable,
.token.constant,
.token.inserted {
color: #36acaa;
}
.token.atrule,
.token.keyword,
.token.attr-value,
.language-autohotkey .token.selector,
.language-json .token.boolean,
.language-json .token.number,
code[class*="language-css"] {
color: #0000ff;
}
.token.function {
color: #393a34;
}
.token.deleted,
.language-autohotkey .token.tag {
color: #9a050f;
}
.token.selector,
.language-autohotkey .token.keyword {
color: #00009f;
}
.token.important {
color: ${theme.palette.secondary.dark};
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.class-name,
.language-json .token.property {
color: #2b91af;
}
.token.tag,
.token.selector {
color: #800000;
}
.token.attr-name,
.token.property,
.token.regex,
.token.entity {
color: #ff0000;
}
.token.directive.tag .tag {
background: #ffff00;
color: #393a34;
}
/* overrides color-values for the Line Numbers plugin
* http://prismjs.com/plugins/line-numbers/
*/
.line-numbers.line-numbers .line-numbers-rows {
border-right-color: #a5a5a5;
}
.line-numbers .line-numbers-rows > span:before {
color: #2b91af;
}
/* overrides color-values for the Line Highlight plugin
* http://prismjs.com/plugins/line-highlight/
*/
.line-highlight.line-highlight {
background: rgba(193, 222, 241, 0.2);
background: -webkit-linear-gradient(
left,
rgba(193, 222, 241, 0.2) 70%,
rgba(221, 222, 241, 0)
);
background: linear-gradient(
to right,
rgba(193, 222, 241, 0.2) 70%,
rgba(221, 222, 241, 0)
);
}
`;
스타일은 조금씩 커스텀했다.
만족스러운 디자인의 포맷을 만들어서 기분이 좋다. 글을 열심히 써야겠다.