Logo

Prolip's Blog

블로그(3) - Dynamic Routing, react-markdown
  • #Blog
  • #Next.js
  • #MarkDown

블로그(3) - Dynamic Routing, react-markdown

Prolip

2024-02-23

react-markdown 이용해 마크다운 표시하기 그런데 Dynamic Routing과 @tailwindcss/typhography을 곁들인..

오늘도 시작..

저번 포스팅까지 게시물을 간략하게 카드 형태로 표시하고, 캐러셀 형태로 사용자에게 표시하는 과정까지 기록해봤습니다.

이번엔 카드를 클릭했을 때 어떻게 md 파일을 읽어와 사용자에게 표시하는지에 대해 기록해보려 합니다..

포스트 데이터 받아오기

주인장은 각 게시물의 간략한 소개 및 정보를 담은 객체를 json 파일에 기록합니다.

그리고 해당 객체엔 path라는 속성이 존재해 이를 통해 md 파일과 썸네일 이미지를 받아올 겁니다.

그렇다면 이를 어떻게 불러오면 좋을까요??

export async function getPostData(filePath: string): Promise<PostData> { const filePath = path.join(process.cwd(), "data/posts", `${filePath}.md`); const postList = await getPostList(); const postData = postList.find((post) => post.path === filePath); if (!post) { throw new Error(`${filePath}를 다시 확인해주세요.`); } const content = await readFile(filePath, "utf-8"); return { ...postData , content }; }

fs 모듈을 이용해 노드 환경에서 서버 상의 데이터에 접근이 가능하다고 했었습니다..
동일하게 이번엔 동적으로 filePath를 전달 받아 해당하는 md 파일을 읽어오기만 하면 됩니다.

그리고 마크다운을 페이지에 표시할 때 title, description, category 등도 같이 출력하면 좋을 거 같기에 저번에 만들어둔 getPostList 함수를 이용해 해당하는 postData도 함께 반환하도록 구현했습니다.

이제 이걸 어떻게 사용하면 좋을까요?? 각 파일마다 페이지를 만들면 아주 번거로울 거 같은데요..
그래서 그 전에 다이나믹 라우팅이 무엇인지 짚고 가면 좋겠습니다.

Dynamic Routing

우리는 아직까진 정적 라우팅을 사용 중입니다. 즉 경로가 고정되어있다는 것입니다.

쉽게 설명하자면 /about, /contact 페이지 등 누가 언제 접속하더라도 항상 같은 페이지를 보여주도록 고정된 정적인 경로입니다.

그런데 게시물의 경로에 해당하는 페이지를 각각 만들어야 된다면 정말 귀찮을 겁니다..

그럴 때 사용자가 접근한 경로 혹은 상황에 따라 변화하는 동적인 라우팅을 제공하고 싶을 때 사용할 수 있는 다이나믹 라우팅을 사용하면 됩니다.

이는 단일 페이지, 즉 하나의 컴포넌트를 생성해 변화하는 데이터를 수용하여 제공된 라우트에 따라 렌더링하는 방법입니다.

어떻게 사용하나요??

Routing: Dynamic Routes | Next.js (nextjs.org) 공식문서에 친절히 나와있습니다.

example

위의 예시에 대해 설명해보겠습니다.

app ├── blog │ └── [slug] │ └── page.js ├── 어쩌구 └── 저쩌구

이렇게 폴더 구성을 해놓는다면 blog 뒤로 올 아직 알지 못하는 동적인 segments에 대한 라우팅을 지원하도록 구성할 수 있습니다.

아하 그럼 /blog/prolipprolip 으로 들어온 요청에 대한 params는 { slug : ‘prolipprolip’ } 이렇게 들어오겠군요!
이제 알았습니다.

아 저 폴더 이름은 일반적으로 id, slug를 사용합니다.

posts ├── [slug] │ └── page.tsx └── page.tsx

저는 이렇게 posts 폴더 내부에 [slug] 폴더를 만들어 /posts/’게시물의 고유 path’ 이렇게 동적인 경로를 처리했습니다..

근데 이렇게 설정은 했는데 해당 경로로 navigate는 어떻게 하나요??

Link

react-router-dom을 이용해보셨다면 아주 익숙한 친구입니다..
이제 다만 리액트에선 to=’/url’ 이었다면 next에선 href=’/url’ 이런식으로 설정해주시면 됩니다.

// Post.tsx export default function Post({ post: { title, description, category, date, path }}: { post: PostType }) { return ( <Link href={`/posts/${path}`}> <section> ~~ 포스트 데이터 </section> </Link> ); }

이렇게 이전에 만들어둔 Post 컴포넌트를 Link를 이용해 감싸주면 해당 포스트를 클릭했을 때 Link에 설정한 href를 이용해 navigate할 수 있습니다.

즉 저렇게 포스트마다 가진 고유한 path를 이용해 동적인 라우팅이 가능하게 됩니다. 그래서 이제 이거 어떻게 받나요??

params

공식문서에 따르면 웹 애플리케이션의 경로(URI)에서 가변적인 부분을 나타내는 동적인 segments는 params라는 prop에 전달된다고 나와있습니다.

해당 slug 폴더 내에 params를 받아 출력해보면 slug에 path가 전달된 것을 알 수 있습니다.
이제 포스트 데이터를 받아오는 함수에 path를 전달하면 데이터를 받아올 수 있게 됩니다.

type Props = { params: { slug: string; }; }; export default async function PostPage({ params: { slug } }: Props) { const post = await getPostData(slug); console.log(post.content) return ( <article></article> ); }

react-markdown

마크다운 형식의 파일을 화면에 출력하고 싶다면 react-markdown을 이용하면 좋습니다.

사용 방법도 아주 간단합니다.

마크다운 파일을 화면에 표시할 컴포넌트를 우선 만들어 줍시다.

import ReactMarkdown from "react-markdown"; import remarkGfm from 'remark-gfm' type Props = { content: string; }; export default function MarkDownRender({ content }: Props) { return <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>; }

react-markdown을 설치해 전달 받은 content를 감싸주시면 됩니다.

근데 remarkGfm이 뭡니까?

remarkGfm

Github Flavored Markdown의 약자로 github에서 기존 마크다운에 몇가지 기능을 추가하여 커스터마이징 한 버전입니다..
표, 링크, 각주, 취소선, 테스크 리스트 등이 있습니다.

이를 표시하기 위해 설치해야되는 플러그인입니다.

근데 딱 이렇게 만들어놓고 보면 엄청 못생긴 마크다운이 반겨줄 겁니다.
코드 인용이나 변수에 대한 색이 없어 구분도 힘들고 밋밋해 좀 아쉬우실 겁니다.

그래서 어떻게 예쁘게 만들 건데요??

syntax-highlighter

저는 코드 인용, 강조, 그리고 마크다운에 삽입한 이미지도 함께 최적화하고 싶어 사용했습니다.
https://github.com/remarkjs/react-markdown

여기 중간에 syntax-highlighter를 어떻게 사용하는지 잘 나와있습니다.

import ReactMarkdown from "react-markdown"; import remarkGfm from 'remark-gfm' import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"; import Image from "next/image"; type Props = { content: string; }; export default function MarkDownRender({ content }: Props) { return ( <ReactMarkdown remarkPlugins={[remarkGfm]} components={{ code(props) { const { children, className, node, ref, ...rest } = props; const match = /language-(\w+)/.exec(className || ""); return match ? ( <SyntaxHighlighter {...rest} PreTag='div' language={match[1]} style={oneDark} > {String(children).replace(/\n$/, "")} </SyntaxHighlighter> ) : ( <code {...rest} className={`${className} whitespace-pre-wrap break-keep mx-1`} > {children} </code> ); }, img: (image) => ( <Image className='w-full max-h-60 object-cover' src={image.src || ""} alt={image.alt || ""} width={500} height={350} /> ), }} > {content} </ReactMarkdown> ); }

위는 제가 사용 중인 코드의 일부입니다..

저는 코드 인용과 강조 표시에 대한 수정이 필요했기에 match를 이용해 사용 언어가 있는지 없는지 판단하여 사용 언어가 있다면 코드 인용, 없다면 강조에 대한 스타일을 덮어씌우고 있습니다.

코드 인용에 대한 부분은 SyntaxHighLighter를 이용해 oneDark라는 스킨을 적용하고 있습니다.

만약 oneDark 말고 더 많은 스타일을 보고 싶다면 React Syntax Highlighter Demo (react-syntax-highlighter.github.io) 여기 링크를 확인해보시면 됩니다..

이미지도 최적화하고 싶어요

저는 아직까진 포스팅으로 인한 용량이 크지 않기에 로컬 상에 모두 저장하고 있습니다.

만약 후에 방대해진다면 따로 어디 업로드하여 이미지를 받아와야겠으나 우선 로컬이라는 가정하에 처리 중입니다.

/postImages/blog-making-3.png 이런식으로 이미지를 마크다운에서 사용하고 있습니다.

Image 컴포넌트를 사용하신다면 로컬상의 경로를 지정하실 때 주의하셔야 됩니다.
해당 컴포넌트는 경로를 public 폴더를 기준으로 설정해주셔야만 합니다.

어떻게 알았냐구요? 저는 루트 폴더에 폴더를 생성해놓고 30분 동안 삽질을 하다 깨달았습니다.

하여튼 저렇게 이미지에 대한 처리까지 해주신다면 이미지 최적화까지 모두 완료되어 예쁜 마크다운을 보실 수 있습니다.

만약 당신이 tailwindcss를 사용 중이라면

만약 tailwindcss를 사용 중이라면 아직도 못생긴 마크다운이 반겨줄 겁니다.

기본적으로 알아두셔야 될 것이 있습니다.
tailwind를 사용하신다면 @tailwind base로 preflight를 설정해줍니다.
preflight는 tailwind에서 설정한 기본 style로 각기 다른 브라우저에서 설정되어 있는 스타일들의 충돌을 막기 위해 제공합니다.

이러한 이유로 heading, list 등 기본 태그의 스타일을 모두 없애버리기 때문에 markdown으로 파싱된 태그들의 스타일이 안 나오고 못생긴 겁니다.

해결 방법은요..?

두가지 선택지가 있습니다.

  1. Preflight - Tailwind CSS를 비활성화하기

    // tailwind.config.js module.exports = { corePlugins: { preflight: false, } }

    preflight을 그냥 비활성화시켜버리는 겁니다.
    근데 저는 이걸 전제로 이미 스타일을 설정했기 때문에 안 했습니다.

  2. @tailwindcss/typography

    • 내가 원하는 부분만 기본 스타일을 지정해줄 수 있습니다.
    • 공식문서에 따라 플러그인을 설치해 config에 추가해주시면 됩니다.
    // tailwind.config.js module.exports = { theme: { // ... }, plugins: [ require('@tailwindcss/typography'), // ... ], }

@tailwindcss/typography

이거 어떻게 사용합니까??

prose 이거 하나 추가해주시면 됩니다.

그리고 여러 옵션이 존재하는데 공식문서 확인해보시면 좋습니다.
예를들자면 다크모드 설정하고 싶으면 dark:prose-invert 이런 거.. 뒤짚는다 이런 겁니다.

<ReactMarkdown className='prose lg:prose-lg dark:prose-invert dark:prose-pre:bg-darkP'> {content} </ReactMarkDown>

제가 설정한 옵션들 처럼 prose엔 개별 태그 또한 설정이 가능하도록 지원합니다.

마치며..

이렇게 장황하게 마크다운 형식을 어떻게 예쁘게 표시하는지 기록해봤습니다.. 뭐 제목이나 카테고리는 css니까 그냥 빼버렸습니다.

어쩌다보니 다이나믹 라우팅이 뭔지.. 타이포그라피는 뭔지 또 주구장창 설명했네요.

다음 포스팅은 칙칙한 블로그 lottie 애니메이션으로 심폐소생술하기 입니다..

블로그(2) - react-multi-carousel로 캐러셀 만들기

블로그(2) - react-multi-carousel로 캐러셀 만들기

블로그(4) - 블로그에 Lottie 사용하기

블로그(4) - 블로그에 Lottie 사용하기