문서 에디터 프로젝트 회고

jh·2023년 11월 8일
0

문서 에디터

노션,옵시디언 같은 에디터 기능을 vanilaJS만 사용하여 간단하게 구현해보았다.
개발기간 - 일주일

기능

  • history API를 통한 SPA 페이지 기능 구현
  • Sidebar를 제공하여, 클릭 시 url을 이동시키면서 오른쪽에 해당 문서를 띄워줌
  • 저장 버튼이 따로 없고, 문서를 수정 시 자동으로 서버에 요청하여 저장이 되고, 저장이 성공하면 자동으로 왼쪽 Sidebar에 있는 제목 또한 바뀌게 됨
  • vscode 사이드바처럼 해당 문서의 하위 문서로 추가 시, 시각적으로 하위 문서임을 알 수 있게 됨
  • 클릭된 문서는 highlight 처리
  • 발생할 수 있는 특이 사항도 처리
    - 현재 편집중인 문서가 삭제되면 , 해당 문서를 편집하지 못하게 화면을 바꿔버림
    • 뒤로가기, 앞으로가기 클릭 시 해당 문서가 삭제되었을 경우 화면 바꾸기

구현 과정

사실 처음에는 생각보다 간단한 프로젝트가 될 거라고 생각했었다.
컴포넌트를 크게 왼쪽에 Sidebar / 오른쪽에 문서의 내용을 보여주는 Content 라는 컴포넌트로 나누어서, 두 컴포넌트가 상호작용할 일은 크게 없지 않을까? 라는 생각을 했다

  • 사이드바를 클릭하면 - url 변경하면 - url 파싱해서 API 요청 - 문서 보여주기

이런식으로
Sidebar에서 이벤트 발생 -> API 요청 통해 Content 렌더
라는 일방적인? 방법만 생각하고 기능 구현에 들어갔다.

Sidebar의 state - {directory : 전체 문서 data}
Content state {id : 선택된 문서의 id , title , content ...}
Sidebar 클릭하면 -> 클릭된 문서의 id값으로 API 요청 후 Content state 변경

하지만 점점 기능을 추가하고 싶다는 생각이 들었고, 예외 케이스가 하나둘씩 생겨나게 되었다.
Ex)
노션 기능중에 문서 제목을 수정하면 -> 왼쪽의 Sidebar 문서 제목도 수정되는 기능이 있다.

  • 한마디로 Content에서 이벤트 발생 -> Sidebar 렌더링이 필요했던 것

또 Sidebar에서 현재 편집중인 문서를 삭제하면 -> 현재 편집중인 문서는 더 이상 편집하지 못하게 만들어야 하는데,
그러려면 Sidebar에서 이벤트가 단순히 API 요청해서 받는 게 아니라, 추가적인 기능이 더 필요하다는 것을 알게 되었다.

여기서 모든 기능의 공통적인 특징으로는

각 컴포넌트에서 어떤 이벤트가 발생해서 특정 값이 바뀌게 된다면, 두 컴포넌트 모두 변경되었다는 것을 인지하고 있어야 된다는 것이다.

Ex)
Sidebar에서 문서를 클릭했다면 - Content에서 API 요청
문서를 삭제 - 삭제된 문서랑 현재 Content에 있는 문서랑 같은지 비교
Content에서 제목 수정 - Sidebar 다시 렌더링

이를 위해 옵저버 패턴(Observer Pattern)이라는 디자인 패턴이 존재한다

Observer Pattern -> Flux 도입

옵저버 패턴(observer pattern)은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다. 발행/구독 모델로 알려져 있기도 하다.

한마디로 특정 데이터(store)를 구독하고 있으면 , 이 데이터가 변경되었을 시 구독자들에게 알릴 수 있다는 뜻이다.

첫번째 생각

Sidebar 컴포넌트에는 존재하는 문서 전체의 data를 API 요청받아 렌더링한다

  • 해당 값이 변경되면-> Sidebar는 당연히 렌더링되어야 한다
  • 하지만 Content는 ? 그럴 필요까지는 없다
  • Sidebar에 새 문서가 추가되면 - 클릭 이벤트를 통해 추가된 문서의 id 값으로 url을 이동시켜주고, Content에서 이를 파싱해서 API 요청을 보낸 후 그 값을 기반으로 렌더링처리 해주고 있음
  • Sidebar에서 문서가 삭제되면 - 이때는 삭제된 문서가 어떤 문서인지, 현재 편집중인 문서인지를 확인할 필요가 있음

그래서 처음에는 각 컴포넌트에서 Store에 대한 구독을 각각 하는 방식으로 했다.
Store - {directory : '전체 파일구조에 대한 데이터', deletedDocument : 삭제된 파일에 대한 데이터}

Sidebar - Store를 구독하여 사용
Content - Store를 구독하고는 있지만, 컴포넌트의 state는 그대로 사용하여 deletedDocument

단순히 변경되었다는 걸 알리기만 하면 - 구독되어있는 함수가 실행되는 패턴이기 때문에 복잡한 기능을 생각하지 않아도 되어서 좋긴 했지만, 각 컴포넌트별로 구독을 진행할 필요가 있을까? 에 대한 의문이 들었다

  • 보통 해당 store의 state값을 같이 사용하는 컴포넌트들을 묶어서, 그 상위 컴포넌트에서 store를 구독한 후 state가 변동되면 - 하위 컴포넌트들도 재렌더링 되는 방식을 많이 택하는 듯 함(이것도 여러 패턴이 있는 것 같은데.. 한번 찾아봐야 겠음)
  • 이를 기반으로 둘의 상위 컴포넌트에서 store를 구독 , store의 state 또한 변경함
{directory : 전체 문서 , 
selectedDocument : 선택된 문서(Content에 렌더링됨) , deletedDocument 
, newDocument ...}
  • state가 변경되면 두 컴포넌트 모두 렌더링 됨
  • Sidebar의 경우 directory , selectedDocument를 사용
  • Content의 경우 selectedDocument , deletedDocument를 사용

Flux 패턴 도입

const initialState = {
  number: 0
};

function reducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return {
        number: state.number + 1
      }
    default:
      return state;
  }
}

간단한 예시로
1. 버튼을 클릭 시 'ADD'라는 액션이 발생하게 코드를 짜놓는다( 보통 dispatch({action : "ADD" , payload : null} 이런식으로 한다)
2. reducer 함수를 통해 'ADD' 라는 action이 발생하면 해당 state를 변경시킨다.
3. state가 변경되었음을 구독중인 컴포넌트에 알려, 렌더링이 발생한다

이를 이용해서,
Sidebar 클릭 시 해당 state의 selectedDocument 값을 현재 클릭한 문서의 id 값으로 변경시키고 싶다 라고 하면
1. 클릭 이벤트 발생 시 , dispatch를 통해 Store에 클릭 이벤트가 발생했다고 알리면서 특정 값을 전달한다.
2. 이후 Store에서는 reducer 함수를 통해 전달받은 액션과, 값을 가지고 state를 변화시킨 다음
3. 다시 Component에 state가 변화되었다는 걸 알린다(이후 다시 렌더링)

이런 식으로 문서의 삭제, 추가, 수정 이벤트마다 특정 리듀서 함수를 통해 state를 변경시키고, 다시 렌더링 시키는 방식으로 구현해 보았다

아쉬웠던 점

  • 컴포넌트의 일관성 유지
    - 리액트에서는 컴포넌트 함수에서 컴포넌트 관련 로직 - 화면 렌더링이 나뉘게 되면서 , 어디서 뷰를 담당하고 어디서 관련 로직을 다루는지가 확연하게 나뉘었다면 vanilaJS에서는 관련 일을 '내가' 해줘야 한다.
    - 또한 리액트에서는 hooks를 통해서 관심사 분리가 쉬웠다면, 이것 또한 vanilaJS에서는 내가 알아서 해줘야 한다..
    • 이 과정을 쉽게 하려면 class형 컴포넌트처럼 Component 상속을 통해서 하는 게 제일 깔끔한 것 같다
  • state 변경은 무조건 dispatch-> reducer를 통해서만 구현해야 했는데, 코드를 짜고 보니 어디서는 dispatch , 어디서는 그냥 fetch해서 변경 이런식으로 하고 있는 걸 발견...
  • 사실 이 두개만 바꿔줘도, 남들이 코드를 보기에도 훨씬 쉽고, 내가 코드를 수정하는 것도 쉬웠을 거 같은데 이때 정말 몸이 안좋았던 나머지 이런 것 까지 신경쓸 겨를이 없었던 게 너무 아쉽다
  • state에 대한 생각을 우선적으로 정하고 가는 게 중요한 것 같다. 내가 만들고자 하는 프로젝트에는 어떤 데이터가 필요하고, 해당 데이터들을 어떤 형식으로 다룰 것인지에 관한 고민을 먼저 한 뒤 -> 각 컴포넌트에 어떤 데이터들을 내려줄 것인지?
    아니면 해당 state를 중앙에서 관리할 필요가 있는지? 를 고려해보는 게 우선적으로 필요할 것 같다

느낀 점

  • 중앙형 상태관리와 상태 관리 라이브러리에 대해서 조금 알 수 있는 시간이었던 것 같다. Flux 패턴이 왜 유용한지, 왜 리덕스에서 리듀서 함수는 순수 함수로 유지해야 하는지 등등을 알 수 있었다
  • 일관된 코드라는 게 얼마나 중요한지, 내가 얼마나 안 지키고 있었는지에 대해 느끼게 되었다
  • 클린 코드, 가독성 등등 다 좋지만 결국 제 시간에 기능을 구현 못하면 아무 의미가 없는 코드라는 걸 명심해야겠다

0개의 댓글