우선 기간안에 완성이냐 클린코드냐 그것이 문제로다

SuweonPark·2023년 11월 22일
1

대시보드

목록 보기
4/5


대시보드 프로젝트를 하면서 위젯 크기 수정,삭제 기능이 있었습니다
예를들어 2x2 크기의 위젯을 2x3 이라든지 3x4로 크기를 수정하는 기능과 삭제하는 기능이죠. 이 기능을 만들당시 시간이 너무 촉박하여 일단 기능이 제대로 돌아가도록 만들고 납품하자! 라는 마인드로 하드코딩을 했었던 거 같아요.. 그 결과 코드가 1600줄이 넘어갔죠! 하하🫠 일단 버그가 없이 제대로 동작해서 다행이지만
너무나 리팩토링을 하고싶어서 최근에 코드를 수정했습니다. 그 결과
500줄로 줄이게 되었습니다!!

그래서 이번엔 그 과정들을 설명해보려고 합니다.

react-grid-layout의 resize

react-grid-layout은 기본적으로 resize모드를 제공합니다 사용자가 위젯에 붙어있는 핸들러를 가지고 자유롭게 리사이즈를 할 수 있죠 하지만 저희 서비스에서는 위젯의 크기에 따라 보여지는 정보가 다르고 사용자가 위젯 크기를 선택하여 바꾸는 방식을 원했습니다 이렇게요

    const layout = [
      { i: "a", x: 0, y: 0, w: 1, h: 2, static: true },
      { i: "b", x: 1, y: 0, w: 3, h: 2, minW: 2, maxW: 4 },
      { i: "c", x: 4, y: 0, w: 1, h: 2 }
    ];
    return (
      <GridLayout
        className="layout"
        layout={layout}
        cols={12}
        rowHeight={30}
        width={1200}
      >
        <div key="a">a</div>
        <div key="b">b</div>
        <div key="c">c</div>
      </GridLayout>
    );

간단한 예시코드입니다 layout 부분에서 w가 위젯의 가로폭, h가 위젯의 세로폭 입니다. 이 w, h 값을 직접 수정해주면 버튼형식으로구현이 가능합니다.

위젯크기 수정 리팩토링

이전 코드

  const [checkedWeather, setCheckedWeather] = useState('');
  const [checkedWeather2, setCheckedWeather2] = useState(true);
  const [checkedDay, setCheckedDay] = useState('');
  const [checkedDay2, setCheckedDay2] = useState(true);
  const [checkedProcess, setCheckedProcess] = useState('');
  const [checkedProcess2, setCheckedProcess2] = useState(true);
  const [checkedCCtv, setCheckedCCtv] = useState('');
  const [checkedCCtv2, setCheckedCCtv2] = useState(true);
  const [checkedDeparture, setCheckedDeparture] = useState('');
  const [checkedDeparture2, setCheckedDeparture2] = useState(true);
  const [checkedLocation, setCheckedLocation] = useState('');
  const [checkedLocation2, setCheckedLocation2] = useState(true);
  const [checkedLocationD, setCheckedLocationD] = useState('');
  const [checkedLocationD2, setCheckedLocationD2] = useState(true);
  const [checkedEquipment, setCheckedEquipment] = useState('');
  const [checkedEquipment2, setCheckedEquipment2] = useState(true);
  const [checkedArea, setCheckedArea] = useState('');
  const [checkedArea2, setCheckedArea2] = useState(true);
  const [widgetType, setWidgetType] = useState([]);

  const updateWidgetSize = (type, w, h, type2) => {
    const deleteWidget = () => {
      const filteredData = layoutState.filter(item => item.i !== type);
      setWidgetType([...widgetType, type]);
      const resultArray = filteredData.filter(
        item => ![...widgetType, type].includes(item.i),
      );
      setLayoutState(resultArray);
    };

    if (checkedWeather === type2 && checkedWeather2) {
      deleteWidget();
      setCheckedWeather2(false);
      return;
    }
    if (checkedDay === type2 && checkedDay2) {
      deleteWidget();
      setCheckedDay2(false);
      return;
    }
    if (checkedLocation === type2 && checkedLocation2) {
      deleteWidget();
      setCheckedLocation2(false);
      return;
    }
    if (checkedLocationD === type2 && checkedLocationD2) {
      deleteWidget();
      setCheckedLocationD2(false);
      return;
    }
    if (checkedDeparture === type2 && checkedDeparture2) {
      deleteWidget();
      setCheckedDeparture2(false);
      return;
    }
    if (checkedCCtv === type2 && checkedCCtv2) {
      deleteWidget();
      setCheckedCCtv2(false);
      return;
    }
    if (checkedProcess === type2 && checkedProcess2) {
      deleteWidget();
      setCheckedProcess2(false);
      return;
    }
    if (checkedEquipment === type2 && checkedEquipment2) {
      deleteWidget();
      setCheckedEquipment2(false);
      return;
    }
    if (checkedArea === type2 && checkedArea2) {
      deleteWidget();
      setCheckedArea2(false);
      return;
    }

    const cleanedData2 = layoutState.map(item => {
      // 필요없는 속성을 제거한 객체 생성
      const cleanedItem = { ...item };
      delete cleanedItem.isBounded;
      delete cleanedItem.isDraggable;
      delete cleanedItem.isResizable;
      delete cleanedItem.maxH;
      delete cleanedItem.maxW;
      delete cleanedItem.minH;
      delete cleanedItem.minW;
      delete cleanedItem.moved;
      delete cleanedItem.resizeHandles;
      delete cleanedItem.static;
      return cleanedItem;
    });
    const updatedData = cleanedData2.map(item => {
      if (item.i === type) {
        return { ...item, w, h };
      }
      return item;
    });
    if (type === 'location_device') {
      setMapRenderState(false);
      setCheckedLocationD2(true);
      setWidgetType(widgetType.filter(item => item !== type));
      setTimeout(() => {
        setMapRenderState(true);
      }, 500);
    }
    if (type === 'location_region') {
      setMapRenderState(false);
      setCheckedLocation2(true);
      setWidgetType(widgetType.filter(item => item !== type));
      setTimeout(() => {
        setMapRenderState(true);
      }, 500);
    }
    if (type === 'weather') {
      setWeatherSize(`w_${w}by${h}`);
      setCheckedWeather2(true);
      setWidgetType(widgetType.filter(item => item !== type));
    }
    if (type === 'dday') {
      setCheckedDay2(true);
      setWidgetType(widgetType.filter(item => item !== type));
    }
    if (type === 'accessmanagement') {
      setCheckedDeparture2(true);
      setWidgetType(widgetType.filter(item => item !== type));
    }
    if (type === 'processrate') {
      setCheckedProcess2(true);
      setProcessrateSize(`w_${w}by${h}`);
      setWidgetType(widgetType.filter(item => item !== type));
    }
    if (type === 'cctv') {
      setCheckedCCtv2(true);
      setCCTVSize(`w_${w}by${h}`);
      setWidgetType(widgetType.filter(item => item !== type));
    }
    if (type === 'healthmonitor') {
      setCheckedEquipment2(true);
      setWidgetType(widgetType.filter(item => item !== type));
    }
    if (type === 'sitestatus') {
      setCheckedArea2(true);
      setWidgetType(widgetType.filter(item => item !== type));
    }

    if (updatedData.some(item => item.i !== type)) {
      setLayoutState([...updatedData, { i: type, w, h, x: 0, y: 0 }]);
      return;
    }
    setLayoutState(updatedData);
  };

정말 if문 지옥이고 숨이 턱 막히는 코드입니다.😇
저는 일단 저 많은 if문을 없애고 싶어서 더 효율적으로 관리할 수 있는 방법이 있는지 고민을 해보았습니다.

    if (checkedWeather === type2 && checkedWeather2) {
      deleteWidget();
      setCheckedWeather2(false);
      return;
    }

위젯을 삭제할지 말지 체크하는 코드입니다. checkedWeather은
해당 위젯이 체크가 되어있다면 이름을 스트링값으로 저장하고 checkedWeather2값은 불리언으로 한번 체크가 되면 true상태가 됩니다 즉 checkedWeather의 값이 props로 넘겨 받은 type2 값과 checkedWeather2값이 true라면 위젯을 삭제해주라는 코드 입니다.

<input
  type="radio"
  name="weather"
  id="weather_01"
  onChange={() => {
    changeHandler('weather_01');
  }}
  checked={
    checkedWeather === 'weather_01' && checkedWeather2
  }
  />
<label
  htmlFor="weather_01"
  onClick={() =>
  updateWidgetSize('weather', 1, 1, 'weather_01')
          }
  >

changeHandler에서 checkedWeather값을 'weather_01' 설정하고 updateWidgetSize 함수 내부에서 checkedWeather2값을 설정해줍니다. 이런식으로 버튼이 눌려있는지 눌려있지 않는지 구분을 했었는데 코드가 너무 비효율적이고
확장성과 추상화가 잘 안되어있다고 생각이 들었습니다.

서버에서 받아오는 데이터는 위젯 타입만 알 수 있고 고유한 id값은 확인할 수 없었습니다.

서버에서 받아오는 데이터 layoutState

[
{w: 2, h: 3, x: 0, y: 0, i: 'location_device',}
,
{w: 1, h: 1, x: 2, y: 0, i: 'dday',}
,
{w: 2, h: 1, x: 0, y: 3, i: 'weather',}
,
{w: 1, h: 1, x: 2, y: 2, i: 'accessmanagement',}
,
{w: 1, h: 4, x: 3, y: 0, i: 'cctv',}
,
{w: 1, h: 1, x: 2, y: 1, i: 'processrate',}
,
{w: 1, h: 1, x: 2, y: 3, i: 'healthmonitor',}
]

현재 체크되어있는 위젯의 정보를 넘겨주고 있지만 제가 필요한 것은 위젯 id 입니다. 그래서 디렉토리에 data.js 생성 후

export const widgetLayoutId = [
  { id: 'weather_01', i: 'weather', w: 1, h: 1 },
  { id: 'weather_02', i: 'weather', w: 1, h: 2 },
  { id: 'weather_03', i: 'weather', w: 2, h: 1 },
  { id: 'day_01', i: 'dday', w: 1, h: 1 },
  { id: 'process_01', i: 'processrate', w: 1, h: 1 },
  { id: 'process_06', i: 'processrate', w: 1, h: 2 },
  { id: 'cctv_01', i: 'cctv', w: 1, h: 1 },
  { id: 'cctv_02', i: 'cctv', w: 2, h: 1 },
  { id: 'cctv_03', i: 'cctv', w: 3, h: 1 },
  { id: 'cctv_04', i: 'cctv', w: 4, h: 1 },
  { id: 'cctv_05', i: 'cctv', w: 2, h: 2 },
  { id: 'cctv_06', i: 'cctv', w: 1, h: 2 },
  { id: 'cctv_07', i: 'cctv', w: 1, h: 4 },
  { id: 'd_info_01', i: 'accessmanagement', w: 1, h: 1 },
  { id: 'location_01', i: 'location_region', w: 2, h: 2 },
  { id: 'location_02', i: 'location_region', w: 2, h: 3 },
  { id: 'location_03', i: 'location_region', w: 2, h: 4 },
  { id: 'location_05', i: 'location_region', w: 3, h: 3 },
  { id: 'location_06', i: 'location_region', w: 3, h: 4 },
  { id: 'location_07', i: 'location_region', w: 4, h: 2 },
  { id: 'location_08', i: 'location_region', w: 4, h: 3 },
  { id: 'location_09', i: 'location_region', w: 4, h: 4 },
  { id: 'e_info_01', i: 'location_device', w: 2, h: 2 },
  { id: 'e_info_02', i: 'location_device', w: 2, h: 3 },
  { id: 'e_info_03', i: 'location_device', w: 2, h: 4 },
  { id: 'e_info_05', i: 'location_device', w: 3, h: 3 },
  { id: 'e_info_06', i: 'location_device', w: 3, h: 4 },
  { id: 'e_info_07', i: 'location_device', w: 4, h: 2 },
  { id: 'e_info_08', i: 'location_device', w: 4, h: 3 },
  { id: 'e_info_09', i: 'location_device', w: 4, h: 4 },
  { id: 'equip_status_01', i: 'healthmonitor', w: 1, h: 1 },
  { id: 'equip_status_02', i: 'healthmonitor', w: 1, h: 2 },
  { id: 'area_01', i: 'sitestatus', w: 2, h: 1 },
];

크기별로 id값을 지정해서 배열로 만들어주고

  const [matchedIds, setMatchedIds] = useState([]);

  const findMatchedIds = () => {
    const matched = [];
    layoutState.forEach(secondItem => {
      widgetLayoutId.forEach(firstItem => {
        if (
          firstItem.i === secondItem.i &&
          firstItem.w === secondItem.w &&
          firstItem.h === secondItem.h
        ) {
          matched.push(firstItem.id);
        }
      });
    });
    setMatchedIds(matched);
  };

  useEffect(() => {
    findMatchedIds();
  }, [layoutState]);

서버에서 layoutState를 받아오면 layoutState와 widgetLayoutid를 서로 비교 후 매칭이 되는 위젯 id값을 matchedIds 값에 설정했습니다.

이렇게 하면 일일이 if문을 사용해 모든 위젯의 아이디 값을 설정해서 비교해줄 필요가 사라졌습니다.

개선된 코드

  const updateWidgetSize = (type, w, h, type2) => {
    if (matchedIds.includes(type2)) {
      deleteWidget(type, type2);
      return;
    }
    const updatedData = cleanedData.map(item => {
      if (item.i === type) {
        return { ...item, w, h };
      }
      return item;
    });

    switch (type) {
      case 'location_device':
      case 'location_region':
        setMapRenderState(false);
        setMatchedIds([...matchedIds, type2]);
        setWidgetType(widgetType.filter(item => item !== type));
        setTimeout(() => {
          setMapRenderState(true);
        }, 500);
        break;

      case 'weather':
        setWeatherSize(`w_${w}by${h}`);
        setMatchedIds([...matchedIds, type2]);
        setWidgetType(widgetType.filter(item => item !== type));
        break;

      case 'processrate':
        setProcessrateSize(`w_${w}by${h}`);
        setMatchedIds([...matchedIds, type2]);
        setWidgetType(widgetType.filter(item => item !== type));
        break;

      case 'cctv':
        setCCTVSize(`w_${w}by${h}`);
        setMatchedIds([...matchedIds, type2]);
        setWidgetType(widgetType.filter(item => item !== type));
        break;

      case 'dday':
      case 'accessmanagement':
      case 'healthmonitor':
      case 'sitestatus':
        setMatchedIds([...matchedIds, type2]);
        setWidgetType(widgetType.filter(item => item !== type));
        break;

      default:
        break;
    }

    if (updatedData.some(item => item.i !== type)) {
      setLayoutState([...updatedData, { i: type, w, h, x: 0, y: 0 }]);
      return;
    }
    setLayoutState(updatedData);
  };
<input
  type="radio"
  name="weather"
  id={list.id}
  checked={matchedIds.includes(list.id)}
  />
<label
  htmlFor={list.id}
  onClick={() =>
  updateWidgetSize(
    'weather',
    list.x,
    list.y,
    list.id,
  )
 }
>
1. matchedIds배열에서 해당 위젯 id가 있는지 체크후 있다면 deleteWidget 함수를 실행하고 없다면 다음으로 넘어 갑니다.
2.해당하는 위젯 타입이 이미 있다면 같은 타입의 다른 크기 w,h 로 업데이트 해주고 없다면 그대로 넘겨줍니다.
3.스위치문으로 공통 된 케이스는 묶고 해당 위젯 id값을 추가해줍니다.

3번까지 가면 이런 데이터 상태입니다.

['e_info_02', 'day_01', 'weather_02', 'd_info_01', 'cctv_07', 'process_01', 'equip_status_01', 'weather_03']

저는 weather 위젯을 클릭해서 weather 위젯 아이디 값이 중복해서 들어간 상태입니다.

마지막으로 setLayoutState 이용해 layoutState값에 변화를 주고 다시

  const findMatchedIds = () => {
    const matched = [];
    layoutState.forEach(secondItem => {
      widgetLayoutId.forEach(firstItem => {
        if (
          firstItem.i === secondItem.i &&
          firstItem.w === secondItem.w &&
          firstItem.h === secondItem.h
        ) {
          matched.push(firstItem.id);
        }
      });
    });
    setMatchedIds(matched);
  };

  useEffect(() => {
    findMatchedIds();
  }, [layoutState]);

다시 비교를 해서

['e_info_02', 'day_01', 'weather_03', 'd_info_01', 'cctv_07', 'process_01', 'equip_status_01']

중복된 위젯 id값이 들어가지 않게 합니다.
결과적으로

<input
  type="radio"
  name="weather"
  id={list.id}
  checked={matchedIds.includes(list.id)}
  />

matchedIds에 해당 위젯 id값이 포함되어 있는지 체크하면 구현이 되도록 리팩토링을 해보았습니다.

결과

결론

  1. 하드코딩을 피하자 나중에 관리하기가 너무 힘들어진다.
  2. 반복된 코드를 줄이자.
  3. 변하지 않는 데이터가 있다면 따로 파일을 만들어서 관리하자

하지만 프로젝트를 기간안에 만드는 것은 가장 중요하게 생각해야 되므로 일단 버그 없이 제대로 동작할 수 있도록 만들고 나중에 꼭 리팩토링을 해서 최적화를 하자!! 그동안 제가 많은 프로젝트를 해온건 아니지만 시간에 쫒기면서 프로젝트를 해오면서 느낀 생각 입니다.😄

다음으로는 프로젝트 후기로 오겠습니다!

profile
프론트엔드 개발자

0개의 댓글