자바스크립트에서 비동기:동시성 프로그래밍을 하는 방법은 크게 callback(나중에 호출할 함수)과 Promise 두가지이다.
(비동기 프로그래밍과 콜백과 프로미스에 대한 개념을 정리해 놓은 글을 첨부하겠다.)
[JavaScript] 비동기(asynchronous) 프로그래밍과 프로미스(Promise)
비동기 프로그래밍에 대해 알아보기 앞서, 이해를 돕기 위해 반대되는 개념인 동기(synchronous) 프로그래밍에 대해 먼저 알아보자. 단어가 생소할 수는 있어도 아주 쉬운 개념이다. 아래 코드는 여
r-ing.tistory.com
하지만 대부분의 최신 비동기 API에서는 콜백을 사용하지 않고 Promise를 사용한다.
callback방식이 무엇인지 먼저 살펴보고 불편한점이 어떤점일지 생각해보자.
의도적으로 비동기 상황을 만들어서(delay를 만들어서) 1000ms 이후에 결과를 전달하는 방식의 add10함수를 만들었다.
콜백 방식은 인자 한 개와 callback함수(이 함수가 모든 동작을 끝냈을 때 결과를 전달할 함수)를 받는다.
function add10(a, callback) {
setTimeout(() => callback(a + 10), 1000);
}
add10(5, (res) => {
log(res); //15
});
똑같은 일을 Promise를 이용해 작성하면 어떻게 될까?
Promise에서는 함수에서 사용할 인자 하나를 받는다. (callback함수는 받지 않음.)
//Promise로 작성
function add10(a) {
return new Promise((resolve) => {
setTimeout(() => resolve(a + 10), 1000);
});
}
add10(5).then(add10).then(add10).then(log); //35
7line처럼 add10을 여러번 수행할고 싶을 때
Promise는 이처럼 then을 통해 간단하게 수행할 수 있지만
콜백 방식은 더 복잡하게 작성할 수 밖에 없다.
//콜백 방식으로 작성
add10(5, (res) => {
add10(res, (res) => {
add10(res, (res) => {
log(res); //35
});
});
});
Promise가 콜백보다 더 간단하다는 점도 장점이지만
가장 큰 차이점은 Promise가 '비동기 상황을 일급 값으로 다룬다.' 라는 점이다.
#0에서 배웠던 개념인데, 일급함수란? 함수가 값으로 다뤄지는 것이다.
그러니까 Promise는 비동기 상황을 값으로 다룰 수 있다는 것이다.
이는 아주 중요한 Promise의 특징이 된다.
다음 코드를 살펴보자.
var a = add10(5, (res) => {
add10(res, (res) => {
add10(res, (res) => {
log(res); //35
});
});
});
var b = add20(5).then(add20).then(add20).then(log); //65
log(a); //undefined
log(b); //Promise {<pending>}
위에 코드를 값으로 선언한 후 log를 찍어보면
콜백으로 만든건 undefined가 나오고
Promise로 만든건 Promise가 나온다.
여기에서 알 수 있듯이 undefined는 어떤 상황이 일어나고 있는지 전혀 알 수 없지만
Promise값은 어떤 함수에 할당되거나 전달될 수 있는 등 다음 일들을 이어나갈 수 있다.
이것이 Promise의 엄청난 장점이다.
Promise가 비동기 상황을 '값'으로 다루는 일급의 성질을 가지고 있다는 것을 활용해서 다양한 일들을 할 수 있다. 이제 이 성질을 활용할 수 있는 함수를 만들어보겠다.
go1함수를 n1과 n2의 동작이 완전히 동일하도록 만든 코드이다.
const delay100 = (a) =>
new Promise((resolve) => setTimeout(() => resolve(a), 100));
const go1 = (a, f) => (a instanceof Promise ? a.then(f) : f(a));
const add5 = (a) => a + 5;
const n1 = 10;
log(go1(go1(n1, add5), log));
const n2 = delay100(10);
log(go1(go1(n2, add5), log));
1line) delay100함수는 100ms 후에 받아뒀던 값(a)을 return하는 함수이다.
4line) go1함수는 a가 Promise인지 아닌지를 판단해서 Promise라면 then을 통해 값을 받아온다.
Promise가 일급이라는 성질을 활용해서 비동기일 때와 아닐 때 모두 동작할 수 있도록 구현했다.
(다형성)
이번에는 함수 합성 관점에서의 Promise를 살펴보자.
Promise는 비동기 상황에서 함수 합성을 안전하게 하기 위한 도구이다.
비동기 값을 가지고 연속적인 함수 실행을 안전하게 하는 모나드(monad)라고도 설명할 수 있다.
자바스크립트는 동적 타입 언어이고, '타입'을 중심적으로 사고하며 프로그래밍하는 언어가 아니라서
모나드라는 개념을 직접적으로 사용하지는 않지만
안전한 함수 합성에 필요한 사고에 도움은 되기 때문에 간단히 알아보자.
모나드(monad)
함수 합성을 안전하게 하기 위한 도구
알기 쉽게 코드 예시를 들어 설명해보겠다.
const g = (a) => a + 1;
const f = (a) => a * a;
log(f(g(1)));
log(g(f()));
이런 함수 합성을 안전한 함수 함성이라고 말할 수 없다.
5line처럼 f안의 값이 안전한 인자가 아니면 결과가 비정상적으로 출력되기 때문이다.
모나드는 연산에 필요한 재료가 들어있는 박스를 가지고 있고,
박스가 가지고 있는 메서드들로 함수 합성을 한다.
(f · g의 함성을 하고 싶다면 map(g).map(f)처럼 작성한다.)
//위 코드 4line과 동일한 코드
[1]
.map(g)
.map(f)
.forEach((r) => log(r));
//위 코드 5line과 동일한 코드
[]
.map(g)
.map(f)
.forEach((r) => log(r));
위 코드의 원리를 풀어보자.
[1].map(g)을 하면 g에 1이 통과되고, 이 값은 다시 array가 된다.
그래서 .map(f)을 또 할 수 있다.
그렇게 [1].map(g).map(f)까지가 함수를 합성한 것이고,
이제 forEach로 실제 외부세상의 원하는 효과를 만든다. (여기서는 log)
그래서 결국 이러한 함수 함성의 이점이 무엇이냐? 하면
위의 8~11line은 실행시키면 아무런 일도 일어나지 않는다.
박스 안에 아무 값이 없어서 forEach 안의 함수 자체가 실행이 되지 않았기 때문이다.
하지만 아까 코드에서 보면 모나드식으로 작성하지 않은 코드에서는 NaN이 출력되었다. (비정상적인 값)
빈 값이 들어와도 강제로 함수 실행이 되었다는거다.
이처럼 모나드 형태의 연산은 박스 안에 값이 없으면 억지로 실행되지 않아 안전한 함수합성이 이루어졌다고 볼 수 있다.