constants 데이터를 효율적인 자료구조로 개선하기

문도연·2023년 12월 4일
0

같은글 노션링크 | 코드 변경 전후를 명확하게 보실수있습니다.

1. 자료구조?

1.1 리팩토링 이유

2. 효율적인 자료구조로 개선하기

3. 데이터를 사용할때 얼마나 편해졌는지 비교하기

3.1 게임페이지에서 src/page/GamePage.tsx
3.2 슬롯머신 커스텀 훅에서 src/hooks/useSlot.ts

4. 마치며


1. 자료구조?

데이터에 편리하게 접근하고 변경하기 위해 데이터를 저장하거나 조직하는 방법

1.1 리팩토링 이유

슬롯머신 기능을 구현하면서, 상수데이터인 MOVIE_SLOTOPTION를 가져다 쓸때마다 불편함을 느꼈다. 다시말해, 이 데이터를 쓰기 위해서 불필요한 코드들이 계속 추가됐다. 기능은 구현했으나, 찜찜했다.

함께 데이터를 가져다 쓸 동료들이 있다고 가정하고, 활용하기 편한 자료구조로 개선해보고자 한다.

2. 효율적인 자료구조로 개선하기

배열형태로 따로따로 관리하던 상수데이터를 한 객체에 담아 다같이 관리한다. 객체 데이터에 접근할때는 Object.xx 메서드를 활용하면 도니다.

  • // src/constants/slotOption.ts
    export const SLOT_MOVIE_COUNTRY: string[][] = [
      ['국내', 'K'],
      ['외국', 'F'],
    ];
    export const SLOT_MOVIE_YEAR: string[][] = [
      ['2022', '2022'],
      ['2021', '2021'],
      ['2020', '2020'],
      ['2019', '2019'],
      ['2018', '2018'],
    ];
    
    export const SLOT_MOVIE_TYPE: string[][] = [
      ['상업영화', 'N'],
      ['다양성영화', 'Y'],
    ];
  • // 한 객체에 담았다.
    export const MOVIE_SLOTOPTION = {
      country: {
        K: '국내',
        F: '외국',
      },
      year: {
        2022: '2022',
        2021: '2021',
        2020: '2020',
        2019: '2019',
        2018: '2018',
      },
      type: {
        N: '상업영화',
        Y: '다양성영화',
      },
    };

3. 데이터를 사용할때 얼마나 편해졌는지 비교하기

3.1 src/page/GamePage.tsx_게임페이지

    1. 일단 기능이 동작되게 하기 위해 불필요한 코드들이 존재함
    2. 리턴문에서 동일한 구조를 가진 Slot 로직이 반복되고 있는모습
import {
  SLOT_MOVIE_COUNTRY,
  SLOT_MOVIE_TYPE,
  SLOT_MOVIE_YEAR,
} from 'libs/constants/slotOptions';

export default function GamePage() {
  const COUNTRY = 'country';
  const YEAR = 'year';
  const TYPE = 'type';

  const option = { country: '', type: '', year: '' };

  const constants = [
    [COUNTRY, SLOT_MOVIE_COUNTRY],
    [YEAR, SLOT_MOVIE_TYPE],
    [YEAR, SLOT_MOVIE_YEAR],
  ];
  ...

  return (
    <section>
     ...

      <div css={slot.container}>
        <div css={slot.flexColumn}>
          <div css={slot.spinningSquare}>
            <Slot
              name={COUNTRY}
              option={SLOT_MOVIE_COUNTRY}
              isFirstEntry={isFirstEntry}
              isSpinning={isSpinning}
              getSelectedOption={getSelectedOption}
              css={slot.spinningText}
            />
          </div>
          <Button
            aria-label={COUNTRY}
            disabled={!isSpinning[COUNTRY]}
            onClick={stopSpinning(COUNTRY)}
            css={slot.button}
          ></Button>
        </div>
        <div css={slot.flexColumn}>
          <div css={slot.spinningSquare}>
            <Slot
              name={YEAR}
              option={SLOT_MOVIE_YEAR}
              isFirstEntry={isFirstEntry}
              isSpinning={isSpinning}
              getSelectedOption={getSelectedOption}
              css={slot.spinningText}
            />
          </div>
          <Button
            aria-label={YEAR}
            disabled={!isSpinning[YEAR]}
            onClick={stopSpinning(YEAR)}
            css={slot.button}
          ></Button>
        </div>
        <div css={slot.flexColumn}>
          <div css={slot.spinningSquare}>
            <Slot
              name={TYPE}
              option={SLOT_MOVIE_TYPE}
              isFirstEntry={isFirstEntry}
              isSpinning={isSpinning}
              getSelectedOption={getSelectedOption}
              css={slot.spinningText}
            />
          </div>
          <Button
            aria-label={TYPE}
            disabled={!isSpinning[TYPE]}
            onClick={stopSpinning(TYPE)}
            css={slot.button}
          ></Button>
        </div>
      </div>
    </section>
  );
}
    1. 슬롯 옵션 데이터(MOVIE_SLOTOPION) 만 useSlot 훅에 넘겨주기만하면 된다.
      • 동일한 구조의 데이터만 넘겨주면 다른 테마의 게임도 쉽게 만들수 있다. (재사용성 up)
    2. 리턴문의 Slot 컴포넌트 로직이 3번 하드코딩 하던 것을 Object.entriesmap을 사용해 코드 길이를 줄였다. (가독성 up)

import { MOVIE_SLOTOPTION } from 'libs/constants/slotOptions';

export default function GamePage() {
  const {
    selected,
    isFirstEntry,
    isSpinning,
    getSelectedOption,
    startSpinning,
    stopSpinning,
    initEntryStateAndSelection,
  } = useSlot({ slotOption: MOVIE_SLOTOPTION });

  ...

  return (
    <section>
    ...
      <div css={slot.container}>
        {Object.entries(MOVIE_SLOTOPTION).map(([name, value]) => {
          return (
            <div key={name} css={slot.flexColumn}>
              <div css={slot.spinningSquare}>
                <Slot
                  name={name}
                  option={value}
                  isFirstEntry={isFirstEntry}
                  isSpinning={isSpinning}
                  getSelectedOption={getSelectedOption}
                  css={slot.spinningText}
                />
              </div>
              <Button
                aria-label={name}
                disabled={!isSpinning[name]}
                onClick={stopSpinning(name)}
                css={slot.button}
              ></Button>
            </div>
          );
        })}
      </div>
    </section>
  );
}

3.2 src/hooks/useSlot.ts _슬롯머신 커스텀 훅

  • getSelectedOption 함수 사용시
    • 로직의 가독성이 높아졌다.
    • 업데이트할 state값을 찾기가 쉬워졌다.
    • 해당 함수는 유저가 뽑은 슬롯옵션의 이름(국가 or 연도 or 타입)과 옵션 중 선택된 값(외국-한국, 2018-2022, Y-N)의 인덱스넘버를 매개변수로 전달받는다. 매개변수를 토대로 상수데이터로부터 유저가 뽑은 값을 찾아 state를 업데이트한다.
  • 함수는 배열을 순회하면서 조건에 일치하는 데이터를 찾는다. 내부 로직을 보면 한눈에 그 의미를 이해하기 어렵다.
     interface Props {
       option: Record<string, string>;
       constants: (string | string[][])[][];
     }
    
    function useSlot({ option, constants }: Props) {
      const initial = option;
    
      const isSpin: Record<string, boolean> = {};
      const keys = Object.keys(option);
      keys.forEach(key => (isSpin[key] = false));
    
      const [selected, setSelected] = useState(option);
      const [isSpinning, setIsSpinning] = useState(isSpin);
      const [isFirstEntry, setIsFirstEntry] = useState(true);
    
     ...
    
      const getSelectedOption = (name: string, num: number) => { // 여기
        const selectedCon = constants.filter(con => {
          if (con[0] === name) return con[1];
        })[0][1];
    
        setSelected(prev => ({
          ...prev,
          [name]: selectedCon[num][1],
        }));
      };
    
      return {
        ...
      };
    }
  • 후: 객체[key] 형태로 조건에 맞는 상수데이터에 바로 접근할 수있게됨, Object.keys() 메서드를 사용해 꺼내야하는 데이터로 범위를 좁힌뒤 인덱스 번호 num을 적용. (성능 up, 가독성up)
    interface Props {
      slotOption: Record<string, Record<string, string>>;
    }
    
    function useSlot({ slotOption }: Props) {
      const initOption: Record<string, string> = {};
      const isSpin: Record<string, boolean> = {};
    
      const keys = Object.keys(slotOption);
    
      keys.forEach(key => (initOption[key] = ''));
      keys.forEach(key => (isSpin[key] = false));
    
      const initial = { ...initOption };
      const [selected, setSelected] = useState(initial);
      const [isSpinning, setIsSpinning] = useState(isSpin);
      const [isFirstEntry, setIsFirstEntry] = useState(true);
    
      ...
    
    const getSelectedOption = (name: string, num: number) => { // 여기
        const selected = Object.keys(slotOption[name])[num];
    
        setSelected(prev => ({
          ...prev,
          [name]: selected,
        }));
      };
    
      ...
    
      return {
       ...
      };
    }
    export default useSlot;

4. 마치며

  • 가독성, 코드길이, 성능* 면에서 개선됐다. 성능*: 배열 → 객체 데이터 구조로 바꾸면서, 객체[조건] 로직을 통해 배열을 순회하지 않고도 조건에 맞는 데이터에 바로 접근할 수 있게됨
  • 어차피 함께 다닐 (비슷하게 생기고, 비슷한 곳에 사용될 가능성이 높은) 데이터들은 따로 관리하는 게 아니라 객체라는 하나의 가두리에 담아서 사용하자~
  • 배열 / 객체 형태로 관리할때의 장단점 등을 고려해서 데이터 구조를 짜자
  • 80줄짜리 코드를 20줄로, 한눈에 알아보기 어려운 코드를 개선하다니, 기뻣다!
profile
중요한건 꺾이지 않는 마음이 맞는 것 같습니다

0개의 댓글