안녕하세요, 개발자 Sean입니다.
저번 포스팅에서 SOLID 원칙에 대한 공부를 해봤으니 본격적으로 코드 리펙토링에 들어가려고 합니다.
우선은 오늘 제가 수정할 코드는 아래와 같습니다.
const Analytics = () => {
//States
const [isLoading, setIsLoading] = useState(false);
//Hooks
const dispatch = useDispatch();
const analytics = useSelector((state) => state.analytics);
const user = useSelector((state) => state.user);
useEffect(() => {
getTabList();
}, [])
//Methods
const getTabList = async () => {
setIsLoading(false)
const results = await getAnalyticsTabList(user.userId);
dispatch(saveAnalyticsTabList(results.data));
setIsLoading(true)
}
return (
<DndProvider backend={HTML5Backend}>
<Wrapper className="content-wrapper">
<SubCard
sx={{
px: 8,
py: 1,
minHeight: '86vh',
'@media(max-width: 768px)': { padding: '30px 10px' },
}}
>
{isLoading &&
<>
<Tabs />
{analytics.tabList.map((el, i) => (
<TabPanel value={analytics.selectedTab} index={i} key={i}>
{(el.tab_type === "data-analytics" || el.tab_type === "data-analytics-timeseries" || el.tab_type === "data-analytics-stock") && <DataAnalysis />}
{(el.tab_type === "index-analytics" || el.tab_type === "index-analytics-multi") && <IndexAnalysis />}
</TabPanel>
))}
<TabPanel value={analytics.selectedTab} index={analytics.tabList.length}>
<InitiateCTA />
</TabPanel>
</>
}
{!isLoading && <LoadingData isLoading={isLoading} /> }
</SubCard>
</Wrapper>
</DndProvider>
);
};
export default Analytics;
네 코드가 어마어마하게 난잡하죠? ㅎㅎ
우선은 위 코드를 Single Responsibility Principle(SRP)에 따라 리펙토링 해보겠습니다.
제일 먼저 위 코드가 어떤 역할을 하고 있는지 "정의"하는 게 중요하겠죠.
제가 나름대로 분석한 결과 위 코드는 아래와 같은 역할들을 수행하고 있습니다.
- 데이터가 로딩중인지 체크하기
- analytics 정보를 가져오기
- user 정보를 가져오기
- tab list를 가져오기
- 뷰를 렌더링하기
- 타입에 따른 다른 탭 컴포넌트를 렌더링 하기
위 코드는 확실히 SRP를 어기고 있습니다. 여기서 "데이터가 로딩 중인지 체크하기" 기능은 아무런 역할을 하지 않는다는 사실을 깨달았습니다. (자식 컴포넌트들이 자체적으로 가지고 있더라고요 허허...) 그래서 과감하게 이 기능은 삭제했습니다.
그리고 아래 세 가지 기능은 하나로 묶을 수 있음을 알 수 있었습니다. 왜냐하면 analytics와 user 정보를 이용해 tablist를 가져오고 있기 때문입니다. 따라서 아래 세 기능은 하나로 묶어 "tab list 가져오기" 혹은 "데이터 가져오기"로 묶어볼 수 있겠습니다.
- analytics 정보를 가져오기
- user 정보를 가져오기
- tab list를 가져오기
그리고 이 기능을 처리하는 커스텀 훅을 아래와 같이 만들고,
export const useTabList = () => {
const dispatch = useDispatch();
const analytics = useSelector((state) => state.analytics);
const user = useSelector((state) => state.user);
useEffect(() => {
getTabList();
}, [])
const getTabList = async () => {
const results = await getAnalyticsTabList(user.userId);
dispatch(saveAnalyticsTabList(results.data));
}
return [analytics];
}
기존의 비즈니스 로직을 대체하면,
아래와 같이 깔끔한 코드가 만들어집니다!
const Analytics = () => {
const [analytics] = useTabList();
return (
<DndProvider backend={HTML5Backend}>
<Wrapper className="content-wrapper">
<SubCard
sx={{
px: 8,
py: 1,
minHeight: '86vh',
'@media(max-width: 768px)': { padding: '30px 10px' },
}}
>
<Tabs />
{analytics.tabList.map((el, i) => (
<TabPanel value={analytics.selectedTab} index={i} key={i}>
{(el.tab_type === "data-analytics" || el.tab_type === "data-analytics-timeseries" || el.tab_type === "data-analytics-stock") && <DataAnalysis />}
{(el.tab_type === "index-analytics" || el.tab_type === "index-analytics-multi") && <IndexAnalysis />}
</TabPanel>
))}
<TabPanel value={analytics.selectedTab} index={analytics.tabList.length}>
<InitiateCTA />
</TabPanel>
</SubCard>
</Wrapper>
</DndProvider>
);
};
export default Analytics;
이 정도만 해도 나름 깔끔한 코드라고 생각할 수 있지만 아래와 같이 아직은 하나 이상의 역할을 하고 있다는 것을 알 수 있습니다.
- 타입에 따른 다른 탭 컴포넌트를 렌더링 하기
이 기능만 따로 처리가 된다면 코드가 정말 깔끔해질 것 같지 않나요?
그러니 아래와 같이 위 기능은 별도의 컴포넌트로 떼어 독립시키도록 하겠습니다.
export const TabPanels = ({ analytics }) => {
return (
<>
{analytics.tabList.map((el, i) => (
<TabPanel value={analytics.selectedTab} index={i} key={i}>
{(el.tab_type === "data-analytics" || el.tab_type === "data-analytics-timeseries" || el.tab_type === "data-analytics-stock") && <DataAnalysis />}
{(el.tab_type === "index-analytics" || el.tab_type === "index-analytics-multi") && <IndexAnalysis />}
</TabPanel>
))}
<TabPanel value={analytics.selectedTab} index={analytics.tabList.length}>
<InitiateCTA />
</TabPanel>
</>
)
};
export default TabPanels;
그리고 기존의 코드를 대체하면...
Ooohooo! 어떤가요? 정말 코드가 간결해지지 않았나요?
const Analytics = () => {
const [analytics] = useTabList();
return (
<DndProvider backend={HTML5Backend}>
<Wrapper className="content-wrapper">
<SubCard
sx={{
px: 8,
py: 1,
minHeight: '86vh',
'@media(max-width: 768px)': { padding: '30px 10px' },
}}
>
<Tabs />
<TabPanels analytics={analytics}/>
</SubCard>
</Wrapper>
</DndProvider>
);
};
export default Analytics;
이제 이 컴포넌트는 단 2개의 역할만을 맡고 있습니다.
1. 데이터를 가져와서
2. 뷰를 렌더링 한다.
모듈/함수/클래스가 단 하나의 역할만을 맡는다는 기존의 원칙에 100% 부합하기 위해선 별도의 컴포넌트를 만들어 props로 내려줄 수는 있지만 그러기엔 폴더가 너무 난잡해질 것 같으니 여기선 "데이터를 가져와서 렌더링 한다"라고 하나의 기능인 셈 쳐 주도록 하겠습니다.
아래는 리펙토링 전과 후 코드 비교입니다.
'Front-End > React' 카테고리의 다른 글
[React] React-dnd 사용방법 (0) | 2022.11.29 |
---|---|
[React] 리엑트 코딩 팁 (0) | 2022.11.16 |
[React] 리엑트에 SOLID 원칙 적용시키기 (0) | 2022.10.31 |
[React] 선언형과 리엑트 (0) | 2022.10.20 |
[React] 커스텀 훅 이용하기 (0) | 2022.10.19 |