React - useFetch

sarang_daddy·2023년 9월 15일
0

React

목록 보기
17/26

외부 리소스와의 상호 작용 중 외부 API 호출할 때는 useEffect 훅을 사용한다.
이는 컴포넌트가 렌더링 된 후에 외부 데이터를 가져와 컴포넌트의 상태를 업데이트 되도록 해준다.

다만, 여러 컴포넌트에서 동일한 데이터 패치 로직을 사용할 경우 코드 중복이 발생할 수 있다. 이러한 중복을 줄이고 코드의 재사용성과 유지보수성을 높이기 위해 공통로직커스텀 훅으로 분리해서 사용할 수 있다.

커스텀 훅

커스텀 훅은 React의 기본 훅을 활용하여 사용자가 만든 재사용 가능한 함수다.
기존 프로젝트에서 반복되는 로직을 따로 함수로 만들어서 사용하던 방식에 React의 기본 훅이 추가되었다고 생각할 수 있다.

  • 훅을 만들기에 이름 앞에는 "use"로 시작한다.
  • 커스텀 훅은 상태 자체를 공유하는 것이 아닌 상태 로직을 공유한다.
  • 커스텀 훅을 사용하는 컴포넌트는 커스텀 훅이 반환해준 결과에만 의존하면 된다.

즉, 데이터를 가져오는 방식은 커스텀 훅에서 고민하고 결과 값을 반환해준다.
컴포넌트에서는 가져온 데이터를 어떻게 보여줄까만 고민하면 된다.
이는 컴포넌트를 보다 더 선언적으로 만들어 준다.

useFetch 커스텀 훅 만들기

1. useEffect 사용하던 컴포넌트

  • 아래 코드는 useEffect를 사용해 외부 데이터를 받아오고 있다.
  • 데이터를 받아오면 컴포넌트 상태를 업데이트 후 화면을 변경한다.
// Home
export const Home = () => {
  const [loading, setLoading] = useState<boolean>(true);
  const [characters, setCharacters] = useState<CharacterTypes[]>([]);

  useEffect(() => {
    let ignore = false;

    const getCharacters = async () => {
      const data = await fetchCharacters(50);

      if (!ignore) {
        setCharacters(data.results);
        setLoading(false);
      }
    };

    getCharacters();

    return () => {
      ignore = true;
    };
  }, []);

  return (
	// --중략
  );
};

// Detail
export const Detail = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const [loading, setLoading] = useState(true);
  const [characterDetail, setCharacterDetail] =
    useState<CharacterDetailTypes>();

  useEffect(() => {
    let ignore = false;

    const getCharacterDetail = async () => {
      const data = await fetchCharacterDetail(id);

      if (!ignore) {
        setCharacterDetail(data.results[0]);
        setLoading(false);
      }
    };

    getCharacterDetail();

    return () => {
      ignore = true;
    };
  }, [id]);

  return (
    <>
		// -- 중략
    </>
  );
};
  • 데이터를 받아오고 컴포넌트를 업데이트하는 작업에는 문제가 없다.
  • 다만, 컴포넌트에서 데이터를 어떻게 가져오는지, 에러 처리는 어떻게 해야할지, 로딩 상태는 어떻게 관리하는지를 관여하고 있다.
  • 또, 두 컴포넌트에서 요청 API만 다를 뿐 같은 로직을 중복 사용하고 있다.

2. useFetch 커스텀 훅을 만들기 전 고려사항

  • useFetch는 API 호출 함수해당 함수에 전달될 인수를 받아야 한다.
  • useEffect를 사용하여 컴포넌트 마운트 시 데이터를 로드한다.
  • status, data, error 상태를 관리한다.
  • API 호출 중에 오류가 발생할 경우를 대비하여 적절한 에러 처리를 고려해야 한다.
  • 컴포넌트가 언마운트될 때 진행 중인 비동기 작업을 취소하거나 무시해서 메모리 누수를 방지 해야한다.

3. useFetch 생성 과정

3-1. 타입을 정의한다

type Status = 'initial' | 'pending' | 'fulfilled' | 'rejected';

interface UseFetch<T> {
  data?: T;
  status: Status;
  error?: Error;
}
  • Status는 요청의 상태를 나타내며, 초기 상태(initial), 로딩 중(pending), 요청 성공(fulfilled), 요청 실패(rejected) 중 하나의 값을 갖는다.
  • UseFetch<T>useFetch 훅이 반환하는 객체의 타입이다. 여기서 T는 fetch 함수의 반환 타입을 나타낸다.

3-2. useFetch 함수 선언

  • 1️⃣ : fetchFunction: 데이터를 가져오는 함수(API 호출 함수)다. 이 함수는 Promise를 반환해야 한다.
  • 2️⃣ : ...args: fetchFunction에 전달될 인수들이다.
  • 3️⃣ : 함수는 UseFetch<T> 타입의 객체를 반환한다.

3-3. 상태 초기화

const [state, setState] = useState<UseFetch<T>>({
  status: 'initial',
  data: undefined,
  error: undefined
});
  • 초기 상태는 statusinitial이며, dataerror는 정의되지 않은 상태다.

3-4. useEffect를 사용한 데이터 로드

useEffect(() => {
  let ignore = false;
  • ignore 변수는 컴포넌트가 언마운트된 후에 상태를 변경하려는 시도를 방지한다.
const fetchData = async () => {
  setState({ ...state, status: 'pending' });

  try {
    const data = await fetchFunction(...args);
    if (!ignore) {
      setState({ status: 'fulfilled', data });
    }
  } catch (error) {
    if (!ignore) {
      setState({ status: 'rejected', error });
    }
  }
};
  • 비동기 함수 fetchData를 선언한다.
  • 함수가 호출되면 상태를 pending으로 설정하여 로딩 중임을 나타낸다.
  • fetchFunction을 호출하여 데이터를 가져온다.
    • 성공적으로 데이터를 가져오면 상태를 fulfilled로 설정하고 데이터를 저장한다.
    • 오류가 발생하면 상태를 rejected로 설정하고 오류를 저장한다.
fetchData();

return () => {
  ignore = true;
};
  • useEffect 내에서 fetchData 함수를 호출하여 데이터 로드를 시작한다.
  • 컴포넌트가 언마운트 되면 클린업 함수에서 ignoretrue로 설정하여 비동기 작업이 일어나도 상태가 변경되는 것을 방지한다.

3-5. 상태 반환

return state;
  • 마지막으로 현재 상태를 반환한다. 이 상태에는 data, status, error가 포함된다.

4. useFetch 전체 코드

import { useState, useEffect } from 'react';

type Status = 'initial' | 'pending' | 'fulfilled' | 'rejected';

interface UseFetch<T> {
  data?: T;
  status: Status;
  error?: Error;
}

export const useFetch = <T>(
  // API 호출 함수 그리고 함께 전달될 인수.
  fetchFunction: (...args: any[]) => Promise<T>,
  ...args: any[]
): UseFetch<T> => {
  // useFetch에서 관리하는 상태 -> 최종 반환값이 된다.
  const [state, setState] = useState<UseFetch<T>>({
    status: 'initial',
    data: undefined,
    error: undefined,
  });

  // useEffect를 통해 데이터를 가져온다.
  useEffect(() => {
    // 컴포넌트가 언마운트 되면 비동기 작업으로 인한 상태 변경 방지.
    let ignore = false;

    const fetchData = async () => {
      // 비동기 함수 fetchData()를 실행하면 "로딩중"이 된다.
      setState({ ...state, status: 'pending' });

      // 요청 API(fetchFunction)를 실행한다.
      try {
        // 요청 API 인수에 함께 전달받았던 "...args"를 준다.
        const data = await fetchFunction(...args);
        if (!ignore) {
          // 성공하면 상태를 'fulfilled로 변경하고 데이터를 저장한다.
          setState({ status: 'fulfilled', data });
        }
      } catch (error) {
        if (!ignore) {
          // 실패하면 상태를 'rejected'로 변경하고 오류를 저장한다.
          setState({ status: 'rejected', error: error as Error });
        }
      }
    };

    fetchData();

    return () => {
      ignore = true;
    };
  }, [fetchFunction]);

  return state;
};

5.useFetch를 사용하는 컴포넌트

// Home
export const Home = () => {
  const { data: characters, status } = useFetch(fetchCharacters, 50);
  const charactersList = characters?.results;

  return (
		// -- 중략
  );
};

// Details
export const Detail = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const { data: characterDetail, status } = useFetch(fetchCharacterDetail, id);
  const detailsInfo = characterDetail?.results[0];

  const handleBackPage = () => {
    navigate(-1);
  };

  return (
    <>
	  // -- 중략
    </>
  );
};
  • 두 컴포넌트에서 중복으로 사용되던 데이터 패치 로직이 생략되었다.
  • 데이터 패치 관련 로직은 useFetch에서만 관리하면 된다.
  • 각 컴포넌트는 데이터를 가져오고 어떻게 보여줄지만 고려하면 된다.

데이터를 받아오는 과정, 과정 중의 상태, 에러 여부는 컴포넌트가 고려하지 않아도 된다.
컴포넌트는 받아온 결과 값과 상태만을 고려해서 화면을 보여주면 된다.

profile
한 발자국, 한 걸음 느리더라도 하루하루 발전하는 삶을 살자.

0개의 댓글