어떤 블로그 포스트에서 Map 패턴이라고 소개된 내용을 다른 예시로 이해해보려고 한다.

유연하지 못한 TypeScript 사용 방식

// WalletResponse.ts
import { type Transaction } from './Transaction';

export type WalletResponse = {
	totalBalance: number;
  	gasFeePenalty: number;
  	transactionFeePenalty: number;
  	addressCount: number;
  	tokens: string[];
    transactions: string[];
    addresses: string[];
    metadata: { key: string; value: string }[];
  	exceeded: {
    	exceededTransactions: string[];
      	frequentTokens: { token: string; count: number }[];
    };
  	transactionByType: {
    	send: Transaction; // 👻
      	receive: Transaction; // 👻
		swap: Transaction; // 👻
      	stake: Transaction; // 👻
      	bridge: Transaction; // 👻
    };
};
// Transaction.ts
export type Transaction = {
	count: number;
    totalValue: number;
};
// walletAnalyzer.ts
export const analyzeWallet = (
	tokens: string[],
    transactions: string[],
  	addresses: string[],
    totalChainActivity: number,
    metadata: { key: string; value: string }[],
    transactionsByType: {
    	send: Transaction // 👻
        receive: Transaction // 👻
        swap: Transaction // 👻
        stake: Transaction // 👻
        bridge: Transaction // 👻
    },
): WalletResponse => { ... };

새로운 트랜잭션 유형으로 'burn'을 추가해야하는 상황을 생각해볼 때, 위와 같은 구조에서는 다음 작업이 필요하다.

  1. WalletResponse.ts 파일을 수정 (burn: Transaction; 추가)
  2. analyzeWallet 함수 수정 (burn: Transaction; 추가)
  3. 이 구조에 의존하는 다른 부분도 수정 ...

결과적으로 한 군데에서 추가하고 끝나는게 아니라, 최소 2개 이상의 파일을 수정해야한다. 이렇게 하면 오류 가능성이 커지고 무엇보다 (중복 작업때문에) 힘들다 ㅋ

이런 구조를 '강하게 결합되어 있다(tightly coupled)'고 말한다.


Map 패턴을 사용하여 동적인 접근 방식 적용하기

한 군데에서만 수정하면 깔끔하게 끝나는 구조를 도입하고 싶다.

우선 생각해볼 수 있는 방식은 Record를 사용해서 객체를 묶어버리는 방식이다.

// WalletResponse.ts
import { Transaction } from './Transaction';

export type TransactionMap = Record<string, Transaction>; // ✅

export type WalletResponse = {
  totalBalance: number;
  gasFeePenalty: number;
  transactionFeePenalty: number;
  addressCount: number;
  tokens: string[];
  transactions: string[];
  addresses: string[];
  metadata: { key: string; value: string }[];
  exceeded: {
    exceededTransactions: string[];
    frequentTokens: { token: string; count: number }[];
  };
  transactionsByType: TransactionMap; // ✅
};
// walletAnalyzer.ts
export const analyzeWallet = (
  tokens: string[],
  transactions: string[],
  addresses: string[],
  totalChainActivity: number,
  metadata: { key: string; value: string }[],
  transactionsByType: TransactionMap, // ✅
) => { ... };

Record<string, Transaction>을 사용해서 key는 어떤 문자열이든 될 수 있고 값은 Transaction 타입으로 고정한다.
이렇게하면 'burn'과 같은 트랜잭션 유형을 추가할 때 코드를 수정하지 않아도 된다.


아무 문자열이나 key가 될 수 없게 제어하기

위에 새로 수정한 코드는 트랜잭션 유형을 string으로 퉁쳤다고 볼 수 있다. 그래서 아무 트랜잭션 유형이나 추가할 수 있는 상태가 되어버린다. 트랜잭션 유형을 지정해서 사용하려면 key in 을 활용하면 된다.

// WalletResponse.ts
import { type Transaction } from './Transaction';

type AllowedTransactionTypes = 
  | 'send'
  | 'receive'
  | 'swap'
  | 'stake'
  | 'bridge'

export type TransactionMap = {
	[key in AllowedTransactionTypes]: Transaction;
};

export type WalletResponse = {
  totalBalance: number;
  gasFeePenalty: number;
  transactionFeePenalty: number;
  addressCount: number;
  tokens<: string[];
  transactions: string[];
  addresses: string[];
  metadata: { key: string; value: string }[];
  exceeded: {
    exceededTransactions: string[];
    frequentTokens: { token: string; count: number }[];
  };
  transactionsByType: TransactionMap;
};

결론

  • AllowedTransactionTypes에 새로운 트랜잭션 유형을 추가만 하면 쉽게 확장된다
  • 유니온 타입을 사용해 허용된 트랜잭션 유형만 추가할 수 있다.

개방/폐쇄의 원칙(Open/Closed Princible, OCP)을 따르는 좋은 코드라고 생각된다.
(확장에는 열려있고 수정에 닫혀있다)



참조 https://medium.com/@perisicnikola37/dont-use-typescript-types-like-this-use-map-pattern-instead-bed75a0e986e

0개의 댓글

Powered by GraphCDN, the GraphQL CDN