tags:
- TIL
- React
created: 2025-06-27
📘 2025-06-27 TIL
도서 목록에서 각 도서 클릭 시 /book/:bookId로 이동하며, 해당 ID를 기반으로 상세 정보를 조회하는 구조를 구현했다.
{
path: 'book/:bookId',
element: <BookDetail />,
}
const { bookId } = useParams();
export const useBook = (bookId: string | undefined) => {
const [book, setBook] = useState<BookDetail | null>(null);
useEffect(() => {
if (!bookId) return;
fetchBook(bookId).then(setBook);
}, [bookId]);
return { book };
};
export const fetchBook = async (bookId: string) => {
const response = await httpClient.get<BookDetail>(`/books/${bookId}`);
return response.data;
};
function BookDetail() {
const { bookId } = useParams();
const { book } = useBook(bookId);
if (!book) return null;
return (
<section className={mainContainer}>
<div className="flex justify-between items-center w-full">
<Title size="lg" color="primary">{book.title}</Title>
</div>
</section>
);
}
Early return은 조건이 맞지 않을 경우 최대한 빨리 함수 실행을 종료(return) 하여 불필요한 연산을 방지하는 패턴
function BookItem({ book }: { book?: Book }) {
if (!book) return null; // 📌 book이 없으면 아무것도 렌더링하지 않고 종료
return (
<div>{book.title}</div>
);
}
이번 프로젝트에서는 로컬 상태 관리(useState)와 글로벌 상태 관리(Zustand)를 역할에 따라 분리해서 병행 사용했습니다. 이 조합은 UI 반응성과 전역 데이터 공유를 동시에 달성하는 데 큰 도움이 되었습니다.
구분 | 역할 | 사용한 곳 | 이유 |
---|---|---|---|
useState | 로컬 UI 상태 관리 | 수량 조절(quantity) / 로딩 처리(loading) | 즉각적인 UI 반응이 필요하고 전역 공유가 필요하지 않음 |
Zustand | 전역 상태 저장 및 공유 | 장바구니(cart) 데이터 | 여러 페이지 간 공통된 상태 공유 필요 (ex. 장바구니 페이지, 헤더 아이콘 등) |
const [quantity, setQuantity] = useState(1);
const [loading, setLoading] = useState(false);
이들은 현재 페이지에서만 사용되는 상태이므로 전역으로 관리할 필요가 없었습니다. useState로 관리하면 컴포넌트가 독립적이고 예측 가능해집니다.
const { addToCart, removeFromCart } = useCartStore();
Zustand는 Redux보다 설정이 훨씬 간단하고, React Context보다 가볍습니다. 복잡한 보일러플레이트 없이 상태를 공유할 수 있어, 중소규모 프로젝트에 특히 유리합니다.
단순한 로컬 상태는 useState로 간결하게 관리
여러 컴포넌트에서 참조하는 전역 상태는 Zustand로 공유
불필요한 리렌더링 최소화
const handleAdd = async () => {
addToCart(book, quantity); // Zustand
setLoading(true); // useState
try {
await addCart({ book_id: book.id, quantity }); // API
toast.success('장바구니에 추가되었습니다!');
} catch (err) {
removeFromCart(book.id); // Zustand rollback
toast.error('실패!');
} finally {
setLoading(false); // useState
}
};
import { create } from 'zustand';
import type { BookDetail } from '@/models/book.model';
interface CartItem {
book: BookDetail;
quantity: number;
}
interface CartState {
items: CartItem[];
addToCart: (book: BookDetail, quantity: number) => void;
removeFromCart: (bookId: number) => void;
clearCart: () => void;
}
export const useCartStore = create<CartState>(set => ({
items: [],
addToCart: (book, quantity) =>
set(state => {
const exists = state.items.find(item => item.book.id === book.id);
if (exists) {
return {
items: state.items.map(item =>
item.book.id === book.id
? { ...item, quantity: item.quantity + quantity }
: item
),
};
} else {
return {
items: [...state.items, { book, quantity }],
};
}
}),
removeFromCart: bookId =>
set(state => ({
items: state.items.filter(item => item.book.id !== bookId),
})),
clearCart: () => set({ items: [] }),
}));
📌 이 상태 구조의 장점: