TIL | API 호출 수를 줄여보자

연정·2023년 3월 26일
0

STUDY

목록 보기
4/4
post-thumbnail

얼마 전, 개발자로써 내가 어느 정도에 와있는지 확인하는 차원에서 면접을 하나 보고 왔다.
결과는 좋지 않았지만, 그 과정에서 무엇이 부족하고 어떤 방향으로 나아가야 하는지 깨달았다는게 아주아주 큰 소득이었다.
몰랐다면 알면 되고, 부족하다면 채워가면 되지 무엇이 문제!! :)

🏅 이 글의 목표!

↓↓↓ 아래의 질문에 명쾌하게 대답하기! ↓↓↓

"input에 입력값이 들어올 때마다 서버 호출을 한다면 부담이 되겠죠? 그렇다면 어떻게 하면 될까요?"

Debounce & Throttle

검색과 같이 API 호출이 반복적으로 이루어지는 상황에서 서버의 부담을 줄여주는 방법을 검색해봤을 때,
Debounce와 Throttle이 가장 먼저 그리고 많이 등장했다.

먼저 Debounce나 Throttle을 적용하지 않은 상태를 확인해보자.
사용자가 인풋에 값을 입력하거나 삭제하는 모든 순간에 counter의 값이 올라가는 걸 확인할 수 있다.

📌 Debounce

여러 번의 호출이 발생하면 기다렸다가 가장 마지막 호출의 n초 뒤에 실행하는 것

Debounce는 들어오는 이벤트들을 특정 시간동안 모아두었다가 한 번에 처리하는 개념이다.
더 자세한 설명은 코드를 통해 보도록 하자 :)

  let [requestCnt, setRequestCnt] = useState(0);
  let [searchValue, setSearchValue] = useState("");

  // 타이머를 생성하고 삭제하는 함수를 반환하는 debounce 함수
  const debounce = (callback, delay) => {
    let timer = null;
    
    return () => {
      if (timer) clearTimeout(timer);
      timer = setTimeout(() => callback(), delay);
    };
  };

  const sendRequest = () => {
    setRequestCnt(requestCnt + 1);
  };

  // 1. 입력값이 들어오면 debounceHandler 실행
  // 2. debounce 호출 & timer 생성 & 함수 반환
  // 3. 다음 입력값이 들어오면 기존 timer 값을 확인
  // 4. timer 값이 있으면 삭제 후 재할당
  // 5. setTimeout 종료 시까지 새로운 입력이 없으면 timer에 넘겨진 콜백함수 실행
  const debounceHandler = useCallback(debounce(sendRequest, 500), [requestCnt]);

  return (
    <div className="App">
      <div className="container">
        <div className="counter">counter : {requestCnt}</div>
        <input
          type="text"
          placeholder="값을 입력하세요"
          value={searchValue}
          onChange={(e) => {
            setSearchValue(e.target.value);
            debounceHandler();
          }}
        />
      </div>
    </div>
  );

❓그렇다면 여기서 왜 useCallback을 사용하는 걸까?

기본적으로 리액트의 컴포넌트는 state나 props의 변경이 있을 때 재랜더링을 한다.
그리고 함수형 컴포넌트 내의 함수는 컴포넌트가 재랜더링될 때 다시 생성된다.

위의 예시를 보면, 우리는 onChange 이벤트를 통해 입력값을 받아 state를 변경시키고 있다.
그렇기 때문에 입력값이 들어오는 매 순간 컴포넌트가 재랜더링하고, 함수 역시 다시 정의되는 것이다.
useCallback을 사용하지 않는다면, 입력값이 들어올 때마다 새로운 함수가 생성되어 원하지 않는 방식으로 동작하게 된다.

useCallback 없이 엉망진창으로 구현되는 사례

콜백함수를 메모이제이션하는 useCallback을 활용하여
의존성 배열에 있는 requestCnt가 변경되지 않는 한 debounce 함수가 유지되도록 하면 문제는 해결된다.
(함수가 새롭게 생성되지 않기 때문에 timer가 이전의 값을 기억)

useCallback

메모이제이션된 콜백을 반환한다.
그 메모이제이션된 버전은 콜백의 의존성이 변경되었을 때에만 변경된다.
useCallback은 불필요한 랜더링을 방지하기 위해 참조의 동일성에 의존적인 최적화된 자식 컴포넌트에 콜백을 전달할 때 유용하다.

쉬운 말로 풀어쓰자면,
의존 배열 안에 있는 값이 바뀌지 않는 한 콜백함수를 기억해두고 재사용한다는 말씀.

const memoizedCallback = useCallback(() => callbackFn(a,b), [a,b]);

출처 - react 공식 문서

↓ Debounce를 활용해 목표 달성⭐️

⭐️ 추가 참고 사항
만약 위의 코드 예시에서 debounce 함수 내의 callback 함수에 인자를 전달해야 한다면 아래와 같이 처리할 수 있다.

return (...arg) => {
   if(timer) clearTimeout(timer);
   timer = setTimeout(() => callback(...arg), delay);
}

여기서 arguments는 함수 객체의 프로퍼티로 함수 호출 시 전달된 인수들의 정보를 담고 있는 유사 배열 객체이다.


📌 Throttle

첫 번째 호출이 발생했을 때 실행하고, 그 이후 발생하는 이벤트는 n초 동안 무시하는 것

Throttle은 호출이 발생하면 그 뒤로 특정한 시간만큼 이벤트를 발생시키지 않는 개념이다.
Debounce가 마지막 호출 기준이라면, Throttle은 처음 호출을 기준으로 한다고 볼 수 있다.

Debounce : 마지막 호출 + n초
Throttle : 첫 번째 호출 + n초 + n초 뒤의 호출 + n초 ....

Debounce와 동일한 UI를 가지고 있는 코드에서 함수만 수정해보았다.

  let timer = null;

  const throttle = (callback, delay) => {
    return () => {
      // 타이머에 할당된 값이 falsy할 때만 콜백 함수 호출 및 타이머 값 할당
      if (!timer) {
        callback();

        timer = setTimeout(() => {
          clearTimeout(timer);
        }, delay);
      }
    };
  };

  const send = () => {
    setRequestCnt(requestCnt + 1);
  };

  const throttleHandler = useCallback(throttle(send, 2000), [requestCnt]);

구조적으로 debounce와 비슷한 코드인데 헤맸던 부분은 재랜더링에 따른 함수/변수 재할당으로 인해 발생한 이슈 때문이었다.
timer 변수를 컴포넌트 내부 혹은 throttle 함수 내부에 선언하게 되면 아래와 같이 timer 값이 휘발되어 버렸다.
이는 throttle 함수가 다른 동작보다 requestCnt 변경을 먼저 하게되면서 생기는 이슈로 파악된다.
timer 선언을 컴포넌트 바깥에 하거나, throttle 함수를 별도의 파일로 분리하면서 이슈를 해결할 수 있었다.

throttle 함수 내부 선언 시

1. throttle 함수 호출로 requestCnt가 변경된다.
2. useCallback의 dependency에 있는 값이 변경되었으므로 throttle 함수가 재생성된다.
3. 함수 내부에 있는 timer 값 역시 함께 리셋된다.

컴포넌트 내부 선언 시

1. throttle 함수 호출로 requestCnt가 변경된다.
2. 컴포넌트가 재랜더링되며 timer 변수가 재생성된다..

이 외에 React v.18에 새롭게 도입된 useTransition & useDeferedValue를 사용해볼 수 있지 않을까 생각했는데, 적용하다보니 지금의 상황에 적합한 훅은 아니라는 생각이 들었다.
분명 방법이 있을 듯도 한데, 이 부분은 해당 훅을 조금 더 깊게 공부하며 고민해봐야겠다.

profile
성장형 프론트엔드 개발자

0개의 댓글