Front-End/React

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

SeanK 2022. 11. 1. 15:03

안녕하세요, 개발자 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로 내려줄 수는 있지만 그러기엔 폴더가 너무 난잡해질 것 같으니 여기선 "데이터를 가져와서 렌더링 한다"라고 하나의 기능인 셈 쳐 주도록 하겠습니다. 

아래는 리펙토링 전과 후 코드 비교입니다.