🐦 Flutter

Debounce, Throttle

ji-hyun 2023. 12. 9. 22:19

오늘은 프론트 개발자라면 중요한 개념인 Debounce, Throttle 에 대해서 좀 더 파헤쳐 보려고 한다.

 

 

서버에 사용자의 요청을 적절히 전달하는 것은 프론트엔드 개발자의 중요한 역할 중 하나라고 생각한다.

 

 

실제 상황에서 많이 발생하는 일 중 하나가 있는데

사용자가 버튼을 누를 때 단 한 번의 요청을 의도했음에도 불구하고, 그 요청을 실행 중이었는데 사용자는 실행 중인지 몰라 버튼을 또 누르게 되는 경우가 있다. (이거 왜 반응을 안 하는거야..?! 이러면서..)

 

 

 

이때 모든 요청을 곧이 곧대로 서버에 보내게 되면 서버는 이 요청을 모두 처리해 사용자는 예상치 못한 문제를 겪게 된다.

 

 

 

개발하면서 생각보다 이런 일들이 많이 발생하는데 그 중 하나의 방어책이 바로 로딩 표시를 버튼 위에 덮어씌워 사용자가 인지하기도 하고 누를 수 없게 만드는 것이 1차적인 방어책이고 (-> 다들 알 것이라 생각한다) 2차적인 방어책은 이 요청에 Debounce, Throttle 을 적용하는 것이라고 생각한다.

 

 

 

 

 

Debounce, Throttle 개념은 구글링해봐도 굉장히 많이 나오는데 한 마디로 정의해보면 아래와 같다.

  • Debounce: 연속적인 요청이 올 때 마지막 요청임을 확인하고 마지막 요청만 처리.
  • Throttle: 일단 요청을 실행하고 일정 시간 내에 오는 이후 요청들은 무시.

 

 

 

 

Debounce

이제 코드로 알아보자!

Debounce 적용 코드는 다음과 같다.

 

import 'dart:async';

typedef EasyDebounceCallback = void Function();

class _EasyDebounceOperation {
  EasyDebounceCallback callback;
  Timer timer;
  _EasyDebounceOperation(this.callback, this.timer);
}

class EasyDebounce {
  static Map<String, _EasyDebounceOperation> _operations = {};

  static void debounce(
      String tag, Duration duration, EasyDebounceCallback onExecute) {
    if (duration == Duration.zero) {
      _operations[tag]?.timer.cancel();
      _operations.remove(tag);
      onExecute();
    } else {
      _operations[tag]?.timer.cancel();

      _operations[tag] = _EasyDebounceOperation(
          onExecute,
          Timer(duration, () {
            _operations[tag]?.timer.cancel();
            _operations.remove(tag);

            onExecute();
          }));
    }
  }

 

 

동일한 요청(여기선 operations)이 여러 번 온다고 가정하자

else 아래 구문이 중요한데 이 코드를 해석하면,

기존에 동일한 operations 객체의 타이머가 존재한다면 이 타이머를 cancel 시키는 작업을 하고 다시 동일한 operations 객체에 타이머를 걸어 시간이 지나면 onExecute() 콜백을 실행한다는 뜻이다.

 

 

그러니까 결국은 여러 번 콜백이 오면 가장 마지막 요청만 실행한다는 뜻이 된다.

 

 

 

 

 

 

Debounce 를 잘못 썼던 경험

개인적인 의견일 수도 있다.

 

일단 내 상황을 소개하자면 회사 코드에 있는 일부 버튼들은 Debounce 가 적용되어 있다.

(단, Throttle 은 없었다.)

나는 단순히 마지막 요청만 처리한다는 것만 알고 짧은 시간 내에 오는 부적절한 중복 요청에는 문제가 없을 것이라 생각해서 Debounce 로 처리를 잘하지 않았나라고 생각했다.

 

 

 

물론 위 생각의 일부는 맞으나, 일반적인 버튼에서 Debounce 로직을 처리하는 것이 조금 적절하지 않은 요소가 있었는데

바로,

사용자가 요청을 했는데 Debounce 타이머가 만약 1초가 적용되어 있다고 가정한다면 이 요청은 1초 뒤에 Request 요청을 서버에 보내게 된다. 왜냐하면 마지막 요청임을 판별하는 기준이 1초 동안 이후 요청이 없으면 마지막 요청으로 정해지기 때문이다. 

 

 

그렇게 되면 사용자가 1초를 기다리는 것은 필수적이게 된다.

또한 1초가 지나 이 요청을 서버에 전송하게 되었는데 사용자가 그 때 버튼을 또 누르게 되면 이 요청은 1초 뒤 다시 전송하게 된다.

 

 

 

 

이러한 상황들이 일반적인 버튼에서는 Debounce 로직이 적용되는 것은 조금 맞지 않다고 판단했다.

그렇다면 Throttle 코드도 한번 살펴볼까?

 

 

 

 

 

 

 

Throttle 

 

typedef EasyThrottleCallback = void Function();

class _EasyThrottleOperation {
  EasyThrottleCallback callback;
  EasyThrottleCallback? onAfter;
  Timer timer;

  _EasyThrottleOperation(
    this.callback,
    this.timer, {
    this.onAfter,
  });
}

class EasyThrottle {
  static Map<String, _EasyThrottleOperation> _operations = {};

  static bool throttle(
    String tag,
    Duration duration,
    EasyThrottleCallback onExecute, {
    EasyThrottleCallback? onAfter,
  }) {
    var throttled = _operations.containsKey(tag);
    if (throttled) {
      return true;
    }

    _operations[tag] = _EasyThrottleOperation(
      onExecute,
      Timer(duration, () {
        _operations[tag]?.timer.cancel();
        _EasyThrottleOperation? removed = _operations.remove(tag);

        removed?.onAfter?.call();
      }),
      onAfter: onAfter,
    );

    onExecute(); // Do!

    return false;
  }

 

 

위 코드를 해석하면,

operations 객체가 이미 존재한다면 return 을 한다.

operations 객체가 존재하지 않는다면 타이머가 있는 operations 객체를 생성하고 아래에 있는 Do! 가 먼저 실행한다.

시간이 지나 위 타이머가 실행되면 이 객체는 제거된다.

 

 

즉, 첫 요청을 먼저 처리하고 이후 요청은 일정 시간동안 무시된다.

 

 

 

그렇게 나는 Debounce 로직이 적용되어 있는 버튼을 일부 Throttle 로직으로 적용시켰고, 느낌상 기존보다 UI 가 좀 더 빨라진 것 같다는 느낌을 받았다!

(요청을 바로 서버에 전송하게 되니깐!)

 

 

 

 

 

 

 

Debounce 는 언제 적용하면 좋을까?

일반적인 요청은 Throttle 로 처리해서 적용한다고 치면 Debounce 는 언제 적용하면 될지 갑자기 궁금해졌다.

인터넷에 흔한 사례로 보이는 검색창에 입력할 때 Debounce 를 적용시키는 것은 다들 알 것이다.

 

 

 

일단 나는 회사 앱에 만보기 기능이 있는데 이때 걸음수를 서버 전송 시, Debounce 처리를 계속 해두었다.

즉, 걸음수를 UI 에 표시하는 것 자체는 바로 적용하도록 하고 이 걸음수를 서버에 Request 날릴 때는 6초 정도? Debounce 타이머를 두어 마지막 걸음수만 보내도록 처리했다. 

 

 

 

Debounce 를 적용할 때는 연속적인 요청에서 마지막 Request 가 중요할 때, Throttle 을 적용할 때는 연속적인 요청이 있을 수 있는 상황에서 첫번째 Request 가 중요할 때 쓰이면 좋지 않을까 하고 생각한다.

 

 

 

 

 

이번에 Debounce, Throttle 을 적용해보면서 느낀 점은 Debounce, Throttle 을 처리하는 기준에는 개발자가 판단하는 역량에 있지 않나 생각이 든다.

요청을 곧이 곧대로 처리하지 않고 개발자가 판단함에 따라 서버 요청을 줄일 수도(= 비용을 절감하는 효과) 사용자의 요청을 적절하게 처리할 수도 있다.

 

 

 

역시 개발 지식은 많으면 많을수록 시스템이 견고해진다는 사실을 다시끔 깨달으며 이번 포스팅을 마무리한다!