Skip to content

fetch API 커스텀 훅 가이드

Howon Shin edited this page Dec 13, 2023 · 1 revision

safeFetch

사용 이유

fetch의 에러 처리 로직을 통합해서 서버 렌더링 오류를 처리하고자 했습니다.

사용법

인자 타입

export async function safeFetch<T>(
  this: any,
  baseUrlType: "backend" | "route",
  pathname?: string,
  options?: RequestInit,
  onSuccess?: (response?: Response) => void,
  onError?: (err?: Error) => void
)

T: api 응답 type, 현재 types/response.ts 에 저장 중

this: 내부에서 this를 활용하려면 명시적으로 인자에 할당해줘야 타입스크립트 오류를 방지, 실제 인수로 전달되지는 않음.

baseUrlType: base url이 백엔드인지 route handler인지 판단.

pathname: base url을 제외한 url 패스 경로. /~~ 형식 처럼 슬래시가 맨 앞에 있어야 함.

options: fetch의 옵션으로 사용하는 인자. mdn 이나 next.js의 fetch 공식 문서를 참조.

onSuccess: 요청 성공 시 실행될 함수

onError: 요청 실패 시 실행될 함수

반환 타입

export type CustomResponse<T> = {
  isError: boolean;
  errorCode?: string;
  errorMessage?: string;
  response?: T;
};

isError: 에러일 경우 true

errorCode: 에러일 경우 에러 코드 표시

errorMessage: 에러일 경우 에러 메세지 표시

response: 요청 성공 시 api 응답


useMutationalFetch

사용 이유

POST, PATCH 같은 메서드의 요청은 핸들러나 조건문 안에서 사용되는 경우가 많다.

요청에 들어갈 데이터도 사용 시점에 접근 가능함.

사용할 때마다 모든 인수를 넣는 것은 휴먼 에러의 우려가 있으므로 선언 시점에 가능한 정보는 추상화할 수 있도록 바인딩.

사용법

인자

export const useCreateMissionFetch = () => {
  return useMutationalFetch<MissionResponse>("route", "/missions") as {
    mutationalFetch: (
      fetchOptions: RequestInit,
      onSuccess?: () => void,
      onError?: () => void
    ) => Promise<CustomResponse<MissionResponse>>;
  };
};

인자는 safeFetch와 동일하나 1~4개를 앞에서부터 선택적으로 고정시킬 수 있다.

useMutationalFetch<MissionResponse>("route", "/missions")

해당 부분의 경우 선언 시에 baseUrlTypepathname만 지정해준 상황.

~~ as {
    mutationalFetch: (
      fetchOptions: RequestInit,
      onSuccess?: () => void,
      onError?: () => void
    ) => Promise<CustomResponse<MissionResponse>>;

해당 부분의 경우 사용할 때 인수의 type 추론이 인텔리센스에 되도록 타입 단언을 사용한 것

반환값

  const customThis = {
    setIsLoading: (value: boolean) => {
      setIsLoadingState(value);
    }
  };

  const safeFetchArguments: MutationalFetchParams[] = [baseUrlType];

  if (pathname) {
    safeFetchArguments.push(pathname);
    if (options) {
      safeFetchArguments.push(options);
      if (onSuccess) {
        safeFetchArguments.push(onSuccess);
        if (onError) safeFetchArguments.push(onError);
      }
    }
  }

 return {
   mutationalFetch: (safeFetch<T>).bind(customThis, ...safeFetchArguments)
 };

지정한 인자가 safeFetch에 binding된 부분 적용 함수를 mutationalFetch로 반환한다.

customThis의 경우 isLoading을 변경시켜야 하는 위치가 safeFetch 내부인데 인수로 전달하면 기존 safeFetch 사용이나 인수 바인딩에 영향을 줄 것이라 생각하여 this를 통해 setIsLoading을 전달시키기 위함. safeFetch 내부에선 thissetIsLoading이 있으면 fetch 전후에 실행한다.

사용 예시

const { response } = await mutationalFetch({
  method: "POST",
  body: JSON.stringify(data)    
});

추가로 전달할 인수를 넣어서 함수를 실행시킨다.

위 예시에선 pathname이 이미 고정되어 fetchOptions이 들어갈 차례이므로 fetchOptions로 요청 메서드와 요청 body를 사용 시점에 전달 후 실행.


useInfiniteFetch

사용 이유

pagenation 방식으로 응답하는 api의 데이터 관리를 추상화하기 위함.

사용법

인자 타입

type useInfiniteFetchProps = {
  baseUrlType: "backend" | "route";
  pathname: string;
  size: number;
  sort?: string;
  observerRef?: MutableRefObject<HTMLDivElement | null>;
  options?: RequestInit;
};

export const useInfiniteFetch = <
  T extends { data: { content: any[]; last: boolean } }
>({

T: pagenation 방식의 api 응답 타입. data 객체 안에 content와 last 필드가 있어야한다.

baseUrlType: base url의 타입, 백엔드 / route handler

pathname: base url을 제외한 url 패스 경로. /~~ 형식 처럼 슬래시가 맨 앞에 있어야 함.

size: 한 번마다 요청할 데이터 아이템의 개수

sort: sort 쿼리스트링이 필요할 경우 사

observerRef: IntersectionObserver 가 연결될 요소의 Ref

options: fetch에 들어갈 options

반환 타입

const returnMethods = {
    data,
    fetchNextPage: async () => {
      if (!hasNextPageRef.current) return { isError: true };

      isLoadingRef.current = true;

      const { isError, response } = await safeFetch<T>(
        baseUrlType,
        `${pathname}${pathname.includes("?") ? "&" : "?"}page=${
          pageRef.current
        }&size=${size}${sort ? `&sort=${sort}` : ""}${
          searchParamsRef.current.length > 0 ? "&" : ""
        }${searchParamsRef.current}`,
        options
      );

      if (!isError && response) {
        setData((prev) => [...prev, ...response.data.content]);
        hasNextPageRef.current = !response.data.last;
        pageRef.current += 1;
      }

      isLoadingRef.current = false;

      return { isError };
    },
    hasNextPage: hasNextPageRef.current,
    refreshPage: async (recordedPage?: number, recordedScroll?: number) => {
      const totalSize = (recordedPage ? recordedPage : pageRef.current) * size;
      const scroll =
        typeof window === "undefined"
          ? 0
          : recordedScroll
          ? recordedScroll
          : window.scrollY;

      isLoadingRef.current = true;

      const { isError, response } = await safeFetch<T>(
        baseUrlType,
        `${pathname}${
          pathname.includes("?") ? "&" : "?"
        }page=0&size=${totalSize}${sort ? `&sort=${sort}` : ""}${
          searchParamsRef.current.length > 0 ? "&" : ""
        }${searchParamsRef.current}`,
        options
      );

      if (!isError && response) {
        setData([...response.data.content]);
        hasNextPageRef.current = !response.data.last;
        pageRef.current = recordedPage ? recordedPage : pageRef.current + 1;
      }

      isLoadingRef.current = false;

      setTimeout(() => {
        if (typeof window === "undefined") return;
        window.scrollTo({
          behavior: "instant",
          top: scroll
        });
      }, 10);
    },
    setSearchParams: (newSearchParams: string) => {
      searchParamsRef.current = newSearchParams;
      pageRef.current = 0;
      hasNextPageRef.current = true;

      setData([]);

      returnMethods.fetchNextPage();
    },
    isLoading: isLoadingRef.current
  };

data: 응답으로 온 데이터의 content 필드 요소가 저장. 다음 페이지를 fetch 하면 데이터가 뒤에 추가됨. state 기반이므로 업데이트 시 리렌더링 작동.

fetchNextPage: 다음 페이지를 불러오는 함수. observer는 자동으로 이 함수를 실행시키므로 더보기 버튼 등 직접적인 요청에 사용. 요청 실패시 isError를 true로 반환.

hasNextPage: 다음 페이지가 존재하는지. 마지막 페이지라면 false.

refreshPage: data state를 최신으로 새로고침. 현재 불러온 페이지까지의 데이터를 한번에 가져와 다시 설정함. 설정 이전의 스크롤 위치를 기억하고 fetch가 완료되었을 때 이동.

setSearchParams: url에 쿼리 스트링을 설정하고 fetch를 초기화.

isLoading: fetch가 진행 중 인지에 대한 값.

페이지 이동 간 스크롤 위치 저장

  useEffect(() => {
    const { page, scroll } = getSessionStorage(localStorageKey, {
      page: 0,
      scroll: 0
    });

    if (page !== 0 || scroll !== 0) {
      returnMethods.refreshPage(page, scroll);
    }

    window.addEventListener("scroll", updataScrollRef);

    return () => {
      setSessionStorage(localStorageKey, {
        page: pageRef.current,
        scroll: scrollRef.current
      });

      window.removeEventListener("scroll", updataScrollRef);
    };
  }, []);

언마운트될 경우 현재의 페이지와 스크롤 위치를 로컬 스토리지에 저장. 마운트 시 로컬 스토리지에 저장된 값이 있다면 refreshPage 에 인수를 추가하여 기존 상태 만큼 새로 가져옴.

Clone this wiki locally