Promise
는 비동기 함수에 의해 반환되는 객체로, 작업의 현재 상태를 나타낸다.
Promise
객체는 비동기 작업이 끝나지 않았더라도 caller에게 반환되고 나중에 작업이 끝났을 때 최종적인 성공 또는 실패를 처리할 수 있는 메서드를 제공한다.
즉 promise기반의 API에서, 비동기 함수는 시작과 동시에 Promise
객체를 반환하고 Promise
객체에 핸들러를 붙여서 성공 또는 실패시 핸들러를 실행시킬 수 있다.
XMLHttpRequest
API를 사용하여 HTTP요청을 수행할 수 있다.
하지만 이것은 옛날 방식이고 모던 자바스크립트에서는 promise기반의 fetch()
API를 이용한다.
const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
console.log(fetchPromise);
fetchPromise.then((response) => {
console.log(`Received response: ${response.status}`);
});
console.log("Started request…");
fetch()
API를 호출하고 fetchPromise
변수에 반환 값을 할당한다.
바로 다음에 fetchPromise
변수를 로깅한다.
Promise{<state>:"pending"}
과 같이 출력될 것이다.
이는 반환된 값이 Promise 객체이고, 그 상태가 pending이라는 것을 알려준다.
참고로 pending은 아직 작업이 진행중이라는 걸 의미한다.
Promise의 then()
메서드에 핸들러를 전달한다.
fetch()
작업이 성공했을 때 Promise는 서버의 응답정보를 담고있는 Response
객체를 핸들러에 전달하여 핸들러를 호출한다.
"Started request..." 로그가 출력된다.
전체 출력은 다음과 같이 나와야 한다.
Promise { <state>: "pending" }
Started request…
Received response: 200
동기화 함수와 달리 fetch()
는 요청이 시작됨과 동시에 반환되므로 프로그램이 멈추지 않는다.
Response
객체에는 200(OK) status code가 표시되며, 이는 요청이 성공했음을 의미한다.
promise 객체의 then()
메서드로 핸들러를 등록하는 것은 XMLHttpRequest
객체에 이벤트 핸들러를 추가하는 모습과 매우 유사하단걸 알 수 있다.
fetch()
API를 이용하여 Response
객체를 얻으면 끝나는게 아니다.
그 후 데이터를 얻기위해 또 다른 함수를 호출해야 한다.
JSON 데이터 객체를 가져온다고 가정하면 Response.json()
메소드를 호출해야 한다.
그런데 Response.json()
메소드는 비동기 함수이다.
즉, JSON 객체를 얻기위해서 fetch()
API와 Response.json()
함수 2개의 비동기 함수를 연속적으로 호출해야 한다.
const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
fetchPromise.then((response) => {
const jsonPromise = response.json();
jsonPromise.then((data) => {
console.log(data[0].name);
});
});
위의 코드에서는 연속적인 비동기 작업을 하기 위해 첫 번째 핸들러 함수 내에서 두 번째 핸들러를 등록했다.
이는 콜백 함수안에서 콜백 함수를 호출하는 "콜백 지옥"과 모양새와 매우 유사하다.
콜백 지옥을 없애기 위해 Promise를 이용했는데 콜백 지옥이 되버리면 말짱 도로묵이다.
물론 Promise를 이용한 연속적인 비동기 작업을 할 땐, 위와 같이 작성하지 않고 다른 방법을 사용한다.
Promise의 개쩌는 특징으로, then()
메서드는 전달받은 핸들러 작업을 끝낸 Promise 객체를 반환한다는 것이다.
이는 위의 콜백 지옥과 같은 코드를 아래와 같이 바꿔쓸 수 있다는 것을 의미한다.
const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
fetchPromise
.then((response) => response.json())
.then((data) => {
console.log(data[0].name);
});
두 번째 핸들러를 첫 번째 핸들러 내부에서 호출하는 대신, 첫 번째 then()
이 반환한 Promise 객체에 또 다시 then()
으로 두 번째 핸들러를 등록한다.
첫 번째 핸들러의 반환값은 두 번째 핸들러에 자동으로 전달된다.
이것을 Promise Chaining이라 부르고, Promise Chaining은 콜백 지옥과 같이 중첩 레벨이 높아지는 현상을 피할 수 있게 한다.
한 가지 더 알아볼 것이 있다.
데이터를 읽어보기 전에, 서버가 요청을 수락하고 처리할 수 있었는지 먼저 확인해보자 한다.
response에서 status code를 확인하고 200(OK)가 아닌 경우 error를 인위적으로 발생시켜서 이 작업을 수행할 수 있다.
const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
fetchPromise
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log(data[0].name);
});
error를 어떻게 다루어야 할까?
fetch()
API는 네트워크 연결이 없거나, URL이 잘못 쓰여졌거나 하는 다양한 이유로 에러를 발생시킨다.
위에서는 서버가 오류 response를 반환하면 스스로 error 객체를 만들어 발생시키고 있다.
중첩된 콜백의 경우, 에러를 다루는 것이 매우 어렵다.
오류 처리를 지원하기 위해 Promise 객체는 catch()
라는 메서드를 제공한다.
then()
과 비슷하게 핸들러를 전달받는다.
그러나 then()
에 전달된 핸들러는 asynchronous 작업이 성공적일 때 호출되고 catch()
에 전달된 핸들러는 asynchronous 작업이 실패할 때 호출된다.
Promise Chain의 끝에 catch()
를 추가하면 Promise Chain 실행 도중에 실패시 호출된다.
따라서 연속적인 여러개의 비동기 작업에 대한 모든 오류를 한 곳에서 처리할 수 있다.
위의 코드에 catch()
를 추가하고 URL를 수정해서 요청이 실패하도록 해보자.
const fetchPromise = fetch('bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
fetchPromise
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log(data[0].name);
})
.catch((error) => {
console.error(`Could not get products: ${error}`);
});
try...catch 문에 finally가 있는 것처럼 then...catch 말고도
finally()
메소드도 사용할 수 있다.
참고로finally()
메서드는 매개변수를 전달받지 않는다.
Promise를 사용하기 위해서는 몇 가지 용어를 알아둘 필요가 있다.
먼저, Promise 객체는 다음 세 가지 중 하나의 상태를 갖는다.
pending : promise가 만들어 졌고, 비동기 함수가 아직 성공 및 실패하지 않은 상태이다.
즉, 여전히 비동기 함수가 실행중인 상태이다.
fulfilled : 비동기 함수가 성공적으로 끝난 상태이다.
이 상태가 되면 then()
에 등록된 핸들러가 호출된다.
rejected : 비동기 함수가 실패로 끝난 상태이다.
이 상태가 되면 catch()
에 등록된 핸들러가 호출된다.
여기서의 "fulfilled(성공)"와 "rejected(실패)"가 의미하는 바는 API에 달려있다.
예를 들어, fetch()
는 서버가 404 Not Found와 같은 오류를 반환한 경우 요청이 실패가 아닌 성공한 것으로 간주하지만 네트워크 오류로 인해 요청이 전송되지 않은 경우에는 실패로 간주한다.
때때로 "fulfilled" 와 "rejected" 둘 다 다루기 위해 "settled" 라는 용어를 사용하기도 한다.
상태가 settled이거나 다른 Promise의 state를 따르도록 "잠금"이 되어 있는 경우, "promise is resolved" 라고 한다.
다음 기사는 promise에 관한 세부사항을 잘 설명해준다.
Let's talk about how to talk about promises➡️
Promise Chain은 여러 비동기 함수들의 실행 및 완료가 순차적으로 일어나야 할 때 필요하다.
그러나 비동기 함수 호출을 결합하는 데 필요한 다른 방법이 있는데 Promise API가 이를 제공한다.
때때로 여러 promise들이 실행되어야 하지만 그 promise끼리 독립적인 경우가 있다.
그렇다면 굳이 순차적으로 실행하는 것이 아닌 병렬적으로 실행하는 것이 더욱 효율적이다.
Promise.all()
메서드가 바로 그 역할을 한다.
이는 Promise 배열을 전달받고 하나의 Promise 객체를 반환한다.
Promise.all()
이 반환한 Promise 객체는 다음과 같다.
all()
에 전달한 promises 배열의 모든 promise가 fulfilled 상태가 되면 fulfilled가 된다.
then()
에 등록된 핸들러가 호출될 때 자동으로 responses 배열이 전달된다.
이 responses 배열은 all()
에 전달한 promises 배열의 각각의 promise에 대한 response를 담고 있다.
all()
에 전달한 promises 배열에 포함된 promise 중 어느 하나라도 rejected 상태가 되면 그 즉시 rejected가 된다.
catch()
에 등록된 핸들러가 호출될 때 자동으로 맨 처음 rejected된 promise의 오류가 전달된다.
예를 들어 다음과 같다.
const fetchPromise1 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
const fetchPromise2 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found');
const fetchPromise3 = fetch('https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json');
Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
.then((responses) => {
for (const response of responses) {
console.log(`${response.url}: ${response.status}`);
}
})
.catch((error) => {
console.error(`Failed to fetch: ${error}`)
});
위의 코드에서, 3개의 다른 URL에 세 개의 fetch()
요청을 하고 있다.
모두 성공하면 각 status를 출력하고, 하나라도 실패하면 에러를 출력한다.
첫 번째와 세 번째와 달리 두 번째 URL 요청은 파일이 없기 때문에 서버가 404(Not Found)를 반환한다.
그러므로 출력은 다음과 같아야 한다.
https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json: 200
https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found: 404
https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json: 200
잘못된 URL로 요청을 하는 코드를 작성해보자.
const fetchPromise1 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
const fetchPromise2 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found');
const fetchPromise3 = fetch('bad-scheme://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json');
Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
.then((responses) => {
for (const response of responses) {
console.log(`${response.url}: ${response.status}`);
}
})
.catch((error) => {
console.error(`Failed to fetch: ${error}`)
});
그러면 catch()
에 등록된 핸들러가 호출되고 다음과 같이 출력될 것이다.
Failed to fetch: TypeError: Failed to fetch
promise 끼리의 AND 연산인 all()
메소드 말고 OR 연산인 any()
메소드도 있다.
any()
가 반환하는 Promise 객체는 any()
메소드로 전달받은 promises 배열 중 하나라도 fulfilled 상태가 되면 그 즉시 fulfilled 상태가 되고, 모두 rejected 상태가 되면 rejected 상태가 된다.
단, then()
에 전달되는 response 객체가 어떤 요청의 response인지 예측할 수 없다.
만약 상태와 무관하게 모든 promise 객체에 대한 결과를 보고 싶다면 Promise.allSettled()
메서드를 이용하면 된다.
async
키워드를 이용하면 promise기반의 비동기 코드를 보다 쉽게 사용할 수 있다.
async
키워드를 함수 앞에 추가만 하면 비동기 함수가 완성된다.
async function myFunction() {
// This is an async function
}
async 함수 안에서 promise를 반환하는 함수 앞에 await
키워드를 사용할 수 있다.
이는 promise 객체가 settled가 될 때까지 그 지점에서 기다리게 한다.
만약 fulfilled가 되면 값을 반환한다.
여기서의 반환되는 값은 async
을 사용하지 않는 경우에서 Promise.then()
의 핸들러로 전달되는 매개변수 값이다.
마찬가지로 rejected가 되면 Promise.catch()
의 핸들러로 전달되는 매개변수 값으로 에러를 발생시킨다.
이렇게 하면 비동기 함수를 사용하지만 동기 코드처럼 보이는 코드를 작성할 수 있다.
fetch()
를 사용한 예제를 async
키워들 이용해서 다시 작성해보자.
async function fetchProducts() {
try {
const response = await fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
console.log(data[0].name);
}
catch (error) {
console.error(`Could not get products: ${error}`);
}
}
fetchProducts();
동기 코드의 경우처럼 오류 처리를 위해 try...catch
block을 사용할 수 있다.
참고로 async
함수는 항상 Promise 객체를 반환하므로 다음과 같은 작업을 수행할 수 없다.
async function fetchProducts() {
try {
const response = await fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return data;
}
catch (error) {
console.error(`Could not get products: ${error}`);
}
}
// "promise" is a Promise object, so this will not work
const promise = fetchProducts();
console.log(promise[0].name);
대신에 아래와 같이 사용한다.
const promise = fetchProducts();
promise.then((data) => console.log(data[0].name));
또한 코드가 JavaScript 모듈이 아닌 이상 await
키워드는 async
함수 내부에서만 사용 가능하다.
즉, 아래와 같이 사용할 수 없다.
try {
// using await outside an async function is only allowed in a module
const response = await fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
console.log(data[0].name);
}
catch(error) {
console.error(`Could not get products: ${error}`);
}
Promise Chain를 구현하고자 할 때 then()
메서드를 이용하는 것 보다 async
함수와 await
키워드를 이용하는 것이 더욱 직관적이므로 권장된다.
물론 Promise Chain이 아닌 독립적인 promise들을 병렬로 실행할때는 Promise.all()
이 더 유리하다.
[참고] : MDN