반응형
오늘의 공부 주제!

 

요구사항 1 : 이미지의 height 값을 계산하자.

f1 함수에서 height 값을 계산하도록 img.height 형태로 로직을 수정해보면, 정확한 값이 계산되지 않는다. 0 이 출력된다.

url 속성과 다르게 height 속성은 객체가 로드가 될 때 제대로 계산이 될 수 있기 때문이다. onLoad 속성의 이벤트(오브젝트가 로드되었을 때 발생하는 이벤트)를 사용해서 객체가 로드될 때 계산이 되도록 변경한다.

 

=>

const loadImage = url => {
    let img = new Image();
    img.src = url;
    img.onload = () => {
        console.log(img.height);
    }
    return img;
};
loadImage(imgs[0].url);

 

요구사항 2 : 이미지의 height 값을 외부로 반환하는 함수로 만들자.

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 으로 해당 함수를 호출하여 일대일 변환해보자.

 

=>

// 실패코드
function f() {
    imgs
    .map(({url}) => loadImage(url))
    .map(img => img.height)
    .forEach(a => console.log(a));
}

 

이렇게 구현하면 loadImage 가 Promise 를 반환하기 때문에 다음 구문들도 Promise 를 인자로 받아 실행이 된다.

이 때 Promise 객체에서는 없는 속성(여기서는 height 값) 을 참조하므로 undefined 가 최종적으로 반환된다. 결과 값을 기다려야 하므로 loadImage 함수를 await 지시어를 사용해 기다리고, await 을 사용했으니 function 에는 async 가 붙는다.

 

그런데 이 async 함수에서 반환하는 값은 또 Promise 이므로 다음 구문에서도 계속해서 Promise 를 사용하기 위해 async-await 구문을 사용해야 한다. async-await 구문 지옥. 일단 해당 함수를 패치해서 기능 구현은 정상적으로 해두었다.

 

=>

function f() {
	imgs
    .map(async ({url}) => {
        const img = await loadImage(url);
        return img.height;
    })
    .forEach(async a => console.log(await a));
}

 

요구사항 4 : 이미지 리스트들을 순회하면서 총 height 값을 구해보자.

이미지 리스트들의 height 값을 더해 총 height 값을 구현해야 하는 요구사항이 추가되었다. 단순히 출력하는 코드를 수정해 reduce 기능을 활용한다.

 

=>

async function f() {
    const total = await imgs
        .map(async ({url}) => {
            const img = await loadImage(url);
            return img.height;
        })
        .reduce(async (total, height) => await total + await height, 0);
	console.log(total);
}

 

더 심각해졌다. 내부에 있는 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 한다.

즉, 예외 전파가 가능한 것이다.

반응형

'언어 > Javascript' 카테고리의 다른 글

[JS] 커링함수  (0) 2023.02.12
[JS] 일급함수  (0) 2023.02.10

+ Recent posts