
이번 미션을 통해 Context API를 사용하면서 다음과 같은 점들을 깨달았습니다. 이러한 깨달음을 바탕으로 컴포넌트 결합도를 낮추고 재사용성을 높이기 위한 리팩터링 과정을 공유하고자 합니다.
이제, 이러한 깨달음을 바탕으로 진행한 리팩터링 과정을 살펴보겠습니다.
// Header.tsx
import * as Styled from "./Header.styled";
import shoppingBag from "/shoppingBag.svg";
import { useState } from "react";
import { Modal } from "../../common/Modal";
import ShoppingCartList from "../ShoppingCartList/ShoppingCartList";
import useShoppingCartData from "../../../hooks/shoppingCart/useShoppingCartData";
function Header() {
const [openModal, setOpenModal] = useState(false);
const handleOpenModal = () => {
setOpenModal(true);
};
const handleCloseModal = () => {
setOpenModal(false);
};
const { cartItems } = useShoppingCartData();
return (
<Styled.Container>
<a href="./">
<Styled.Title>SHOP</Styled.Title>
</a>
<Styled.ButtonWrapper>
<Styled.Button onClick={handleOpenModal}>
<Styled.Image src={shoppingBag} />
<Styled.ShoppingBag>{cartItems.length}</Styled.ShoppingBag>
</Styled.Button>
</Styled.ButtonWrapper>
<Modal isOpen={openModal} onClose={handleCloseModal}>
<Modal.Container position="bottom">
<Modal.Title title="장바구니" />
<ShoppingCartList handleCloseModal={handleCloseModal} />
</Modal.Container>
</Modal>
</Styled.Container>
);
}
export default Header;
// ShoppingCartList.tsx
import * as Styled from "./ShoppingCartList.styled";
import ShoppingCartItem from "../ShoppingCartItem/ShoppingCartItem";
import useShoppingCartData from "../../../hooks/shoppingCart/useShoppingCartData";
import useShoppingCartActions from "../../../hooks/shoppingCart/useShoppingCartActions";
interface ShoppingCartListProps {
handleCloseModal: () => void;
}
function ShoppingCartList({ handleCloseModal }: ShoppingCartListProps) {
const { cartItems } = useShoppingCartData();
const {
handleRemoveProduct,
handleIncreaseCartItemQuantity,
handleDecreaseCartItemQuantity,
} = useShoppingCartActions();
const totalPrice = cartItems.reduce(
(total, item) => total + item.product.price * item.quantity,
0
);
return (
<Styled.Container>
<Styled.UlContainer>
{cartItems.map((cartItem) => (
<ShoppingCartItem
key={cartItem.product.id}
cartItem={cartItem}
handleRemoveProduct={handleRemoveProduct}
handleIncreaseCartItemQuantity={handleIncreaseCartItemQuantity}
handleDecreaseCartItemQuantity={handleDecreaseCartItemQuantity}
/>
))}
</Styled.UlContainer>
<Styled.TotalPriceContainer>
<Styled.TotalPriceTitle>총 결제 금액</Styled.TotalPriceTitle>
<Styled.TotalPrice>{totalPrice.toLocaleString()}원</Styled.TotalPrice>
</Styled.TotalPriceContainer>
<Styled.Button onClick={handleCloseModal}>닫기</Styled.Button>
</Styled.Container>
);
}
export default ShoppingCartList;
적용한 깨달음: 특정 도메인 컴포넌트에서만 useContext를 사용하면 UI 컴포넌트의 재사용성을 확보할 수 있다.
UI 레이아웃과 장바구니 관련 로직을 분리하여 Header 컴포넌트의 재사용성을 높였습니다. Header는 기본적인 컨테이너 역할을 하고, CartHeader에서 Context API를 사용하여 장바구니 데이터를 관리합니다.
// Header.tsx (Base Header - UI only)
import * as Styled from "./Header.styled";
interface HeaderProps {
children?: React.ReactNode;
}
function Header({ children }: HeaderProps) {
return <Styled.Container>{children}</Styled.Container>;
}
export default Header;
import * as Styled from "./Header.styled";
import shoppingBag from "/shoppingBag.svg";
import { useState } from "react";
import { Modal } from "../../common/Modal";
import ShoppingCartList from "../ShoppingCartList/ShoppingCartList";
import useShoppingCartData from "../../../hooks/shoppingCart/useShoppingCartData";
import Header from "./Header";
function CartHeader() {
const [openModal, setOpenModal] = useState(false);
const handleOpenModal = () => {
setOpenModal(true);
};
const handleCloseModal = () => {
setOpenModal(false);
};
const { cartItems } = useShoppingCartData();
return (
<Header>
<a href="./">
<Styled.Title>SHOP</Styled.Title>
</a>
<Styled.ButtonWrapper>
<Styled.Button onClick={handleOpenModal}>
<Styled.Image src={shoppingBag} />
<Styled.ShoppingBag>{cartItems.length}</Styled.ShoppingBag>
</Styled.Button>
</Styled.ButtonWrapper>
<Modal isOpen={openModal} onClose={handleCloseModal}>
<Modal.Container position="bottom">
<Modal.Title title="장바구니" />
<ShoppingCartList handleCloseModal={handleCloseModal} />
</Modal.Container>
</Modal>
</Header>
);
}
export default CartHeader;
적용한 깨달음: "조합 패턴 (children 활용)을 통해 컴포넌트의 역할을 분리하고 재사용성을 높일 수 있다.", "Props를 사용하는 것이 컴포넌트 재사용성을 높이는 방법이다."
<ShoppingCartList>의 children으로 <ShoppingCartItem>들을 넘겨 <ShoppingCartList>는 단순히 아이템들을 감싸는 컨테이너 역할을 하게 됩니다. 이는 <ShoppingCartList>가 특정 아이템 컴포넌트에 종속되지 않고 다양한 형태의 아이템 목록을 렌더링하는 데 재사용될 수 있도록 합니다.
또한 <CartHeader>에서 <ShoppinCartItem>으로 데이터와 핸들러를 props로 바로 넘길 수 있도록 했습니다.
// CartHeader.tsx (Updated)
import React, { useState } from "react";
import Header from "./Header";
import * as Styled from "./Header.styled";
import shoppingBag from "/shoppingBag.svg";
import { Modal } from "../../common/Modal";
import ShoppingCartList from "../ShoppingCartList/ShoppingCartList";
import useShoppingCartData from "../../../hooks/shoppingCart/useShoppingCartData";
import useShoppingCartActions from "../../../hooks/shoppingCart/useShoppingCartActions";
import ShoppingCartItem from "../ShoppingCartItem/ShoppingCartItem";
function CartHeader() {
const [openModal, setOpenModal] = useState(false);
const { cartItems } = useShoppingCartData();
const {
handleRemoveProduct,
handleIncreaseCartItemQuantity,
handleDecreaseCartItemQuantity,
} = useShoppingCartActions();
const handleCloseModal = () => {
setOpenModal(false);
};
return (
<Header>
<a href="./">
<Styled.Title>SHOP</Styled.Title>
</a>
<Styled.ButtonWrapper>
<Styled.Button onClick={handleOpenModal}>
<Styled.Image src={shoppingBag} />
<Styled.ShoppingBag>{cartItems.length}</Styled.ShoppingBag>
</Styled.Button>
</Styled.ButtonWrapper>
<Modal isOpen={openModal} onClose={handleCloseModal}>
<Modal.Container position="bottom">
<Modal.Title title="장바구니" />
<ShoppingCartList
cartItems={cartItems}
handleCloseModal={handleCloseModal}
>
{cartItems.map((cartItem) => (
<ShoppingCartItem
key={cartItem.product.id}
cartItem={cartItem}
handleRemoveProduct={handleRemoveProduct}
handleIncreaseCartItemQuantity={handleIncreaseCartItemQuantity}
handleDecreaseCartItemQuantity={handleDecreaseCartItemQuantity}
/>
))}
</ShoppingCartList>
</Modal.Container>
</Modal>
</Header>
);
}
export default CartHeader;
// ShoppingCartList.tsx (Updated)
import * as Styled from "./ShoppingCartList.styled";
import { CartItem } from "../../../types/FetchCartItemsResult";
interface ShoppingCartListProps {
cartItems: CartItem[];
handleCloseModal: () => void;
children?: React.ReactNode;
}
function ShoppingCartList({ cartItems, handleCloseModal, children }: ShoppingCartListProps) {
const totalPrice = cartItems.reduce(
(total, item) => total + item.product.price * item.quantity,
0
);
return (
<Styled.Container>
<Styled.UlContainer>{children}</Styled.UlContainer>
<Styled.TotalPriceContainer>
<Styled.TotalPriceTitle>총 결제 금액</Styled.TotalPriceTitle>
<Styled.TotalPrice>{totalPrice.toLocaleString()}원</Styled.TotalPrice>
</Styled.TotalPriceContainer>
<Styled.Button onClick={handleCloseModal}>닫기</Styled.Button>
</Styled.Container>
);
}
export default ShoppingCartList;
재사용성 증대 예시
만약 장바구니 목록 외에, "최근 본 상품 목록"을 표시해야 한다고 가정해 봅시다. 만약
<ShoppingCartList>가 내부적으로<ShoppingCartItem>을 직접 렌더링하도록 구현되어 있다면, "최근 본 상품 목록"을 위해 또 다른 리스트 컴포넌트를 만들어야 할 가능성이 높습니다.하지만 children을 사용하는 방식에서는
<ShoppingCartList>는 단순히 children으로 전달된 컴포넌트들을 나열하기 때문에, "최근 본 상품"의 UI를 담당하는<RecentlyViewedItem>컴포넌트들을 children으로 전달하기만 하면 동일한<ShoppingCartList>컴포넌트를 재활용할 수 있습니다.
이번 미션을 통해 Context API를 사용하면서 Context를 사용하는 컴포넌트와 Context 사이의 결합도가 높아질 수 있다는 점, 그리고 UI 컴포넌트와 도메인 로직을 분리하고 조합 패턴을 활용하는 것이 이러한 결합도를 낮추고 컴포넌트의 재사용성을 높이는 효과적인 방법임을 깨달았습니다. 이 글을 통해 Context API를 더 깊이 이해하고, 더욱 유연하고 재사용 가능한 컴포넌트를 설계하는 데 조금이나마 도움이 되었으면 좋겠습니다.
매번 좋은 글 감사합니다🙇♂️