들어가며
오늘은 바닐라 js 강의 조금이랑 함수형 프로그래밍 강의 조금 들었다. 점점 이해하는 데 시간이 오래 걸려서 모든 걸 이해하고 배운 내용을 정리할 수는 없을 것 같다. 최대한 이해해보고 이해한 부분까지만 정리하고 이해 못한 부분들은 나중에 반복하면서 이해하고 정리하는 게 나을 것 같다.
Promise
프로미스는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과값을 나타내는 객체이다. 다음 중 하나의 상태를 가진다.
- 대기(Pending): 이행하지도, 거부하지도 않은 초기 상태
- 이행(Fulfilled): 연산이 성공적으로 완료됨
- 거부(Rejected): 연산이 실패함
프로미스 객체는 resolve 또는 reject를 사용해 성공 또는 실패를 반환하고, then, catch, finally 메서드를 사용해 프로미스의 결과를 확인할 수 있다. 프로미스는 ES6에 정식으로 채택되어 사용되고 있는데 프로미스 이전에는 콜백 함수를 사용해 비동기 작업을 처리했다.
1 function asyncFunc(name, callback) {2 console.log(`hello ${name}`);3 setTimeout(callback, 1000);4 }56 asyncFunc(name, function nextFunc(){7 console.log('next');8 }
위와 같이 함수의 인자로 콜백 함수를 받아 비동기 작업을 진행했다. 그러나 여러 개의 콜백 함수를 사용하면 실행 순서를 보장하기 위해 콜백 함수가 중첩되는데 코드의 가독성이 떨어지게 된다. 프로미스를 사용해 비동기 코드를 작성해보자.
1 const promise = new Promise((resolve) =>2 setTimeout(() => resolve("hello"), 1000)3 );4 promise.then(console.log);
위 코드는 프로미스 객체를 사용해 1초 뒤에 hello 라는 문자열을 반환한다. new Promise에 함수를 전달할 수 있는데 이 함수는 자바스크립트에서 자체적으로 제공하는 resolve와 reject라는 콜백을 인자로 받는다. 작업이 성공적으로 끝나면 resolve를 사용해 값을 반환하고, 실패했다면 reject를 사용해 에러를 반환한다.
현재 promise 변수에는 프로미스 객체가 담겨 있다. 이 객체의 수행 결과를 확인하기 위해서는 then, catch, finally의 메서드를 사용한다.
- then: 프로미스가 이행되거나 거부 되었을 때 실행하기 위해 사용한다. 두 개의 인자를 받을 수 있는데 첫 번째 인자는 프로미스가 이행되었을 때 실행할 함수를 전달하고, 두 번째 인자는 프로미스가 거부되었을 때 실행할 함수를 전달한다.
- catch: then 과 비슷하지만 오직 에러가 발생했을 때만 실행된다. .then(null, function) 으로 사용하는 것과 동일하다.
- finally: 프로미스의 실행 결과와 상관 없이 프로미스가 처리되었을 때 실행하는 메서드이다.
함수형 프로그래밍에서의 Promise
사실 많은 설명에서 프로미스의 장점을 이야기할 때 콜백 지옥을 해결하는 것에 중점을 두는 경우가 많은데 프로미스가 가진 진짜 장점은 일급으로 비동기 상황을 다룬다는 점에 있다고 한다.
일급이란? 값으로 다룰 수 있고, 변수에 담을 수 있고, 함수의 인자로 사용될 수 있고, 함수의 결과로 사용될 수 있는 것
앞에서 얘기한 것 처럼 프로미스는 인스턴스를 반환하는데 이는 대기, 성공, 실패를 다루는 일급 값으로 이루어져 있다. 콜백 함수와 비교했을 때 콜백 함수는 아무것도 리턴하지 않는데 Promise는 프로미스 객체를 반환하기 때문에 비동기 상황을 값으로 다룰 수 있다.
함수 합성 과정에서의 Promise
함수의 합성은 수학 시간에 배운 함수의 합성을 의미한다. 함수 f와 함수 g가 있을 때 두 함수를 합성하면 f(g(x)) 와 같이 나타낼 수 있다.
1 const f = (a) => a * a; // 함수 f2 const g = (a) => a + 1; // 함수 g3 const fg = (a) => f(g(a)); // f와 g의 합성 함수4 console.log(fg(1)); // 45 console.log(fg()); // NaN
코드를 통해 함수 f와 g를 정의하고 합성 함수 fg를 정의했다. fg에 1을 입력 했을 때에는 의도한 대로 동작하지만 아무 값도 넣지 않았을 때는 의도한 대로 동작하지 않는다. 이는 안전하게 함수 합성이 되었다고 할 수 없다. 그럼 어떻게 안전하게 함수를 합성할 수 있을까?
1 const f = (a) => a * a; // 함수 f2 const g = (a) => a + 1; // 함수 g3 [1]4 .map(g)5 .map(f)6 .forEach((a) => console.log(a)); // 47 []8 .map(g)9 .map(f)10 .forEach((a) => console.log(a)); // 아무것도 출력되지 않음
위 코드에서 배열과 배열의 메서드를 사용해 함수의 합성을 진행했다. 1 값이 입력 됐을 때 의도한 대로 4가 출력되고, 아무 값이 입력되지 않은 경우 아무것도 출력되지 않는다. 이 때 안전한 함수의 합성을 위해 배열을 사용했는데, 이처럼 연속적으로 함수가 실행되고 합성을 할 때 안전하게 합성하기 위해 사용하는 것을 모나드라고 한다. (사실 모나드가 뭔지 정확하게 몰라 검색해봤는데 굉장히 어려운 개념인 것 같다. 모나드를 이해하기 위해 2년 동안 함수형 언어도 공부하고 수학도 공부했다는 분도 있었다.)
비슷하게 프로미스를 사용하면 비동기 상황에서 함수를 안전하게 합성할 수 있다. 코드를 보자
1 Promise.resolve(1)2 .then(g)3 .then(f)4 .then((r) => console.log(r)); // 4
프로미스를 사용해 함수의 합성을 진행했다. 그러나 프로미스의 경우 위의 배열의 예시처럼 값이 있거나 없거나 하는 상황에 안전한 것이 아닌 비동기 상황에 안전한 함수의 합성을 위해 사용하는 것이다. 비동기 상황에서 일정 시간 기다린 후 함수를 평가하고 안전하게 합성하기 위한 도구로써의 Promise이다.
Kleisli Composition 관점에서의 Promise
Promise는 Kleisli Composition을 지원한다. 이름부터 어려운 Kleisli Composition이란 오류가 있을 수 있는 상황에서의 함수 합성을 안전하게 하는 방법이다. 수학에서는 함수 f와 g가 있을 때, 항상 f(g(x)) = f(g(x)) 를 만족한다. 그러나 프로그래밍에서는 상태에 따라 값이 달라질 수 있다. 만약 g(x) 함수에서 오류가 난다면 f(g(x)) = g(x) 로 만들어주는 함수 합성 방법이 Kleisli Composition이다.
(추가 예정...)
정리
함수형 프로그래밍 강의를 들으면서 느끼는 점은 함수형 프로그래밍을 위해 유용하게 사용되는 함수를 정의하고 다형성을 위해 함수를 수정하는 과정을 겪고 있는 것 같다. 이터러블/이터레이터 프로토콜을 만족하기 위한 함수를 작성하고, 지연성을 위해 함수를 수정하고, 비동기 상황에서 프로미스 객체를 입력으로 받기 위해 함수를 수정하기도 하면서 그 과정에서 함수형 사고를 익히는 것 같다. 아직은 이해가 부족하지만 배워 놓으면 나중에 분명 도움이 되는 시기가 올 것 같다.
참고자료
프로미스 https://poiemaweb.com/es6-promise
https://ko.javascript.info/promise-basics
3분 모나드 https://overcurried.com/3%EB%B6%84%20%EB%AA%A8%EB%82%98%EB%93%9C/
함수형 프로그래밍 - 비동기/동시성 https://velog.io/@codenmh0822/%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%8F%99%EC%8B%9C%EC%84%B1-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D