서비스 개발을 하다 보면, 사용자에게 팝업을 띄우는 것처럼 단순해 보이는 기능조차도 의외로 많은 고민을 요구합니다.
예를 들어, openModal() 함수를 어떻게 설계하느냐에 따라 코드의 확장성과 유지보수성이 크게 달라질 수 있습니다.
openModal(data);
openModal({ mode: 'card', ... });
단순히 “깔끔해 보인다”는 이유 때문이 아닙니다.
예측 가능성
과 확장성
이라는 중요한 원칙을 지킬 수 있기 때문입니다.
호출부에서 mode만 봐도 어떤 UI가 렌더링될지 알 수 있습니다.
이 작은 차이가 협업 시 오해를 줄이고, 버그 발생 가능성을 낮춥니다.
새로운 모달 타입이 추가되더라도 기존 호출부에는 영향을 주지 않습니다.
반면, data 객체를 통째로 넘기는 방식은 기존 필드와 충돌하거나 불필요한 필드 관리가 필요해져 유지보수 비용이 커집니다.
다음은 예제 코드입니다.
// 공통 data 객체
interface ModalData {
title?: string;
description?: string;
cardNumber?: string;
productId?: string;
// 앞으로 계속 추가될 수 있음...
}
function openModal(data: ModalData) {
if (data.cardNumber) {
// 카드 모달
} else if (data.productId) {
// 상품 모달
} else {
// 기본 모달
}
}
새로운 모달 타입(예: couponModal)이 추가된다면,
ModalData에 { couponId?: string } 같은 필드를 추가하고,
openModal() 내부에도 else if (data.couponId) 분기를 또 작성해야 합니다.
이 경우 호출부는 openModal이 내부 조건 분기에 따라 어떻게 동작할지 예측하기 어렵습니다.
또한 새로운 타입 추가로 인해 불필요한 필드 주입까지 요구되면서, 호출부 개발자는 어떤 데이터를 넘겨야 할지 혼란스러워집니다.
결국 이는 연관 없는 모달 로직에도 사이드 이펙트를 유발합니다.
각 모달이 필요로 하는 데이터만 interface로 정의됩니다.
덕분에 한 모달이 다른 모달의 데이터 구조에 불필요하게 의존하지 않게 됩니다.
// 모달별 interface 정의
interface CardModalProps {
mode: 'card';
cardNumber: string;
}
interface ProductModalProps {
mode: 'product';
productId: string;
}
interface CouponModalProps {
mode: 'coupon';
couponId: string;
}
// union 타입으로 확장
type ModalProps = CardModalProps | ProductModalProps | CouponModalProps;
function openModal(props: ModalProps) {
switch (props.mode) {
case 'card':
// 카드 모달
break;
case 'product':
// 상품 모달
break;
case 'coupon':
// 쿠폰 모달
break;
}
}
팝업 호출이라는 단순한 기능에서도 원칙 있는 설계는 큰 차이를 만듭니다.
저는 interface 분리 방식을 통해 예측 가능한 코드, 변경에 강한 구조를 지향합니다.
결국 좋은 아키텍처는 화려한 기술이 아니라, 작은 선택에서 비롯되기에 간단한 코드 작성 또한 심의를 기울여야합니다.