[TOJ] 개발기록 - 리액트에서 인라인 코드 하이라이팅 적용하기

조민호·2023년 11월 9일
0

타입 챌린지 채점 플랫폼에 맞게, UX에 가장 필수적인 요소 중 하나는

바로 웹에서도 실제 md문법에 맞게 하이라이팅을 해줘야 하는 것이다.

서버로부터 깃허브 타입 챌린지 에서 제공하는 문제 관련 정보들을

전달해 주지만 이는 가공되지 않은, 매우 크기가 큰 문자열 덩어리다.

위의 문자열 중에서 아래의 문제 설명 부분을 정규식으로 추출해 낸 다음,

T의 모든 프로퍼티를 읽기 전용(재할당 불가)으로 바꾸는 내장 제네릭 Readonly<T>를 이를 사용하지 않고 구현하세요. ”

이처럼 TypeScript문자열 코드를 md문법에 맞게 하이라이팅을 진행 해 볼 것이다




1. 필요한 라이브러리 설치

  • react-markdown은 널리 사용되는 라이브러리로, Markdown을 React 컴포넌트로 변환한다

  • remark-gfm은 GitHub Flavored Markdown (GFM)을 지원한다

    이건 기본 Markdown에 테이블, 리스트, 자동링크, 취소선 등에 대한 몇 가지 추가적인 기능을 포함하고 있다

    react-markdown과 remark-gfm를 함께 사용하면, 이런 GFM의 추가적인 기능들을 React 컴포넌트에서 렌더링할 수 있게 된다

    사용법은 ReactMarkdown 태그 안의 remarkPlugins속성에 추가하면 된다


다만, 버전의 호환성 문제가 생겨서 나의 경우 remark-gfm의 버전을

3.0.1로 다운그레이드 해줬다


2. 최초 적용

import React from 'react';
import ReactMarkdown from 'react-markdown';
import gfm from 'remark-gfm';

const MyComponent = () => {
  const markdownString = `
# Hello World
This is **Markdown** content.
`;

  return (
    <div>
      <ReactMarkdown remarkPlugins={[gfm]}>
        {markdownString}
      </ReactMarkdown>
    </div>
  );
};

export default MyComponent;

동작은 잘 되지만 코드를 표시하는 백틱 `` 같은 곳에 제대로 하이라이팅이 되지 않는다.

그러므로 보다 다양한 설정을 해줘야 한다


3. 공식문서를 참조한 템플릿을 사용

react-syntax-highlighter 와 함께 사용하는 것을 볼 수 있었고

이를 따라서 내 코드도 같이 수정해 보았다

import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { dark } from 'react-syntax-highlighter/dist/esm/styles/prism';

interface ProblemDescriptionProps {
  description: string;
}
const ProblemDescription = ({ description }: ProblemDescriptionProps) => {
 // console.log(description) //  "`Array.unshift`의 타입 버전을 구현하세요."
  return (
    <div>
      <ReactMarkdown
        remarkPlugins={[remarkGfm]}
        components={{
          code({ node, inline, className, children, ...props }) {
            const match = /language-(\w+)/.exec(className || '');
            return !inline && match ? (
              // !inline && match 조건에 맞으면 하이라이팅
              <SyntaxHighlighter {...props} style={dark} language={match[1]} PreTag="div">
                {String(children).replace(/\n$/, '')}
              </SyntaxHighlighter>
            ) : (
              // 안 맞다면 문자열 형태로 반환
              <code {...props} className={className}>
                {children}
              </code>
            );
          },
        }}
      >
        {description}
      </ReactMarkdown>
    </div>
  );
};

export default ProblemDescription;

components 속성을 ReactMardown 태그 안에 추가한 다음,

수정하고 싶은 태그를 적고 수정된 결과물을 return에 넣는다.

이렇게 하면 components 속성은 다음과 같이 작동한다.

  1. ReactMarkdown은 주어진 Markdown 문자열을 파싱하며 HTML로 변환한다

  2. 변환하는 과정에서 components prop에서 지정한 커스텀 컴포넌트들을 찾아서 해당하는 Markdown 요소를 렌더링할 때 사용한다

    위 예시에 사용된 code 컴포넌트는 Markdown 내의 코드 블록 (백틱 3개) 또는 인라인 코드 (백틱 1개)를 렌더링할 때 참조된다.

  3. 컴포넌트 내에서 조건문을 사용하여 특정 조건에 따라 다르게 렌더링할지 결정한다. 위 예시에서는 inlinematch 값을 기반으로 조건을 설정했다

    • inline prop은 현재 렌더링하는 코드가 인라인 코드인지 아니면 코드 블록인지를 나타낸다

    • matchclassName에서 language-를 기준으로 어떤 프로그래밍 언어로 코드가 작성되었는지를 판별하기 위해 사용된다

      만약 javascript로 작성 됐다면, 해당 코드 블록의 classNamelanguage-javascript가 되고 match[1]에는 javascript와 같은 언어 이름이 저장된다

  4. !inline && match 조건에 맞는다면 해당 부분을 SyntaxHighlighter 를 통해 하이라팅해서 렌더링한다

    그게 아니라면 초기 문자열 그대로 반환한다



그렇지만 이렇게 공식문서에 예시로 작성한 코드로는 여전히

내 경우에 사용할 수가 없었다

Array.unshift 같은 부분은 백틱1개로 감싸져 있으므로 code 컴포넌트는 해당 부분에 대해 인라인 코드(inline code)로 판단하게 된다.

그렇기 때문에 !inline && match 조건에서는 해당 부분이 실행되지 않는 것이다.


4. 조건을 수정하여 인라인 코드에 대해서도 신텍스 하이라이팅을 적용할 수 있도록 변경한다

import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { dark } from 'react-syntax-highlighter/dist/esm/styles/prism';

interface ProblemDescriptionProps {
  description: string;
}
const ProblemDescription = ({ description }: ProblemDescriptionProps) => {
 // console.log(description) //  "`Array.unshift`의 타입 버전을 구현하세요."
   return (
    <div>
      <ReactMarkdown
        remarkPlugins={[remarkGfm]}
        components={{
          code({ node, inline, className, children, ...props }) {
            const match = /language-(\w+)/.exec(className || '');
            if (!inline && match) {
              return (
                <SyntaxHighlighter {...props}  language={match[1]} PreTag="div">
                  {String(children).replace(/\n$/, '')}
                </SyntaxHighlighter>
              );
            }

             **// 인라인 코드에 대한 신텍스 하이라이팅 적용**
            else if (inline) {
              return (
                <SyntaxHighlighter
                  {...props}
                  language={match ? match[1] : undefined}
                  PreTag="span"
                >
                  {String(children)}
                </SyntaxHighlighter>
              );
            } else {
              return (
                <code {...props} className={className}>
                  {children}
                </code>
              );
            }
          },
        }}
      >
        {description}
      </ReactMarkdown>
    </div>
    );
};

export default ProblemDescription;

!inline && match 조건 자체를 수정해도 상관은 없지만 혹시 코드 블럭이 들어가 있는 경우를 대비하기 위해 남겨뒀다


5. match제거

위에서 언급듯이 match는 사용된 언어를 파악하는 것이다

그렇지만 현재 넘어오는 description는 대부분 인라인 코드 형태 자체적으로 언어 파악이 불가능하지만

타입챌린지 플랫폼인만큼, 사용 언어는 무조건 TS일 것이므로 언어를 강제로 지정해서 사용했다.


SyntaxHighlighter의 props로 넘겨주는 language부분에 match의

값을 사용하지 않고 typescript라고 강제하는 것이다

import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { base16AteliersulphurpoolLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { ProblemDescriptionWrapperStyle } from './ProblemDescription.css';

interface ProblemDescriptionProps {
  description: string;
}

const ProblemDescription = ({ description }: ProblemDescriptionProps) => {
  // console.log(description) //  "`Array.unshift`의 타입 버전을 구현하세요."
  return (
    <div className={ProblemDescriptionWrapperStyle}>
      <ReactMarkdown
        remarkPlugins={[remarkGfm]}
        components={{
          code({ node, inline, className, children, ...props }) {
            const match = /language-(\w+)/.exec(className !== undefined ? className : '');
            if (!inline && match) {
              return (
                <SyntaxHighlighter {...props}  language={match[1]} PreTag="div">
                  {String(children).replace(/\n$/, '')}
                </SyntaxHighlighter>
              );
            }
            else if (inline === true) {
              return (
                <SyntaxHighlighter
                  {...props}
                  language="typescript" **// 타입스크립트 코드 강제**
                  PreTag="span"
                >
                  {String(children)}
                </SyntaxHighlighter>
              );
            } else {
              return (
                <code {...props} className={className}>
                  {children}
                </code>
              );
            }
          },
        }}
      >
        {description}
      </ReactMarkdown>
    </div>
  );
};

export default ProblemDescription;

6. 커스텀 스타일 추가

하이라이팅이 될 때 , 하이라이팅 되는 배경의 padding과

폰트 크기를 커스텀해줬다.

import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { base16AteliersulphurpoolLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { ProblemDescriptionWrapperStyle } from './ProblemDescription.css';

interface ProblemDescriptionProps {
  description: string;
}

**// 커스텀 스타일**
const customStyle = {
  ...base16AteliersulphurpoolLight,
  'code[class*="language-"]': {
    ...base16AteliersulphurpoolLight['code[class*="language-"]'],
    padding: '5px',
    fontSize: '1.2rem',
  },
  'pre[class*="language-"]': {
    ...base16AteliersulphurpoolLight['pre[class*="language-"]'],
    padding: '5px',
    fontSize: '1.2rem',
  },
};

const ProblemDescription = ({ description }: ProblemDescriptionProps) => {
  // console.log(description) //  "`Array.unshift`의 타입 버전을 구현하세요."
  return (
    <div className={ProblemDescriptionWrapperStyle}>
      <ReactMarkdown
        remarkPlugins={[remarkGfm]}
        components={{
          code({ node, inline, className, children, ...props }) {
            const match = /language-(\w+)/.exec(className !== undefined ? className : '');
            if (!inline && match) {
              return (
                <SyntaxHighlighter {...props} style={customStyle} language={match[1]} PreTag="div">
                  {String(children).replace(/\n$/, '')}
                </SyntaxHighlighter>
              );
            }
            else if (inline === true) {
              return (
                <SyntaxHighlighter
                  {...props}
                  style={customStyle}
                  language="typescript" 
                  PreTag="span"
                >
                  {String(children)}
                </SyntaxHighlighter>
              );
            } else {
              return (
                <code {...props} className={className}>
                  {children}
                </code>
              );
            }
          },
        }}
      >
        {description}
      </ReactMarkdown>
    </div>
  );
};

export default ProblemDescription;




참고 :

[GitHub Blog 개발기] Markdown 적용 및 Require 함수

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

profile
웰시코기발바닥

0개의 댓글