Notion 클로닝 프로젝트 회고

김영준·2023년 7월 11일
0

일주일 동안 Notion 클로닝 프로젝트를 진행하면서 느낀 점을 간략하게 작성해 보려고 한다.

먼저 프로젝트의 요구사항은 다음과 같다.

📖 기본 요구사항

  • 바닐라 JS만을 이용해 노션을 클로닝 합니다.
  • 기본적인 레이아웃은 노션과 같으며, 스타일링, 컬러 값 등은 원하는 대로 커스텀 합니다.
  • 글 단위를 Document라고 합니다. Document는 Document 여러 개를 포함할 수 있습니다.
  • 화면 좌측에 Root Documents를 불러오는 API를 통해 루트 Documents를 렌더링 합니다.
  • Root Document를 클릭하면 오른쪽 편집기 영역에 해당 Document의 Content를 렌더링 합니다.
  • 해당 Root Document에 하위 Document가 있는 경우, 해당 Document 아래에 트리 형태로 렌더링 합니다.
  • Document Tree에서 각 Document 우측에는 + 버튼이 있습니다. 해당 버튼을 클릭하면, 클릭한 Document의 하위 Document로 새 Document를 생성하고 편집화면으로 넘깁니다.
  • 편집기에는 기본적으로 저장 버튼이 없습니다. Document Save API를 이용해 지속적으로 서버에 저장되도록 합니다.
  • History API를 이용해 SPA 형태로 만듭니다.
  • 루트 URL 접속 시엔 별다른 편집기 선택이 안 된 상태입니다.
  • /documents/{documentId} 로 접속 시, 해당 Document 의 content를 불러와 편집기에 로딩합니다.

📖 보너스 요구사항

  • 기본적으로 편집기는 textarea 기반으로 단순한 텍스트 편집기로 시작하되, 여력이 되면 div와 contentEditable을 조합해서 좀 더 Rich 한 에디터를 만들어봅니다.
  • 편집기 최하단에는 현재 편집 중인 Document의 하위 Document 링크를 렌더링하도록 추가합니다.
  • 편집기 내에서 다른 Document name을 적은 경우, 자동으로 해당 Document의 편집 페이지로 이동하는 링크를 거는 기능을 추가합니다.
  • 추가로 더 구현하면 좋겠다는 기능들을 추가합니다.

일단 이 요구사항을 맞닥뜨렸을 때 바닐라 JS로만 구현하라고 해서 상당히 힘들 것이라고 생각이 들었다.
처음 구현을 시작할 때 페이지를 어떻게 나눠야 하는지부터 의문이 들었다.
App.js에서 state를 관리해서 각 페이지들에게 뿌려줘야 할지,
또는 App.js는 state를 갖지 않고 Page에서 state를 관리하고
Component에 뿌려줘야 할지 고민하다가 페이지를 2개로 나눴다.

시작 구조

하지만 각각의 페이지에서 state를 따로 관리하다 보니 서로 상호작용할 수 없는 문제가 발생했다.

예를 들어서 editPage에서 documentList의 state를 바꾸고 싶었지만 접근할 수 없었다.

결국 리팩토링을 통해 하나의 mainPage로 2개의 컴포넌트를 갖는 구조로 변경하였다.

변경한 구조

하지만 이게 맞는 건지는 잘 모르겠다.. 모든 라이브러리와 프레임워크를 사용할 수 없었기 때문에 최선이었다고 생각한다.

기본적인 요구사항들은 쉽게 구현하였지만 난관은 따로 있었다.
보너스 요구사항 중 하나인 div와 contentEditable을 조합해서 마크다운이 적용되는 에디터를 만드는 것이었다.

마크다운 에디터를 만드는 것이 이렇게 까다로운 것인지 몰랐다.

정규 표현식과 replace를 통해 div를 없애주었고 </div><br>로 치환하여 줄넘김을 구현하였다.

마크다운 문법에 해당하는 사용자의 입력을 한 줄씩 순환하여 html 문법으로 치환하였다.

하지만 a 태그와 li 태그가 끝나지 않고 다음 줄로 계속해서 적용되는 오류가 발생했고 리팩토링을 통해서 개선할 예정이다.

let richContent = "";
    if (this.state.content) {
      richContent = this.state.content.replace(/<div>/g, "<br>").replace(/<\/div>/g, "");
    }

    let htmlText = richContent
      .split("<br>")
      .map((line) => {
        if (line.indexOf("# ") === 0) {
          return `<h1>${line.substr(2)}</h1>`;
        } else if (line.indexOf("## ") === 0) {
          return `<h2>${line.substr(3)}</h2>`;
        } else if (line.indexOf("### ") === 0) {
          return `<h3>${line.substr(4)}</h3>`;
        } else if (line.indexOf("- ") === 0) {
          return `<li>${line.substr(2)}</li>`;
        } else {
          if (this.state.titleList) {
            for (const titleItem of this.state.titleList) {
              console.log(titleItem);
              if (line.indexOf(titleItem.title) !== -1) {
                const replacedLine = line.replace(
                  titleItem.title,
                  `<a class="link" data-id=${titleItem.id}>${titleItem.title}</a>`
                );
                return replacedLine;
              }
            }
          }
        }
        return line;
      })
      .join("<br>");

또한 리렌더링이 발생할 때 편집 중이던 에디터의 포커스가 사라지는 현상이 발생해서 일단 range와 selection을 통해 content 에디터의 마지막 요소로 자동적으로 포커스가 잡히게 설정한 상태다.

바닐라 JS로만 프로젝트를 구현하다 보니까
range와 selection 등 몰랐던 기능들도 많이 알게 되는 계기가 되는 것 같다.

const moveCursorToEnd = (element) => {
      const range = document.createRange();
      const selection = window.getSelection();
      range.selectNodeContents(element);
      range.collapse(false);
      selection.removeAllRanges();
      selection.addRange(range);
    };
    moveCursorToEnd($contentInput);

그 외에 요구사항들은 다 구현하였고, 현재 과제를 제출하여 vercel에 배포까지 한 상태이다.

구현 상태

현재 개선해야 할 사항들

  • 불필요한 리렌더링이 너무 많이 발생해서 커서가 몇 초 동안 깜빡이는 현상 수정하기
  • 에디터를 편집하고 DB에 저장이 되어 리렌더링이 발생해도 커서에 영향이 가지 않게 변경하기
  • title을 바꾸면 documentList에 title도 실시간으로 변경되게 적용하기
  • 마크다운 에디터를 오류 없이 구현하기

멘토님의 코드 리뷰가 완료되면 리팩토링을 통해 지속적으로 개선해 나아갈 예정이다.

profile
꾸준히 성장하는 개발자 블로그

0개의 댓글