방구석에 놔둔 개발 노트

1년차 웹 / 앱 프론트엔드 엔지니어의 좌충우돌 얼렁뚱땅 앞뒤짱구 생존기

2023-09-10: follow/unfollow 를 최적화하는 방법, React를 이용한 Debouncing

⚠️ 이슈가 들어왔다.

이번에 작업한 업무와 관련하여 QA 팀과 디자인 팀에서 속도 개선에 대한 건의 사항이 들어왔다.

이번에 작업한 내용 중에 follow와 unfollow를 하는 페이지가 있는데, 이미 있는 기능을 그대로 가져와 쓰고 있는 데다 해당 기능은 누를 때마다 서버 통신을 통해서 값을 바꿔주는 식으로 적용되어 있었다.

실제로 적용 전에 내부에서 확인을 해봤는데, 거기서도 반응이 느려서 그냥 쓰면 되겠다 싶었지만, 아무래도 QA와 디자인 팀 내에서는 느려도 너무 느리다고 생각해서 그런지 개선에 대한 문의가 들어왔다.

기능 자체는 이미 있는 것을 바꾸는 건 어렵고, 서버를 최적화하는 것도 요청한다고 해서 마음대로 될 수 있는 것도 아니니 클라이언트에서 아무튼 최적화를 할 수 밖에 없을 것 같은데…

이런 방법에 대해서 찾아보니 좋은 키워드를 발견했다. 바로 디바운싱(debouncing)과 쓰로틀링(throttling)이다.

두 기능을 간략하게 요약하자면 다음과 같다.

  • 쓰로틀링: 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것
  • 디바운싱: 연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것

(JavaScript) 쓰로틀링과 디바운싱

follow, unfollow는 여러 번 행동을 반복할 수 있는 것이므로 최적화를 한다면 쓰로틀링보다는 디바운싱이 어울릴 거라고 생각했다.

그래서 이번에는 디바운싱을 통해 API 호출을 최소화해 follow 기능을 최적화하는 걸 시도해보기로 했다.

⛹️ 디바운싱을 하는 이유?

흔히 말하는 ‘좋아요’ 기능과 ‘팔로우’ 기능은 유저가 마음만 먹으면 언제든지 버튼 하나로 툭툭 상태를 바꿀 수 있는 기능이다.

이 기능 한 번을 작동시킬 때마다 서버에서 상태가 변화됐다는 내용을 보내줘야 하는데 만약 이게 여러 번 작동되거나 한다면 그만큼 서버에 변화된 상태를 여러 번 보내줘야하는 문제가 발생한다.

이럴 경우, 의도치 않게 서버에 부하를 줄 수 있는 사항이 발생할 수 있으므로 부하를 주지 않으면서도 기능이 잘 작동되도록 할 수 있는 방법으로 디바운싱을 이용할 수 있을 것 같다.

디바운싱은 위에서 말했듯이 계속되서 호출되는 이벤트나 함수들을 작동을 시키되, 그 동작의 ‘마지막’ 만을 호출시키는 것을 가리킨다.

예를 들어 좋아요를 10번이나 눌렀다고 한다면, 9번까지 눌렀던 좋아요의 동작에 따른 호출은 무시하고 10번째의 동작 만을 기억해서 작동할 내용을 호출시키는 거라고 보면 되겠다.

이를 통해 우리가 거둘 수 있는 부분은 두 가지가 있는데,

  • 클라이언트에서는 유저가 특정 동작을 반복하는 부분을 마지막 동작만을 처리해 이벤트 처리를 줄일 수 있다.
  • 서버에서는 API 호출이 줄어듬으로서 부하를 덜 수 있다.

🛠️ 커스텀훅을 통한 디바운싱을 구현

우선 다른 사람들이 구현한 디바운싱 관련 커스텀 훅을 바탕으로 아래와 같이 useDebounce를 만들었다.

import {useEffect, useState} from 'react';

export const useDebounce = (followState: boolean | undefined, delayTime: number) => {
  const [currentFollowState, toggleCurrentActiveState] = useState<any>(followState);

  useEffect(() => {
    // 외부에서 좋아요 상태가 변경되면 해당 값을 기록하기 위한 기능
    const activeTimeoutEvent = setTimeout(() => {
      toggleCurrentActiveState(followState);
    }, delayTime);

    // 도중에 좋아요나 팔로우 상태가 변경된다면 기존 이벤트를 취소
    return () => clearTimeout(activeTimeoutEvent);
  }, [followState, delayTime]);

  return {currentFollowState};
};

위의 훅에서 currentFollowState만을 받아와서, 화면 상의 followState와 동일한지를 비교한다. currentFollowState는 setTimeout을 통해 일시적인 시간 후에 동작하게 되므로 해당 시간이 지나면 setState가 동작하게 될텐데 그 때가 follow 상태가 진짜 바뀌게 되는 시점이라고 판단하고 API 통신을 적용하게 유도했다.

const FollowButton = (item) => {
  const [followState, toogleFollowState] = useState(item.state);
    // 2초 뒤에 디바운싱 훅이 작동되도록 설정
  const {currentFollowState} = useDebounce(followState, 500);

  const onPostFollowStateToServer = (currentState) => {
        if (currentState) {
            // 팔로우/좋아요하는 서버 동작 진행
        } else {
            // 언팔로우/좋아요 해제하는 서버 동작 진행
    }
  };

    // 클라 상에서 팔로우, 언팔로우 동작하는 함수
  const handleClickFollow = () => {
    if (followState) {
      toogleFollowState(false);
    } else {
      toogleFollowState(true);
    }
  };

  useEffect(() => {
    // 디바운싱으로 받아온 상태 값이 기존 적용된 값하고 다른가?
    if (item.state !== currentFollowState) {
      // 다르다면 상태가 변했으므로 서버에 변경 사항을 적용한다.
      onPostFollowStateToServer(currentFollowState);
    }
  }, [currentFollowState]);

    return (
        <Button style={{border: followState ? "1px solid Blue" : "1px solid gray"}} onClick={()=>handleClickFollow()} />
    )
}

위의 방식을 바탕으로 일부 스타일 및 코드 바꿔서 적용한 결과는 아래와 같다.

이런 방식으로 API를 전달하는 이벤트도 최소화하고, 그만큼 API 사용량도 줄일 수 있다.

🔖 참고 자료

(JavaScript) 쓰로틀링과 디바운싱

웹프론트엔드에서 쓰로틀링(Throttling)과 디바운싱(Debouncing)의 개념

Debounce로 성능 향상 시키기

[회고] 좋아요에 디바운싱(Debouncing)을 더하다 (feat. my buddy)