React-markdown 적용시켜보기

박준서·2023년 12월 3일
3

Velog 클론코딩

목록 보기
1/1
post-thumbnail

설치

npm install react-markdown

Markdown 문법의 장점

1. 간결하다.
2. 별도의 도구없이 작성가능하다.
3. 다양한 형태로 변환이 가능하다.
4. 텍스트(Text)로 저장되기 때문에 용량이 적어 보관이 용이하다.
5. 텍스트파일이기 때문에 버전관리시스템을 이용하여 변경이력을 관리할 수 있다.
6. 지원하는 프로그램과 플랫폼이 다양하다.

Markdown 문법의 단점

1. 표준이 없다.
2. 표준이 없기 때문에 도구에 따라서 변환방식이나 생성물이 다르다.
3. 모든 HTML 마크업을 대신하지 못한다.

remarkGfm

GFM이란 Github Flavored Markdown의 약자로 github에서 기존 마크다운에 몇가지 기능을 추가하여 커스터마이징 한 버전이다.

예를들면 테이블, 링크, 체크리스트 등이 있다.

link, table, checklist 등의 형식을 표현할 수 있게 remark-gfm 플러그인을 같이 설치해 주도록 한다.

react-syntax-highlighter

여기까지 진행하면 조금은 못생긴 모양의 markdown viewer가 생성된다.

아무런 색이 없어 키워드와 변수들이 잘 구분되지 않는다.

이럴때는 syntax-highlighter를 사용하면 조금 더 이쁘게 커스터마이징 할 수 있다.

타입스크립트용 react-syntax-highlighter도 설치해준다.

npm install remark-gfm
npm install --save @types/react-syntax-highlighter

마크다운 적용하기

MarkdownPost.tsx

import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { nord } from "react-syntax-highlighter/dist/esm/styles/prism";
import remarkGfm from "remark-gfm";
import styled from "styled-components";

const Preview = styled.div`
  font-size: 1.125rem;
  color: #ececec;
  transition: color 0.125s ease-in 0s;
  line-height: 1.7;
  letter-spacing: -0.004em;
  word-break: keep-all;
  overflow-wrap: break-word;
  max-width: 54rem;
`;

const MarkdownPreview = ({ markdown }: { markdown: string }) => {
  // const input = "This is a header\nAnd this is a paragraph";
  return (
    <Preview>
      <div>
        <ReactMarkdown
          remarkPlugins={[remarkGfm]}
          components={{
            code({ className, children }) {
              const match = /language-(\w+)/.exec(className || "");
              return match ? (
                // 코드 (```)
                <SyntaxHighlighter
                  style={nord}
                  language={match[1]}
                  PreTag="div"
                >
                  {String(children)
                    .replace(/\n$/, "")
                    .replace(/\n&nbsp;\n/g, "")
                    .replace(/\n&nbsp\n/g, "")}
                </SyntaxHighlighter>
              ) : (
                <SyntaxHighlighter
                  style={nord}
                  background="green"
                  language="textile"
                  PreTag="div"
                >
                  {String(children).replace(/\n$/, "")}
                </SyntaxHighlighter>
              );
            },
            // 인용문 (>)
            blockquote({ children, ...props }) {
              return (
                <blockquote
                  style={{
                    background: "#7afca19b",
                    padding: "1px 15px",
                    borderRadius: "10px",
                  }}
                  {...props}
                >
                  {children}
                </blockquote>
              );
            },
            img({ ...props }) {
              return (
                <img
                  style={{ maxWidth: "40vw" }}
                  src={props.src?.replace("../../../../public/", "/")}
                  alt="MarkdownRenderer__Image"
                />
              );
            },
            em({ children, ...props }) {
              return (
                <span style={{ fontStyle: "italic" }} {...props}>
                  {children}
                </span>
              );
            },
          }}
        >
          {markdown
            .replace(/\n/gi, "\n\n")
            .replace(/\*\*/gi, "@$_%!^")
            .replace(/@\$_%!\^/gi, "**")
            .replace(/<\/?u>/gi, "*")}
        </ReactMarkdown>
      </div>
    </Preview>
  );
};

export default MarkdownPreview;

엔터키가 두 번을 눌러야 실제로는 한 번 적용되는 현상이 발생했다. 이를 해결하기 위해 .replace(/\n/gi, "\n\n") 를 활용해서 엔터가 한 번 입력되면 실제로도 한 번 출력되도록 수정하였다.

PostingPage.tsx

<RightBox>
      <h1
        style={{
          fontSize: "2.5em",
            marginBottom: "4rem",
              marginTop: "26.8px",
                fontWeight: "800",
        }}
        >
        {title}
      </h1>
      <div>
        <MarkdownPreview markdown={markdown} />
      </div>
</RightBox>

우리 블로그의 경우, 글을 작성하는 부분과 글을 보여주는 부분을 분리하여, 보여주는 부분의 경우 백엔드와 통신하기 전에 React-markdown을 활용하여 사용자가 입력한 내용이 즉각적으로 보이게 처리하였다. (MarkdownPost.tsx 부분)

이미지 즉각적으로 올라가게 하기

벨로그의 경우 이미지 버튼을 누르면 이미지 input 창이 나오고 즉각적으로 이미지의 링크를 반환해서 markdown 형식으로 보여주게 만들어져 있다.
따라서, 이를 구현하기 위해서 사용자가 이미지를 업로드하면 s3로 보낸다음에 s3 url로 변환받아서 이를 markdown 형식으로 변환하여 글에 보여지는 반응이 한 번에 일어나도록 구현하였다.

 const [selectedImage, setSelectedImage] = useState<File | null>(null);
  const fileInputRef = useRef(null);

  const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const { files } = e.target;

    if (files && files.length === 1) {
      setSelectedImage(files[0]);
    }
  };
  const handleImageUpload = async () => {
    if (fileInputRef.current)
      (fileInputRef.current as HTMLInputElement).click();
  };

  useEffect(() => {
    const uploadImage = async () => {
      if (selectedImage) {
        try {
          formData.append("multipartFile", selectedImage);

          const response = await axios.post("api/image/upload", formData, {
            headers: {
              "Content-Type": "multipart/form-data",
              authorization: accesstoken,
            },
          });

          setMarkdown(markdown + `![](${response.data.data})`);
          setSelectedImage(null);
        } catch (error) {
          alert("더 작은 용량의 이미지를 업로드해주세요!");
        }
      }
    };

    uploadImage();
  }, [selectedImage]);
<input
  style={{ display: "none" }}
  type="file"
  accept="image/*"
  onChange={handleImageChange}
  ref={fileInputRef}
  />
<Scale onClick={handleImageUpload}>
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="1em"
    height="1em"
    viewBox="0 0 24 24"
    fill="currentColor"
    stroke="currentColor"
    strokeWidth="0"
    >
    <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"></path>
  </svg>
</Scale>

display를 none으로 준 input에 ref를 지정한 뒤, 사용자가 이미지 버튼을 클릭하면 useRef hook를 활용해 input이 즉각적으로 반응하게 만들었으며, 이미지가 입력된 순간 useEffect가 발동해 s3 url을 반환하는 post가 가도록 하였고, 이후 마크다운이 적용된 형태의 게시글이 출력되도록 하였다.

결과

참고

GitHub - remarkjs/react-markdown: Markdown component for React

react-markdown 공식문서 (깃허브 리드미)

마크다운(Markdown) 사용법

마크다운 문법

profile
https://medium.com/@jswing5267

1개의 댓글

comment-user-thumbnail
2024년 5월 5일

ㅎㅎ

답글 달기