Object Maps를 사용해 사이클로매틱 복잡도를 O(1)로 바꾸기
Source: Dev.to
소개
복잡한 비즈니스 로직, 특히 UI를 건드릴 때마다 마치 달걀 껍질 위를 걷는 듯한 느낌을 아시나요? 주문 상태, 결제 게이트웨이, 권한 레벨을 다루든, 코드 일관성을 유지하는 것이 종종 지뢰밭을 걷는 것처럼 느껴집니다.
많은 개발자들의 자연스러운 반응(이것은 저의 초창기를 떠올리게 합니다 :D)은 이러한 변형들을 switch 혹은 끝없는 if/else 시퀀스로 처리하는 것입니다. 문제는 하나의 case를 놓치거나 체인을 잘못 연결하기만 하면 조용히 프로덕션에 버그가 떠돌게 된다는 점입니다.
많은 사람들이 Guard Clauses(소위 “return early” )를 적용하려고 시도합니다. 이는 검증을 정리하고 과도한 중첩을 피하는 훌륭한 기법이며, 별도의 포스트로 다룰 만큼 충분히 가치가 있습니다.
하지만 오늘 제안은 그보다 한 걸음 더 나아갑니다. ifs를 정리하는 수준을 넘어 제거하고자 합니다. 목표는 방어적인 조건문 작성을 멈추고 의도를 직접 표현하는 코드를 작성하는 것입니다.
실제 적용 방법
예측 가능한 규칙을 다룰 때 Object Maps와 Mapped Types를 결합하면 훨씬 더 견고한 접근 방식을 얻을 수 있습니다. 핵심 아이디어는 조건문의 취약성을 계약(contract)의 안전성으로 바꾸는 것입니다.
실제 사례 (React Native + Zustand)
각 주문 상태가 텍스트, 색상, 취소 가능 여부 및 클릭 시 동작을 정의하는 배달 앱을 상상해 보세요.
도메인 정의
export type OrderStatus =
| 'pending'
| 'confirmed'
| 'preparing'
| 'delivered'
| 'cancelled';
type OrderStatusConfig = {
label: string;
canCancel: boolean;
// Note que tirei a cor daqui. Já explico o porquê! 👇
};
전체 커버리지를 보장하는 Mapped Type
// O compilador obriga que TODAS as chaves de OrderStatus existam aqui.
type OrderStatusMap = {
[K in OrderStatus]: OrderStatusConfig;
};
export const orderStatusMap: OrderStatusMap = {
pending: { label: 'orderStatus.pending', canCancel: true },
confirmed:{ label: 'orderStatus.confirmed', canCancel: true },
preparing:{ label: 'orderStatus.preparing', canCancel: false },
delivered:{ label: 'orderStatus.delivered', canCancel: false },
cancelled:{ label: 'orderStatus.cancelled', canCancel: false },
};
새로운 상태를 추가하고 매핑을 잊어버리면 TypeScript가 빌드를 깨뜨립니다.
보다 실용적인 접근
타입 추론을 더 정확히 하면서도 체크를 유지하고 싶다면 satisfies 연산자가 제격입니다. 객체가 계약을 만족하는지 검증하면서도 값들의 리터럴 타입을 보존합니다.
export const orderStatusMap = {
pending: { label: 'orderStatus.pending', canCancel: true },
confirmed:{ label: 'orderStatus.confirmed', canCancel: true },
preparing:{ label: 'orderStatus.preparing', canCancel: false },
delivered:{ label: 'orderStatus.delivered', canCancel: false },
cancelled:{ label: 'orderStatus.cancelled', canCancel: false },
} satisfies Record;
책임 분리 (Domain vs UI)
규칙 맵(orderStatusMap)은 디자인 시스템을 알 필요가 없습니다. 상태를 테마와 연결하는 전용 맵을 하나 더 만들 수 있습니다:
// Vinculando chaves do status às chaves do seu tema
// (Ex: NativeBase, Restyle, etc)
export const orderStatusColorMap = {
pending: 'warning',
confirmed:'info',
preparing:'orange',
delivered:'success',
cancelled:'danger',
} satisfies Record;
UI에서 사용 (Zero ifs)
Zustand 스토어
import { create } from 'zustand';
type OrderStore = {
status: OrderStatus;
setStatus: (status: OrderStatus) => void;
};
export const useOrderStore = create(set => ({
status: 'pending',
setStatus: status => set({ status }),
}));
React Native 컴포넌트
import { useTranslation } from 'react-i18next';
import { useTheme } from '@react-navigation/native';
import { Pressable, Text } from 'react-native';
import { orderStatusMap, orderStatusColorMap } from './maps';
import { useOrderStore } from './store';
export function OrderStatusView() {
const { t } = useTranslation();
const status = useOrderStore(state => state.status);
const theme = useTheme();
// Desestruturação direta da regra de negócio
const { label, canCancel } = orderStatusMap[status];
// Resolução da cor via Design System
const color = theme.colors[orderStatusColorMap[status]];
return (
<>
{t(label)}
{canCancel && (
console.log('Cancelar')
)}
</>
);
}
한 단계 더 나아가기
이 패턴은 UI에만 국한되지 않습니다. 서로 다른 게이트웨이(Stripe, Mercado Pago)에서 처리되는 PIX와 카드 결제를 생각해 보세요. 중첩된 조건문 대신 Object Map을 사용해 올바른 실행 전략을 반환합니다:
export const paymentStrategies = {
pix: async (data, gateway) => {
return adaptersMap[gateway].pix(data);
},
credit_card: async (data, gateway) => {
return adaptersMap[gateway].creditCard(data);
},
} satisfies Record;
// Uso na aplicação:
const result = await paymentStrategies[method](negotiation, selectedGateway);
왜 이것이 게임 체인저인가?
- Complexidade O(1) – 여러
else를 읽는 인지적 비용을 없앱니다. - 마음의 평화 – 새로운 상태나 결제 방식을 추가할 때
if를 찾아다닐 필요가 없습니다. 컴파일러가 오류를 알려줍니다. - 명확한 계약 – 코드는 방어적인 모습을 벗어나 의도를 표현하게 됩니다.
여러분은 Object Maps를 자주 사용하시나요, 아니면 아직도 switch 케이스를 기억에 의존해 관리하고 계신가요? 👇