Promise
의 유틸 메서드
- 상황에 따라 편리하게 이용 가능한 유틸 메서드도 제공한다.
Promise.all()
- 모든
Promise
가resolve()
여야then()
을 실행한다. - 서로 의존하지 않고 병렬로 수행될 수 있는 여러 개의 비동기 작업이 있는데, 결과를 내기 위해서는 각 비동기 작업의 결과 값이 필요할 때 유용하다.
ex) 3개의 영상의 조회수를 구하고 그 합을 구해야 한다면?
const viewCount1 = getAsyncViewCount1();
const viewCount2 = getAsyncViewCount2();
const viewCount3 = getAsyncViewCount3();
viewCount1.then((count1) => {
viewCount2.then((count2) => {
viewCount3.then((count3) => {
console.log(count1 + count2 + count3);
});
});
});
- 위와 같이 하지 않아도 된다.
const viewCount1 = getAsyncViewCount1();
const viewCount2 = getAsyncViewCount2();
const viewCount3 = getAsyncViewCount3();
Promise.all([getAsyncViewCount1(), getAsyncViewCount2(), getAsyncViewCount3()])
.then(([count1, count2, count3]) => {
console.log(count1 + count2 + count3);
})
.catch((error) => {
console.error(error);
});
- 이렇게 작성할 수 있다
- 비동기 중 단 하나라도
reject
가 호출된다면,catch()
로 넘어간다.
class FetchError extends Error {
constructor(response, message = "HTTP error " + response.status) {
super(message);
this.response = response;
}
}
const myFetch = (...args) => {
return fetch(...args).then((response) => {
if (!response.ok) {
throw new FetchError(response);
}
return response.json();
});
};
Promise.all([
myFetch("/json-100"),
myFetch("/json-200"),
myFetch("/json-300"),
400,
500,
])
.then(
([
{ data: count1 },
{ data: count2 },
{ data: count3 },
count4,
count5,
]) => {
console.log(count1 + count2 + count3 + count4 + count5); // 1500
}
)
.catch((error) => {
console.error(error);
});
- api 결과값 3개와 상수 2개를 더하는 예제를 작성해보았다.
/json-xxx
api 는 각각 뒤의 숫자를{data: xxx}
형태로 반환하게 해두었다.
API 가 하나라도 동작하지 않는다면 아래의 에러 메세지를 만난다.
GET http://localhost:8080/json-1100 404
Error: HTTP error 404
at <anonymous>:11:13
at async Promise.all (:8080/index 0)
Promise.race()
- 가장 빠른 비동기 응답을 이용한다.
const timeout = (ms) =>
new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("Timeout Error!!!"));
}, ms);
});
Promise.race([fetch("/json-100"), timeout(100)])
.then((res) => res.json())
.then((json) => console.log(json))
.catch((error) => {
console.log("타임아웃 에러!");
});
- 위는 100ms 안에 응답하지 않으면, API 를 타임아웃 시키는 예제이다.
Promise.allSettled()
Promise.all()
과 흡사하지만,Promise.allSettled()
는 모든Promise
가 전부resolve()
를 호출하지 않아도 된다.
Promise.allSettled([Promise.resolve("hello"), Promise.reject("bye")])
.then((results) => {
console.log(results);
})
.catch((error) => console.log(error));
/*
0: {status: 'fulfilled', value: 'hello'}
1: {status: 'rejected', reason: 'bye'}
*/
- 1번은 상태가
fulfilled
이며, 2번은 상태가rejected
이다. catch()
는then()
에서throw new Error()
를 던진다면, 호출된다.
Promise.any()
Promise.all()
의 반대다 성공된 어느 하나로then()
을 수행한다.
class FetchError extends Error {
constructor(response, message = "HTTP error " + response.status) {
super(message);
this.response = response;
}
}
const myFetch = (...args) => {
return fetch(...args).then((response) => {
if (!response.ok) {
throw new FetchError(response);
}
return response.json();
});
};
Promise.any([myFetch("/json-100"), myFetch("/json-200"), myFetch("/json-300")])
.then((v) => {
console.log(v);
})
.catch((error) => {
console.error(error); // {data: 100} or {data: 200} or {data: 300} 중 아무거나 선택된다.
});
Promise
의 작업 패턴
Promise
오브젝트를 사용할 때 유용한 몇가지 패턴이 존재한다.
반드시 오류를 처리하거나 값을 반환하는 작업을 하자.
- 때때로
then()
,catch()
,finally()
를 성실히 작성하는 것만으로도 간단히 파악할 수 있던 오류를 단순히 콜백함수를 작성하지 않은 이유로 늦게 발견하는 일이 생긴다.
fetch("https://cat-fact.herokuapp.com/facts")
.then(
(response) => response.json(),
(rejected) => console.log(`rejected in myFetch method: ${rejected}`)
)
.then((json) => {
console.log(json);
})
.catch((error) => console.log(`rejected after fetch method: ${error}`));
- 위와 같이 성실하게
reject()
시 호출되는 콜백과catch()
를 작성하면,fetch()
메서드를 호출하다 에러가 난건지,response.json()
을 수행하다 에러가 난건지 쉽게 알 수 있다.
루프를 이용해 Promise
체인을 구성할 수 있다.
- 용례는 javascript 를 이용해서 구성한 웹서버의 미들웨어 기능(
Interceptor
혹은Filter
와 같은 기능)을 구성할 때 유용하게 사용할 수 있다.
function handleTransforms(value, transforms) {
return transforms.reduce(
(p, transform) => p.then((v) => transform(v)), // 콜백
Promise.resolve(value) // 시드 값
);
}
handleTransforms(50, [(v) => v + 100, (v) => v + 200, (v) => v + 300])
.then((v) => {
console.log(v); // 650
})
.catch((error) => {
console.error(error);
});
then(v => v + 100)
과 같이then()
에서 값을 반환하면, 반환된 값이Promise
객체에 싸여서 반환된다는 것을 아는 것이 포인트이다.- 이로 인해
Promise
체인 연결이 가능한 것이다. then(v => v + 100)
은 결국v
값을 알 때,Promise.resolve(v + 100)
을 하는 것과 동일한 결과다.
- 이로 인해
[].map()
과 Promise.all()
조합하기
map()
과Promise.all()
을 조합하는 패턴을 사용하면, 멋지게 병렬 프라미스 작업을 완성할 수 있다.
const urls = ["/api/profile", "/api/friends", "/api/recommendFriends"];
Promise.all(urls.map((url) => fetch(url).then((res) => res.json())))
.then((results) => {
console.log(results);
})
.catch((error) => {
console.error(error);
});
안티 패턴
Promise
는 대부분 직접 객체를 만들기보다는 만들어진Promise
를 활용하게 된다.- 직접 만들다가 잘못 쓰기 쉽다.
불필요한 new Promise()
를 사용하지 말자.
function getData(id) {
return new Promise((resolve, reject) => {
fetch("/url/for/data?id=" + id)
.then((res) => res.json())
.then((data) => resolve(data))
.catch((error) => reject(error));
});
}
- 위 코드의 잘못된 점은
fetch()
는 이미Promise
를 반환하는데, 또Promise
안에 감쌀 필요가 전혀 없다.
function getData(id) {
return fetch("/url/for/data?id=" + id).then((res) => res.json());
}
getData(10).then((json) => console.log(json));
- 위와 같이 간략화 할 수 있다.
오류를 처리하지 않거나 적절하게 처리하지 않는 실수를 하지 말자.
catch()
블록이 오류를 처리하거나reject()
를 통해 적절히 넘겨주어야 한다.- 오류를 직접 처리하거나 호출자가 처리할 수 있도록 도와주자.
오류가 눈에 띄지 않게 하지 말자.
const getAllRows = (query) => {
return new Promise((resolve, reject) => {
query.execute((err, resultSet) => {
if (err) {
reject(err); // 혹은 `reject(new Error(err))`
} else {
const results = [];
while (resultSet.next()) {
results.push(resultSet.getRow());
}
resolve(results);
}
});
});
};
- 얼핏 보면 안티패턴을 사용하지 않았다고 생각하기 쉬운데, 문제는
resultSet.next()
이다.- 현재
new Promise()
내부에 있는 프라미스 콜백 안에서 동작한다. - 만일
query.execute()
가Promise
를 반환한다면,resultSet.next()
에서 문제가 발생했을 때, 조용히 실패하고Promise
는 영원히pending
상태로 남게 된다.
- 현재
const helloPromise = new Promise((resolve, reject) => {
console.log("hello");
});
- 이를테면, 위에 있는
helloPromise
는pending
에서 절대 변하지 않는다.new Promise()
에 인자로 주어진 콜백이라면, 반드시resolve()
혹은reject()
를 호출하게 해야 비동기 반환값을 활용할 수 있다.
함수의 결과로 Promise
의 거부를 반환하는 행위
function getData(id) {
return fetch("/url/for/data?id=" + id)
.then((res) => res.json())
.catch((err) => {
reportErr(err);
});
}
- 위의 경우 성공하면
json
이 잘 반환되지만, 실패하는 경우Promise<undefined>
가 반환된다.- 그 이유는
catch()
체인이 맨 마지막에 달려있기 때문이다.
- 그 이유는
- 위와 같이 작성하는 경우, 클라이언트가 함수를 이용할 때 성공 (
then()
) 혹은 실패 (catch()
) 로직을 깔끔하게 제어할 수 없게 된다.undefined
인 경우를if
처리해서 살펴봐야 한다.
외부에서 Promise
결과 사용 시도
Promise
를 이제 쓰기 시작한 초보 개발자들이 많이 저지르는 실수다.
let result;
startSomething().then((res) => (result = res.result));
doSomethingWith(result);
doSomethingWith(result)
는 콜백 안으로 넣어줘야 잘 동작할 것이다.
의도치 않게 아무 동작도 없는 핸들러 사용하기
startSomething()
.then((v) => v)
.then((res) => doSomething(res))
.catch((err) => {
throw new err();
});
- 위의 핸들러 중 2개는 아무것도 안한다.
then()
은 딱 봐도 아무것도 안하는 것을 쉽게 알 수 있다.catch()
는throw
로 에러를 던져버리면, 생성된 프라미스catch()
를 거부하게 된다.
startSomething().then((res) => doSomething(res));
- 그러면 사실상 위의 코드와 같은 역할인데, 코드만 길어지고 읽기 어려워진 코드인 것이다.
틀린 연결 분기
const p = startSomething();
p.then((res) => {
doSomethingWith(response.result);
});
p.catch(handleError);
- 위 코드도 유효한 코드이다.
- 콜스택에 있는 작업이 끝나기 전까지
Promise
내부의 비동기 작업은 실행되지 않음을 인지하자.
- 콜스택에 있는 작업이 끝나기 전까지
- 위 코드의 문제점은
then()
에서 예외가 일어났을 때, 이를catch()
로 받아주지 못한다는 점이다.
startSomething()
.then((res) => {
doSomethingWith(response.result);
})
.catch(handleError);
- 위 코드가 훨씬 간결하면서도 제 역할을 다한다.
반응형
'자바스크립트 > 모던 자바스크립트' 카테고리의 다른 글
모던 자바스크립트, 제너레이터 (Generator) (0) | 2023.02.28 |
---|---|
모던 자바스크립트, 이터러블 (iterable) 과 이터레이터 (iterator) (0) | 2023.02.28 |
모던 자바스크립트, 프라미스 1 - 기본 개념 (0) | 2023.02.15 |
모던 자바스크립트, 디스트럭처링 (Desctructuring) (0) | 2023.02.08 |
자바스크립트 클래스 3 - 상속 (0) | 2023.02.01 |