이번 포스트에서는 React Custom Hook을 만들어 API 요청 로직을 효율적으로 관리하는 방법을 다룹니다. 기존의 Home.jsx
컴포넌트를 리팩토링하여 API 요청을 커스텀 훅으로 분리함으로써 코드의 재사용성과 가독성을 향상시켰습니다.
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
함수:
true
로 설정하고, 오류 상태를 초기화합니다.getCanvases
함수를 호출하여 데이터를 가져온 후, data
상태를 업데이트합니다.error
상태를 설정하고, 로딩 상태를 false
로 변경합니다.useEffect
훅:
searchText
가 변경될 때마다 fetchData
를 호출하여 데이터를 다시 불러옵니다.handleDeleteItem
함수:
deleteCanvas
함수를 호출하여 데이터를 삭제합니다.fetchData
를 호출하여 최신 데이터를 다시 불러옵니다.handleCreateCanvas
함수:
createCanvas
함수를 호출하여 데이터를 생성합니다.fetchData
를 호출하여 최신 데이터를 다시 불러옵니다.기존의 Home.jsx
컴포넌트는 API 요청 로직을 직접 포함하고 있어 코드의 재사용성이 떨어지고, 컴포넌트가 비대해지는 문제가 있었습니다. 이를 해결하기 위해 API 요청 로직을 별도의 커스텀 훅으로 분리하여 코드의 가독성과 유지보수성을 향상시켰습니다.
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
커스텀 훅:
상태 관리:
isLoading
: API 요청의 로딩 상태를 관리합니다.error
: API 요청 중 발생한 오류를 저장합니다.execute
함수:
onSuccess
콜백을, 실패 시 onError
콜백을 호출합니다.true
로 설정하고, 오류 상태를 초기화합니다.apiFunction
을 호출하여 데이터를 가져옵니다.onSuccess
를, 실패 시 onError
를 호출합니다.false
로 변경합니다.이 커스텀 훅을 사용함으로써, API 요청 로직을 각 컴포넌트에서 중복 없이 사용할 수 있게 되었습니다. 또한, 요청의 성공 및 실패에 따른 후속 처리를 유연하게 할 수 있어 코드의 유연성과 가독성이 향상되었습니다.
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
를 호출하여 데이터를 다시 불러옵니다.커스텀 훅을 도입함으로써 Home.jsx
컴포넌트 내의 API 요청 로직이 간결해졌습니다. useApiRequest
훅을 통해 API 요청의 로딩 상태와 오류 처리를 중앙에서 관리할 수 있게 되어, 코드의 재사용성과 유지보수성이 향상되었습니다. 또한, handleCreateCanvas
함수에서 주석 처리된 기존 로직을 대체하여 코드의 중복을 줄이고, API 요청의 일관성을 유지할 수 있게 되었습니다.
src/hooks/useApiRequest.js
추가:
useApiRequest
커스텀 훅을 구현했습니다.src/page/Home.jsx
수정:
fetchData
, handleCreateCanvas
함수를 커스텀 훅을 사용하도록 리팩토링했습니다.useApiRequest
훅을 통해 API 요청 로직을 간소화하고, 코드의 중복을 제거했습니다.한글 입력 이슈 처리:
이번 커스텀 훅 만들기 작업을 통해 다음과 같은 주요 내용을 학습하고 적용할 수 있었습니다:
커스텀 훅 (useApiRequest
) 구현:
React 컴포넌트 리팩토링:
Home.jsx
컴포넌트를 커스텀 훅을 사용하도록 리팩토링하여 코드의 중복을 줄이고, 로직의 명확성을 높였습니다.코드 유지보수성 향상:
이번 작업을 통해 커스텀 훅의 강력한 기능과 React 컴포넌트의 효율적인 관리 방법을 깊이 있게 이해할 수 있었습니다.