Search
3️⃣

Pattern 3. 인터페이스 일반화 (Idiomatic)

인터페이스 일반화란?

인터페이스 일반화는 특정 도메인에 종속되지 않고 "행동의 본질"을 드러내는 보편적인 API를 설계하는 것입니다. 핵심은 "같은 것은 같게" 만들어서, 어디서든 예측 가능한 사용법을 제공하는 거예요.
예를 들어, Modal이든 Popup이든 Dialog든 본질은 "무언가를 보여주고 숨기는 것"이죠. 그럼 모두 같은 방식(open()/close())으로 동작해야 합니다. 왜 어떤 건 show()고 어떤 건 display()일까요?

"재사용"이 아닌 "재인식"

많은 개발자가 컴포넌트를 만들 때 "재사용성"에 집착합니다. 하지만 진짜 중요한 건 "재인식"이에요.
처음 보는 라이브러리를 쓴다고 생각해보세요.
문서를 읽기도 전에 "아, Modal이니까 당연히 open prop이 있겠지?"라고 예상할 수 있다면? 그게 바로 재인식의 힘입니다. openProductModal() 같은 특수한 이름 대신 그냥 open()이라고 하면, 모든 개발자가 설명 없이도 사용법을 압니다.
새로운 도메인이 추가되어도 문제없어요. 오늘은 ProductModal, 내일은 UserModal, 모레는 OrderModal이 추가되어도 모두 같은 open()/close() 인터페이스를 쓰니까요.
즉, 인터페이스 일반화는 단순히 코드를 예쁘게 만드는 일이 아닙니다. 이것은 팀 동료를 위한 인지 부하 관리 기술입니다.

Universal Language - 도메인을 넘어서는 보편적 언어

개발 세계에는 이미 합의된 "보편적 언어"가 있습니다. 이걸 사용하면 설명이 필요 없어요. 가급적 표준을 따르는 것이 소통 비용을 줄이고 예측 가능성을 높여줄 수 있습니다.
🌍 Actions: open/close, show/hide, enable/disable 🌍 States: loading/loaded/error, active/inactive, valid/invalid 🌍 Events: onChange, onSubmit, onCancel, onComplete
Plain Text
복사
이런 단어들은 어떤 도메인에서든 같은 의미로 통합니다. 굳이 startProductLoading, beginUserFetch 같은 걸 만들 필요가 없어요. 그냥 loading이면 충분하죠.

예시: 본질을 바라보기 연습

함수 이름을 지을 때도 "이게 본질적으로 뭐하는 거지?"를 생각해보세요.
// 🤔 이 셋의 공통점은? handleProductSelection(productId) handleUserChoice(userId) handleOrderPick(orderId) // 💡 본질: "무언가를 선택하는 행위" onSelect(id) // 도메인(Product/User/Order)은 벗겨내고 // 행동(Select)의 본질만 남김
TypeScript
복사
실제 코드에서는 이렇게 쓰면 됩니다:
// Don't ❌ handleSomeProduct({ clickProductSelection: (productId) => {} }) // Do ✅ handleSomeProduct({ onSelect: (productId) => {} })
TypeScript
복사
onSelect가 더 나을까요? 이 패턴을 한 번 배운 개발자는 다른 컴포넌트에서도 onSelect를 찾을 거예요. 학습 전이가 일어나는 거죠.

Trigger & Ability: 패턴 인식 - "이상한 낌새가 나는데?"

트리거 체크리스트

코딩하다가 이런 순간이 오면 "아, 인터페이스 일반화가 필요하구나!"라고 떠올리세요:
비슷한 컴포넌트인데 메서드명이 다 다를 때
"왜 여기는 show()고 저기는 open()이지?"
메서드명에 특정 도메인 용어가 들어있을 때
openProductModal, displayUserInfo 같은 이름을 보면 경고등!
새 기능 추가할 때마다 인터페이스에 메서드 추가해야 할 때
"카테고리별로 다르게 열어야 해서 openForElectronics() 추가했어요..."
다른 곳에서 쓸 생각하는데 이름이 고민될 때
"이거 장바구니에서도 쓰고 싶은데 openProductModal이라는 이름이..."
비슷한 일 하는데 파라미터 구조가 제각각일 때
어떤 건 (id, options), 어떤 건 ({id, ...options})

실제로 마주치는 나쁜 예시

// 이런 코드를 보면 "일반화하자!"를 떠올려야 함 interface ProductModal { openProductQuickView(productId: string) closeProductDetail() isProductModalVisible: boolean } interface UserProfilePopup { showUserInfo(userId: string) hideProfile() profilePopupOpened: boolean } interface OrderDialog { displayOrderSummary(orderId: string) dismissOrderView() orderDialogState: 'open' | 'closed' } // 😵 매번 다른 이름, 다른 구조...
TypeScript
복사
뭐가 문제일까요? 셋 다 본질적으로는 "무언가를 보여주는 UI"인데, 각자 다른 언어를 쓰고 있어요. 새로 온 개발자가 "주문 상세를 닫으려면... close? hide? dismiss?" 하면서 헤맬 거예요.

"추상화 레벨" 맞추기 연습

인터페이스를 설계할 때는 모든 메서드가 같은 추상화 레벨에 있어야 합니다.
// Set A interface DataManager { fetch() update() delete() saveUserProfile() // 🤔 너무 구체적. 나머지는 일반 동작 } // Set B interface Modal { open() close() minimize() openWithProductData() // 🤔 도메인 특화. 나머지는 추상 동작 } // Set C interface Form { validate() submit() reset() validateEmailFormat() // 🤔 특정 필드 검증. 나머지는 폼 전체 동작 }
TypeScript
복사
각 세트에서 하나씩 이상한 게 보이시나요? 나머지는 일반적인 동작을 나타내는데, 하나만 특정 도메인이나 구체적인 동작을 나타내고 있죠.
올바른 추상화는 이렇게:
saveUserProfile()save() 또는 update()
openWithProductData()open(data?)
validateEmailFormat()validate() + validator 주입

"이상한 이름" 찾기 게임

실제 프로젝트에서 자주 보는 문제입니다. 같은 Card 컴포넌트를 만들었는데...
// 🚨 같은 일을 하는데 왜 이렇게 다를까? // AS-IS: 도메인이 덕지덕지 붙은 Card <ProductCard productName="맥북" productPrice={2000000} onProductAddToCart={handleAddToCart} isProductAvailable={true} /> // TO-BE: 깔끔하게 일반화된 Card <Card title="맥북" subtitle="2,000,000원" onAction={handleAddToCart} disabled={!isAvailable} />
TypeScript
복사
AS-IS의 문제점이 보이시나요?
모든 prop에 "product"가 붙어있어요
다른 도메인(User, Order)에서 쓰려면 비슷한 카드를 또 만들어야 해요
onProductAddToCart는 너무 구체적이에요. 카드의 액션이 꼭 "장바구니 추가"일 필요는 없잖아요?
TO-BE는 어떤가요?
도메인 중립적인 이름 (title, subtitle)
범용적인 이벤트명 (onAction)
표준적인 상태 (disabled)
이제 이 Card는 상품 카드로도, 사용자 카드로도, 주문 카드로도 쓸 수 있습니다. 진짜 "재사용 가능한" 컴포넌트가 된 거죠.
핵심: 인터페이스를 설계할 때는 항상 "이게 다른 맥락에서도 말이 되나?"를 생각하세요. 특정 도메인 용어가 들어가는 순간, 그 컴포넌트의 수명은 짧아집니다.