어떤 블로그 포스트에서 Map 패턴이라고 소개된 내용을 다른 예시로 이해해보려고 한다.
// 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'을 추가해야하는 상황을 생각해볼 때, 위와 같은 구조에서는 다음 작업이 필요하다.
결과적으로 한 군데에서 추가하고 끝나는게 아니라, 최소 2개 이상의 파일을 수정해야한다. 이렇게 하면 오류 가능성이 커지고 무엇보다 (중복 작업때문에) 힘들다 ㅋ
이런 구조를 '강하게 결합되어 있다(tightly coupled)'고 말한다.
한 군데에서만 수정하면 깔끔하게 끝나는 구조를 도입하고 싶다.
우선 생각해볼 수 있는 방식은 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'과 같은 트랜잭션 유형을 추가할 때 코드를 수정하지 않아도 된다.
위에 새로 수정한 코드는 트랜잭션 유형을 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;
};
개방/폐쇄의 원칙(Open/Closed Princible, OCP)을 따르는 좋은 코드라고 생각된다.
(확장에는 열려있고 수정에 닫혀있다)