TradeLog.tsx 페이지는 이적시장 기록을 테이블 형태로 보여주는 컴포넌트이다
디자인이 완성된건 아니지만 대략적으로 아래와 같다
여기서 이적시장 목록에 필터링 기능도 추가가 됐으면 좋겠다는 생각이 들어서
select box를 이용해서 아래와 같은 필터링 목록을 추가 했다
나는 TradeLog.tsx 컴포넌트에서 select박스를 mantine라이브러리로 사용하고 있다
그래서 select 관련 상태와 mantine라이브러리를 선언했다
const [selectedValue, setSelectedValue] = useState<string | null>('latest');
<Select
value={selectedValue}
onChange={setSelectedValue}
transitionProps={{ transition: 'pop-top-left', duration: 80, timingFunction: 'ease' }}
radius="md"
data={[
{ value: 'priceDown', label: '가격 내림차순' },
{ value: 'priceUp', label: '가격 오름차순' },
{ value: 'latest', label: '최신순' },
{ value: 'oldest', label: '오래 된 순' },
]}
/>
현재 이적시장 관련 데이터는 아래와 같이 사용하고 있다
{
buy: [ // 구매 목록
{ tradeDate: '2023-03-22T18:55:07', saleSn: '641acd4ea430e79103bdb95e', spid: 279228251, grade: 5, value: 13300000000 },
{ tradeDate: '2023-03-22T18:55:07', saleSn: '641acd4ea430e79103bdb95e', spid: 279228251, grade: 5, value: 13300000000 },
...
],
sell: [ // 판매 목록
{ tradeDate: '2023-03-22T18:55:07', saleSn: '641acd4ea430e79103bdb95e', spid: 279228251, grade: 5, value: 13300000000 },
{ tradeDate: '2023-03-22T18:55:07', saleSn: '641acd4ea430e79103bdb95e', spid: 279228251, grade: 5, value: 13300000000 },
...
],
}
// { "tradeDate": "2023-05-31T01:07:25",
// "saleSn": "64761f3cad134d6873034f84",
// "spid": 265204024,
// "grade": 1,
// "value": 499000000 }
interface TradeLogInfo {
tradeDate: string;
saleSn: string;
spid: number;
grade: number;
value: number;
}
const [tradeInfo, setTradeInfo] = useState<{ buy: TradeLogInfo[]; sell: TradeLogInfo[] } | null>(null);
이제 select에서 선택된 상태 selectedValue에 따라 이적시장 정보를 재정렬해서 보여주면 된다
JSX부분에서 selectedValue에 따라 조건부 로직을 통해 tradeInfo를 각각 sort해주면 되지만
그렇게 하지 않고 useEffect를 통해 selectedValue가 변경되면 tradeInfo 상태를 sort해줘서
업데이트 해주는 방식을 사용했다
이렇게 하면 굳이 JSX에 번거로운 코드를 작성할 필요 없이 깔끔하게 sort된 tradeInfo 상태를 그대로 사용하면 되기 때문이다
최초에는 아래와 같이 작성했다
사실 selectedValue에 맞춘 sort를 진행하는게 전부이며 가장 직관적으로 작성했다
useEffect(() => {
if (!tradeInfo) return;
const copyTradeInfo = JSON.parse(JSON.stringify(tradeInfo)); //sort는 원본을 바꾸므로
if (selectedValue === 'priceDown') {
copyTradeInfo.buy.sort((a: TradeLogInfo, b: TradeLogInfo) => {
return b.value - a.value;
});
copyTradeInfo.sell.sort((a: TradeLogInfo, b: TradeLogInfo) => {
return b.value - a.value;
});
}
if (selectedValue === 'priceUp') {
copyTradeInfo.buy.sort((a: TradeLogInfo, b: TradeLogInfo) => {
return a.value - b.value;
});
copyTradeInfo.sell.sort((a: TradeLogInfo, b: TradeLogInfo) => {
return a.value - b.value;
});
}
if (selectedValue === 'latest') {
copyTradeInfo.buy.sort((a: TradeLogInfo, b: TradeLogInfo) => {
return new Date(b.tradeDate).getTime() - new Date(a.tradeDate).getTime();
});
copyTradeInfo.sell.sort((a: TradeLogInfo, b: TradeLogInfo) => {
return new Date(b.tradeDate).getTime() - new Date(a.tradeDate).getTime();
});
}
if (selectedValue === 'oldest') {
copyTradeInfo.buy.sort((a: TradeLogInfo, b: TradeLogInfo) => {
return new Date(a.tradeDate).getTime() - new Date(b.tradeDate).getTime();
});
copyTradeInfo.sell.sort((a: TradeLogInfo, b: TradeLogInfo) => {
return new Date(a.tradeDate).getTime() - new Date(b.tradeDate).getTime();
});
}
setTradeInfo(copyTradeInfo);
}, [selectedValue]);
작동은 잘 되지만 , 동일한 코드가 너무 반복되는 것 같아서
리팩토링을 해보고 싶었다
TS에 맞춰서 리팩토링을 진행했다
useEffect(() => {
if (!tradeInfo) return;
const copyTradeInfo = JSON.parse(JSON.stringify(tradeInfo));
type SelectedValue = 'priceDown' | 'priceUp' | 'latest' | 'oldest';
const sortFunctions: { [K in SelectedValue]: (a: TradeLogInfo, b: TradeLogInfo) => number } = {
priceDown: (a, b) => b.value - a.value,
priceUp: (a, b) => a.value - b.value,
latest: (a, b) => new Date(b.tradeDate).getTime() - new Date(a.tradeDate).getTime(),
oldest: (a, b) => new Date(a.tradeDate).getTime() - new Date(b.tradeDate).getTime(),
};
['buy', 'sell'].forEach((i) => {
// selectedValue가 null인 경우를 체크하고 type assertion을 사용
if (selectedValue && sortFunctions[selectedValue as SelectedValue]) {
copyTradeInfo[i].sort(sortFunctions[selectedValue as SelectedValue]);
}
});
setTradeInfo(copyTradeInfo);
}, [selectedValue]);
원래 TS가 아닌 JS였다면 코드는 아래와 같을 것이다
useEffect(() => {
if (!tradeInfo) return;
const copyTradeInfo = JSON.parse(JSON.stringify(tradeInfo));
const sortFunctions = {
priceDown: (a, b) => b.value - a.value,
priceUp: (a, b) => a.value - b.value,
latest: (a, b) => new Date(b.tradeDate).getTime() - new Date(a.tradeDate).getTime(),
oldest: (a, b) => new Date(a.tradeDate).getTime() - new Date(b.tradeDate).getTime(),
};
['buy', 'sell'].forEach((type) => {
if (sortFunctions[selectedValue]) {
copyTradeInfo[type].sort(sortFunctions[selectedValue]);
}
});
setTradeInfo(copyTradeInfo);
}, [selectedValue]);
그렇지만 sortFunctions 에 사용되는 각 함수들은 아래와 같이 인자의 타입과 , 리턴 타입또한 명시가 되어야 한다
{
priceDown: (a: TradeLogInfo, b: TradeLogInfo) => number,
priceUp: (a: TradeLogInfo, b: TradeLogInfo) => number,
latest: (a: TradeLogInfo, b: TradeLogInfo) => number,
oldest: (a: TradeLogInfo, b: TradeLogInfo) => number,
}
따로 함수의 타입을 지정해준 다음에 사용해도 무방하지만
이를 보다 간편하게 사용하기 위해 Mapped Type 을 적용했다
type SelectedValue = 'priceDown' | 'priceUp' | 'latest' | 'oldest';
// Mapped Type 적용
const sortFunctions: { [K in SelectedValue]: (a: TradeLogInfo, b: TradeLogInfo) => number } = {
priceDown: (a, b) => b.value - a.value,
priceUp: (a, b) => a.value - b.value,
latest: (a, b) => new Date(b.tradeDate).getTime() - new Date(a.tradeDate).getTime(),
oldest: (a, b) => new Date(a.tradeDate).getTime() - new Date(b.tradeDate).getTime(),
};
SelectedValue에 있는 값들을 키값으로 사용하며
그 값으로는 TradeLogInfo타입의 a,b인자를 가지며 number를 반환하는 함수가 있는 것을 의미하게 된다
위에서 언급했듯이 , 현재 select에 사용되고 있는 상태는
아래와 같이 선언이 되어 있다
const [selectedValue, setSelectedValue] = useState<string | null>('latest');
다만 mantine라이브러리의 특징 때문에 어쩔 수 없이
useState의 타입을 <string | null> 로 지정해야한 했다
그러므로 null일 수 있기 때문에 조건문에서
if (selectedValue && …)
를 사용했다
또한 , selectedValue가 string | null 타입인데
sortFunctions는 'priceDown' | 'priceUp' | 'latest' | 'oldest' 이라는 타입의 키로 정의되어 있기 때문에
sortFunctions[selectedValue]
같은 형태로 사용하게 되면 에러를 발생시킨다
이런 경우에는 다른 해결 방법들이 많지만
mantine라이브러리는 useState의 타입을 <string | null> 로 강제하고 있기에 useState의 타입을 바꿀 순 없었다
그러므로 type Assertion을 사용해서
selectedValue가 string이며 , 무조건
'priceDown' | 'priceUp' | 'latest' | 'oldest' 중 하나라는 것을 보장하는 것이다
이렇게 되면 selectedValue를 SelectedValue 타입으로 간주한다