[React/node] 하드코딩 방어하기 _ notion 문서 react 렌더링 (html-react-parser)
Code/React - Node

[React/node] 하드코딩 방어하기 _ notion 문서 react 렌더링 (html-react-parser)

반응형

개발자 스타일마다 다를 순 있지만 개인적으로는 하드코딩을 피하기 위해 정말 노력하는 편이다.

데이터가 변경될 가능성 + 추후 유지보수 + 확장 가능성 등등을 고려하며 추후 데이터의 변경이 있을 때에도 간단하게 변경할 수 있는 구조를 생각하며 구현하는 편인데 처음엔 조금 공수가 드는 것 처럼 보여도 결국 조금만 지나도 큰 효용을 느낄 수 있다고 생각하기 때문이다.

문제

서비스 릴리즈를 앞두고 있던 때에 다양한 이용약관이 추가되어야 하는 상황이었는데

이용약관들은 법적 검토 등의 과정을 통해 다소 여러가지 형태의 각기 다른 레이아웃을 가진 채 노션 (notion)에 정의되어 있었고,

약관을 보여주는 공통 모달 컴포넌트 디자인은 타이틀, 소제목, 본문, bullet, numbering, 여백 등등이 따로 커스텀 되어있는 디자인이었다. 이게 또 약관마다 레이아웃이 다른데 디자인에서는 통일된 부분도 있고 없는 부분도 있는 그런 상황이었다.

논의 초기에는 서버에서 html 을 string으로 변환하여 약관을 정적 데이터로 받을 수 있도록 하려고 했었는데 이런 여러 복잡한 조건들이 있어 백엔드에서 가공된 데이터를 보내주기 힘들 것이라 판단이 들었고, 백엔드 선배님께서 클라이언트 하드코딩으로 넣어야 할 것 같다고 말씀해주셨다.

하지만 매번 약관이 추가되고 변경될 때마다 1만 자가 넘는 약관들을 죄다 넣고 빼고 수정하고 하는 건 현실적으로 좋은 방안도 아닐 뿐더러 그 약관들을 클린하게 관리하거나 유지 보수할 자신도 없었기 때문에 이런 저런 방안을 생각해 보았다.

 

고려 사항

1. 약관은 모두 약관용 모달 컴포넌트에서 공통적으로 등장해야 한다

 > 기본적으로 정해진 약관의 타이틀, Bullets, numberling, list, 조항 타이틀, 본문 등의 스타일이 있었고 해당하는 경우 그에 맞는 스타일로 보여주어야 했었다.  

2. 약관은 추후 추가, 변경 및 삭제될 여지가 많다 (서비스 확장 또는 변경 등)

> 확장성을 고려하고, 유지 보수가 간단해야 한다. (스크립트를 실행해서 간단하게 관리하고 싶었다)

 

방안

그렇게 찾은 방안은 notion 에 정의된 약관을 export 하여 사용하는 방법을 먼저 떠올렸다.

디자인에 정의된 부분들에 대해 따로 식별하여 스타일을 줄 수 있도록 노션 약관 문서에 타이틀은 heading1 / 조항 부분은 heading3 등의 요구 사항들을 정리한 후 요청을 드리며 약관 노션 레이아웃 형식을 정의했다.

처음에는 마크다운으로 추출하여 html로 이를 변환하고 변환한 html string 데이터를 ts 파일로 만들어서 컴포넌트에서 상수를 임포트할 수 있도록 구현했다.

구현하고 나니 문제가 한 가지 있었는데, markdown 을 html 로 변환할 때 사용하는 툴에 따라 html 태그 요소가 달라질 수 있었는데 그러다 보니 실제로는 같은 스타일이 적용되어야 하지만 요소가 달라져서 다시 하나하나 요소를 파악해야 한다는 문제가 있었다.

그렇다면 그냥 노션에서 html 파일을 익스포트해서 사용하는 게 낫겠다 생각했고 html 파일을 익스포트 하고, 불필요한 스타일과 태그들을 지운 뒤 스크립트 태그를 실행하여 ts 파일을 만들어 내도록 했고,

그 후 모든 약관을 띄워줄 수 있는 모달 컴포넌트를 만들어 그 곳에 html 데이터를 파싱해서 보여줄 수 있도록 했다.

결과물

 

구현 코드

htmlToTs.js

const fs = require('fs');
const path = require('path');

const CONVERT_DIR_PATH = path.join(__dirname, 'src', 'converter');
const INPUT_DIR_PATH = path.join(CONVERT_DIR_PATH, 'html');
const OUTPUT_DIR_PATH = path.join(CONVERT_DIR_PATH, 'ts');

const convertHtmlToTs = (inputPath, outputPath) => {
  const htmlFileList = fs.readdirSync(inputPath);

  htmlFileList.forEach(fileName => {
    const html = fs.readFileSync(path.join(inputPath, fileName), {
      encoding: 'utf-8',
    });

    const outputFileName = fileName.replace('.html', '.ts');
    const tsConstantsName = fileName.replace('.html', '').toUpperCase();
    const tsCode = `export const ${tsConstantsName} = ${JSON.stringify(html)};`;

    fs.writeFileSync(path.join(outputPath, outputFileName), tsCode, {
      encoding: 'utf-8',
    });
  });
};

convertHtmlToTs(INPUT_DIR_PATH, OUTPUT_DIR_PATH);

converter/html 폴더에 있는 파일들을 읽어 온 뒤, html string 값이 상수로 선언되어있는 ts 파일로 변환해주는 node 코드를 작성했다.

그리고 package.json에 아래 스크립트 코드를 추가 해, 스크립트 실행 시 파일 변환이 일어날 수 있도록 했다.

script 코드
변환된 파일

짜잔 그럼 이렇게 상수로 변환된 html string 값을 확인할 수 있다.

이제 약관에 수정사항이 생긴다면 노션 문서를 html로 익스포트 하여 파일을 추가한 뒤 스크립트 실행을 하면 된다.

그럼 이제 이 약관들을 어떻게 관리하며 보여줄 지 고민하다가 아래와 같은 형식으로 구현했다.

terms

...
import {SERVICE_TERMS} from './../converter/ts/service_terms';

export enum TermsType {
  ...,
  SERVICE_TERMS = 'service-terms',
  AGE_CONFIRMATION_14UP = 'age-confirmation-14up',
}

export const TERMS = new Map([
  ...,
  [TermsType.SERVICE_TERMS, SERVICE_TERMS],
  [TermsType.AGE_CONFIRMATION_14UP, null],
]);

termsType을 enum 문자열 상수로 선언해서 관리하고 실제 모달 컴포넌트에서는 이 값을 통해 약관에 들어갈 html 컨텐츠를 가져 올 수 있도록 설계했다.

Modal.tsx

...
import {TERMS, TermsType} from ...;
import parse from 'html-react-parser';

const Modal = ({type} : {type : TermsType | undefined}) => {
  ...

  const html = useMemo(() => {
    if (!type) return null;

    return TERMS.get(type as TermsType) ?? null;
  }, [type]);

  if (!type || !html) return null;

  return (
    <div >
      {parse(html)}
    </div>
  );
};

약관 모달 타입을 통해 html 파일을 가져오고 그 html 파일을 html-react-parser 라이브러리를 통해 JSX.Element 로 파싱한 html을 보여주고, 세부 스타일 등을 한 컴포넌트에서 공통으로 관리할 수 있게 만들었다.

 

 

참고 : https://stackoverflow.com/questions/39758136/how-to-render-html-string-as-real-html

 

How to render HTML string as real HTML?

Here's what I tried and how it goes wrong. This works: <div dangerouslySetInnerHTML={{ __html: "<h1>Hi there!</h1>" }} /> This doesn't: <div dangerouslySetInnerHTML={{ __ht...

stackoverflow.com

 

 

반응형