테이블을 만들고, 각 row에 하위 데이터들을 펼쳤다 닫았다 볼 수 있는 토글 형태의 테이블 리스트를 만들었다.
한 페이지에서 만들었는데, 생각보다 여기저기 사용할 일이 많길래 custom hooks로 만들어보았다!
우선 리팩토링 전 기본 컴포넌트 코드를 살펴보면 아래와 같다
const ProductPvInfoTable = ({ pvs }: P) => {
const [openedPvIds, setOpenedPvIds] = useState<number[]>([]);
const handlePvOpen = (id: number) => {
setOpenedPvIds([...openedPvIds, id]);
};
const handlePvClose = (id: number) => {
setOpenedPvIds(openedPvIds.filter((openPvId) => openPvId !== id));
};
return (
<Container gap={16}>
<ContentH2Title>PV 정보</ContentH2Title>
<FlexEndBox>
{openedPvIds.length ? (
<button
onClick={() => {
setOpenedPvIds([]);
}}
>
<FlexBox gap={10}>
<span>접기</span> <ArrowDownIcon width={18} />
</FlexBox>
</button>
) : (
<button
onClick={() => {
setOpenedPvIds(pvs.map((pv) => pv.id));
}}
>
<FlexBox gap={10}>
<span>모두 펼치기</span> <ArrowRightIcon width={18} />
</FlexBox>
</button>
)}
</FlexEndBox>
<Table clickable>
<thead>
<tr>
{TABLE_ROW.map((column) => (
<th key={column}>{column}</th>
))}
</tr>
</thead>
<tbody>
{pvs.map((pv) => (
<>
<tr key={pv.id}>
<td>
<SpaceBetweenBox>
{pv.skus.length}
{openedPvIds.includes(pv.id) ? (
<button onClick={() => handlePvClose(pv.id)}>
<ArrowDownIcon />
</button>
) : (
<button onClick={() => handlePvOpen(pv.id)}>
<ArrowRightIcon />
</button>
)}
</SpaceBetweenBox>
</td>
<td>{pv.id}</td>
<td>브랜드명</td>
<td>상품명</td>
<td>모델명</td>
<td>{pv.colorId}</td>
</tr>
{openedPvIds.includes(pv.id) && (
<SkuTr>
<td colSpan={TABLE_ROW.length}>
<SkuTableContainer>
<h2> 하위 sku 정보</h2>
<SkuTable>
<thead>
<tr>
{TABLE_ROW_SKU.map((column) => (
<th key={column}>{column}</th>
))}
</tr>
</thead>
<tbody>
{pv.skus.map((sku) => (
<tr key={sku.id}>
<td>{sku.id}</td>
<td>{'공급사'}</td>
<td>{sku.stock}</td>
<td>{sku.isVisible ? 'Y' : 'N'}</td>
</tr>
))}
</tbody>
</SkuTable>
</SkuTableContainer>
</td>
</SkuTr>
)}
</>
))}
</tbody>
</Table>
</Container>
);
};
다시보니 엄청 지저분하군요 ㅋ
import { useState } from 'react';
export const useToggleList = (items: any[]) => {
const [openedIds, setOpenedIds] = useState<number[]>([]);
};
const isOpened = (itemId: number) => openedIds.includes(itemId);
const isAllOpened = openedIds.length === items.length;
const toggleItem = (itemId: number) => {
if (isOpened(itemId)) {
setOpenedIds(openedIds.filter((id) => id !== itemId));
} else {
setOpenedIds([...openedIds, itemId]);
}
};
const toggleAllItems = () => {
if (isAllOpened) {
setOpenedIds([]);
} else {
setOpenedIds(items.map((item) => item.id));
}
};
import { useState } from 'react';
export const useToggleList = (items: any[]) => {
const [openedIds, setOpenedIds] = useState<number[]>([]);
const isOpened = (itemId: number) => openedIds.includes(itemId);
const isAllOpened = openedIds.length === items.length;
const toggleItem = (itemId: number) => {
if (isOpened(itemId)) {
setOpenedIds(openedIds.filter((id) => id !== itemId));
} else {
setOpenedIds([...openedIds, itemId]);
}
};
const toggleAllItems = () => {
if (isAllOpened) {
setOpenedIds([]);
} else {
setOpenedIds(items.map((item) => item.id));
}
};
return { toggleItem, toggleAllItems, isOpened, isAllOpened, openedIds };
};
// before
const [openedPvIds, setOpenedPvIds] = useState<number[]>([]);
const handlePvOpen = (id: number) => {
setOpenedPvIds([...openedPvIds, id]);
};
const handlePvClose = (id: number) => {
setOpenedPvIds(openedPvIds.filter((openPvId) => openPvId !== id));
};
// after
const { toggleItem, toggleAllItems, isOpened, isAllOpened } =
useToggleList(pvs);
// before
{openedPvIds.length ? (
<button
onClick={() => {
setOpenedPvIds([]);
}}
>
<FlexBox gap={10}>
<span>접기</span> <ArrowDownIcon width={18} />
</FlexBox>
</button>
) : (
<button
onClick={() => {
setOpenedPvIds(pvs.map((pv) => pv.id));
}}
>
<FlexBox gap={10}>
<span>모두 펼치기</span> <ArrowRightIcon width={18} />
</FlexBox>
</button>
)}
// after
<button onClick={toggleAllItems}>
<FlexBox gap={10}>
{isAllOpened ? (
<>
<span>접기</span> <ArrowDownIcon width={18} />
</>
) : (
<>
<span>모두 펼치기</span> <ArrowRightIcon width={18} />
</>
)}
</FlexBox>
</button>
// before
<td>
<SpaceBetweenBox>
{pv.skus.length}
{openedPvIds.includes(pv.id) ? (
<button onClick={() => handlePvClose(pv.id)}>
<ArrowDownIcon />
</button>
) : (
<button onClick={() => handlePvOpen(pv.id)}>
<ArrowRightIcon />
</button>
)}
</SpaceBetweenBox>
</td>
// after
<td>
<SpaceBetweenBox>
{pv.skus.length}
<button onClick={() => toggleItem(pv.id)}>
{isOpened(pv.id) ? <ArrowDownIcon /> : <ArrowRightIcon />}
</button>
</SpaceBetweenBox>
</td>
각각 컴포넌트의 역할이 더 눈에 잘보이고, 코드의 가독성 또한 높아졌다!
그리고 같은 기능을 하는 다른 컴포넌트에서 또 한번 함수 정의를 하지 않아도 되고 쓰고 싶을떄 마다 꺼내쓰기 가능