[FIFAPulse] 개발기록 - TS의 MappedType으로 리팩토링 해보기

조민호·2023년 6월 6일
0


현재 상황

TradeLog.tsx 페이지는 이적시장 기록을 테이블 형태로 보여주는 컴포넌트이다

디자인이 완성된건 아니지만 대략적으로 아래와 같다

여기서 이적시장 목록에 필터링 기능도 추가가 됐으면 좋겠다는 생각이 들어서

select box를 이용해서 아래와 같은 필터링 목록을 추가 했다

  • 최신 순
  • 오래 된 순
  • 가격 오름차순
  • 가격 내림차순

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: '오래 된 순' },
	]}
/>

이적시장 관련 데이터

현재 이적시장 관련 데이터는 아래와 같이 사용하고 있다

  • 여기서, 이적시장 정보 api를 호출하게 되면 아래와 같이 받아와지고
    {
        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 },
    			...
        ],
     }
  • 위의 객체배열 안에 있는 요소의 타입을 TradeLogInfo로 지정했다
    // { "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;
    }
  • 그리고 이적시장 api 반환값은 tradeInfo라는 상태로 관리하고 있다
    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에 맞춰서 리팩토링을 진행했다

  • sort 로직을 sortFunctions객체에 따로 두고 select에서 선택된 상태값을 키값으로 둬서 대괄호 표기법으로 접근해서 바로 사용하는 것이다
  • 그리고 구매 목록인 buy 와 판매 목록인 sell 둘 다 sort를 해야 했으므로 ['buy', 'sell']에 forEach를 돌며 업데이트 할 대상을 키값으로 사용해서 접근한다
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]);





위의 리팩토링 과정에서 중요한 것 2가지


1. Mapped Type

원래 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를 반환하는 함수가 있는 것을 의미하게 된다



2. null 체크 및 type assertion

위에서 언급했듯이 , 현재 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' 중 하나라는 것을 보장하는 것이다

이렇게 되면 selectedValueSelectedValue 타입으로 간주한다

profile
웰시코기발바닥

0개의 댓글