닉네임 중복을 체크하는 api 연결하기 이슈를 해결하면서 겪은 문제입니다.
해당 글의 목표는 비동기 함수를 debounce 로 감싼 경우, 값을 반환 하도록 개조하는 것입니다.
export const debounce = (func) => {
let timer;
return () => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(func, 200);
};
};
클로저를 활용한 기가막히고 코가막히는 debounce 함수
위의 debounce 함수는 3가지의 단점이 있습니다.
간단하게 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(()=>{...})
링크로 걸어놓았던, 실제로 겪은 문제를 예로 들어보겠습니다.
사용자에게 닉네임을 입력받으면, 해당 닉네임이 중복되는지 여부를 체크하는 경우입니다. (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로 감싼 함수에 인자를 전달할 수 있게 됩니다.
마지막으로, 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))
|| '이미 존재하는 닉네임입니다.'
})/>
사실 이 방법이 반드시 정답이다라고는 못하겠습니다 ㅎㅎ;
요런 방식으로도 해결은 되는구나 정도로 참고해주시면 감사하겠습니다 :)