바닐라 JS로 노션 클론하기

김유진·2023년 7월 21일
3

Javascript

목록 보기
22/22
post-thumbnail

약 1주 반의 기간동안 바닐라 JS를 이용하여 협업 도구로 많이 사용하는 노션을 클론하였다.
문서 정보에 대한 것은 바닐라 JS를 담당하시는 강사님께서 만들어주셨기 때문에 서버를 직접 생성할 필요는 없었고, 온전히 구현에만 집중할 수 있었다.
처음에는 이걸 어떻게 구현해야 하나..막막한 기분이 들었지만 막상 구현을 시작하니 아직은 부족한 부분이 많지만 해낼 수 있었던 것 같아 뿌듯한 프로젝트였다.
배포 링크

설계

가장 먼저 곧 React에 대한 프로젝트도 진행할 것이기 때문에 React스러운 상태관리를 하고 싶다라는 생각을 하였다. 그래서 고려한 점은 아래와 같다.

  • 되도록이면 지역 상태를 이용할 것, 컴포넌트끼리 의존하기보다는 개별 컴포넌트에서 각자의 데이터를 관리하기
  • 만약 지역상태로 각자의 상태를 관리하는 데 어렵다면 전역상태로 관리하기

뒤에서도 언급하겠지만, 설계에 대해서 굉장히 아쉬운 점이 아직은 많다 (..) 되도록 지역상태를 이용하고 컴포넌트 내부에서 알아서 자신이 필요한 데이터를 받아 와 관리하는 과정에서 매끄럽지 못한 데이터 관리가 된 것 같아 많이 반성하게 되었다. 😅

데이터 관리 흐름


위 다이어그램은 데이터를 관리하고 있었던 흐름이다.
사실, App 컴포넌트에서 대부분의 데이터를 받아오나, App에서 따로 관리하고 있는 state는 존재하고 있지 않은 것을 알 수 있다. (사실상 전역 상태가 없다고 볼 수 있는..)

구현 로직

DocumentPage

DocumentPage는 문서 목록을 불러오는 컴포넌트이다. 해당 컴포넌트에서는 모든 노션 문서들을 불러오되, 노션처럼 접힌 문서를 모두 화면에 그려내면 초기 로딩이 너무 오래 걸리게 되기 때문에 더보기 를 클릭하였을 때에만 하위 페이지가 랜더링되도록 하도록 하였다.

onClick 이벤트를 주시하고 있다가, 해당 이벤트가 발생하면 문서 목록을 그리는 객체가 동작하여 DOM을 추가해 주는 것이다.
클릭 이벤트를 통하여 만들어지는 문서 객체가 props로 받아오는 값은 아래와 같다.

  • parent : 부모 노드의 id
  • selectedNode: 현재 선택한 노드의 id 정보
  • isOpen: 오픈된 상태인지 확인(오픈되어 있다면 close를 해주어야 하므로)
  • depth: 깊이 (몇번째 깊이에 있는 문서인지)

부모 노드와 현재 노드에 대한 id를 이용하여 문서 생성, 삭제에 대한 이벤트를 관리하고, depth를 통하여 하위 노드로 갈 수록 왼쪽 padding 을 가질 수 있도록 한다.

SPA로 구현하기

React 라이브러리는 대표적인 SPA 라이브러리이다.SPA는 Single Page Application의 약자이다. 페이지를 랜더링 할 때마다 모든 리소스를 받아오도록 하는 것이 아니라, 단순히 보여지는 소스를 랜더링하여 페이지가 전환된 것 같은 효과를 주도록 구현하는 것이다. 정리하자면 페이지를 넘길 때마다 서버에서 그 정보를 받아오는 것이 아니라 기존 자바스크립트 소스를 이용하는 것이다.
EditorPage는 현재 url의 파라미터를 받아와 현재 페이지의 id를 가진 글을 편집할 수 있도록 구현하였다.

//App.js
this.render = () => {
  documentPage.render();
  const { pathname } = window.location;
  const [, , documentId] = pathname.split('/');
  if(documentId){
    editorPage.setState({...editorPage.state, id: documentId})
  }
}

이렇게 현재 url에서 글에 대한 id를 뽑아온 뒤에 editorPage의 state로 관리할 수 있도록 넘겨준다.

//EditorPage.js
const fetchPost = async() => {
        const { id } = this.state;
        const post = await getEditableDocuments(id)
        this.setState({
            ...this.state,
            post
        })
    }

export const getEditableDocuments = async(id) => {
    const documentData = await request(`/documents/${id}`)
    return documentData
}

이후, 해당 문서에 대한 자세한 내용을 불러올 수 있도록 API를 호출하면 된다.

SPA router 구현

페이지의 url이 바뀔 때마다 이를 인식할 수 있는 이벤트 함수를 등록해야 한다. 그렇기 때문에 CustomEvent라는 메서드를 이용하여 커스텀 이벤트를 생성해야 했다. (url 변화에 대한 이벤트를 자동으로 인식하는 메서드는 자바스크립트에 존재하지 않기 때문이다.)

const ROUTE_CHANGE_EVENT_NAME = 'route-change'
export const initRouter = ({onRoute}) => {
    window.addEventListener(ROUTE_CHANGE_EVENT_NAME , (e) => {
        const { nextUrl } = e.detail;
        if(nextUrl){
            history.pushState(null, null, nextUrl);
            onRoute();
        }
    })
}

이러한 이벤트를 만들고, App.js에 다음과 같은 콜백 함수를 넘겨준다.

this.route = () => {
  const { pathname } = window.location;
  if (pathname.indexOf('/documents/') === 0){
    const [, , documentId] = pathname.split('/');
    editorPage.setState({...editorPage.state, id : documentId});
  }
}
initRouter({onRoute : () => this.route()});

그럼 이제 url이 바뀌는 SPA를 이용할 때마다 url과 함께 아래와 같은 코드를 작성해 주면 된다.

export const push = (nextUrl) => {
    window.dispatchEvent(new CustomEvent(ROUTE_CHANGE_EVENT_NAME, {
        detail: {
            nextUrl
        }
    }))
}
 push(`/documents/${id}`);

그럼 라우팅이 될 때마다 자동적으로 이벤트가 발생하게 되고, history 객체에 라우터에 대한 정보가 저장되는 것이다. 자바스크립트의 history에 대해서는 이 링크를 참고하자.

코드리뷰, 피드백

이번 과제를 하면서 가장 챌린징했던 부분은 바로 부모-하위 노드에 대한 관리였다.
하위 노드는 자신을 호출하고 있는 부모가 누구인지에 대한 상태를 가지고 있어야 하고, 글을 삭제하거나 생성할 때에도 요구하는 명세사항이 제대로 지켜져야 할 필요성이 있었다.
이번에 멘토님과 팀원들에게 내가 한 프로젝트에 대해서 코드리뷰를 받을 수 있는 기회가 있었고, 아쉬웠던 점과 좋았던 점을 몇가지 적어보고자 한다.

1. 랜더링 최적화

아무래도 전역 상태관리를 하지 않고 컴포넌트별로 필요한 정보 페이지를 관리하다 보니, 다른 컴포넌트를 많이 불러와야 할 때가 많았다. 무분별하게 state가 변할 때마다 관련 있는 기능이 있는 컴포넌트들을 랜더링 하다 보니, 하나의 상태 변경에 대하여 불필요한 랜더링이 많이 존재하였다.
이를 개선하기 위해서는 state가 변하지 않았을 때 랜더링을 방지할 수 있는 방어코드를 작성하는 것이다. 현재 state와 다음 state를 비교하는 로직을 꼭 추가하여 불필요하게 성능을 떨어뜨리는 코드를 작성하면 안되겠다는 것을 한번 더 다짐할 수 있었다.

2. class를 적극적으로 이용하자.

이번 과제는 생성자 함수를 이용하여 컴포넌트를 관리하였다. 하지만 현재 ES6버전이 나오고 나서 class에 대한 문법이 잘 나와 있는 상황에서 과거에 많이 사용하였던 생성자 함수를 이용하고 있는 부분에 있어서 아쉬웠다. 객체 지향에 대해서 과거 열심히 공부했던 적도 있으니, 조금 더 적극적으로 사용해도 좋을 것 같다는 생각을 하게 되었다. 멘토님이 생성자 함수 vs class 에 대한 메모리 사용에 대해서 고민해보라고 하셨는데, 이에 대해서는 다음 글에 이어서 작성할 예정이다 ✌️

3. 상태관리를 해야 할 때와 아닐 때를 구분하자

컴포넌트에는 다양한 변수들이 존재하고, 어떤 변수는 state에 포함시켜 상태관리를 하였지만 어떤 변수는 단순히 값을 변화시키는 변수로 사용하였다. 이러한 부분에 있어 똑같이 랜더링하는 데 있어 값이 변화하는 변수들에 대해서 state 에 대한 기준을 명확히 해야 한다는 피드백을 들었다. 컴포넌트에서 여러번 사용한다고 해서, 값이 변한다고 해서 무작정 상태관리에 포함시키는 것이 아니라, stateless한 특성을 극복하고 컴포넌트에 대한 기능에 중요한 역할을 끼치는 변수들을 주시해야 한다는 것을 깨달을 수 있었다.

0개의 댓글