ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • NextJS 15에서 MDX 파일 렌더링
    Front-end/Next.js 2024. 12. 18. 09:35
    반응형

     

    이번에 프로젝트 마이그레이션 작업을 준비하던 와중에 React가 19버전으로, 그리고 NextJS가 15버전으로 업데이트 되었다.

    기왕 버전이 업데이트 된 김에 우리 프로젝트도 최신의 버전으로 한 번 가져가보기로 했다.

    다행히  아직 메인 페이지와 라우팅 정도가 전부이다 보니 업데이트를 해도 크게 달라지는 점은 없었다.

     

    1. Verseion 15로 업그레이드

    Nextjs 공식 홈페이지의 설명

     

    우선 NextJS 홈페이지에 나와있는 방식대로 커맨드를 입력하여 업그레이드를 진행했다.

    그리고 package.json에서 확인해보니 dependencies 버전이 바뀐 것을 확인했다.

    package.json

    그리고 다시 실행시켜보니 아직 만들어진게 없고 라우팅 정도만 해놓은지라 특별히 문제 될 만한 것은 없어보여서 다음 해야할 일을 진행했다.

     

    2.  Markdown and MDX

    우리 프로젝트 뿐 아니라 많은 경우 Privacy ploicy and Terms and Conditions 즉, 개인정보보호 동의서와 같은 것들을 받는 작업을 종종하고 있을 것이다.

    나 또한 그 작업을 하려고 하고 있는데 여러가지 방법이 있겠지만 MDX 파일로 만들어서 보여주는 방식으로 하려고 했다.

    처음에 이 작업을 해야한다고 했을 때 떠오른 방법은 세 가지가 있었다.

    1. MDX 파일을 Serialize 시켜서 렌더링 한다.

    2. <div> 태그에 dangerouslySetInnerHTML을 사용해서 렌더링 한다

    3. HTML을 string으로 바꿔서 div 혹은 textarea에 넣어서 렌더링 한다

     

    물론 각각은 장단점이 있을 것이나 나는 그 중에서 첫 번째 방법을 택했고, 그 이유는 아래와 같다.

     

    1. MDX 파일을 Serialize 시켜서 보여주는 방법

    우선 MDX 관련해서는 NextJS 문서에 설치방법부터 시작해서 잘 설명이 되어있다. 

    필요한 라이브러리들을 커맨드를 사용해서 설치를 하고, next.config.mjs를 수정해주면 된다.

    import createMDX from '@next/mdx';
    
    /** @type {import('next').NextConfig} */
    const nextConfig = {
      output: 'standalone',
      pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
      transpilePackages: ['next-mdx-remote'],
      webpack: (config, { isServer }) => {
        if (!isServer) {
          config.resolve.fallback = {
            fs: false
          };
        }
    
        return config;
      }
    };
    
    const withMDX = createMDX({
      // Add markdown plugins here, as desired
      extension: /\.(md|mdx)$/
    });
    
    // Merge MDX config with Next.js config
    export default withMDX(nextConfig);

     

    webpack 수정의 경우 굳이 하지 않아도 되는데 나는 mdx 파일을 로컬에 가지고 있기 때문에 fs를 이용해서 가져와서 사용하려다보니 필요하게 되었다.

    그리고 getMdxServer.ts라는 파일을 만들고 파일 경로를 받은 경우 serialize를 통해서 content를 만들어내는 함수를 정의했다.

    import { serialize } from 'next-mdx-remote/serialize';
    
    export async function getMdxContent(filePath: string) {
      const res = await fetch(filePath);
      const mdxContent = await res.text();
      const serializedContent = await serialize(mdxContent);
    
      return serializedContent;
    }

     

    프로젝트에서 총 세 가지의 정보동의서가 필요했는데 민감정보동의, CCTV활용동의, 마케팅 정보수신동의였다.

    이들을 public 폴더에 넣어두고 getMdxContent 함수를 통해서 serializedContent를 만든 후 useState에 넣어두고 MDXRemote 라는 것을 사용해서 렌더링 시켜줬다.

      const [privacy, setPrivacy] = useState<MDXRemoteSerializeResult | undefined>( undefined);
      const [cctv, setCctv] = useState<MDXRemoteSerializeResult | undefined>( undefined);
      const [marketing, setMarketing] = useState< MDXRemoteSerializeResult | undefined >(undefined);
    
      const AGREEMENT_DATA: AGREEMENT_DATA_TYPE[] = [
        {
          id: 'privacy',
          label: '민감 정보 동의(필수)',
          description: '*본 약관은 법적효력을 지닌 약관입니다.',
          content: privacy
        },
        {
          id: 'cctv',
          label: '상담실 CCTV 동의(필수)',
          description: '*본 약관은 법적효력을 지닌 약관입니다.',
          content: cctv
        },
        {
          id: 'marketing',
          label: '마케팅 정보 수신 동의(선택)',
          description: '',
          content: marketing
        }
      ];
    
      /** Function */
      useEffect(() => {
        async function fetchData() {
          const privacyFilePath = '/mdx/privacy-agreement.mdx';
          const cctvFilePath = '/mdx/cctv-agreement.mdx';
          const marketingFilePath = '/mdx/marketing-agreement.mdx';
    
          const privacyContent = await getMdxContent(privacyFilePath);
          const cctvContent = await getMdxContent(cctvFilePath);
          const marketingContent = await getMdxContent(marketingFilePath);
    
          setPrivacy(privacyContent);
          setCctv(cctvContent);
          setMarketing(marketingContent);
        }
    
        fetchData();
      }, []);
    
      /** Render */
      return {
        ...
         <MDXRemote {...data.content} />
        ...  
      }

     

    이런 식으로 Mdx 파일을 렌더링할 수 있는데 장점이라고하면 아무래도 비교적 XSS와 같은 보안 취약점이 방지되고, 콘텐츠에 대해서 유지보수가 용이하다.
    그러나 단점은 MDX를 처리하는 추가 라이브러리를 설치해야하고, 결국 거기에 의존성이 생긴다는 점과 아무래도 직렬화 시키는 과정이 필요하므로 빌드 시간이나 초기 렌더링 시간이 조금 길어질 수 있다.

     

    이러한 단점에도 MDX를 선택한 이유는 dangerouslySetInnerHTML 속성을 사용하는 경우의 단점이 너무 크게 보였기 때문이다.
    이 속성을 사용하게 되면 HTML을 문자열로 전달하면서 바로 렌더링할 수 있고, 추가 처리 과정이 비교적 적어서 빠른 구현을 할 수 있다는 장점은 있겠지만 XSS 공격에 노출될 위험이 크고, HTML 문자열을 직접 렌더링하게 되므로 JSX 컴포넌트 기반 아키텍쳐와 어울리지 않는다고 생각했다.

    하지만 이런 방법을 사용하게 되는 경우 sanitize를 시킬 수 있는 라이브러리(DOMPurify 등)가 있으니 사용해서 적용하면 도움이 될 것 같다.

     

    마지막으로 HTML을 string으로 바꿔서 div 또는 textarea에 넣는 방법의 경우에는 XSS 공격을 방지할 수 있고, 간단하다는 장점이 있지만 마크업이나 스타일 적용이 되지 않는 문제가 있어서 치명적이라는 생각이 들었다.

    지금 당장은 특별한 스타일이 필요하지 않지만 나중에 바뀌면 그 때 문제가 발생 할 것 같고, 스타일이 너무 적용이 되지 않으면 UX가 떨어진다는 생각도 들어서 최종적으로는 결국 MDX가 나을 것 같다는 판단을 했다.

    그리고 MDX 파일을 Nextjs에서 돌려보고 싶은 개인적인 욕심도 크게 한 몫 한 것 같다.

    반응형
Designed by Tistory.