이전 글에서는 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