문제상황
버튼 클릭 시 네트워크 탭에서 확인해봤을때 불필요한 요청이 여러번 가고 있었던 상황이다.
해결과정
debounce / throttle 의 한계
debounce 는 정해진 시간동안 발생한 여러 이벤트 중, 앞(leading) 혹은 뒤(trailing)에 하나의 이벤트만 트리거 시키는 방법입니다.
Debounce를 react 에 적용한 코드입니다. debounce 는 import debounce from "lodash.debounce 를 가지고 왔습니다. Button 컴포넌트의 click handler에 debounce를 래핑한 컴포넌트 입니다. debounce 의 속성 중, leading 을 사용합니다. 사용자의 반응에 즉각적으로 이벤트를 발생시키기 위함입니다.
import debounce from "lodash.debounce";
import { useMemo } from "react";
export default function DebounceButton({
waitMS,
onClick,
}: {
waitMS: number;
onClick: () => void;
}) {
const handleClick = useMemo(() => {
return debounce(onClick, waitMS, { leading: true, trailing: false });
}, []);
return (
<button type="button" onClick={handleClick}>
DebounceButton
</button>
);
}
debounce 를 활용한 방식은 해피 케이스에는 문제가 없을 것입니다. 여기서 고민이 되는 부분은 waitMS 를 얼마로 설정할 것이냐 입니다. 상황에 따라 직관으로 정할 순 없는데.. API latency는 서버, DB 상황에 따라서 언제나 달라질 수 있습니다. 이를 고정한다는 것은 엄밀하지 않은 사고입니다.
debounce 를 사용하면, 2가지 케이스가 발생한다는 것을 알 수 있습니다.
1. api latency < debounce wait
1번 케이스를 보겠습니다. api가 빠르게 응답이 온다면, 보통의 경우에 큰 문제가 없습니다.
문제가 없는 경우는, API 가 성공했을 때입니다. API 가 성공해서 다음 유저 플로우를 타게 된다면, button disabled 시간(debounce wait - api latency)이 존재해도 문제가 되지 않습니다.
API 가 실패하면 어떻게 될까요? 일정 시간(debounce wait - api latency) 만큼, 사용자는 버튼을 다시 누르지 못 합니다. 그러므로 중복호출을 막기 위한 목적으로 함부로 debounce wait 를 길게 해서는 안 됩니다.
2. api latency > debounce wait
2번 케이스를 보겠습니다. api latency 가 더 길다면, 정말로 문제 입니다. debounce wait 가 끝난다면, 다시 clickable 한 상태가 되고, 서버에 중복호출을 할 수 있습니다. 이때는 서버에서 중복호출을 막고 있길 바래야될 것 입니다.
정리하자면, api latency 과 debounce wait 가 차이가 있기 때문에, debounce로 완벽한 중복호출을 막는 것은 본질적으로 불가능하다는 것입니다. (throttle 도 마찬가지 논리이므로 생략)
isLoading 상태
handler가 호출되면, isLoading 상태를 true로 만들고, promise 가 settled 되면, isLoading 을 다시 false로 되돌립니다.
function LoadingStateButton({ onClick }: { onClick: () => Promise<void> }) {
const [isLoading, setIsLoading] = useState(false);
return (
<button
type="button"
onClick={async () => {
if (isLoading) {
return;
}
setIsLoading(true);
await onClick();
setIsLoading(false);
}}
>
Button
</button>
);
}
위 코드가 문제가 없어보일 수 있습니다. 하지만 setState의 본질을 이해하면, 이는 문제가 됨을 짐작할 수 있습니다. react state는 성능 관리를 위해서, 일정 요청을 모아서 batch 로 상태를 업데이트 합니다. 즉, setState는 비동기 요청입니다.
위 코드를 예시로 설명하면, setIsLoading(true) 는 호출되는 시점에 바로 isLoading 상태를 수정하지 않습니다. 조건이 충족되었을 때 비동기로 update 합니다. 즉, 그 transition 기간 동안, click이 한번 더 발생한다면, 중복호출이 발생할 수 있습니다. 저는 실제로 이 방식을 활용하여 많은 중복호출 에러를 맞았습니다.
그러므로 isLoading 상태를 활용한 해결책도 근본적으로 중복호출을 막아주지 못 합니다.
useRef
react에서 즉각적인 변수값 변경을 위해서 사용하는 것은 useRef 입니다. Ref 값은 상태는 아니기 때문에 re-render는 발생시키지 않으면서, 참조값을 즉시 변경시킵니다.
function LoadingRefButton({ onClick }: { onClick: () => Promise<void> }) {
const buttonRef = useRef(false);
const reRender = useReRenderer();
return (
<button
type="button"
disabled={buttonRef.current}
onClick={async () => {
if (buttonRef.current) {
return;
}
buttonRef.current = true;
reRender();
await onClick();
buttonRef.current = false;
}}
>
Button
</button>
);
}
isLoading 을 useRef로 선언하여, onClick 의 중복호출을 막고 있습니다!!!
이제는 React 19로 업데이트 되면서 ref를 사용하지 않고 useTransition를 사용해서 해봐도 좋을 것 같다 !
'트러블슈팅' 카테고리의 다른 글
[Chart.js] Canvas is already in use. Chart with ID '0' must be destroyed before the canvas with ID 'myChart' can be reused. (0) | 2024.08.05 |
---|---|
TypeError: Cannot read properties of undefined (reading 'length') (0) | 2024.08.01 |
CORS Error를 만나다 (0) | 2024.07.25 |
Next.js에서 middleware 설정 (0) | 2024.07.12 |
React-hook-form value 값 설정 (0) | 2024.07.11 |