리액트로 개발을 하다 보면 컴포넌트가 기본 기능대로만 동작하기보다는, 원하는 방식으로 확장되어 동작하길 바랄 때가 종종 있습니다. 이럴 때 우리는 IoC(Inversion of Control) 즉, 제어 역전 패턴을 통해 컴포넌트를 사용하는 개발자에게 컴포넌트의 제어권을 넘겨줌으로써, 개발자가 원하는 대로 컴포넌트를 컨트롤하도록 할 수 있습니다. - 출처
이 "제어의 역전"속에는 다양한 방법론이 있는데, 그중 하나가 바로 합성 컴포넌트
이다.
합성 컴포넌트는 리액트의 Context
/Provider
를 사용하여 여러 종류의 컴포넌트가 하나의 로직을 공유할 수 있게 하는 방법이다.
Component를 "잘" 만들기 위해 등장한 것이라고 생각하면 쉽다 ㅎㅎㅎ
이러한 이유로 우리 8팀은 Compound Component를 도입하기로 하였다.
처음에는 코드 짜기 급급해서 사용하지 못했는데, 프로젝트 완료하고 리펙토링에 돌입하였다.
다른 의미로 재미진 코드다 정말 ㅋㅋㅋㅋㅎㅋㅎㅋ
위 코드가 나오게 된 이유는 컴포넌트를 아래와 같이 설계했기 때문이다.
기능적으로 분리한 것이 절대 아니라, UI만 따져 분리한 것 😭💣
그렇다보니 하위에 숨겨진 자식 컴포넌트들에게 넘겨줘야하는 데이터가 너무 많았다.
재사용성에 초점을 맞춰 컴포넌트를 나눠 줄 것이다.
이렇게 세분화 시켜주면, 추후 카드 UI 배치가 변경되어도 문제가 전혀 없어진다.
즉 유지보수와 재사용에 유용해지는 것이다.
그리고 레고처럼 원하는 컴포넌트를 배치하면 되기 때문에 개발자의 자유도가 높아질 수 있을 것이다!
(하지만 더 나은 방법이 있다면 꼭 댓글로 알려주세요 순한맛으로,,,, 🥹,,,)
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;
생성한 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가 사라졌고,
합성 컴포넌트의 사용으로 가독성마저 깔끔해 진 모습이다.
세분화 했던 컴포넌트에도 자식 컴포넌트들이 존재하는데,
걔네들 마저 이렇게나 깔끔해 졌다 ✨
아직 context안에 함수를 저장하는 부분이 어렵다.
카드 내부에는 답변 보내기/수정/삭제/거절 등 다양한 기능이 존재하는데
이것들도 전부 context안에 담아줄 수 있는 방법이 있지 않을까?
카드 삭제할 때 "진짜 삭제할거야?" 재차 확인하는 confirm alert가 뜨게 되어있는데
해당 모달을 전역으로 관리해줄 수 있는 방법을 찾아보고 싶다...
글 다 쓰고나니 배고팡!