안녕하세요, 이번에 회고해볼 내용은 코드스테이츠 메인 프로젝트에서 진행했던 바로 MY BUDDY🐶입니다.
그 중에서도 좋아요에 왜 디바운싱을 적용하게 되었는지 이야기해보려고 합니다 🧑💻
기술적인 이야기에 앞서 MY BUDDY부터 뭔지 보실까요? ㅎㅎ
강아지들만을 위한, 강아지들의 SNS 웹 서비스 ( MY BUDDY 구경하러가기 )
MY BUDDY는 545만 강아지 친구들을 위한 SNS 서비스로 친구들의 생생한 사진들과 추천장소들을 확인해볼 수 있습니다. 더 궁금하시다면 사이트를 방문해주세요!
간단하게 소개해드렸으니 본론으로 들어가보도록 하겠습니다
디바운싱(Debouncing)은 자바스크립트에서 이벤트 처리를 최적화에 많이 사용되는 기술로 짧은 시간 동안 여러 번 발생하는 이벤트를 하나로 줄이는 최적화 기법입니다. 이를 통해 이벤트가 연속적으로 발생해도 몇 초 동안 한 번만 처리되도록 합니다. 디바운싱은 검색어 자동완성, 윈도우 리사이징, 스크롤 이벤트 등에서 효율적인 이벤트 처리를 위해 사용됩니다.
디바운스는 자바스크립트의 setTimeout을 이용하면 쉽게 구현할 수 있습니다. 아래는 React에서 디바운스를 커스텀 훅으로 구현한 예제입니다.
import { useState, useEffect } from "react";
function useDebounce(initValue, delay) {
const [value, setValue] = useState(initValue);
useEffect(() => {
const timeoutId = setTimeout(() => {
setValue(initValue);
}, delay);
return () => clearTimeout(timeoutId);
}, [initValue, delay]);
return value;
}
export default useDebounce;
매번 이벤트를 setTimeout을 통해 주어진 시간 이후에 실행되도록 비동기로 등록해두고, 해당 시간내에 새로운 이벤트가 들어오면 앞서 등록한 이벤트를 무효화하여 마지막 이벤트를 실행하도록 합니다.
하지만, 눈으로만 보면 이해하기 어려우니까 실제 동작을 확인해볼까여?
[예제 - input 입력 디바운스 콘솔로그 확인]
위의 동작에서 확인할 수 있듯이 디바운스는 사용자 입력한 값들에 대해서 바로 바로 처리되는 것이 아니라 사용자가 입력을 멈춘 후의 마지막 이벤트가 실행하게 됩니다.
이러한 이유로 페이지를 나갈때(React로 치자면 unmount 시점이겠죠?)에 적용하는 것을 검토하게 되었습니다. 하지만, 걱정많은 개발자인 저는 또 다른 고민이 생기게 되었습니다.
뿐만아니라, 좋아요 눌렀을때 바로 알고 싶은 사용자에 니즈가 있지 않을까라는 의견도 나와 더욱 고민하게 되었습니다.
그래서 생각한게 바로 디바운싱(Debouncing) 입니다. 디바운싱을 적용한다면, 사용자가 무자비하게 누른다해도 결국에는 마지막의 클릭 상태만 발생하게 되어 이를 해결할 수 있다는 확신이 들었습니다.
좋아요 버튼의 상태 변경에 useDebounce hook을 적용하여, 사용자가 버튼을 클릭 후 3초 내의 동안 재클릭이 없는 경우 서버에 전송하도록 구현하였습니다.
function PostDetailHeart({ likeCount, likeByUser, bulletinId }) {
//...코드 생략
const [heart, setHeart] = useState(likeByUser);
const [count, setCount] = useState(likeCount);
const debouncedHeart = useDebounce(heart, 3000);
//...코드 생략
const handleLike = newLikeByUser => {
if (newLikeByUser === 0) {
unlikeMutate({ bulletinId });
return;
}
likeMutate({ bulletinId });
};
const handleHeartClick = () => {
if (heart === 0) {
setCount(preCount => preCount + 1);
setHeart(1);
return;
}
if (heart === 1) {
setCount(preCount => preCount - 1);
setHeart(0);
}
};
useEffect(() => {
if (likeByUser !== debouncedHeart) {
handleLike(debouncedHeart);
}
}, [debouncedHeart]);
return (
<HeartContainer>
<HeartButton onClick={handleHeartClick} likeByUser={heart}>
<HeartIcon />
</HeartButton>
<HeartText>맘에 들어요</HeartText>
<HeartCount>{count}</HeartCount>
</HeartContainer>
);
}
버튼을 연속적으로 누르면 이벤트가 발생하지 않았고, 마지막 3초 뒤에 정상 전송되는 것을 모두 테스트가 완료되어 배포도 무사히 마쳤습니다. 하지만, 팀원분이 화면들을 테스트하던 중 생각하지 못한 버그가 발생하였습니다.
[버그 상황]
[버그 원인]
(cleanUp을 쉽게 설명하면 화면상 사라질때에 취해야할 행동들을 cleanUp통해 정의한다고 생각하시면 됩니다)
const [heart, setHeart] = useState(likeByUser);
const debouncedHeart = useDebounce(heart, 3000);
(이 부분은 React와 관련되어 있는 내용이라 길어질거 같아 자세한 내용은 생략하겠습니다.)
처음에는 해결책이 생각나지 않아 정말 많은 시도를 했습니다. 너무 고통스러웠습니다...
하지만, 하루 자고 일어나서 생각해보니 간단한 해결책이 있었습니다.
저는 여기에 useRef를 사용하였습니다. 일반 변수(let, const)를 사용하는 경우 리렌더링시 초기화되기 때문에 이를 유지할 수 있는 useRef를 사용하였습니다.
function PostDetailHeart({ userId, likeCount, likeByUser, bulletinId }) {
const [heart, setHeart] = useState(likeByUser);
const debounceRef = useRef();
const heartRef = useRef(heart);
const likeByUserRef = useRef(likeByUser);
useEffect(() => {
heartRef.current = heart;
debounceRef.current = setTimeout(() => {
if (likeByUser !== heart) {
handleLike(heart);
}
}, 3000);
return () => {
clearTimeout(debounceRef.current);
};
}, [heart]);
useEffect(() => {
const timerId = debounceRef.current;
return () => {
if (timerId) {
clearTimeout(timerId);
}
if (likeByUserRef.current !== heartRef.current) {
handleLike(heartRef.current);
}
};
}, []);
return (
<HeartContainer>
<HeartButton onClick={handleHeartClick} likeByUser={heart}>
<HeartIcon />
</HeartButton>
<HeartText>맘에 들어요</HeartText>
<HeartCount>{count}</HeartCount>
</HeartContainer>
);
}
조금은 불필요한 로직들도 껴있지는 아닌가라는 확인이 필요해보이지만, 프로젝트에 알맞는 최적화 기법을 찾고 적용하며 유의미한 결과물을 냈다고 생각되어 뿌듯하다