노션클론 리팩토링 (7) - debounce 함수 분리, 문서 highlight로직 분리, BreadCrumb 조금 꾸며주기

김영현·2024년 2월 18일
0

debounce

debounce란, 연속적인 요청이 들어왔을때 맨 마지막요청만 처리해주는 기술이다.

예를들어 유저가 악의적이든 아니든 input창 포커스하여 엔터키를 꾹 누르고 있다고 해보자.
이때 api호출로직이 있다면, 윈도우 기준 보통 초당 33번의 엔터키 이벤트가 발생하여...끔찍
물론 가드처리를 할 수는 있겠다만, 결국 맨 마지막 요청만 처리해야하는 것도 맞다.

노션클론 프로젝트에선 문서 업데이트 호출시에 적용됐다. 하지만 로직분리가 안되어있었음.

let timerOfSetTimeout = null;

    new Editor({
      $target: this.wrapper,
      props: {
        ...
        },
        documentAutoSave: (documentData) => {
          if (timerOfSetTimeout !== null) {
            clearTimeout(timerOfSetTimeout);
          }
          timerOfSetTimeout = setTimeout(() => {
            store.dispatch(updateDocumentAsync(documentData));
          }, 1000);
        },
      },
    });

이 로직을 분리해보자. 더불어 혹시모르니 debounce로 등록된 콜백함수를 삭제하는 기능도 추가해보았다. 예전에 강의해서 한 번 구현하고 바로 이전 프로젝트에서 구현해봐서 쉽게 구현했다.

로직 분리

핵심은 closure, ...args, this다.

export const debounce = (callback, delay = 1000) => {
  let timerId = null;
  const _debounce = (...args) => {
    clearTimeout(timerId);
    timerId = setTimeout(() => callback.apply(this, args), delay);
  };

  _debounce.stop = () => {
    clearTimeout(timerId);
  };

  return _debounce;
};

timerId변수가 클로저로인하여 살아있다. 그렇기에 내부 로직(_debounce)이 정상적으로 작동한다. 뿐만 아니라 함수일급 객체기에 stop처럼 메서도 등록해줄 수 있다.

해결!


문서 highlight로직 변경

문서가 클릭됐을때 그 문서의 배경화면을 강조해주는 로직이다.
지금까지 사용했던 로직은 대충 이렇다.

//커스텀 이벤트를 이용한다. 또한 뒤로가기가 호출되었을때도 적용한다.
export const push = (nextUrl, callback) => {
  window.dispatchEvent(
    new CustomEvent("route-change", {
      detail: {
        nextUrl,
        callback,
      },
    })
  );
};

export const addDependOnPathEvent = (callback) => {
  window.addEventListener("popstate", () => callback());
};

//문서 리스트 로직 중. 화살표, +, x버튼을 제외한 나머지부분을 클릭했을때 이벤트가 발생한다.

      if (!e.target.closest("button")) {
        e.stopPropagation();
        push(
          `/documents/${this.wrapper.dataset.id}`,
          this.props.highlightSelectedDocument
        );
      }

//처음 렌더링될때도 이 로직을 실행한다
  constructor({ $target, props }) {
    super({ $target, tagName: "div", props });
    this.highlightSelectedDocument();
    addDependOnPathEvent(this.highlightSelectedDocument);
  }


// 콜백으로 전달한 로직
highlightSelectedDocument() {
    const documentList = document.querySelectorAll(".document-item-inner");
    const { pathname } = window.location;
    const [, , pathdata] = pathname.split("/");
    documentList.forEach((node) => {
      if (node.parentNode.dataset.id === pathdata) {
        node.classList.add("selected-document");
      } else {
        node.classList.remove("selected-document");
      }
    });
  }

나만의 리덕스를 사용하고 있다고 가정했을때, 위 로직의 문제점은 뭘까?(사실 아니더라도 문제점이 있다).

=> 메서드라서 다른곳에서 사용하기 껄끄러움 + 부모에서는 사용할 수 없다.

분리하자

export const highlightSelectedDocument = () => {
  const documentList = document.querySelectorAll(".document-item-inner");
  const { pathname } = window.location;
  const [, , pathdata] = pathname.split("/");
  documentList.forEach((node) => {
    if (node.parentNode.dataset.id === pathdata) {
      node.classList.add("selected-document");
    } else {
      node.classList.remove("selected-document");
    }
  });
};

유틸함수로 분리한뒤 push를 사용하는 곳에서 같이 써먹었다.
즉, url이 바뀔때마다 유틸함수를실행한다~!

결과는...

BreadCrumb를 눌러 이동해도 색이 잘 바뀐다.

하지만 문제점이 몇가지 남아있다.

1.push메서드는 그저 라우팅을 위한 메서드다.
하나의 함수는 하나의 일만!
인자로 콜백을 받는건 적절치 않아보인다.

  1. 만약 이번처럼 url에 의존하는 함수가 또 생긴다면, 생기는 족족 push를 사용하는 곳에 모두 넣어주어야한다.

어떻게 해결할지는 곰곰히 생각해보자..!!


debounce 버그

//디바운스 테스트
   new Editor({
      $target: this.wrapper,
      props: {
        initialState: {
          id,
          title,
          content,
        },
        documentAutoSave: {
          const test = debounce(() => console.log("디바운스 테스트"));
          test();
      },
    });

documentAutoSave를 3번 호출함 => 콘솔로그가 3번 다 찍힘.

클로저 문제?

디바운스에서 클로저를 이용하고 있어서 클로저 문제라고 생각했다.
하지만 정확히는 어떤 부분이 문제인지 몰라서 일단 패스.
디바운스 함수 자체는 검색해보니 똑같아서 문제가 없다.

this 문제?

this를 apply로 걸어줄때 잘못걸리나? => 이 역시 의미가 없음. this를 활용하지 않음...

class 문제!

클래스형 컴포넌트 구현체여서 문제가 있는걸까?
class component debounce 등으로 검색해보니 결과가 나왔다.

https://stackoverflow.com/questions/23123138/how-can-i-perform-a-debounce

디바운스는 상태를 저장함.
=> 컴포넌트 하나당 하나의 디바운스드 된 함수가 존재해야 한다.

즉, 내 코드는 documentAutoSave를 호출할때마다 새로운 디바운스드 된 함수를 만들어 호출했다. 따라서 다른 참조를 가지고있기에 새로운 함수로 취급되었다.

//이런식으로 붙여주거나
  constructor({ $target, props }) {
    ...
    this.documentAutoSave = debounce((documentData) =>
      store.dispatch(updateDocumentAsync(documentData))
    );
  }

//이런식으로 public field를 이용해준다.
//public field는 인스턴스 바인딩이 자동으로 되어있다

documentAutoSave = debounce((documentData) =>
    store.dispatch(updateDocumentAsync(documentData))
  );

느낀점

로직이 쌓이면 쌓일수록 디버깅이 힘들다. 제대로 짠 코드가 아닐 수도 있어서 더욱 그렇다.
그래서 테스트가 중요한거고 짤때부터 제대로짜야한다...!!
특히 바닥부터 짜서 올라왔을땐 더 심각할 수 있음...바닥이 잘못된거라면...?!😨

다음 리팩토링은 에디터쪽 로직 분리다!
그게 끝나면 컴포넌트 구현체도 손봐야하고....SOLID원칙에 대해 공부도 해봐야겠다.

profile
모르는 것을 모른다고 하기

0개의 댓글