모나드와 옵셔널 체이닝
함수형 프로그래밍은 순수 함수와 불변성을 통해 예측 가능하고 안정적인 코드를 지향합니다. 하지만 실제 애플리케이션 개발에서는 null 또는 undefined 값 처리, 오류 관리, 비동기 작업 처리 등 복잡한 상황을 마주하게 됩니다. 이러한 복잡성을 함수형 방식으로 우아하게 다루기 위한 추상화 개념 중 하나가 바로 모나드(Monads) 입니다.
모나드는 다소 추상적이고 이해하기 어려운 개념으로 알려져 있지만, 사실 우리는 이미 모나드와 유사한 패턴들을 일상적으로 사용하고 있습니다. 타입스크립트의 옵셔널 체이닝(Optional Chaining) 은 모나드적 사고방식이 적용된 구문 설탕(Syntactic Sugar)의 좋은 예시입니다.
모나드 (Monads) 개념 이해
정의: 모나드는 특정 연산(함수)들을 순서대로 실행하면서 컨텍스트(Context)를 안전하게 전달하는 방법을 제공하는 디자인 패턴입니다. 이는 값을 컨텍스트(wrapper) 안에 넣고, 그 컨텍스트 안에서 값을 변환하는 일련의 연산을 정의하는 것입니다.
설명:
모나드는 함수형 프로그래밍에서 "어떤 타입 <T>
에 $T$ 값을 가지고 있는 경우, 이 T
값을 인자로 받는 함수 f: T-> M<U>
를 안전하게 적용하여 M<U>
를 얻을 수 있게 해주는 패턴"이라고 설명할 수 있습니다. 여기서 M
은 컨텍스트를 의미합니다.
가장 흔하게 접하는 모나드의 예시는 다음과 같습니다.
-
Promise
: 비동기 컨텍스트에서 값을 다룹니다.Promise<T>
는 비동기적으로 얻게 될 값T
를 감싸고 있는 컨텍스트입니다.Promise.resolve(value)
는 값을 Promise 컨텍스트에 넣는 "리프트(lift)" 또는 "unit" 연산과 유사합니다..then()
메서드는T
를U
로 변환하는 함수f: T -> Promise<U>
를 받아Promise<U>
를 반환합니다. 이는 모나드의 핵심 연산인flatMap
(또는bind
)과 유사합니다.
// Promise는 비동기 모나드의 한 종류 asyncOperation<number>(10) // Promise<number> 컨텍스트 .then(result => processResult(result)) // result(number)를 받아 Promise<string> 반환 .then(finalResult => console.log(finalResult)); // 최종 Promise<string> 컨텍스트
-
Array
: 컬렉션 컨텍스트에서 값을 다룹니다.Array<T>
는 여러 개의 값T
를 담고 있는 컨텍스트입니다.map
메서드는T
를U
로 변환하는 함수f: T -> U
를 받아Array<U>
를 반환합니다.flatMap
(또는map
후flat
)은T
를Array<U>
로 변환하는 함수f: T -> Array<U>
를 받아Array<U>
를 반환합니다.
const numbers = [1, 2, 3]; // Array<number> 컨텍스트 numbers.flatMap(num => [num, num * 2]); // num(number)를 받아 number[] 반환 -> Array<number> // 결과: [1, 2, 2, 4, 3, 6]
모나드를 이해하는 가장 중요한 열쇠는 컨텍스트를 유지하면서 연산을 연결(chain)하는 능력입니다. 모나드는 함수형 프로그래밍에서 이러한 컨텍스트의 '흐름'을 안전하게 제어하고, 오류 처리나 비동기 처리와 같은 복잡한 부수 효과를 순수 함수형 방식으로 캡슐화하는 데 사용됩니다.
Maybe
/Optional
모나드 (Null 안전성)
가장 쉽게 이해할 수 있는 모나드 중 하나는 null
또는 undefined
값을 안전하게 다루는 Maybe
또는 Optional
모나드입니다. 이는 null
체크를 덕지덕지 붙이는 대신, 값을 컨텍스트 안에 가둬두고, 컨텍스트 내부에서만 연산이 일어나도록 합니다.
// Maybe 모나드 (간단한 예시 구현)
class Maybe<T> {
private value: T | null | undefined;
private constructor(value: T | null | undefined) {
this.value = value;
}
// 값을 Maybe 컨텍스트에 넣는 팩토리 메서드
static of<U>(value: U | null | undefined): Maybe<U> {
return new Maybe(value);
}
// 핵심 연산: map (컨텍스트 안의 값을 변환)
map<U>(fn: (value: T) => U): Maybe<U> {
if (this.value === null || this.value === undefined) {
return Maybe.of(null); // 값이 없으면 null 컨텍스트 유지
}
return Maybe.of(fn(this.value)); // 값이 있으면 함수 적용 후 새 Maybe 생성
}
// flatMap (또는 bind): 컨텍스트 안의 값을 다른 Maybe 컨텍스트로 변환
flatMap<U>(fn: (value: T) => Maybe<U>): Maybe<U> {
if (this.value === null || this.value === undefined) {
return Maybe.of(null);
}
return fn(this.value); // 함수 적용 결과가 이미 Maybe이므로 바로 반환
}
// 최종 값을 추출 (안전하지 않으므로 주의)
getOrElse(defaultValue: T): T {
return this.value === null || this.value === undefined ? defaultValue : this.value;
}
}
// 사용 예시
interface User {
id: number;
name?: string;
address?: {
street?: string;
city?: string;
};
}
const user1: User = { id: 1, name: "Alice", address: { street: "123 Main St", city: "Anytown" } };
const user2: User = { id: 2, name: "Bob" };
const user3: User = { id: 3, address: { city: "Othertown" } }; // name 없음
// Maybe 모나드를 사용하여 안전하게 속성 접근
const userName1 = Maybe.of(user1)
.flatMap(u => Maybe.of(u.name)) // name이 Maybe<string>을 반환
.map(name => name.toUpperCase()) // name이 있으면 대문자로 변환
.getOrElse("UNKNOWN");
console.log("User 1 name:", userName1); // ALICE
const userName2 = Maybe.of(user2)
.flatMap(u => Maybe.of(u.name)) // u.name이 undefined이므로 Maybe.of(null) 반환
.map(name => name.toUpperCase()) // map 호출 안됨
.getOrElse("UNKNOWN");
console.log("User 2 name:", userName2); // UNKNOWN
const userCity3 = Maybe.of(user3)
.flatMap(u => Maybe.of(u.address)) // address가 있으면 Maybe<Address>
.flatMap(a => Maybe.of(a.city)) // city가 있으면 Maybe<string>
.getOrElse("Unknown City");
console.log("User 3 city:", userCity3); // Othertown
Maybe
모나드는 null
값의 전파를 캡슐화하여, 중간에 null
이 발생하면 이후의 모든 연산을 건너뛰고 기본값을 반환하도록 합니다. 이는 중첩된 if (obj && obj.prop)
체크를 제거하여 코드를 깔끔하게 만듭니다.
옵셔널 체이닝 (?.
)
정의: 옵셔널 체이닝은 ECMAScript 2020(ES2020)에서 도입된 문법으로, 객체의 속성에 접근할 때 해당 속성이 null
또는 undefined
인지 명시적으로 확인하지 않고도 안전하게 접근할 수 있도록 해줍니다.
설명:
옵셔널 체이닝은 앞서 설명한 Maybe
모나드의 아이디어를 언어 차원에서 구현한 일종의 null
안전 모나드적 설탕이라고 볼 수 있습니다. 속성 접근 (?.
), 메서드 호출 (?.()
), 배열 인덱싱 (?.[]
)에 사용할 수 있습니다.
타입스크립트 적용 예시
interface UserData {
id: number;
name?: string;
contact?: {
email?: string;
phone?: string;
};
getProfile?(): string; // 메서드도 옵셔널
roles?: string[]; // 배열도 옵셔널
}
const user1: UserData = {
id: 1,
name: "Charlie",
contact: {
email: "charlie@example.com",
phone: "111-222-3333"
},
getProfile: () => "Charlie's Profile"
};
const user2: UserData = {
id: 2,
name: "David"
// contact 속성 없음
};
const user3: UserData = {
id: 3,
roles: ['admin', 'editor']
};
// 1. 옵셔널 속성 접근
const email1 = user1.contact?.email;
console.log("User 1 email:", email1); // charlie@example.com
const email2 = user2.contact?.email; // user2.contact가 undefined이므로 전체 표현식은 undefined
console.log("User 2 email:", email2); // undefined
const email3 = user3.contact?.email;
console.log("User 3 email:", email3); // undefined
// 2. 옵셔널 메서드 호출
const profile1 = user1.getProfile?.(); // user1.getProfile이 존재하면 호출
console.log("User 1 profile:", profile1); // Charlie's Profile
const profile2 = user2.getProfile?.(); // user2.getProfile이 undefined이므로 호출되지 않음
console.log("User 2 profile:", profile2); // undefined
// 3. 옵셔널 배열 인덱싱
const firstRole = user3.roles?.[0]; // user3.roles가 존재하고 첫 번째 요소가 있으면 반환
console.log("User 3 first role:", firstRole); // admin
const fourthRole = user3.roles?.[3]; // user3.roles는 존재하지만 네 번째 요소는 없으므로 undefined
console.log("User 3 fourth role:", fourthRole); // undefined
const noRolesUser: UserData = { id: 4 };
const firstRoleNoUser = noRolesUser.roles?.[0]; // noRolesUser.roles가 undefined이므로 전체 표현식은 undefined
console.log("No Roles User first role:", firstRoleNoUser); // undefined
// 4. 병합 연산자 (Nullish Coalescing)와 함께 사용
// ?? 연산자는 null 또는 undefined일 때만 기본값을 제공합니다.
const userNameMaybe = user2.name ?? "Guest";
console.log("User 2 name (with default):", userNameMaybe); // David (name이 존재하므로)
const userContactEmail = user2.contact?.email ?? "no-email@example.com";
console.log("User 2 contact email (with default):", userContactEmail); // no-email@example.com
옵셔널 체이닝의 이점
- 코드 간결성:
if (obj && obj.prop && obj.prop.subprop)
와 같은 긴 중첩null
체크 코드를 제거하여 가독성을 크게 향상시킵니다. - 안전성: 런타임에
TypeError: Cannot read properties of undefined (reading 'prop')
와 같은 오류를 방지합니다. - 유지보수성: 속성 구조가 변경될 때 관련된
null
체크 로직을 일일이 수정할 필요 없이, 옵셔널 체이닝만 확인하면 됩니다. - 타입스크립트 통합: 타입스크립트는 옵셔널 체이닝의 결과를 정확히 추론합니다. 예를 들어,
user?.contact?.email
의 타입은string | undefined
로 추론됩니다.
모나드와 옵셔널 체이닝의 관계
옵셔널 체이닝은 Maybe
또는 Optional
모나드의 아이디어를 특정 문제(null/undefined 값 처리)에 특화시켜 언어 기능으로 제공하는 것입니다.
Maybe
모나드는flatMap
과 같은 메서드를 통해 컨텍스트 내부의 값을 안전하게 변환하고 컨텍스트를 유지합니다.null
값이 나타나면 이후의 모든 변환을 스킵하고Maybe<null>
컨텍스트를 계속 반환합니다.- 옵셔널 체이닝(
?.
)은 유사하게null
또는undefined
값을 만나면 즉시 연산을 중단하고undefined
를 반환합니다. 이는 마치null
컨텍스트가 전파되는 것과 같습니다.
결론적으로, 옵셔널 체이닝은 모나드의 개념을 이해하는 데 도움이 되는 매우 실용적인 예시이며, 함수형 프로그래밍의 원칙(특히 안전한 값 처리)을 따르면서도 개발자에게 편리함을 제공하는 구문 설탕입니다.
모나드는 복잡한 개념이지만, Promise, Array, 그리고 옵셔널 체이닝을 통해 이미 그 아이디어를 일상적으로 사용하고 있습니다. 함수형 프로그래밍에서 모나드는 이처럼 부수 효과(오류, 비동기, null 등)를 안전하게 캡슐화하고 순수 함수들 간의 조합을 가능하게 하는 강력한 추상화 도구로 활용됩니다.