
1주가 순삭당했던 노션 클로닝 프로젝트 기간을 회고해보려고 한다.
어찌저찌 그럴듯한 노션 페이지를 구현했지만 아직까지 구현 못한 보너스 요구사항과 구현한 기능에 자잘한 버그들이 많다. 이후 이것들을 하나씩 해결해나갈 것이다.
이제부터 1주간 프로젝트를 진행하면서 만났던 난관들을 공유하고자 한다.
🚨 2번째 뎁스 문서가 보이지 않는 문제 발생

사진에서 문서 5에 해당하는 문서가 나타나지 않는 문제가 발생했다.
정확하게 말하면 루트 문서(1 depth)가 아닌 2,3...depth에 해당하는 문서가 중첩될 경우 찾지 못하는 것이었다.
💡 root 문서 구조 파악 오류였기에 배열 탐색을 통해 해결했다.
export function makeDocTree(root, depth, domTree) {
domTree += ` // 해당 문서 html 구조 `;
return makeDocTree(root.documents[0], depth + 1, domTree);
}
기존 코드에서는 root 문서 (depth가 1일 때만) 체크해서 재귀탐색을 진행했다.
그러니까 당연히 루트 문서만 기준으로 깊이 탐색을 진행했던 것이다.
export function makeDocTree(root, depth, domTree = []) {
root.forEach(child => {
const dom = ` // 해당 문서 html 구조 `;
domTree.push(dom);
if (child.documents.length === 0) {
return domTree;
}
makeDocTree(child.documents, depth + 1, domTree);
});
}
따라서 child 문서를 타고 들어갔을 때 forEach 메서드를 통해 하나의 뎁스에 여러 문서가 존재할 경우를 모두 탐색하도록 코드를 수정했다.
또한 문서 트리 형태를 문자열에서 배열 형태로 구한 뒤 join을 통해 최종적인 html 요소로 변환하여 렌더링했다.
🚨 state 변경 후 재렌더링을 하면 커서가 해제되어 이어서 문서 작성이 어려웠다.
이벤트 디바운싱을 통해 문서 작성 이벤트가 2초간 끊기면 해당 내용을 서버에 저장하고 상태를 업데이트했다.
상태 업데이트 시 다시 렌더링을 하기 때문에 커서가 풀리게 되는데 이렇게 되면 UX적인 측면에서 커서가 뚝뚝 끊기는 문서 처리를 하게 된다.
💡 수정 중인 항목도 상태에 추가하여 커서를 재설정하였다.
const nextState = {
...this.state,
docsTree: docs,
selectedDoc: editDoc,
currentFocus: {
id: newDoc.id,
element: 'title', // or 'content'로 설정
},
};
currentFocus 상태값을 추가해서 에디터에서 이벤트 디바운싱을 통해 api에 데이터 저장 호출을 보낼 때 해당 요소가 title인지 content인지를 저장했다.
이후 아래처럼 렌더링할 때 currentFocus 값에 따라 포커싱을 유지하게 만들었다.
// Editor.js
$editor.value = !content ? '' : `${content}`;
// content 수정 중이었으므로 editor에 포커스 위치 설정
if (currentFocus.element === 'content') $editor.focus();
}

🚨 삭제한 문서의 id를 url에 입력하여 접속하거나 삭제 즉시 새로고침을 하게 되면 해당하는 데이터가 없어 api 에러가 발생했다.
💡 try catch 문을 통해 해당 document의 id로 GET 요청을 하고 응답이 없으면 새로운 Error을 던지고 catch문에서 confirm을 통해 메인 화면으로 돌아갈지에 대한 여부를 사용자가 선택하도록 했다.
else if (pathname.indexOf('/documents/') === 0) {
const [, , documentId] = pathname.split('/');
try {
const doc = await request(`/documents/${documentId}`, {
method: 'GET',
});
if (!doc) {
throw new Error(
'해당 페이지를 없는 페이지입니다. 메인 화면으로 돌아가시겠습니까?'
);
}
...
} catch (e) {
const check = confirm(e.message);
if (check) {
push('/');
...
});
}
}
}
🚨 <textarea>가 아닌 일반 <div>태그에 contenteditable 속성을 부여하면 내부 텍스트가 innerText로 지정된다. 이것 때문에 단순히 focus()로 포커싱하게 되면 문자열 처음으로 커서가 설정되는 문제가 발생했다.
💡 검색을 통해 Range와 Selection 객체에 대해서 새롭게 알게 되었다. Range를 통해 새로운 선택 범위를 생성하고 selection 객체의 getSelection() 메서드를 통해 윈도우의 선택 위치를 재설정해서 원하는 위치인 문서의 마지막으로 커서를 설정하였다. 하지만 아직 엔터키를 누르거나 백스페이스 등을 누를 때 원하는 대로 설정이 잘 되지 않아 추후 계속 해결할 예정이다.
// 화살표 위쪽을 눌렀을 때 이전 줄이 있을 경우 해당 줄 처음으로 커서 이동
const selection = window.getSelection();
const $preLine = target.previousElementSibling;
if ($preLine) {
const range = document.createRange();
range.selectNodeContents($preLine);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
🚨 문서 제목이 빈 값일 경우 해당 div의 width 값을 따로 지정해주지 않아 네비바에서 해당 문서 영역의 빈 부분을 클릭해도 클릭이벤트가 적용되지 않는 문제가 발생했다.
💡 해당 innerText의 길이와 상관없이 width를 지정해주어 클릭 이벤트가 적용되도록 하였고 상하단 문서끼리의 이벤트 버블링을 방지하기 위해서 e.stopPropagation()을 적용해주었다.
/* 네비바 문서 컨테이너 */
.nav-document-container {
width: 248px;
}
// 클릭 이벤트
$sideNav.addEventListener('click', async (e) => {
e.stopPropagation();
const { className, dataset, classList } = e.target;
if (
className === 'nav-document' ||
className === 'nav-document-container'
) {
onClickDoc(dataset.id);
}
});
contenteditable을 활용한 rich한 에디터를 만드는 것이 생각보다 고려해야 할 점이 많았다.
특히 enter, 백스페이스, 화살표 등 커서를 이동해야 하는 모든 케이스들에 대한 동작 제어가 필요했다. 아직까지 구현한 것이 이외에도 마우스 드래그 범위 선택 및 외부 텍스트 복사와 같은 에디터 필수 기능에 대한 구현도 계속 추가해나가야 한다...
문서 내용 자동 저장 기능이 아직까지 부드럽지 못하다.
실제 노션은 사용자가 입력하는 즉시 내용을 저장하다가 내용이 서버에 자동저장되기 전에 새로고침과 같은 동작을 하면 변경 내용이 되지 않을 수 있다는 안내창이 나온다.
아직 난 그런 것을 묻지 않고 1.5초 후에 자동저장하다가 보니 사용자가 원치 않는 타이밍이 자동저장되면서 커서가 마음대로 지정되는 이슈가 남아있어 해결해야 한다.
보너스 요구사항에 해당하는 에디터 내 문서 링크 추가 기능을 완성하지 못했다.
<a>태그를 서버에 저장한 뒤에 div의 내용을 추가 수정하면 기존의 <a>태그가 날아가는 버그가 발생하여 미완성 상태이다. 뿐만 아니라 <, >와 같은 특수 기호가 JSON 변환 과정에서 깨지는 버그도 발생했다. 이 점도 해결해야 rich한 에디터라고 부를 수 있을 것 같다 ㅠㅠ
하지만 그래도 기본적인 문서 CRUD 작업들이 어느 정도 원활히 진행되도록 구현했다!
게다가 SPA 형태로 구현했기에 새로고침없이 부드러운 문서 간 페이지 이동이 가능하도록 구현을 성공해서 뿌듯하다.
바닐라 JS로 리액트와 같은 컴포넌트 방식으로 상태 관리를 함께 구현해보면서 앞으로 배운 Vue, React 프레임워크 동작 원리에 대해서도 미리 공부할 수 있어 좋았다.