debounce 개조하기

In9_9yu·2023년 5월 24일
0

🎯 목표

닉네임 중복을 체크하는 api 연결하기 이슈를 해결하면서 겪은 문제입니다.

해당 글의 목표는 비동기 함수를 debounce 로 감싼 경우, 값을 반환 하도록 개조하는 것입니다.

🤯 문제점

기존 debounce 코드

export const debounce = (func) => {
  let timer;

  return () => {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(func, 200);
  };
};

클로저를 활용한 기가막히고 코가막히는 debounce 함수

위의 debounce 함수는 3가지의 단점이 있습니다.

  1. setTimeout의 delay가 200으로 고정되어 있는 문제
  2. func 인자로 받는 함수가 인자가 있는 경우, 인자를 받지 못함
  3. func가 값을 반환하고, 그 값이 필요한 경우 반환받을 수 없음

🥳 해결 방법

1번 문제를 해결하는 방법

간단하게 debounce 함수의 인자로 delay를 하나 더 받으면 됩니다.

export const debounce = (func,delay) => {
  let timer;
  return () => {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(func, delay);
  };
};

const debouncedFunc = debounc(()=>{...},200) 

만약 기존에 쓰던 debounce 함수가 너무 많아서, 하나하나 delay 값을 적어주기 귀찮다면, Default parameters 를 이용해서 다음과 같이 작성하면 됩니다.

export const debounce = (func, delay=200) => {
  let timer;
  return () => {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(func, delay);
  };
};

// 기존에 사용하는 debounce 함수에 따로 delay를 전달하지 않아도 동작합니다.
const debouncedFunc = debounc(()=>{...}) 

2번 문제를 해결하는 방법

링크로 걸어놓았던, 실제로 겪은 문제를 예로 들어보겠습니다.

사용자에게 닉네임을 입력받으면, 해당 닉네임이 중복되는지 여부를 체크하는 경우입니다. (react-hook-form을 사용하였습니다.)

const handleValidateNickname = debounce((nickname)=>isUsableNickname(nickname))

<input {...register('nickname',{
	validate: {
      isUsableNickname: async(nickname) => (await handleValidateNickname(nickname)) 
      || '이미 존재하는 닉네임입니다.'
})/>

해당 코드를 잠시 설명하면, 사용자가 닉네임을 입력할 때마다 validate 내에 있는 isUsableNickname 함수를 실행시키게 됩니다.

handleValidateNickname의 값이 false 인 경우에는 이미 존재하는 닉네임입니다 라는 에러메시지가 나타나게 됩니다.

결과는 어떻게 될까요?
사용자가 어떠한 값을 입력하더라도 항상 에러메시지가 발생하게 될 것입니다.

handleVlidateNickname의 결과를 보시면 이해가 됩니다.

// A
const handleValidateNickname = debounce((nickname)=>isUsableNickname(nickname))

// B
const handleValidateNickname = () => {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout((nickname)=>isUsableNickname(nickname), delay);
  };

A와 B는 같은 함수입니다. 이해를 돕기 위해 debounce의 결과를 풀어놓은 것 뿐입니다.

보면, nickname을 알 방법이 없습니다. 그리고 애초에 명시적으로 값을 반환하고 있지 않으므로, 암시적으로 undefined 를 반환하게 됩니다.

undefined는 falsy한 값이므로 이미 존재하는 닉네임입니다 가 나타나게 됩니다.

이 문제는 debounce를 반환하는 함수의 인자에 나머지 매개변수 를 사용해주면 됩니다.

export const debounce = (func, time = 200) => {
  let timer;

  return (...args) => {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => func(...args), time);
  };
};

이렇게 작성해주면 debounce로 감싼 함수에 인자를 전달할 수 있게 됩니다.

3번 문제를 해결하는 방법

마지막으로, debounce로 감싼 함수가 값을 반환하는 경우 어떻게 그 값을 바깥으로 전달할 수 있을까요?

가장 문제가 되는 점은 우리가 값을 반환받기 위해 실행하는 함수가 setTimeout 의 콜백함수로 존재한다는 점일 것입니다.

export const debounce = (func, time = 200) => {
  let timer;

  return (...args) => {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => func(...args) <- '이걸 어떻게 값을 return 하지?', time);
  };
};

지난번 글이었던 await이 함수를 멈추는 방법의 내용에서 다루었던 Promise 에서 아이디어를 얻을 수 있습니다.

현재 debounce에서 단순히 함수를 반환하는 것을, Promise 를 반환하는 함수로 바꾸고, 그와 동시에 setTimeout 내부에서 resolve 해주면, debounce로 감싼 비동기 함수의 값도 받아올 수 있습니다.

(아래 코드에서는 성공하는 경우만 다루었습니다.)

export const debounce = (func, time = 200) => {
  let timer;

  return (...args) => {
    if (timer) {
      clearTimeout(timer);
    }

    return new Promise((resolve) => {
      timer = setTimeout(async () => {
        const result = await func(...args);
        resolve(result);
      }, time);
    });
  };
};

그럼 위에서 작성했던 다음과 같은 코드를 원하는 대로 동작시킬 수 있게 됩니다.

const handleValidateNickname = debounce((nickname)=>isUsableNickname(nickname))

<input {...register('nickname',{
	validate: {
      isUsableNickname: async(nickname) => (await handleValidateNickname(nickname)) 
      || '이미 존재하는 닉네임입니다.'
})/>

결론

사실 이 방법이 반드시 정답이다라고는 못하겠습니다 ㅎㅎ;
요런 방식으로도 해결은 되는구나 정도로 참고해주시면 감사하겠습니다 :)

profile
FE 임니다

0개의 댓글