본문으로 건너뛰기

Promise와 Monad - 2

· 약 6분
Jake Son

이전 글에서는 promise 를 반환하는 함수들을 then 을 이용해 합성하는 과정을 살펴보았다.
이제 파라미터를 2개 이상 요구하는 함수가 있는 경우를 살펴보자.

add 함수와 비동기 함수

간단하게 add 함수를 정의하고 각 파라미터는 비동기 함수의 반환값을 통해 가져온다고 생각해보자.

declare function getFirstNumber(): Promise<number>;
declare function getSecondNumber(first: number): Promise<number>;
declare function add(a: number, b: number): number;

이전 글에서 소개한 방법대로 then 을 이용해보면 문제가 발생한다.

getFirstNumber()
.then((a) => getSecondNumber(a))
.then((b) => add(a, b)); // a 를 참조할 수 없다

getFirstNumber 가 반환한 값은 오직 첫번째 then 내부 함수에서만 접근 가능하고 두번째에서는 b 만 참조할 수 있다.
물론 Promise.all 이나 상단에 임시변수를 선언하는 방식으로 해결할 수 있지만 깔끔한 방법은 아니다.

// temporary variable
let temp: number;
getFirstNumber()
.then((a) => {
temp = a;
return getSecondNumber(a);
})
.then((b) => add(temp, b));

// Promise.all
getFirstNumber()
.then((a) => Promise.all([getSecondNumber(a), a]))
.then(([a, b]) => add(a, b));

내부 함수가 외부 함수의 변수를 참조할 수 있는 성질을 활용해서 아래와 같이 활용할 수도 있다.

getFirstNumber().then((a) => {
return getSecondNumber(a).then((b) => {
return add(a, b);
});
});

가장 안쪽에 있는 (b) => { return add(a, b) } 함수는 바깥쪽 함수의 파라미터인 a를 참조할 수 있기 때문에 위 코드는 정상적으로 동작한다.
하지만 이 방법도 특정 함수가 필요한 파라미터 개수가 많아질수록 callback hell 처럼 들여쓰기가 많아질 수 있는 문제가 있다.

A().then((a) => {
return B().then((b) => {
return C().then((c) => {
return D().then((d) => {
return a + b + c + d;
});
});
});
});

async / await 와 do notation

위와 같은 문제를 해결하기 위해 async/await 가 도입되었고 아래와 같이 작성할 수 있다.

async function addNumber() {
const a = await getFirstNumber();
const b = await getSecondNumber(a);

return add(a, b);
}

마치 2개의 함수의 반환값을 지역변수 a 와 b 에 할당한 후 add 를 호출하는 코드처럼 작성할 수 있다.
순수 함수형 언어인 하스켈도 이와 비슷한 do notation 이 존재한다.

addNumber :: IO Int
addNumber = do
a <- getFirstNumber
b <- getSecondNumber a
return add a b

하스켈의 do natation 이 monad 를 반환하는 함수들을 활용한 로직을 절차적으로 표시하는 역할을 해준다.
마찬가지로 async/await 도 promise 를 반환하는 함수들을 활용한 로직을 절차적으로 표시하는 역할을 해준다.

promise 와 monad

이쯤에서 monad 에 대한 설명을 하자면 어떤 타입에 특별한 문맥을 더해주는 컨테이너라 말할 수 있다.
예를들면 number 는 일반적인 숫자타입이지만 거기에 promise 로 감싸면 안의 숫자는 일정시간이 지난 후 값을 알게된다는 문맥을 더해준다.

const a: number = 3; // a 는 평가시점에 그 값과 타입을 바로 알 수 있다.
const b: Promise<number> = new Promise((resolve) =>
setTimeout(() => resolve(5), 1000),
); // 컨테이너 안의 값은 평가시점이후 일정 시간이 지난 후에 알 수 있는데 그 타입은 number 이다.

사실 컨테이너의 종류는 promise 외에 여러가지가 있으며 직접 컨테이너를 만들수도 있다.

Monad laws

만약 컨테이너를 직접 만드는 경우 만든 컨테이너는 어떤 법칙을 만족해야 monad 라고 부를 수 있게된다.
monad 는 category theory 에서 나온 개념이기에 다분히 수학적인 법칙이며 다음 3가지이다.

  • Left identity
  • Right identity
  • Associativity

예전에 배운 덧셈의 항등법칙과 결합법칙과 비슷한 개념으로 생각하면 된다.
이 글의 목적은 monad 에 대한 완벽한 이해보다 그 유용성을 알리기 위함이기에 자세한 설명은 생략하려고 한다.

정보

사실 promise 는 특정 조건에서는 위 법칙을 만족하기 않기에 monad 라고 할 수 없다.
하지만 js 개발자에게 monad 의 유용성을 설명하는데 promise 만한게 없다고 생각하기에 약간의 정확성을 포기하였다.
만약 어떤 이유로 법칙을 만족하지 않는지 알고싶다면 참고자료의 링크를 참조한다.

참고자료

No, Promise is not a monad: https://buzzdecafe.github.io/2018/04/10/no-promises-are-not-monads