icon안동민 개발노트

모나드와 옵셔널 체이닝


 모나드는 함수형 프로그래밍에서 계산의 추상화와 조합을 가능하게 하는 중요한 개념입니다.

 타입스크립트에서 모나드를 활용하면 더 안전하고 표현력 있는 코드를 작성할 수 있습니다.

모나드의 개념과 구현

 모나드는 값을 래핑하고, 계산을 추상화하는 데 사용되는 구조체입니다.

 모나드는 다음 세 가지 요소로 구성됩니다.

  1. 타입 생성자
  2. unit (또는 return) 함수
  3. bind (또는 flatMap) 함수

 타입스크립트에서의 기본적인 모나드 구현

interface Monad<T> {
    flatMap<U>(f: (value: T) => Monad<U>): Monad<U>;
}
 
class Identity<T> implements Monad<T> {
    constructor(private value: T) {}
 
    static of<T>(value: T): Identity<T> {
        return new Identity(value);
    }
 
    flatMap<U>(f: (value: T) => Identity<U>): Identity<U> {
        return f(this.value);
    }
}
 
// 사용 예
const result = Identity.of(5)
    .flatMap(x => Identity.of(x * 2))
    .flatMap(x => Identity.of(x + 1));
 
console.log(result); // Identity { value: 11 }

Maybe 모나드와 Either 모나드

  1. Maybe 모나드
type Maybe<T> = Just<T> | Nothing;
 
class Just<T> {
    constructor(private value: T) {}
 
    flatMap<U>(f: (value: T) => Maybe<U>): Maybe<U> {
        return f(this.value);
    }
}
 
class Nothing {
    flatMap<U>(f: (value: never) => Maybe<U>): Maybe<U> {
        return this;
    }
}
 
function maybe<T>(value: T | null | undefined): Maybe<T> {
    return value != null ? new Just(value) : new Nothing();
}
 
// 사용 예
const result = maybe("Hello")
    .flatMap(str => maybe(str.toUpperCase()))
    .flatMap(str => maybe(str.length));
 
console.log(result); // Just { value: 5 }
  1. Either 모나드
type Either<L, R> = Left<L> | Right<R>;
 
class Left<L> {
    constructor(private value: L) {}
 
    flatMap<U>(f: (value: never) => Either<L, U>): Either<L, U> {
        return this;
    }
}
 
class Right<R> {
    constructor(private value: R) {}
 
    flatMap<U>(f: (value: R) => Either<never, U>): Either<never, U> {
        return f(this.value);
    }
}
 
// 사용 예
function divide(a: number, b: number): Either<string, number> {
    return b === 0 ? new Left("Division by zero") : new Right(a / b);
}
 
const result = divide(10, 2)
    .flatMap(result => divide(result, 2));
 
console.log(result); // Right { value: 2.5 }

옵셔널 체이닝 연산자

 타입스크립트의 옵셔널 체이닝 연산자(?.)는 Maybe 모나드와 유사한 기능을 제공합니다.

interface User {
    name: string;
    address?: {
        street?: string;
        city?: string;
    };
}
 
const user: User = { name: "John" };
 
// 옵셔널 체이닝 사용
const city = user.address?.city;
 
// Maybe 모나드와 비교
const cityMaybe = maybe(user.address)
    .flatMap(address => maybe(address.city));

 옵셔널 체이닝은 더 간결하고 직관적이지만, 모나드는 더 복잡한 연산과 에러 처리에 유용합니다.

모나드를 활용한 비동기 작업 처리

 Promise는 실제로 모나드의 한 형태입니다.

function fetchUser(id: number): Promise<User> {
    // 사용자 데이터 가져오기
}
 
function fetchPosts(userId: number): Promise<Post[]> {
    // 사용자의 게시물 가져오기
}
 
fetchUser(1)
    .then(user => fetchPosts(user.id))
    .then(posts => console.log(posts))
    .catch(error => console.error(error));

타입 안정성 보장

 타입스크립트의 타입 시스템을 활용하여 모나드의 타입 안정성을 보장할 수 있습니다.

interface Monad<T> {
    flatMap<U>(f: (value: T) => Monad<U>): Monad<U>;
    map<U>(f: (value: T) => U): Monad<U>;
}
 
class Maybe<T> implements Monad<T> {
    private constructor(private value: T | null) {}
 
    static just<T>(value: T): Maybe<T> {
        return new Maybe(value);
    }
 
    static nothing<T>(): Maybe<T> {
        return new Maybe<T>(null);
    }
 
    flatMap<U>(f: (value: T) => Maybe<U>): Maybe<U> {
        return this.value === null ? Maybe.nothing() : f(this.value);
    }
 
    map<U>(f: (value: T) => U): Maybe<U> {
        return this.flatMap(x => Maybe.just(f(x)));
    }
}

모나드 트랜스포머

 모나드 트랜스포머는 여러 모나드를 조합하여 사용할 때 복잡성을 줄여줍니다.

class MaybeT<M extends Monad<any>> {
    constructor(private value: M) {}
 
    static of<M extends Monad<any>, T>(m: M, x: T): MaybeT<M> {
        return new MaybeT(m.of(Maybe.just(x)));
    }
 
    flatMap<U>(f: (x: T) => MaybeT<M>): MaybeT<M> {
        return new MaybeT(this.value.flatMap(maybe =>
            maybe.flatMap(x => f(x).value)
        ));
    }
}

'do' 표기법 구현

 타입스크립트에서 'do' 표기법과 유사한 접근 방식을 구현할 수 있습니다.

function doNotation<T>(generator: () => IterableIterator<Maybe<T>>): Maybe<T> {
    const iterator = generator();
    let result: Maybe<T> = Maybe.just(null as any);
 
    function step(value?: any): Maybe<T> {
        const { value: maybeValue, done } = iterator.next(value);
        if (done) return maybeValue;
        return maybeValue.flatMap(step);
    }
 
    return step();
}
 
// 사용 예
const result = doNotation(function* () {
    const a = yield Maybe.just(5);
    const b = yield Maybe.just(10);
    return Maybe.just(a + b);
});
 
console.log(result); // Maybe { value: 15 }

Best Practices와 주의사항

  1. 모나드 사용 시 일관성 유지 : 프로젝트 전체에서 동일한 모나드 패턴을 사용하세요.
  2. 타입 안정성 확보 : 제네릭을 활용하여 모나드의 타입 안정성을 보장하세요.
  3. 가독성 고려 : 모나드 체인이 너무 길어지면 가독성이 떨어질 수 있으므로 적절히 분리하세요.
  4. 옵셔널 체이닝과의 균형 : 간단한 null 체크는 옵셔널 체이닝을 사용하고, 복잡한 로직에는 모나드를 사용하세요.
  5. 에러 처리 : Either 모나드를 활용하여 명시적인 에러 처리를 구현하세요.
  6. 테스트 작성 : 모나드를 사용한 코드에 대한 단위 테스트를 작성하여 동작을 검증하세요.
  7. 문서화 : 복잡한 모나드 사용에 대해서는 주석이나 문서를 통해 설명을 제공하세요.
  8. 성능 고려 : 모나드의 과도한 중첩은 성능에 영향을 줄 수 있으므로 주의하세요.
  9. 팀 교육 : 모나드 개념에 대한 팀 내 교육을 통해 이해도를 높이세요.
  10. 점진적 도입 : 모나드를 프로젝트에 점진적으로 도입하여 팀원들의 적응을 돕습니다.

 모나드는 복잡한 계산과 부수 효과를 추상화하는 강력한 도구이지만 학습 곡선이 가파르고 과도하게 사용하면 코드의 복잡성을 증가시킬 수 있습니다. 반면 옵셔널 체이닝은 간단하고 직관적이지만, 복잡한 로직을 표현하는 데는 한계가 있습니다.

 간단한 null 체크에는 옵셔널 체이닝을 사용하고 복잡한 계산이나 에러 처리가 필요한 경우에는 모나드를 활용하는 것이 좋습니다.