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처럼 메서도 등록해줄 수 있다.
해결!
문서가 클릭됐을때 그 문서의 배경화면을 강조해주는 로직이다.
지금까지 사용했던 로직은 대충 이렇다.
//커스텀 이벤트를 이용한다. 또한 뒤로가기가 호출되었을때도 적용한다.
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
메서드는 그저 라우팅을 위한 메서드다.
하나의 함수는 하나의 일만!
인자로 콜백을 받는건 적절치 않아보인다.
push
를 사용하는 곳에 모두 넣어주어야한다.어떻게 해결할지는 곰곰히 생각해보자..!!
//디바운스 테스트
new Editor({
$target: this.wrapper,
props: {
initialState: {
id,
title,
content,
},
documentAutoSave: {
const test = debounce(() => console.log("디바운스 테스트"));
test();
},
});
documentAutoSave를 3번 호출함 => 콘솔로그가 3번 다 찍힘.
디바운스에서 클로저를 이용하고 있어서 클로저 문제라고 생각했다.
하지만 정확히는 어떤 부분이 문제인지 몰라서 일단 패스.
디바운스 함수 자체는 검색해보니 똑같아서 문제가 없다.
this
를 apply로 걸어줄때 잘못걸리나? => 이 역시 의미가 없음. this
를 활용하지 않음...
클래스형 컴포넌트 구현체여서 문제가 있는걸까?
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원칙에 대해 공부도 해봐야겠다.