ET네 만물상 프로젝트 복기 - 커스텀 Hooks

DD·2021년 9월 6일
0

우아한 테크캠프

목록 보기
9/14
post-thumbnail

📣 이 시리즈는...

  • 우아한 테크캠프 4기에서 마지막으로 진행한 프로젝트 전체를 복기해본 문서 시리즈입니다.
  • 제가 작성하지 않은 코드도 포함해서 복기했기에, 오류가 있을 수도 있는 개인적인 학습 기록을 위한 문서입니다!

ET네 만물상 - GitHub Repository / 배포 링크

  • 현재 배포 링크는 내부 문제로 API서버가 동작하지 않습니다.. 조만간 해결할 예정..



커스텀 Hooks

  • 프로젝트에서 사용했던 리액트 커스텀 훅입니다.
  • 제가 작성한 건 하나 뿐이지만, 팀원 분이 작성한 코드 공부용으로 작성 ^ㅆ^

🔨 useDebounce

import { useEffect, useState } from "react";

const DEFAULT_DELAY = 500;

export default <T>(value: T, delay: number = DEFAULT_DELAY) => {
  const [debouncedValue, setDebounceValue] = useState(value);

  useEffect(() => {
    const handler: NodeJS.Timeout = setTimeout(() => {
      setDebounceValue(value);
    }, delay);
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
};

사용목적

  • 디바운스를 구현하기 위한 훅

동작 원리

  • 디바운스는 같은 요청이 반복적으로 발생할 때, 그 간격이 지정한 시간보다 길 때 까지 요청을 유예하는 것"** 이라고 요약할 수 있다.

  • 이 useDebounce 외부에서 빠르게 변하는 값, 가령 클릭시 변경되는 값을 인자로 받는다. 함수 내부의 useEffect의 의존성 배열에 추가함으로써 해당 값이 변경되면 useEffect를 실행하도록 하며, useEffect가 실행되면 새로운 timer로 debounce값을 set하고, 기존 timer를 clear한다

  • 외부에서는 이 debouncedValue가 변경되면 감지하는 useEffect를 사용해서 debounce값이 변경되었을 때에만 의도한 동작을 수행하도록 하면 됨!

개선할 점

  • 이 경우 useEffect로 debouncedValue 값이 변경되었을 때 동작한다는 원리인데, 가장 큰 문제는 내가 의도한 동작(예를 들어 클릭)이 아닌데 지정값을 변경시키는 경우가 있다면 의도치 않게 useEffect가 동작하곤 했다.

  • 특히 react-query에서 받아오는 값을 default 값으로 지정하려 할 때 문제가 생겼다.

//1
const {
  status,
  data: product,
  error,
  refetch,
} = useProduct(parseInt(productId));
const [isMyWish, setIsMyWish] = useState(product?.isWish);
const debounceIsMyWish = useDebounce<boolean>(isMyWish, 300);

//2
const handleClickWish = async (e: Event) => {
  e.stopPropagation();
  if (!isLoggedin) {
    return;
  }
  setIsMyWish((isMyWish) => !isMyWish);
};

//3
useEffect(() => {
  if (product) {
    setIsMyWish(product?.isWish);
  }
}, [product]);

//4
useDidMountEffect(async () => {
  if (debounceIsMyWish !== product.isWish) {
    debounceIsMyWish
      ? await postWishProduct(product.id)
      : await deleteWishProduct(product.id);
  }
}, [debounceIsMyWish]);
  • react-query는 초기에 loading status, 데이터를 받아오면 success status가 되어 자동으로 컴포넌트를 최소 1번 리렌더시킨다. useProduct로 가져온 product 값이 초기엔 undefined이기 때문에 product안에 있는 isWish 값을 default로하는 값을 상태로 관리하고 싶다면, //3 에 있는 useEffect로 초기화를 해줘야한다.

  • 이 때, debounceIsMyWish도 초기 undefined에서 product.isWish 값으로 변경됨으로 인지되기 때문에 //4 의 useDidMountEffect가 실행되어버린다

  • 참고로 useDidMountEffect는 첫 렌더링을 무시하는 useEffect 커스텀 hooks이다.

  • 실제 프로젝트에서는 조건문을 추가해서 클릭이 아닌 페이지 렌더시에 API 요청이 수행되는 걸 막았으나, 좀 더 개선이 필요해보인다.




🔨 useThrottle

import { useEffect, useState } from "react";

const DEFAULT_DELAY = 500;

export default <T>(value: T, delay: number = DEFAULT_DELAY) => {
  const [throttledValue, setThrottleValue] = useState(value);
  const [isWaiting, setIsWaiting] = useState(false);

  useEffect(() => {
    if (!isWaiting) {
      const handler: NodeJS.Timeout = setTimeout(() => {
        setIsWaiting(false);
        clearTimeout(handler);
      }, delay);

      setThrottleValue(value);
      setIsWaiting(true);
    }
  }, [value]);

  return throttledValue;
};

사용목적

  • 쓰로틀링을 구현하기 위한 훅

동작원리

  • 쓰로틀링은 디바운스와 비슷하면서도 다른데, 빠르게 반복되는 요청 중 지정한 시간에 최소 1번은 실행하는 것이다 .

  • useDebounce와 마찬가지로 외부에서 변경되는 값을 받아서 자체 state인 throttledValue를 생성하고, useEffect로 관리한다.

  • 차이점은 isWaiting이라는 boolean 상태가 추가되어 지정한 시간이 될 때 마다 isWaiting이 true일 때만 throttledValue를 변경한다.

개선사항

  • 사용하면서 문제가 발생하진 않았지만 useDebounce와 같은 원리이기 때문에 예상치 못한 변경에 대해 문제가 발생할 수 있다.
const AutoList = ({ keyword, handleSearch }) => {
  const handleClick = (v) => {
    handleSearch(v);
  };
const throttledSearchInput = useThrottle<string>(keyword, 200);
  const { data: autoList, status } = useKeywords(throttledSearchInput);
  ...
  return
}
  • useDebounce 설명에서는 react-query와 충돌이 발생할 수 있다 설명했지만, 반대로 hooks의 값을 useQuery의 인자로 사용할 때에는 아주 찰떡인 거 같다

  • useQuery가 해당파라미터를 key로 가지고 있기 때문에 캐싱처리가 되기 때문이다! throttledValue가 변하지 않는다면 useQuery는 캐싱된 값을 반환할 것이며 불필요한 요청을 하지 않는다.




🔨 useInput

export type InputType = {
  value: string;
  onChange: ({
    target,
  }: {
    target: HTMLInputElement | HTMLTextAreaElement;
  }) => void;
  setValue: React.Dispatch<React.SetStateAction<string>>;
};

export default (
  defaultValue: string,
  filter?: (text: string) => string
): InputType => {
  const [value, setValue] = useState(
    filter ? filter(defaultValue) : defaultValue
  );

  const onChange = ({
    target,
  }: {
    target: HTMLInputElement | HTMLTextAreaElement;
  }) => {
    const { value } = target;
    setValue(filter ? filter(target.value) : value);
  };

  return { value, onChange, setValue };
};

사용목적

  • input의 값과 변화를 하나의 객체에서 관리하기 위한 훅

동작원리

  • default값을 기반으로 상태로 관리하며 이벤트 객체에서 target.value를 추출, setState한다.

  • input tag의 value값과 onChange 속성에 넣어두어 사용할 수 있다.

 const addressDetail = useInput(defaultAddress?.detailAddress ?? "");

 ...

 <input
        placeholder="상세주소 입력"
        defaultValue={addressDetail.value}
        disabled={!address.address}
        onChange={addressDetail.onChange}
  />



🔨 useValidation

import { useState } from "react";

export type ValidationType = {
  isValid: boolean;
  onCheck: (input: string) => void;
  setIsValid: (boolean) => void;
};

export default (
  checkValidation: (input: string) => boolean,
  defaultValue: boolean = null
): ValidationType => {
  const [isValid, setIsValid] = useState<boolean>(defaultValue);

  const onCheck = (input: string) => {
    setIsValid(checkValidation(input));
  };

  return { isValid, onCheck, setIsValid };
};

사용목적

  • 유효성 검사 대상과, 검사 로직을 합쳐 하나의 객체로 관리할 수 있도록 만들기 위한 훅

동작원리

  • 유효성 검사 대상 값과, 유효성 검사 함수를 전달 받은 후 유효성 boolean / 체크 함수 등을 반환한다.

  • 어디서든 원하는 시점에 check를 하고, isValid로 대상이 유효한지 판단할 수 있다

  • 강제로 유효성 값을 변경시킬 수도 있다!




🔨 useLazyLoad

import { useEffect, useRef } from "react";

const THRESHOLD = 0.05;

export default (action: () => void, threshold: number = THRESHOLD) => {
  const ref = useRef(null);

  useEffect(() => {
    if (ref.current) {
      const observer = new IntersectionObserver(
        (entries) => {
          entries.forEach((entry) => {
            if (entry.isIntersecting && ref.current) {
              observer.unobserve(ref.current);
              action();
            }
          });
        },
        { threshold }
      );
      observer.observe(ref.current);

      return () => observer.disconnect();
    }
  }, [ref.current]);
  return { ref };
};

사용목적

  • 무한스크롤을 구현하기 위해 viewport에 나타남을 감지할 DOM을 지정할 ref를 관리하기 위한 훅

동작원리

  • intersectionObserver를 사용한다.

  • viewport에 나타나는 걸 감지할 dom이 필요한데, useRef를 사용한다.

  • intersection이 감지되면 실행할 함수를 전달받고, 감지 대상인 Dom에 지정할 ref를 반환해서 외부에서 해당 반환값을 대상 dom의 ref속성에 넣으면 된다!




🔨 useDidMountEffect

import { useEffect, useRef } from "react";

export default (func, deps) => {
  const didMount = useRef(false);

  useEffect(() => {
    if (didMount.current) func();
    else didMount.current = true;
  }, deps);
};

사용목적

  • 단순하게, 첫 번째 렌더링을 건너뛰고 두 번째 렌더링부터 동작하는 useEffect을 구현하기 위한 훅

동작원리

  • ref를 사용해서 변경되어도 리렌더를 일으키지 않는 값으로 boolean을 설정한다

  • 첫 렌더 때만 default 값인 false로 useEffect 동작을 무시하면서 ref값을 true로 바꾼다

  • 두 번째 렌더부터는 useEffect가 계속 동작한다.

profile
기억보단 기록을 / TIL 전용 => https://velog.io/@jjuny546

0개의 댓글