문제 상황 🤷‍♂️

타입스크립트를 활용하여 프로젝트를 진행하던 중 다른 분이 제작해주신 컴포넌트를 재사용하여 새로운 컴포넌트를 만들어야 하는 일이 생겼습니다.
새롭게 제작해야 하는 컴포넌트는 LoginModal 컴포넌트였는데 현재 Modal 컴포넌트가 organisms 레벨의 Modal 안에 molecule 레벨의 컴포넌트를 만들어서 갈아끼우는 형태를 띄고 있었기에 LoginModalContent라는 molecule 컴포넌트를 제작하고 상황에 맞게 주입 시키고자 했습니다.
기존에 하나의 컴포넌트만 제작되어 있을 때는 크게 문제가 되지 않았지만 새로운 컴포넌트가 제작되게 되면서 타입 선언에 대한 이슈가 발생했습니다.

modal example

하나의 모달을 공유하지만 내용이 다른 경우

// organisms/ModalPopup.tsx
import { TimeTableModalType } from '@/components/UI/molecules';
...

if (nowModalType === TimeTableModalType.TAB_ADD_MODAL)
    return (
        <TimeTableModalPopup
            modalOpen={nowModalState}
            modalType={nowModalType}
            onModalBtnClick={onTabAddModalBtnClickListener}
            onModalAreaClose={onModalCloseListener}
        />
    );
    return (
        <TimeTableModalPopup
        modalOpen={nowModalState}
        modalType={nowModalType}
        onModalBtnClick={onTabRemoveModalBtnClickListener}
        onModalAreaClose={onModalCloseListener}
        />
);

...

ModalPopup 같은 경우 다음과 같이 분기 처리를 통해 어떤 Modal을 보여줄 지 결정하는 형태를 띄고 있는데 컴포넌트가 추가 되면서 여러 종류의 Modal에 대해 하나로 묶어서 보여줄 수 있는 타입이 필요하게 된 것입니다.

생각해보기 👀

처음에 최상위 컴포넌트인 ModalPopup에 타입을 선언하고 export 한 후 하위 컴포넌트에서 import하여 사용하는 방식으로 타입을 관리하면 되지 않을까 생각했습니다. Modal 컴포넌트를 제작해주신 우진님께 말씀드리니 다음과 같은 생각을 말씀해주셨습니다.

  1. ModalPopup은 비즈니스 로직을 관리하기 위한 컴포넌트이다. Type은 View와 밀접한 관련이 있기 때문에 ModalPopup 컴포넌트에 들어오는 것에 대한 확신이 서지 않는다.
  2. 스토리북 테스팅 관점에서도 생각해보면 좋을 것 같다. 스토리북 역시 View를 테스팅하기 위한 라이브러리인데 ModalPopup에서 Type을 가져와서 사용한다면 직관적이지 않은 코드가 될 수 있을 것 같다.

말씀해주신 내용에 저도 동의를 하였고 좀 더 좋은 구조를 생각해보기로 했습니다. ModalPopup에서 아예 Type을 빼는 것도 고려를 해보았으나 개발자들이 사용하게 될 컴포넌트는 최상위 컴포넌트인 ModalPopup이 될 것이고 Type이 없다면 코드를 분석하는데 어려움을 겪을 것이라고 생각했습니다.

결론 😊

따라서 다음과 같이 결론 내렸습니다.

  1. 기본적으로 Type 선언은 하위 컴포넌트에서 하는 걸로 하자.
  2. ModalPopup에도 Type을 선언하자. 하지만 하위 컴포넌트에서 선언된 Type만 사용할 수 있게 제한을 하자.

위와 같은 구조를 가져간다면 View 작업에 있어서 비즈니스 로직과 관련된 ModalPopup을 참고할 필요가 없으며 ModalPopup을 보는 개발자 역시 코드를 분석하는데 큰 어려움이 없을 것이라고 생각했습니다. 완성된 구조는 다음과 같습니다.

// organisms/ModalPopup.tsx

import { TimeTableModalType, LoginModalType, SignUpModalType } from '@/components/UI/molecules';
...

type ModalType = TimeTableModalType | LoginModalType | SignUpModalType;

const modalTypes = { ...TimeTableModalType, ...LoginModalType, ...SignUpModalType };

...

if (nowModalType === modalTypes.TAB_ADD_MODAL)
    return (
    <TimeTableModalPopup
        modalOpen={nowModalState}
        modalType={nowModalType}
        onModalBtnClick={onTabAddModalBtnClickListener}
        onModalAreaClose={onModalCloseListener}
    />
    );
if (nowModalType === modalTypes.TAB_REMOVE_MODAL)
    return (
    <TimeTableModalPopup
        modalOpen={nowModalState}
        modalType={nowModalType}
        onModalBtnClick={onTabRemoveModalBtnClickListener}
        onModalAreaClose={onModalCloseListener}
    />
    );
if (nowModalType === modalTypes.SIGN_UP_MODAL)
    return <SignUpModalPopup modalOpen={nowModalState} onModalBtnClick={() => {}} onModalAreaClose={onModalCloseListener} />;

return <LoginModalPopup modalOpen={nowModalState} onModalBtnClick={() => {}} onModalAreaClose={onModalCloseListener} />;

...

제가 제대로 된 정보를 찾지 못해서 생긴 문제일 수도 있으나 TypeScript에 다른 곳에서 선언된 enum을 그대로 덮어 쓰는 기능이 존재하지 않았고 위와 같은 방법으로 의도했던 구조를 완성시켰습니다.
Union Type을 활용하여 Type을 선언한 후 Union Type을 이루는 각각의 Type의 값에 접근하기 위하여 새로운 변수를 하나 선언해주었습니다.
새로운 변수가 하나 추가 된다는 단점이 있긴 하지만 보다 직관적으로 코드를 짤 수 있음에 만족하기로 했습니다.