고윤태의 개발 블로그

누구나 접근하기 쉽지만 얻어 가는 것이 있는 글을 쓰기 위해 노력 중입니다.

개발

useDebounce를 활용하기

고윤태 2023. 6. 7. 01:23

개요

안녕하세요 제가 이번에 작성할 내용은 useDebounce라는 custom hook은 어떤 상황에서 어떤 식으로 활용했는지에 작성해 보겠습니다.


개발 환경

React-native(expo) + TypeScript로 진행되었습니다.


상황 설명

이번에 진행 중인 토이 프로젝트에서 아래 사진과 같은 Ui가 존재합니다.

 

화면

첫 화면
flow


Flow 설명

처음에 GPS를 활용하여 주변 동네에 대한 정보를 받고 사용자가 Text를 입력하게 된다면 해당 입력한 값으로 검색 결과를 보여주는 화면입니다.


개발 계획

 

위의 Flow대로 개발을 진행하기로 결정했습니다.
여기서 사용자가 주소 검색 시 디바운싱을 적용하기로 했습니다.

 

디바운싱이란?
연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것

디바운싱을 사용해야 하는 이유

 

요즘 서비스들은 검색어 치자마자 엔터 없이도 결과가 바로바로 나옵니다.
만약에 '고윤태'를 검색창에 입력한다면
엔터 없이도 결과를 즉시 보여주려면 항상 input 이벤트에 대기를 하고 있어야 합니다.

디바운싱을 사용하지 않는다면 한 글자를 입력할 때마다 API 요청이 실행된다는 것입니다.
'ㄱ', '고', '공', '고윤' 모두 요청이 실행됩니다.
사용자가 입력하고 싶던 것은 '고 윤태' 였고 'ㄱ', '고', '공', '고윤' 은 제대로 된 검색어가 나오지 않을 수도 있습니다.

'고윤태'를 입력하는 동안 요청된 API 횟수는 만약 유료 API를 사용했을 때 큰 문제가 됩니다.
디바운싱은 비용적인 문제와도 관련이 있습니다.

저는 사용자가 '고윤태'를 다 쳤을 때 API 요청을 보내고 싶습니다. 이런 경우에 사용하는 게 디바운싱입니다.


useDebounce

import { useEffect, useState } from 'react';

const useDebounce = <T>(value: T, delay?: number): T => {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay || 500);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
};

export default useDebounce;

코드 설명

 

setTimeout을 사용하여 특정 시간이 지난 후 전달받은 value의 값을 변경합니다.
useEffect 내부에서 return을 하게 되면 컴포넌트가 제거될 때 해당 코드들이 실행됩니다(clean-up).
위의 코드에서는 제거될 때 clearTimeout을 활용하여 setTimeout의 시간을 초기화 시킵니다.
useEffect의 두 번째 인자로 배열을 전달하게 되면 배열에 포함된 값이 변할 때만 useEffect를 호출하게 됩니다. 위에서는 value 값이 변할 때만 useEffect를 호출하게 됩니다.


사용 예시

import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import InputField from '../../components/ui/inputs/InputField';
import useDebounce from '../../hooks/useDebounce';

const Test = () => {
  const [testValue, setTestValue] = useState<string>('');
  const debouncedValue = useDebounce<string>(testValue, 600);

  useEffect(() => {
    if (debouncedValue) {
      console.log(debouncedValue);
    }
  }, [debouncedValue]);

  return (
    <View>
      <InputField value={testValue} onChangeHandler={(value => {
        setTestValue(value);
      })}/>
    </View>
  );
};
export default Test;

구현

사용자에 위치 받아오기

  const [location, setLocaion] = useState<LocationType | null>(null);
  
  useEffect(() => {
    const getLocation = async () => {
      try {
        const response = await Location.requestForegroundPermissionsAsync();

        if (response.status === 'granted') {
          const location = await Location.getCurrentPositionAsync();

          setLocaion({
            longitude: location.coords.longitude,
            latitude: location.coords.latitude,
          });
        } else {
          // 권한이 거부되었을 때 처리할 내용 추가
        }
      } catch (error) {
        setIsPermissionError(true);
        console.log(error);
      }
    };

    getLocation();
  }, []);

 

 

LocationType

export interface Location {
  latitude: number;
  longitude: number;
}

 

근처 동네 받아오기(API)

  const [nearbyAddressList, setNearbyAddressList] = useState<Address[]>([]);
  
  const getNearbyAddress = async () => {
    try {
      const {
        data: { data },
      } = await axios.get(
        `/address/nearby?longitude=${location?.longitude}&latitude=${location?.latitude}`
      );

      setNearbyAddressList(data);
    } catch (error) {
    	//에러 처리
    }
  };

  useEffect(() => {
    if (location) {
      getNearbyAddress();
    }
  }, [location]);

AddressType

interface Address extends LocationType {
  address: string;
}

사용자에 입력에 대한 주소 검색하기

  const [searchAddressList, setSearchAddressList] = useState<Address[]>([]);
  const [search, setSearch] = useState<string>('');
  const debouncedValue = useDebounce<string>(search, 600);
  const timeStamp = useRef('');
  const [isLoading, setIsLoading] = useRecoilState(LoadingState);

  const getSearchAddress = async () => {
    setIsLoading(true);
    try {
      const {
        data: { data, dataTimestamp },
      } = await axios.get(`/address?address=${search}`);
      if (dataTimestamp > timeStamp.current) {
        setSearchAddressList(data);
        timeStamp.current = dataTimestamp;
      }
    } catch (error) {
    } finally {
      setIsLoading(false);
    }
  };

  useEffect(() => {
    if (debouncedValue) {
      getSearchAddress();
    }
  }, [debouncedValue]);
//JSX

          <InputField
            onChangeHandler={(value: string) => {
              setSearch(value);
            }}
            isSearch={true}
            placeholder="동명(읍, 면)으로 검색 (ex. 신대방동)"
          />

 


코드 설명

 

사용자가 Input에 입력을 하면 search라는 state에 set을 합니다.
그 후 useDebounce를 통해 사용자가 입력이 멈췄을 때 디바운싱 된 debouncedValue 값이 바뀐 경우에 useEffect를 통해 값이 변경될 때마다 API 요청을 합니다.

timeStamp를 비교하는 코드는 이러한 이유 때문에 넣었습니다.

 

사용자가 "강남"을 입력하려는 도중 '강'을 친 뒤 멈췄다가 '남'을 추가로 입력하여 '강남'을 검색했습니다.
이런 경우 "강", "강남" 둘 다 요청이 갈 수 있습니다.
이유는 사용자가 "강"을 치고 잠깐 멈췄기 때문이죠
하필 운이 없게도 "강남"을 요청한 API보다 "강"을 요청한 API 요청에 대한 결과가 늦게 오게 된다면 사용자는 "강남"을 입력했음에도 "강"에 대한 검색 결과를 보게 될 수 있습니다.
이런 경우를 방지하기 위해 timestamp를 활용해 늦게 요청한 결과를 사용하기 위해 저런 코드를 작성했습니다.

 

여기서 timeStamp를 변수 또는 state를 사용하지 않은 이유는 ref는 변경된다 해서 리렌더링이 일어나지 않을뿐더러 리렌더링이 일어나도 값이 초기화가 되지 않기 때문입니다.

 


완성 결과


다른 곳에서 활용해보기

이번에 디바운싱을 활용할 곳은 사용자가 프로필을 생성할 때 닉네임을 검증하는 곳입니다.
사용자가 닉네임을 입력하면 API를 통해 사용이 가능한 닉네임인지 체크를 한 뒤 사용 가능 여부를 알려주는 곳입니다.

 


구현

 

코드

  const [nickname, setNickname] = useState<string>('');
  const debouncedValue = useDebounce<string>(nickname, 600);
  const [errorLog, setErrorLog] = useState({
    isError: false,
    message: '',
  });

  const checkDuplicateNickname = async () => {
    try {
      await axios.post('/auth/nickname', {
        nickname,
      });

      setErrorLog({ isError: false, message: '' });
    } catch (error) {
      const errorResponse = (error as AxiosError).response;

      if (errorResponse) {
        const { message } = errorResponse.data as { message: string };
        setErrorLog({ isError: true, message });
      }
    }
  };

  useEffect(() => {
    if (debouncedValue) {
      checkDuplicateNickname();
    }
  }, [debouncedValue]);

코드 설명

 

이전 주소 검색과 다를 부분이 없습니다. 디바운싱 된 값이 변경될 때마다 닉네임 검증 API를 요청하고
response를 활용하여 사용자에게 사용 가능 여부를 알려주고 있습니다.


완성 결과


후기

 

이로써 디바운싱을 활용하여 사용자가 입력을 할 때마다 API 요청이 아닌 입력이 끝난 뒤에 API를 요청하도록 구현했습니다.
예전에 처음 디바운싱을 개발할 때 당시에는 "디바운싱"이라는 키워드조차 몰라서 검색을 못 했던 기억이 있네요

"입력 끝나면 api 요청" 이런 식으로 검색했던 것 같습니다.
그러던 도중 운이 좋게도 "디바운싱"이라는 키워드를 발견했었습니다.
지금에서야 키워드를 알고 있는 상태고 구현해 본 경험이 있어서 구현하기 수월했습니다.
검색도 실력이니 검색을 하기 위한 공부를 요즘 좀 느슨해진 경향이 있어서 다 잡는 좋은 경험이 된 것 같습니다.