🚨 고민 발생

해당 컴포넌트는 공통 컴포넌트로 만들어져있다.
여러 곳에서 사용되기 때문에 공통 컴포넌트로 분리했던 건데,, 문제가 생겼다.
사용처마다 스타일이나 요소 배치 등의 컴포넌트 요구사항이 조금씩 다르는 것이다.
결국 여러 조건을 걸 수밖에 없었고 아래와 같이 꽤 복잡한 공통 컴포넌트가 만들어졌다.
const ProductBoardItem = forwardRef<HTMLDivElement, IProductBoardItem>(
(
{ item, children, checkHandler, checkedItems, selectedCnt, isCart },
ref
) => {
return (
<div className={`flex py-2 ${isCart ? "gap-2" : "gap-5"}`} ref={ref}>
<Checkbox
onCheckedChange={(checked) => {
checkHandler(
checked as boolean,
selectedCnt
? { itemId: item.id, itemCount: selectedCnt }
: { itemId: item.id }
);
}}
checked={checkedItems.map((el) => el.itemId).includes(item.id)}
/>
<div className={`${isCart ? "w-32" : "w-28"} aspect-square`}>
<img
src={item.productThumbnail}
className="object-cover w-full h-full rounded-sm"
/>
</div>
<div className="flex flex-col gap-1 justify-between w-full py-1">
<span className={`${isCart ? "text-sm" : "text-md"}`}>
{item.productName}
</span>
{isCart ? (
<>
<span className={`${isCart ? "text-[11px]" : "text-sm"}`}>
{convertPriceUnit(item.productPrice)}원 · 남은수량:{" "}
{item.productQuantity}개
</span>
{children}
</>
) : (
<>
<span className={`${isCart ? "text-xs" : "text-sm"}`}>
{convertPriceUnit(item.productPrice)}원
</span>
<div className="flex items-end justify-between">
<p className="text-xs">남은수량: {item.productQuantity}개</p>
{children}
</div>
</>
)}
</div>
</div>
);
}
);
export default ProductBoardItem;
공통 컴포넌트 내부 변경되는 부분을 children으로 받아 렌더링시키니 코드 이해도도 많이 떨어진다고 느꼈다.
<ProductListItem
item={item}
ref={lastItemRef}
checkHandler={handleSingleCheck}
checkedItems={checkedItems}
>
<div className="flex gap-1">
<AdminBoardEditBtn productId={item.id} />
<AdminBoardDeleteBtn deleteHandler={() => onClickItemDelete(item.id)} />
</div>
</ProductListItem>;
📍 고민해결 방법
두가지 방법으로 해결할 수 있을 것 같았다.
1) HOC 사용
- 컴포넌트를 함수로 받아 새로운 컴포넌트 반환
- 컴포넌트 기능 확장에 유리
2) 컴파운드 컴포넌트 패턴 사용
- props로 내려오는 데이터 없이 내부 context를 사용하여 내부에서 데이터 처리
- context를 공유하는 작은 단위의 컴포넌트로 이루어져있어 자유롭게 사용 / 여러 하위 컴포넌트와 상태 공유에 유리
HOC는 기존 기능에 확장하는 경우 사용하는 게 좋을 것 같았고, 디자인적 자유도도 없어보였다.
좀 더 유연한 변경이 가능하면서도 컴포넌트의 기능적 가독성이 향상된 컴파운드 컴포넌트가 더 적합하다고 생각했다!
컴파운드 컴포넌트 패턴을 사용하자!!!
📍 컴파운드 컴포넌트 도입 과정
1. 내부 context 생성
데이터 공유를 위한 내부 context를 만든다.
const ProductListItemContext = createContext<IProductListItem>({
item: {
productThumbnail: "",
productImage: [],
productName: "",
productCategory: "",
productPrice: 0,
productQuantity: 0,
productDescription: "",
sellerId: "",
createdAt: "",
updatedAt: "",
id: "",
},
checkHandler: () => {},
checkedItems: [],
});
const useProductListItem = () => useContext(ProductListItemContext);
2. 최상위 컴포넌트 Root 생성
하위 컴포넌트들이 context에 접근할 수 있도록 생성한다.
const Root = ({
item,
children,
checkHandler,
checkedItems,
selectedCnt,
isCart,
lastItemRef,
gapSize,
}: IProductListItemWrapper) => {
const value = { item, checkHandler, checkedItems, selectedCnt, isCart };
return (
<ProductListItemContext.Provider value={value}>
<div className={`flex py-2 gap-${gapSize}`} ref={lastItemRef}>
{children}
</div>
</ProductListItemContext.Provider>
);
};
3. 기능별로 하위 컴포넌트 생성
const Image = ({ ...props }) => {
const { item } = useProductListItem();
return (
<div {...props}>
<img
src={item.productThumbnail}
className="object-cover w-full h-full rounded-sm"
/>
</div>
);
};
const Name = ({ ...props }) => {
const { item } = useProductListItem();
return <span {...props}>{item.productName}</span>;
};
전체코드
📍 컴파운드 컴포넌트 도입 결과
아래는 컴파운드 컴포넌트를 활용한 결과다. 깔끔하고 가독성이 좋게 변경된 것을 얼핏봐도 확인할 수 있다.
새로운 사용처에서 요구사항이 변경되면 항상 조건을 추가하기 바빴는데, 컴파운드컴포넌트를 통해 개발 효율도 올라간 것 같아 앞으로 많이 사용하지 않을까 싶다!
return (
<ListItem.Root
item={item}
checkHandler={handleSingleCheck}
checkedItems={checkedItems}
lastItemRef={lastItemRef}
>
<ListItem.CheckBox />
<ListItem.Image />
<div className="flex flex-col gap-1 justify-between w-full py-1">
<ListItem.Name />
<ListItem.Price />
<div className="flex items-end justify-between">
<ListItem.LeftQuantity />
<div className="flex gap-1">
<AdminBoardBtn name="미리보기" onClickHandler={onClickPreview} />
<AdminBoardBtn name="수정" onClickHandler={onClickEdit} />
<AdminBoardDeleteBtn
deleteHandler={() => onClickItemDelete(item.id)}
/>
</div>
</div>
</div>
</ListItem.Root>
);
'Tech > ReactJS' 카테고리의 다른 글
에러 바운더리를 사용한 선언적 에러 핸들링 (0) | 2024.05.21 |
---|---|
상세 상품 prefetch로 인한 불필요한 네트워크 요청 해결 (0) | 2024.05.20 |
Grid 아이템 이미지 로딩 후 리플로우 이슈 해결 (0) | 2024.05.06 |
결제 후 장바구니 속 아이템이 삭제되었다가 재생성되는 이슈 (0) | 2024.04.29 |
비동기적으로 동작하지만 비동기 함수는 아닌 setState (0) | 2024.04.13 |
🚨 고민 발생

해당 컴포넌트는 공통 컴포넌트로 만들어져있다.
여러 곳에서 사용되기 때문에 공통 컴포넌트로 분리했던 건데,, 문제가 생겼다.
사용처마다 스타일이나 요소 배치 등의 컴포넌트 요구사항이 조금씩 다르는 것이다.
결국 여러 조건을 걸 수밖에 없었고 아래와 같이 꽤 복잡한 공통 컴포넌트가 만들어졌다.
const ProductBoardItem = forwardRef<HTMLDivElement, IProductBoardItem>(
(
{ item, children, checkHandler, checkedItems, selectedCnt, isCart },
ref
) => {
return (
<div className={`flex py-2 ${isCart ? "gap-2" : "gap-5"}`} ref={ref}>
<Checkbox
onCheckedChange={(checked) => {
checkHandler(
checked as boolean,
selectedCnt
? { itemId: item.id, itemCount: selectedCnt }
: { itemId: item.id }
);
}}
checked={checkedItems.map((el) => el.itemId).includes(item.id)}
/>
<div className={`${isCart ? "w-32" : "w-28"} aspect-square`}>
<img
src={item.productThumbnail}
className="object-cover w-full h-full rounded-sm"
/>
</div>
<div className="flex flex-col gap-1 justify-between w-full py-1">
<span className={`${isCart ? "text-sm" : "text-md"}`}>
{item.productName}
</span>
{isCart ? (
<>
<span className={`${isCart ? "text-[11px]" : "text-sm"}`}>
{convertPriceUnit(item.productPrice)}원 · 남은수량:{" "}
{item.productQuantity}개
</span>
{children}
</>
) : (
<>
<span className={`${isCart ? "text-xs" : "text-sm"}`}>
{convertPriceUnit(item.productPrice)}원
</span>
<div className="flex items-end justify-between">
<p className="text-xs">남은수량: {item.productQuantity}개</p>
{children}
</div>
</>
)}
</div>
</div>
);
}
);
export default ProductBoardItem;
공통 컴포넌트 내부 변경되는 부분을 children으로 받아 렌더링시키니 코드 이해도도 많이 떨어진다고 느꼈다.
<ProductListItem
item={item}
ref={lastItemRef}
checkHandler={handleSingleCheck}
checkedItems={checkedItems}
>
<div className="flex gap-1">
<AdminBoardEditBtn productId={item.id} />
<AdminBoardDeleteBtn deleteHandler={() => onClickItemDelete(item.id)} />
</div>
</ProductListItem>;
📍 고민해결 방법
두가지 방법으로 해결할 수 있을 것 같았다.
1) HOC 사용
- 컴포넌트를 함수로 받아 새로운 컴포넌트 반환
- 컴포넌트 기능 확장에 유리
2) 컴파운드 컴포넌트 패턴 사용
- props로 내려오는 데이터 없이 내부 context를 사용하여 내부에서 데이터 처리
- context를 공유하는 작은 단위의 컴포넌트로 이루어져있어 자유롭게 사용 / 여러 하위 컴포넌트와 상태 공유에 유리
HOC는 기존 기능에 확장하는 경우 사용하는 게 좋을 것 같았고, 디자인적 자유도도 없어보였다.
좀 더 유연한 변경이 가능하면서도 컴포넌트의 기능적 가독성이 향상된 컴파운드 컴포넌트가 더 적합하다고 생각했다!
컴파운드 컴포넌트 패턴을 사용하자!!!
📍 컴파운드 컴포넌트 도입 과정
1. 내부 context 생성
데이터 공유를 위한 내부 context를 만든다.
const ProductListItemContext = createContext<IProductListItem>({
item: {
productThumbnail: "",
productImage: [],
productName: "",
productCategory: "",
productPrice: 0,
productQuantity: 0,
productDescription: "",
sellerId: "",
createdAt: "",
updatedAt: "",
id: "",
},
checkHandler: () => {},
checkedItems: [],
});
const useProductListItem = () => useContext(ProductListItemContext);
2. 최상위 컴포넌트 Root 생성
하위 컴포넌트들이 context에 접근할 수 있도록 생성한다.
const Root = ({
item,
children,
checkHandler,
checkedItems,
selectedCnt,
isCart,
lastItemRef,
gapSize,
}: IProductListItemWrapper) => {
const value = { item, checkHandler, checkedItems, selectedCnt, isCart };
return (
<ProductListItemContext.Provider value={value}>
<div className={`flex py-2 gap-${gapSize}`} ref={lastItemRef}>
{children}
</div>
</ProductListItemContext.Provider>
);
};
3. 기능별로 하위 컴포넌트 생성
const Image = ({ ...props }) => {
const { item } = useProductListItem();
return (
<div {...props}>
<img
src={item.productThumbnail}
className="object-cover w-full h-full rounded-sm"
/>
</div>
);
};
const Name = ({ ...props }) => {
const { item } = useProductListItem();
return <span {...props}>{item.productName}</span>;
};
전체코드
📍 컴파운드 컴포넌트 도입 결과
아래는 컴파운드 컴포넌트를 활용한 결과다. 깔끔하고 가독성이 좋게 변경된 것을 얼핏봐도 확인할 수 있다.
새로운 사용처에서 요구사항이 변경되면 항상 조건을 추가하기 바빴는데, 컴파운드컴포넌트를 통해 개발 효율도 올라간 것 같아 앞으로 많이 사용하지 않을까 싶다!
return (
<ListItem.Root
item={item}
checkHandler={handleSingleCheck}
checkedItems={checkedItems}
lastItemRef={lastItemRef}
>
<ListItem.CheckBox />
<ListItem.Image />
<div className="flex flex-col gap-1 justify-between w-full py-1">
<ListItem.Name />
<ListItem.Price />
<div className="flex items-end justify-between">
<ListItem.LeftQuantity />
<div className="flex gap-1">
<AdminBoardBtn name="미리보기" onClickHandler={onClickPreview} />
<AdminBoardBtn name="수정" onClickHandler={onClickEdit} />
<AdminBoardDeleteBtn
deleteHandler={() => onClickItemDelete(item.id)}
/>
</div>
</div>
</div>
</ListItem.Root>
);
'Tech > ReactJS' 카테고리의 다른 글
에러 바운더리를 사용한 선언적 에러 핸들링 (0) | 2024.05.21 |
---|---|
상세 상품 prefetch로 인한 불필요한 네트워크 요청 해결 (0) | 2024.05.20 |
Grid 아이템 이미지 로딩 후 리플로우 이슈 해결 (0) | 2024.05.06 |
결제 후 장바구니 속 아이템이 삭제되었다가 재생성되는 이슈 (0) | 2024.04.29 |
비동기적으로 동작하지만 비동기 함수는 아닌 setState (0) | 2024.04.13 |