대시보드 프로젝트를 하면서 위젯 크기 수정,삭제 기능이 있었습니다
예를들어 2x2 크기의 위젯을 2x3 이라든지 3x4로 크기를 수정하는 기능과 삭제하는 기능이죠. 이 기능을 만들당시 시간이 너무 촉박하여 일단 기능이 제대로 돌아가도록 만들고 납품하자! 라는 마인드로 하드코딩을 했었던 거 같아요.. 그 결과 코드가 1600줄이 넘어갔죠! 하하🫠 일단 버그가 없이 제대로 동작해서 다행이지만
너무나 리팩토링을 하고싶어서 최근에 코드를 수정했습니다. 그 결과
500줄로 줄이게 되었습니다!!
그래서 이번엔 그 과정들을 설명해보려고 합니다.
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값이 포함되어 있는지 체크하면 구현이 되도록 리팩토링을 해보았습니다.
하지만 프로젝트를 기간안에 만드는 것은 가장 중요하게 생각해야 되므로 일단 버그 없이 제대로 동작할 수 있도록 만들고 나중에 꼭 리팩토링을 해서 최적화를 하자!! 그동안 제가 많은 프로젝트를 해온건 아니지만 시간에 쫒기면서 프로젝트를 해오면서 느낀 생각 입니다.😄
다음으로는 프로젝트 후기로 오겠습니다!