모나드와 옵셔널 체이닝
함수형 프로그래밍은 순수 함수와 불변성을 통해 예측 가능하고 안정적인 코드를 지향합니다. 하지만 실제 애플리케이션 개발에서는 null 또는 undefined 값 처리, 오류 관리, 비동기 작업 처리 등 복잡한 상황을 마주하게 됩니다. 이러한 복잡성을 함수형 방식으로 우아하게 다루기 위한 추상화 개념 중 하나가 바로 모나드(Monads)입니다.
모나드는 다소 추상적이고 이해하기 어려운 개념으로 알려져 있지만, 사실 우리는 이미 모나드와 유사한 패턴들을 일상적으로 사용하고 있습니다. 타입스크립트의 옵셔널 체이닝(Optional Chaining)은 모나드적 사고방식이 적용된 구문 설탕(Syntactic Sugar)의 좋은 예시입니다.
모나드 (Monads) 개념 이해
정의: 모나드는 특정 연산(함수)들을 순서대로 실행하면서 컨텍스트(Context)를 안전하게 전달하는 방법을 제공하는 디자인 패턴입니다. 이는 값을 컨텍스트(wrapper) 안에 넣고, 그 컨텍스트 안에서 값을 변환하는 일련의 연산을 정의하는 것입니다.
설명:
모나드는 함수형 프로그래밍에서 "어떤 타입 <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); // OthertownMaybe 모나드는 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 등)를 안전하게 캡슐화하고 순수 함수들 간의 조합을 가능하게 하는 강력한 추상화 도구로 활용됩니다.