React - Custom Hook 만들기

김명원·2025년 1월 12일
0

learnReact

목록 보기
22/26

Custom Hook 만들기 🛠️✨

이번 포스트에서는 React Custom Hook을 만들어 API 요청 로직을 효율적으로 관리하는 방법을 다룹니다. 기존의 Home.jsx 컴포넌트를 리팩토링하여 API 요청을 커스텀 훅으로 분리함으로써 코드의 재사용성과 가독성을 향상시켰습니다.


기존 코드

Home.jsx

import { useEffect, useState } from 'react';
import CanvasList from '../components/CanvasList';
import SearchBar from '../components/SearchBar';
import ViewToggle from '../components/ViewToggle';
import { createCanvas, deleteCanvas, getCanvases } from '../api/canvas';
import Loading from '../components/Loading';
import Error from '../components/Error';
import Button from '../components/Button';

function Home() {
  const [searchText, setSearchText] = useState();
  const [isGridView, setIsGridView] = useState(true);
  const [data, setData] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  async function fetchData(params) {
    try {
      setIsLoading(true);
      setError(null);
      await new Promise(resolver => setTimeout(resolver, 1000));
      const response = await getCanvases(params);
      setData(response.data);
    } catch (err) {
      setError(err);
    } finally {
      setIsLoading(false);
    }
  }

  useEffect(() => {
    fetchData({ title_like: searchText });
  }, [searchText]);

  const handleDeleteItem = async id => {
    if (confirm('삭제 하시겠습니까?') === false) {
      return;
    }

    // delete logic
    try {
      await deleteCanvas(id);
      fetchData({ title_like: searchText });
    } catch (error) {
      alert(error.message);
    }
  };

  const [isLoadingCreate, setIsLoadingCreate] = useState(false);
  const handleCreateCanvas = async () => {
    try {
      setIsLoadingCreate(true);
      await new Promise(resolver => setTimeout(resolver, 1000));

      await createCanvas();
      fetchData({ title_like: searchText });
    } catch (err) {
      alert(err.message);
    } finally {
      setIsLoadingCreate(false);
    }
  };
  
  return (
    <>
      <div className="mb-6 flex flex-col sm:flex-row items-center justify-between">
        <SearchBar searchText={searchText} setSearchText={setSearchText} />
        <ViewToggle setIsGridView={setIsGridView} isGridView={isGridView} />
      </div>
      <div className="flex justify-end mb-6">
        <Button onClick={handleCreateCanvas} loading={isLoadingCreate}>
          등록하기
        </Button>
      </div>
      {isLoading && <Loading />}
      {error && (
        <Error
          message={error.message}
          onRetry={() => fetchData({ title_like: searchText })}
        />
      )}
      {!isLoading && !error && (
        <CanvasList
          filteredData={data}
          isGridView={isGridView}
          searchText={searchText}
          onDeleteItem={handleDeleteItem}
        />
      )}
    </>
  );
}

export default Home;

코드 설명:

  • 상태 관리:

    • searchText: 사용자가 입력한 검색어를 저장합니다.
    • isGridView: 캔버스 목록을 그리드 뷰로 표시할지 여부를 결정합니다.
    • data: API로부터 받아온 캔버스 데이터를 저장합니다.
    • isLoading: 데이터 로딩 상태를 관리합니다.
    • error: API 요청 중 발생한 오류를 저장합니다.
  • fetchData 함수:

    • API 요청을 통해 캔버스 데이터를 가져옵니다.
    • 로딩 상태를 true로 설정하고, 오류 상태를 초기화합니다.
    • getCanvases 함수를 호출하여 데이터를 가져온 후, data 상태를 업데이트합니다.
    • 오류 발생 시 error 상태를 설정하고, 로딩 상태를 false로 변경합니다.
  • useEffect:

    • searchText가 변경될 때마다 fetchData를 호출하여 데이터를 다시 불러옵니다.
  • handleDeleteItem 함수:

    • 특정 캔버스를 삭제하는 기능을 수행합니다.
    • 사용자에게 삭제 확인 메시지를 표시하고, 확인 시 deleteCanvas 함수를 호출하여 데이터를 삭제합니다.
    • 삭제 후 fetchData를 호출하여 최신 데이터를 다시 불러옵니다.
  • handleCreateCanvas 함수:

    • 새로운 캔버스를 생성하는 기능을 수행합니다.
    • 로딩 상태를 관리하며, createCanvas 함수를 호출하여 데이터를 생성합니다.
    • 생성 후 fetchData를 호출하여 최신 데이터를 다시 불러옵니다.

추가 설명:

기존의 Home.jsx 컴포넌트는 API 요청 로직을 직접 포함하고 있어 코드의 재사용성이 떨어지고, 컴포넌트가 비대해지는 문제가 있었습니다. 이를 해결하기 위해 API 요청 로직을 별도의 커스텀 훅으로 분리하여 코드의 가독성과 유지보수성을 향상시켰습니다.


실습 코드

src/hooks/useApiRequest.js

import { useCallback, useState } from 'react';

export default function useApiRequest(apiFunction) {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  // options: {onSuccess, onError}
  const execute = useCallback(
    async (params, { onSuccess, onError }) => {
      try {
        setIsLoading(true);
        setError(null);

        await new Promise(resolver => setTimeout(resolver, 1000));
        const response = await apiFunction(params);
        if (onSuccess) {
          onSuccess(response);
        }
      } catch (err) {
        setError(err);
        if (onError) {
          onError(err);
        }
      } finally {
        setIsLoading(false);
      }
    },
    [apiFunction],
  );

  return {
    isLoading,
    error,
    execute,
  };
}

코드 설명:

  • useApiRequest 커스텀 훅:

    • 목적: API 요청 로직을 재사용 가능한 훅으로 분리하여, 여러 컴포넌트에서 공통으로 사용할 수 있게 합니다.
  • 상태 관리:

    • isLoading: API 요청의 로딩 상태를 관리합니다.
    • error: API 요청 중 발생한 오류를 저장합니다.
  • execute 함수:

    • 역할: API 요청을 실행하고, 성공 시 onSuccess 콜백을, 실패 시 onError 콜백을 호출합니다.
    • 로직:
      • 로딩 상태를 true로 설정하고, 오류 상태를 초기화합니다.
      • apiFunction을 호출하여 데이터를 가져옵니다.
      • 성공 시 onSuccess를, 실패 시 onError를 호출합니다.
      • 최종적으로 로딩 상태를 false로 변경합니다.

추가 설명:

이 커스텀 훅을 사용함으로써, API 요청 로직을 각 컴포넌트에서 중복 없이 사용할 수 있게 되었습니다. 또한, 요청의 성공 및 실패에 따른 후속 처리를 유연하게 할 수 있어 코드의 유연성과 가독성이 향상되었습니다.


src/page/Home.jsx

import { useEffect, useState } from 'react';
import { createCanvas, deleteCanvas, getCanvases } from '../api/canvas';

import CanvasList from '../components/CanvasList';
import SearchBar from '../components/SearchBar';
import ViewToggle from '../components/ViewToggle';
import Loading from '../components/Loading';
import Error from '../components/Error';
import Button from '../components/Button';
import useApiRequest from '../hooks/useApiRequest';

function Home() {
  const [searchText, setSearchText] = useState();
  const [isGridView, setIsGridView] = useState(true);
  const [data, setData] = useState([]);

  // API call
  const { isLoading, error, execute: fetchData } = useApiRequest(getCanvases);
  const { isLoading: isLoadingCreate, execute: createNewCanvas } =
    useApiRequest(createCanvas);

  useEffect(() => {
    fetchData(
      { title_like: searchText },
      {
        onSuccess: response => setData(response.data),
      },
    );
  }, [searchText, fetchData]);

  const handleDeleteItem = async id => {
    if (confirm('삭제 하시겠습니까?') === false) {
      return;
    }
    try {
      await deleteCanvas(id);
      fetchData({ title_like: searchText });
    } catch (err) {
      alert(err.message);
    }
  };

  const handleCreateCanvas = async () => {
    createNewCanvas(null, {
      onSuccess: () => {
        fetchData(
          { title_like: searchText },
          {
            onSuccess: response => setData(response.data),
          },
        );
      },
      onError: err => alert(err.message),
    });
    // 기존 로직을 주석 처리하고 커스텀 훅으로 대체
    // try {
    //   setIsLoadingCreate(true);
    //   await new Promise(resolver => setTimeout(resolver, 1000));
    //   await createCanvas();
    //   fetchData({ title_like: searchText });
    // } catch (err) {
    //   alert(err.message);
    // } finally {
    //   setIsLoadingCreate(false);
    // }
  };

  return (
    <>
      <div className="mb-6 flex flex-col sm:flex-row items-center justify-between">
        <SearchBar searchText={searchText} setSearchText={setSearchText} />
        <ViewToggle isGridView={isGridView} setIsGridView={setIsGridView} />
      </div>
      <div className="flex justify-end mb-6">
        <Button onClick={handleCreateCanvas} loading={isLoadingCreate}>
          등록하기
        </Button>
      </div>
      {isLoading && <Loading />}
      {error && (
        <Error
          message={error.message}
          onRetry={() => fetchData({ title_like: searchText })}
        />
      )}
      {!isLoading && !error && (
        <CanvasList
          filteredData={data}
          isGridView={isGridView}
          searchText={searchText}
          onDeleteItem={handleDeleteItem}
        />
      )}
    </>
  );
}

export default Home;

코드 설명:

  • 커스텀 훅 사용:

    • useApiRequest 훅을 사용하여 API 요청 로직을 간소화했습니다.
    • fetchData: 캔버스 데이터를 가져오는 API 요청을 처리합니다.
    • createNewCanvas: 새로운 캔버스를 생성하는 API 요청을 처리합니다.
  • useEffect:

    • searchText가 변경될 때마다 fetchData를 호출하여 데이터를 다시 불러옵니다.
    • onSuccess 콜백을 통해 데이터를 data 상태에 저장합니다.
  • handleDeleteItem 함수:

    • 기존과 동일하게 캔버스를 삭제하고, 삭제 후 fetchData를 호출하여 최신 데이터를 불러옵니다.
  • handleCreateCanvas 함수:

    • createNewCanvas를 호출하여 새로운 캔버스를 생성합니다.
    • 성공 시 fetchData를 호출하여 데이터를 다시 불러옵니다.
    • 주석 처리된 기존 로직을 대체하여 코드의 중복을 제거하고, 커스텀 훅을 통한 간결한 API 요청 처리를 가능하게 했습니다.

추가 설명:

커스텀 훅을 도입함으로써 Home.jsx 컴포넌트 내의 API 요청 로직이 간결해졌습니다. useApiRequest 훅을 통해 API 요청의 로딩 상태와 오류 처리를 중앙에서 관리할 수 있게 되어, 코드의 재사용성과 유지보수성이 향상되었습니다. 또한, handleCreateCanvas 함수에서 주석 처리된 기존 로직을 대체하여 코드의 중복을 줄이고, API 요청의 일관성을 유지할 수 있게 되었습니다.


전체 수정 사항 요약 📋

  1. src/hooks/useApiRequest.js 추가:

    • API 요청 로직을 관리하는 useApiRequest 커스텀 훅을 구현했습니다.
    • 로딩 상태와 오류 관리를 포함하여 API 요청의 재사용성을 높였습니다.
  2. src/page/Home.jsx 수정:

    • 기존의 fetchData, handleCreateCanvas 함수를 커스텀 훅을 사용하도록 리팩토링했습니다.
    • useApiRequest 훅을 통해 API 요청 로직을 간소화하고, 코드의 중복을 제거했습니다.
    • 삭제 및 생성 후 데이터를 다시 불러오는 로직을 유지하면서, 커스텀 훅의 장점을 활용했습니다.
  3. 한글 입력 이슈 처리:

    • 별도의 커스텀 훅에서 한글 입력 시 발생할 수 있는 문제를 해결하기 위한 추가 로직을 구현할 수 있습니다. (추후 필요 시 구현)

배운 내용 요약 📝

이번 커스텀 훅 만들기 작업을 통해 다음과 같은 주요 내용을 학습하고 적용할 수 있었습니다:

  1. 커스텀 훅 (useApiRequest) 구현:

    • API 요청 로직을 별도의 훅으로 분리하여 코드의 재사용성과 가독성을 향상시켰습니다.
    • 로딩 상태와 오류 관리를 통합하여 API 요청의 일관성을 유지했습니다.
  2. React 컴포넌트 리팩토링:

    • Home.jsx 컴포넌트를 커스텀 훅을 사용하도록 리팩토링하여 코드의 중복을 줄이고, 로직의 명확성을 높였습니다.
    • 커스텀 훅을 통해 API 요청 후속 처리를 유연하게 구현할 수 있게 되었습니다.
  3. 코드 유지보수성 향상:

    • API 요청 로직을 커스텀 훅으로 분리함으로써 코드의 유지보수성과 확장성을 높였습니다.
    • 컴포넌트 간의 의존성을 줄이고, 로직을 중앙에서 관리할 수 있게 했습니다.

이번 작업을 통해 커스텀 훅의 강력한 기능과 React 컴포넌트의 효율적인 관리 방법을 깊이 있게 이해할 수 있었습니다.

profile
개발자가 되고 싶은 정치학도생의 기술 블로그

0개의 댓글