4장 : React 훅 기초실습
reducer·Context·데이터 요청
장바구니 reducer와 Context 및 데이터 요청 커스텀 훅으로 상태 계층을 구성합니다.
cartReducer.js (useReducer)
장바구니 상태를 관리하는 리듀서 함수를 정의합니다.
// 액션 타입 상수 정의
export const ADD_ITEM = 'ADD_ITEM';
export const REMOVE_ITEM = 'REMOVE_ITEM';
export const UPDATE_QUANTITY = 'UPDATE_QUANTITY';
// 초기 장바구니 상태
export const initialCartState = [];
// 리듀서 함수
export const cartReducer = (state, action) => {
switch (action.type) {
case ADD_ITEM:
const existingItem = state.find(item => item.id === action.payload.item.id);
if (existingItem) {
// 이미 있는 상품이면 수량만 증가
return state.map(item =>
item.id === action.payload.item.id
? { ...item, quantity: item.quantity + action.payload.quantity }
: item
);
} else {
// 새 상품이면 목록에 추가
return [
...state,
{ ...action.payload.item, quantity: action.payload.quantity }
];
}
case REMOVE_ITEM:
return state.filter(item => item.id !== action.payload.id);
case UPDATE_QUANTITY:
return state.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
).filter(item => item.quantity > 0); // 수량이 0이하가 되면 제거
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};CartContext.js (useContext)
장바구니 상태와 디스패치 함수를 전역적으로 제공하는 Context를 생성합니다.
import React, { createContext, useReducer, useEffect } from 'react';
import { cartReducer, initialCartState } from '../reducers/cartReducer';
// 1. Context 객체 생성
export const CartContext = createContext();
// 2. 초기화 함수 (로컬 스토리지에서 장바구니 불러오기)
const initCart = (initialState) => {
try {
const savedCart = localStorage.getItem('shoppingCart');
return savedCart ? JSON.parse(savedCart) : initialState;
} catch (error) {
console.error("Failed to parse cart from localStorage", error);
return initialState;
}
};
// 3. Provider 컴포넌트 생성
export const CartProvider = ({ children }) => {
// useReducer와 지연 초기화(initCart)를 사용하여 장바구니 상태 관리
const [cartState, dispatch] = useReducer(cartReducer, initialCartState, initCart);
// cartState가 변경될 때마다 로컬 스토리지에 저장 (useEffect)
useEffect(() => {
localStorage.setItem('shoppingCart', JSON.stringify(cartState));
}, [cartState]);
// Context Provider를 통해 상태와 dispatch 함수 제공
return (
<CartContext.Provider value={{ cartState, dispatch }}>
{children}
</CartContext.Provider>
);
};Context와 reducer가 함께 움직이는 범위를 한 번 더 분리해 보면, 어느 컴포넌트가 읽고 어느 컴포넌트가 요청하는지 명확해집니다.
useFetch.js (Custom Hook)
API 호출 로직을 재사용 가능한 커스텀 훅으로 만듭니다.
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController(); // 요청 취소를 위한 AbortController
const signal = abortController.signal;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (e) {
if (e.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setError(e);
}
} finally {
setLoading(false);
}
};
fetchData();
// 클린업 함수: 컴포넌트 언마운트 또는 의존성 변경 시 요청 취소
return () => {
abortController.abort();
};
}, [url]); // url이 변경될 때마다 데이터를 다시 가져옵니다.
return { data, loading, error };
}
export default useFetch;useFetch는 성공 사례만 보는 훅이 아니라, 요청 시작부터 취소와 실패까지 화면 상태로 나누는 연습입니다.