합성 컴포넌트로(compound component) 리펙토링 해보기

0

리펙토링

목록 보기
1/5
post-thumbnail

🎀 제어의 역전과 합성 컴포넌트

리액트로 개발을 하다 보면 컴포넌트가 기본 기능대로만 동작하기보다는, 원하는 방식으로 확장되어 동작하길 바랄 때가 종종 있습니다. 이럴 때 우리는 IoC(Inversion of Control) 즉, 제어 역전 패턴을 통해 컴포넌트를 사용하는 개발자에게 컴포넌트의 제어권을 넘겨줌으로써, 개발자가 원하는 대로 컴포넌트를 컨트롤하도록 할 수 있습니다. - 출처

이 "제어의 역전"속에는 다양한 방법론이 있는데, 그중 하나가 바로 합성 컴포넌트이다.

합성 컴포넌트는 리액트의 Context/Provider를 사용하여 여러 종류의 컴포넌트가 하나의 로직을 공유할 수 있게 하는 방법이다.

Component를 "잘" 만들기 위해 등장한 것이라고 생각하면 쉽다 ㅎㅎㅎ

  1. 컴포넌트의 재사용성을 높여준다.
  2. 자유도를 높여 컴포넌트를 레고처럼 조립하여 사용할 수 있다.
  3. 데이터 로직은 같지만 디자인이 바뀌는 경우에 대처하기 쉽다.

이러한 이유로 우리 8팀은 Compound Component를 도입하기로 하였다.

처음에는 코드 짜기 급급해서 사용하지 못했는데, 프로젝트 완료하고 리펙토링에 돌입하였다.

🎀 기존 코드의 문제점

  1. 상태관리 라이브러리를 쓰고있지 않아 생긴 엄청난 props
  2. 재사용 절대 불가능한 컴포넌트들

다른 의미로 재미진 코드다 정말 ㅋㅋㅋㅋㅎㅋㅎㅋ
위 코드가 나오게 된 이유는 컴포넌트를 아래와 같이 설계했기 때문이다.

기능적으로 분리한 것이 절대 아니라, UI만 따져 분리한 것 😭💣
그렇다보니 하위에 숨겨진 자식 컴포넌트들에게 넘겨줘야하는 데이터가 너무 많았다.

🎀 설계부터 다시 가보즈아

재사용성에 초점을 맞춰 컴포넌트를 나눠 줄 것이다.
이렇게 세분화 시켜주면, 추후 카드 UI 배치가 변경되어도 문제가 전혀 없어진다.
즉 유지보수와 재사용에 유용해지는 것이다.

그리고 레고처럼 원하는 컴포넌트를 배치하면 되기 때문에 개발자의 자유도가 높아질 수 있을 것이다!

(하지만 더 나은 방법이 있다면 꼭 댓글로 알려주세요 순한맛으로,,,, 🥹,,,)

🎀 이 리펙토링을 보신 적 있습니까?

📌 context API 사용

context API를 활용하여 Provider 파일을 생성한다.

// props를 줄이기 위해 상태를 전역으로 관리할 수 있도록 했다.
// cardData와 userInfo는 각각 api에서 받아온 데이터가 저장될 것이다.
const CardProvider = ({ cardData, userInfo, setRequestType, children }) => {
  // 수정 상태도 전역으로 관리할 수 있도록 저장하였다.
  const [isEdit, setIsEdit] = useToggle();

  const providerValue = useMemo(
    () => ({ cardData, userInfo, setRequestType, isEdit, setIsEdit }),
    [cardData, userInfo, setRequestType, isEdit, setIsEdit],
  );

  return (
    <CardContext.Provider value={providerValue}>
      <StCardContainer>
        {/* children에 세분화해 주었던 컴포넌트들이 담길 것이다. */}
        {children}
      </StCardContainer>
    </CardContext.Provider>
  );
};

export default CardProvider;

📌 custom hook 생성

생성한 Provider를 사용할 수 있도록 커스텀 훅도 생성한다.

export const useCardProvider = () => {
  const context = useContext(CardContext);
  // context 안에서만 공유되는 전역 상태들이 있기 때문에
  // provider 내부에서 사용되지 않았다면 오류를 띄운다.
  if (context === undefined) throw new Error('useCardProvider 는 CardProvider안에서만 사용되어야 합니다.');

  return context;
};

📌 세분화 시킨 컴포넌트들 생성

빨간 네모로 표시한 부분들을 컴포넌트화 해준다. 폴더 구조는 아래와 같이 짰다.
context 폴더 안에는 cardProvider 파일만 넣어주었고, card 폴더에 세분화된 컴포넌트들을 저장했다.

/src
└─/components
   └─/ui
      └─/atoms
         └─/card
            ├─/context
            │  └─ cardProvider.jsx
            │
            ├─CardBadgeBox.jsx
            ├─CardProfileImage.jsx
            ├─CardQuestionBox.jsx
            ├ ...
            └─ index.js

📌 마지막 병합!

위 폴더 구조에서 보이는 index.js를 생성한다.
그리고 assign을 통해 생성된 컴포넌트들을 묶어주어 코드의 가독성을 높힐 것이다.

// ...
import CardStateBadge from './CardStateBadge';
import CardEditButton from './CardEditButton';
import CardNameBox from './CareNameBox';
import CardProvider from './context/cardProvider';

const Card = Object.assign(CardProvider, {
  Badge: CardStateBadge,
  Name: CardNameBox,
  EditButton: CardEditButton,
  // ...
});

export default Card;

🎀 어떻게 바뀌었을까?

📌 놀랍게도 이 두 코드는 같은 목적을 가진 코드가 맞습니다.

분명 똑같은 것을 보여주고 기능하는 코드인데 이렇게나 바뀌었다.
context로 전역 상태를 저장하고 꺼내와 쓰기 때문에 props가 사라졌고,
합성 컴포넌트의 사용으로 가독성마저 깔끔해 진 모습이다.

세분화 했던 컴포넌트에도 자식 컴포넌트들이 존재하는데,
걔네들 마저 이렇게나 깔끔해 졌다 ✨

  1. 코드 가독성이 굉장히 좋아졌다.
  2. context 사용으로 프롭 드릴링에서 벗어날 수 있었다.
  3. answer페이지와 question 페이지에서 디자인은 같지만 다른 카드 구성을 보여주어야하는데, 그 때마다 props로 자식 컴포넌트를 제어해 주고 있었다.
    하지만 리펙토링으로 컴포넌트 세분화를 해주었기 때문에 props 없이 컴포넌트를 내가 원하는대로 제어해줄 수 있게 되었다.

🎀 하지만 아쉬움이 남기 마련...

  • 완벽하게 compound component의 사용법을 숙지한것이 아니여서 그런가 스스로 제대로 리펙토링했다는 확신이 느껴지지 않는 부분이 아쉽다.
    실제 현업에서는 어떤 방식으로 이용되고 있을까 너무 궁금하다.
    능숙한 시니어 개발자들은 어떤식으로 compound하고있을까...
  • 아직 context안에 함수를 저장하는 부분이 어렵다.
    카드 내부에는 답변 보내기/수정/삭제/거절 등 다양한 기능이 존재하는데
    이것들도 전부 context안에 담아줄 수 있는 방법이 있지 않을까?

  • 카드 삭제할 때 "진짜 삭제할거야?" 재차 확인하는 confirm alert가 뜨게 되어있는데
    해당 모달을 전역으로 관리해줄 수 있는 방법을 찾아보고 싶다...

  • 글 다 쓰고나니 배고팡!


👉🏻 아래 링크들도 같이 구경해 보세요 제발~~

profile
일단 해. 그리고 잘 되면 잘 된 거, 잘 못되면 그냥 해본 거!

0개의 댓글