본문 바로가기
Front-End/React

[React] Single Responsibility Principle 리펙토링 하기

by SeanK 2022. 11. 1.

안녕하세요, 개발자 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