변수에 할당할 수 있고, 함수의 인자나 함수의 리턴값으로 사용될 수 있는 객체를 일급객체라고 말한다. 즉 객체를 값으로 다룰 수 있다.
일급함수는 함수가 다른 일급객체(변수)와 동일하게 다루어 질 수 있다는 말이다. 즉 함수가 변수로 사용될 수 있다.
// 함수의 결과값으로 함수를 사용할 수 있다.
const returnFunc = () => () => 1;
console.log(returnFunc) // 함수의 인자로 함수가 사용된 모습이다.
console.log(returnFunc()); // () => 1; 함수가 출력됨
// 만들어진 함수를 변수로 담을 수 있다.
const wrapper = returnFunc();
console.log(wrapper); // () => 1; 함수가 출력됨
이렇게 함수가 변수로 사용되면 아래와 같은 장점을 가지게 된다.
1. 즉시 실행되지 않아도 된다. 지연 실행을 지원한다.
2. 함수의 합성을 통해 고차 함수(함수를 인자로 받아서 실행하는 함수), 클로저 등을 만들 수 있다.
const apply = f => f(1);
const add10 = a => a+10;
console.log(apply(add10)); // 함수의 합성~! 11
console.log(apply(a => a - 1)); // 직접 함수를 인자로 전달하여 함수를 합성할 수도 있다. 0
클로저란, 함수 내부에서 외부 변수를 기억하고 있는 구조를 클로저라고 한다. (인수와 함수 모두를 통틀어 클로저라고 부른다.)
// 클로저
const addMaker = a => b => a + b;
const add10 = addMaker(10);
log(add10(5)); // 15
log(add10(10)); // 20
함수가 조합과 추상의 도구로 사용될 수 있고 고차 함수를 통해 함수형 프로그래밍이 가능해진다. 다른 OOP 언어에서는 자바 언어의 함수형 인터페이스(Supplier, Predicate, Consumer, Function) 들이 대표적인 예가 될 수 있다.
onLoad 속성은 이벤트다. 오브젝트가 로드되었을 때 발생하는 이벤트로 내부 표현식에 있는 변수들을 외부로 반환하기 위해서는 onLoad 이벤트가 발생할 때까지 기다려야 한다. 순차적으로 실행되지 않고 어떤 특정 시점에 호출되는 함수 내부에 있는 변수들을 반환해야 하므로 Promise 객체를 반환하도록 변경한다.
=>
const loadImage = url => new Promise(resolve => {
let img = new Image();
img.src = url;
img.onload = () => {
resolve(img);
}
});
// loadImage(imgs[0].url).then(img => console.log(img.height));
요구사항 3 : 이미지 리스트들을 순회하면서 height 값을 출력하자.
image url 이 주어졌을 때 height 값을 구하는 함수를 만들었으니 리스트들을 순회하면서 map 으로 해당 함수를 호출하여 일대일 변환해보자.
이렇게 구현하면 loadImage 가 Promise 를 반환하기 때문에 다음 구문들도 Promise 를 인자로 받아 실행이 된다.
이 때 Promise 객체에서는 없는 속성(여기서는 height 값) 을 참조하므로 undefined 가 최종적으로 반환된다. 결과 값을 기다려야 하므로 loadImage 함수를 await 지시어를 사용해 기다리고, await 을 사용했으니 function 에는 async 가 붙는다.
그런데 이 async 함수에서 반환하는 값은 또 Promise 이므로 다음 구문에서도 계속해서 Promise 를 사용하기 위해 async-await 구문을 사용해야 한다. async-await 구문 지옥. 일단 해당 함수를 패치해서 기능 구현은 정상적으로 해두었다.
더 심각해졌다. 내부에 있는 async-await 구문들이 외부까지 전파가 된 모습이다. 합계를 출력해야 되기 때문에 함수 내부에 total 변수를 생성하였는데 async 구문 안에서 사용하므로 await 을 통해 결과 값을 기다려주어야 한다. await 을 붙였으니 async 가 붙을테고 Promise 가 계속 전파되어버린다.
리팩토링 1 : 제너레이터와 코드 분리를 통해 자주 사용하는 로직을 관심사 분리한다.
리팩토링 1-1 : map 을 Promise 객체가 와도 컨트롤 할 수 있게 변경하자.
Promise 가 오면 then 과 함수를 합성해서 반환하고 일반 객체가 오면 그 객체를 함수 호출해서 그대로 반환한다. 원조 map 은 Iterable 한 객체를 인자로 받아서 가공하고 하나씩 결과 값을 방출하는 Generator 이다.(Iterator 의 특수한 형태) 새롭게 만들 map 도 Generator 형태로 만들면 된다.
=>
function* map(f, iter) {
for (const a of iter) {
yield a instanceof Promise ? a.then(f) : f(a);
}
}
리팩토링 1-2 : reduce 함수도 Promise 객체를 컨트롤 할 수 있는 비동기 버전을 만든다.
인자들이 Promise 이면 계속해서 모든 인자들을 await 을 붙여 신경 써주어야 하고 함수 자체를 async 하게 만들어야 하므로 map 과 똑같이 비동기 버전을 만들 필요가 있다. 다만, map 과 다른 점은 Promise 를 반환할 일이 없기 때문에 제너레이터 성격이 아닌 이터레이터만 만들면 되므로 굳이 then 과 합성할 필요가 없다.
=>
async function reduceAsync(f, acc, iter) {
for await (const a of iter) {
acc = f(acc, a);
}
return acc;
}
참고로, Promise.all() 과의 차이점은 다음과 같다. Promise.all() 은 인자의 프로미스 배열을 동시에 실행한다. for await ( ~ of ~ ) 내의 비동기 작업은 루프를 돌며 순차적으로 실행된다.
만들어 둔 두 가지 함수로 리팩토링하면 아래와 같이 간결해진다.
async function f(imgs) {
return await reduceAsync((a, b) => a + b, 0,
map(img => img.height,
map(({url}) => loadImage(url), imgs)));
}
어차피 이 코드도 Promise 를 반환하기 때문에 async 와 await 을 없애도 된다.
const f = (imgs) =>
reduceAsync((a, b) => a + b, 0,
map(img => img.height,
map(({url}) => loadImage(url), imgs)));
만약에 여기서 img 중 하나가 잘못된 img 라면 어떻게 될까? 어떻게 에러 핸들링을 해야 깔끔한 코드가 될까?
정답은 이 코드 바깥에서 예외처리 한 번만 해주는 것이다. 이 함수 내부에서 어떻게든 처리하려고 하면 에러가 숨게 되고 못 찾을 가능성이 있다.
불필요하게 에러 핸들링을 미리 해두어 에러를 숨기는 것보다차라리 에러가 발생해야 좋은 코드이다.
(인자 값으로 들어오는 입력 값이 정확한 값이라면 절대 에러가 나지 않는 코드.) 즉 순수함수를 작성하고, 해당 순수함수를 사용하는 쪽에서 에러 핸들링을 해야 에러 핸들링도 적게 하고, 깔끔한 코드를 만들 수 있게 된다.최대한 해당 로직을 사용하는 코드 쪽에서 최종 예외 처리 해주는 것이 좋다.
Q & A
1. 에러가 발생했을 때 실행이 멈추어 버리는 이유는?
Exception 이 then 함수와 만나 합성하면 reject 이 반환되고 이 reject 이 await 이라는 구문을 만나면 예외를 throw 한다.