노션 클로닝 기능 회고

최익·2023년 10월 31일
0
post-thumbnail

자동 마크업 적용 기능

  1. contentEditable=ture인 엘리먼트에서 텍스트를 입력하고 innerText로 모든 Text를 뽑아 util 폴더의 applyMarkup함수(마크업을 적용하는 함수)에 전달하고, 정규표현식과 startsWith 메소드를 이용해 태그를 입혀 리턴하기.

  2. 리턴 받은 태그 데이터를 서버에 저장

  3. 새로고침 후 마크업 반영

  4. 에디터 수정 -> 에디터가 focuson이 되면 각 태그에 맞는 마크업 문법이 텍스트에 나타남
    ex) <h1>h1 태그입니다</h1> -> # h1 태그입니다
    위의 예제처럼 각각의 태그가 맞는 마크업 문법을 각각의 태그의 innerText에 적용.

  5. 수정 후 2초뒤(디바운스) 서버에 마크업 태그를 다시 입혀 저장

위 순서가 처음 마크업을 자동 적용하기 위한 설계이다.

간단한 구현 내용

아래와 같이 별도의 함수를 만들어주고, 매개변수로 에디터의 innerText를 받아 태그를 입혀 리턴해준다.

export const applyMarkup = (text) => {
  const H1 = "# ";
  const H2 = "## ";
  const H3 = "### ";
  const BOLD = /\*\*(.*?)\*\*/g;
  const STRIKETHROUGH = /\~\~(.*?)\~\~/g;
  const UNDERSCORE = /\_\_(.*?)\_\_/g;

  // 개행을 기준으로 h1, h2, h3 태그로 파싱
  const scanEditor = text.split("\n").map((tag) => {
    if (tag.startsWith(H1)) return `<h1>${tag.slice(2)}</h1>`;
    else if (tag.startsWith(H2)) return `<h2>${tag.slice(3)}</h2>`;
    else if (tag.startsWith(H3)) return `<h3>${tag.slice(4)}</h3>`;
    else if (tag.match(BOLD)) return tag.replace(BOLD, "<b>$1</b>");
    else if (tag.match(STRIKETHROUGH))
      return tag.replace(STRIKETHROUGH, "<s>$1</s>");
    else if (tag.match(UNDERSCORE)) return tag.replace(UNDERSCORE, "<u>$1</u>");
    else if (tag.startsWith("📃")) return;
    else return `<p>${tag}</p>`;
  });

  return scanEditor.join("");
};

넘겨 받은 태그 데이터는 디바운스로 2초 뒤 서버에 저장!
서버에는 "<h1>h1</h1><p></p><h2>h2</h2><p></p><h3>h3</h3><p></p><u> 밑줄 </u><p></p><p>~~ 취소선 ~</p><p></p><b>볼드체</b>" 이런식으로 데이터가 저장이 된다.

이제 수정을 하기 위해 에디터가 포커싱이 되면 해당 태그의 innerText에 해당 태그의 마크업 문법을 붙혀준다.

// 마크업 제거 함수
export const removeMarkup = ($target) => {
  $target.addEventListener(
    "focusin",
    (e) => {
      document.querySelectorAll("h1").forEach((e, i) => {
        if (i !== 0 && !e.innerText.startsWith("# "))
          e.innerText = `# ${e.innerText}`;
      });
      document.querySelectorAll("h2").forEach((e) => {
        if (!e.innerText.startsWith("## ")) e.innerText = `## ${e.innerText}`;
      });
      document.querySelectorAll("h3").forEach((e) => {
        if (!e.innerText.startsWith("### ")) e.innerText = `### ${e.innerText}`;
      });
      document.querySelectorAll("b").forEach((e) => {
        if (!e.innerText.startsWith("**")) e.innerText = `**${e.innerText}**`;
      });
      document.querySelectorAll("s").forEach((e) => {
        if (!e.innerText.startsWith("~~")) e.innerText = `~~${e.innerText}~~`;
      });
      document.querySelectorAll("u").forEach((e) => {
        if (!e.innerText.startsWith("__")) e.innerText = `__${e.innerText}__`;
      });
    },
    { once: true }
  );
};

addEventListener의 3번째 매개변수에 {once: true}가 있는 걸 볼 수 있다.

세번째 매개변수가 가진 기능은 이벤트 리스너를 한 번만 실행하게 해주는거다. 이번에 이벤트 핸들링을 하면서 처음 알게된 기능이다.

만약 위 focuson 이벤트가 한 번만 실행하지 않으면 어떻게 될까?

에디터가 포커싱이 될 때마다 h1 태그를 예로 들면 # 텍스트 -> (포커싱) # # 텍스트 -> (포커싱) # # # h1 텍스트 이런식으로 innerText에 마크업 문법이 무한으로 늘어나는 버그가 생긴다.

처음 시도해본 것은 flag 변수를 사용해서 한번만 이벤트를 등록하게 해볼까? 라고 생각했지만 이것은 바보 같은 생각.. ㅋㅋㅋ

이미 한번 등록된 이벤트를 flag 변수로 막을 수 있을리가.. ㅋㅋㅋㅋ

flag 변수를 작성하자마자 바로 지워버리고 구글링을 시작했다. '이벤트 한번만 실행하는 법' 이라고 구글링하자마자 바로 {once:true}를 알게되었고, 이 방법을 앞으로도 종종 유용하게 쓸 것 같다.

아쉬운 점

현재 이 기능은 새로고침을 하거나, 다른 문서를 클릭하여 다른 페이지에 갔다오면 적용이된다. API를 뿌려주는 곳이 최상단 App.js라서 한번에 전체가 새로고침되는 구조인데, 이 구조를 좀 수정해서 에디터가 실시간으로 마크업이 반영될 수 있도록 리팩토링 해보야겠다 ㅎㅎ

profile
https://choi-ik.tistory.com/ 👈🏻 여기로 블로그 이전했습니다 ㅎ

0개의 댓글