throttle & debounce ํด๋ฆฌํ•„

throttle๊ณผ debounce๋Š” ์ด๋ฒคํŠธํ•ธ๋“ค๋ง์— ๋นผ๋†“์„ ์ˆ˜ ์—†๋Š” ์š”์†Œ์ด๋‹ค. ๊ฒ€์ƒ‰๊ธฐ๋Šฅ์ด๋‚˜ ์Šคํฌ๋กค ์ด๋ฒคํŠธ์™€ ๊ฐ™์ด ์ˆ˜์‹ญ๋ฒˆ์˜ ์ฝœ๋ฐฑ์ด ๋‹จ ์‹œ๊ฐ„์— ํ˜ธ์ถœ๋˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ํ•ธ๋“ค๋งํ• ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค. ์ฃผ๋กœ lodash์—์„œ ๋ถˆ๋Ÿฌ์™€ ์‚ฌ์šฉํ•˜๊ณค ํ–ˆ์ง€๋งŒ, ๊ฐœ๋…์„ ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•ด ์ง์ ‘ ๋งŒ๋“ค์–ด ๋ณด์•˜๋‹ค. lodash์—๋Š” tailing๊ณผย leading ์˜ต์…˜์ด ์žˆ์ง€๋งŒ ์—ฌ๊ธฐ์„œ๋Š” ์ƒ๋žตํ•˜์˜€๋‹ค. (์‚ฌ์‹ค ์žˆ์–ด๋„ false๋กœ ํ•ด๋†จ๋˜ ์ ์ด ๋Œ€๋ถ€๋ถ„์ด๋ผ..)


throttle

throttle์€ ์„ค์ •ํ•œ ์‹œ๊ฐ„(delay)์— ํ•œ๋ฒˆ์”ฉ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. ๋งŒ์•ฝ ์Šคํฌ๋กค ์ด๋ฒคํŠธ์˜ ์ฝœ๋ฐฑ์„ throttle ํ•จ์ˆ˜๋กœ ๊ฐ์‹ธ๊ณ  delay๋ฅผ 1์ดˆ๋กœ ์„ค์ •ํ–ˆ์„๋•Œ, ์Šคํฌ๋กค์„ 5์ดˆ๋™์•ˆ ๋Š์ž„์—†์ด ์ด๋™ํ–ˆ๋‹ค๋ฉด, ์ฝœ๋ฐฑ์€ 5๋ฒˆ ํ˜ธ์ถœ๋œ๋‹ค.

function throttle(fn, delay) {
  var timer = null;
  return function () {
    var context = this;
    var args = arguments;
    if (!timer) {
      timer = setTimeout(function() {
        fn.apply(context, args);
        timer = null;
      }, delay);
    }
  };
}

debounce

dobouce๋Š” ์ด๋ฒคํŠธ๊ฐ€ ๋๋‚œ๋’ค ์„ค์ •ํ•œ ์‹œ๊ฐ„(delay)์ด ์ง€๋‚˜์•ผ ์ฝœ๋ฐฑ์ด ์‹คํ–‰๋œ๋‹ค. throttle๊ณผ ๋น„์Šทํ•ด๋ณด์ด๋Š”๋ฐ ๊ฒฐ์ •์ ์œผ๋กœ ๋‹ค๋ฅธ์ ์ด ์žˆ๋‹ค. debounce๋Š” ์ด๋ฒคํŠธ๋ฅผ ๊ทธ๋ฃนํ™” ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๋งŒ์•ฝ ์Šคํฌ๋กค ์ด๋ฒคํŠธ์˜ ์ฝœ๋ฐฑ์„ debounce๋กœ ๊ฐ์‹ธ๊ณ  delay๋ฅผ 1์ดˆ๋กœ ์„ค์ •ํ–ˆ์„๋•Œ, ์Šคํฌ๋กค์„ 5์ดˆ๋™์•ˆ ๋Š์ž„์—†์ด ์ด๋™ํ–ˆ๋‹ค๋ฉด, ์ฝœ๋ฐฑ์€ 1๋ฒˆ ํ˜ธ์ถœ๋œ๋‹ค. ์Šคํฌ๋กค์„ ๋ฉˆ์ถ˜ ๋’ค 1์ดˆ์•ˆ์— ๋‹ค์‹œ ์Šคํฌ๋กค์„ ์›€์ง์ด๋ฉด ์ฝœ๋ฐฑ์ด ์ผ์–ด๋‚˜์ง€ ์•Š๊ณ , 1์ดˆ๊ฐ€ ์ง€๋‚˜๋ฉด ์ฝœ๋ฐฑ ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค.

function debounce(fn, delay) {
  var timer;
  return function() {
    var context = this;
    var args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function(){
      fn.apply(context, args);
    }, delay);
  }
}

โ˜ฏ๏ธ useDebounce

useDebounce๋Š” ๋ฆฌ์•กํŠธ16.9 ๋ฒ„์ „์— ์ถ”๊ฐ€๋œ useState์™€ useEffect๋ฅผ ์‚ฌ์šฉํ•ด state์˜ ๋ณ€ํ™”๋ฅผ debouncingํ•˜๋Š” ์ปค์Šคํ…€ Hooks์ด๋‹ค.

import { useState, useEffect } from 'react';

export function useDebounce(value, delay) {
  // ๋””๋ฐ”์šด์Šค ํ•  ๊ฐ’์„ ๊ด€๋ฆฌํ•˜๊ธฐ์œ„ํ•œ ์ƒํƒœ๊ฐ’๊ณผ setterํ•จ์ˆ˜
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // ๋”œ๋ ˆ์ด ์ดํ›„ ๊ฐ’์„ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // ๋”œ๋ ˆ์ด ๊ธฐ๊ฐ„์ค‘์— valueํ˜น์€ delay๊ฐ’์ด ์—…๋ฐ์ดํŠธ ๋˜์—ˆ๋‹ค๋ฉด ์ด(cleanup)ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
    return () => {
      clearTimeout(timer);
    };
  },[value, delay]); // delay๊ฐ’์ด๋‚˜ value๊ฐ’์ด ์—…๋ฐ์ดํŠธ ๋˜์—ˆ๋‹ค๋ฉด ๋‹ค์‹œ ํ˜ธ์ถœํ•œ๋‹ค.

  return debouncedValue;
}

useDebounce๋ฅผ ์‚ฌ์šฉํ•œ SearchBar ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. keyword ์ƒํƒœ๊ฐ’์ด input์„ ํ†ตํ•ด ๊ณ„์† ๋ณ€๊ฒฝ๋˜์–ด useDebounce๋‚ด์˜ value๊ฐ’์ด ๊ณ„์† ์—…๋ฐ์ดํŠธ ๋˜๋ฉด์„œ useEffect์˜ ์ฝœ๋ฐฑํ•จ์ˆ˜๊ฐ€ ๊ณ„์† ์‹คํ–‰๋œ๋‹ค. ์ด๋•Œ ์ฝœ๋ฐฑํ•จ์ˆ˜์˜ return๊ฐ’์œผ๋กœ clearTimeoutํ•จ์ˆ˜๋ฅผ ๋„ฃ์–ด์ค˜ ์ฝœ๋ฐฑ์ด ์‹คํ–‰๋˜๊ธฐ ์ง์ „์— ๋งค๋ฒˆ timer๋ฅผ ํด๋ฆฌ์–ดํ•œ๋‹ค.

import React, { useState, useEffect } from 'react';
import { useDebounce } from 'client/hooks';

const SearchBar = props => {
  const [keyword, setKeyword] = useState('');
  const [canSearch, setCanSearch] = useState(false);
  const debouncedKeyword = useDebounce(keyword, 250);

  useEffect(() => {
    if(debouncedKeyword && canSearch){
      searchApi(debouncedKeyword);
    }
  }, [debouncedKeyword]);
}

์ฐธ์กฐ