비동기 자바스크립트(Asynchronous JavaScript)
JS는 기본적으로 이벤트 주도적인 일이 많다. 따라서 비동기적인 형태의 프로그래밍이 필요한데, 이를 위해 JS는 아주 강력한 기능들이 있다.
📌 콜백(Callback)
Callback은 다른 함수에 전달되는 함수를 말한다. Callback을 전달 받은 함수는 어떤 조건을 만족하거나 이벤트가 발생했을 때 제공한 함수를 호출 한다.
- setTimeout
- EventListener
- Network Event
📌 콜백의 문제와 Promise의 등장
Callback 기반 비동기 프로그래밍의 심각한 문제는 콜백안에 콜백이 그 안에 콜백이 이어지는 콜백 헬(callback hell) 이 발생할 수 있음. 아래는 그 예시인데 읽기도 어렵고 디버깅도 힘들다.
getImage("/images/profile.png", (image,err) => {
if(err) throw new Error(err);
compressImage(image, (CompressedImage, err)=>{
if(err) throw new Error(err);
uploadImage(CompressedImage, (uploadedImage, err)=>{
if(err) throw new Error(err);
saveToDisk(uploadedImage, (savedImage, err)=>{
if(err) throw new Error(err);
console.log("Image saved to disk");
});
});
});
})
또한, 에러처리가 어렵다. 안쪽 Callback에서 일어나면 바깥쪽 try-catch로 전달되지 않는다. 이를 위해서 Promise가 등장했다.
📌 Promise
Promise는 비동기 작업의 결과를 나타내는 객체다.
const promise = new Promise(() => {})
/* __proto__
[[PromiseState]]: "pending"
[[PromiseResult]]: undefined
*/
PromiseState
- pending: 작업이 완료되지 않은 상태
- fulfilled: 작업이 성공적으로 완료된 상태
- rejected: 작업이 실패한 상태
Promise는 비동기 작업을 수행할 함수를 인자로 받아서 실행하며, 이 함수는 resolve와 reject를 Callback으로 받는다.
resolve(): Promise의 상태를 fulfilled로 변경하고 결과를 전달하는 함수 reject(): Promise의 상태를 rejected로 변경하고 결과를 전달하는 함수
1. then()
Promise에서 이행(fulfilled)된 값을 then()에 전달한 함수에 전달한다. then()에 등록된 각 함수는 단 한 번만 호출된다. 이때 에러처리를 하고 싶으면 then()의 두 번째 인자를 사용한다.
getJson("/api/user/profile").then(displayUser, displayError);
2. catch()
하지만, 현실적으로 then()의 두 번째 인자를 사용하는 일은 별로 없다. 이때 사용하는 것이 바로 catch()다.
catch()는 Promise가 rejected 상태일 때 호출되는 함수다.
메서드 체인(method chaining)
then()은 항상Promise를 반환한다. 따라서 여러then()을 연속적으로 호출할 수 있다.
3. finally()
finally()는 Promise가 완료될 때 호출되는 함수다. 이때, 상태는 관계없이 항상 실행된다.
fetch('/api/user')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))
.finally(() => console.log('작업 완료'));
4. Promise.all()
Promise.all()은 여러 Promise를 동시에 실행하고, 모든 Promise가 완료되면 결과를 배열로 반환한다. 하나라도 실패하면 즉시 reject된다.
const promise1 = Promise.resolve(3);
const promise2 = new Promise(resolve => setTimeout(() => resolve('foo'), 100));
const promise3 = fetch('/api/data');
Promise.all([promise1, promise2, promise3])
.then(values => console.log(values)); // [3, 'foo', Response]
5. Promise의 생성
- 다른 Promise에 기반한 Promise:
fetch()등의 API - 동기적인 값을 기반:
Promise.resolve(),Promise.reject() - 직접 생성:
new Promise((resolve, reject) => {...})
Promise 해석
- c(Callback)를 then()에 전달하면 Promise인 p를 반환
- Promise p는 나중에 c를 비동기적으로 호출할 수 있도록 준비
- c는 작업을 마치면 v를 반환. 즉 c가 완료되면 p는 v값으로 resolve(해석)
- 이때, Promise는 Promise객체가 아닌 값으로 반환될 경우 그 즉시 이행(fulfilled)됨
- v 역시 Promise 객체일 경우, resolve(해석)된 것이지만, 이행(fulfilled)은 되지 않은 상태
- v의 상태(fulfilled, rejected)에 따라 p도 같은 값으로 결정됨
너무 어렵다. 이를 위해서 ES2017에서 엄청난 것이 등장했는데...
📌 async / await
async/await는 비동기 코드에서 Promise를 숨겨, 읽기 쉽고 이해하기 쉬운 동기적 코드와 비슷하게 만들어준다.
1. await 표현식
await는 Promise를 받아 반환 값이나 예외로 바꾼다. p가 이행(fulfilled)되면 await p는 p가 이행(fulfilled)된 값이 된다.
// Promise 체이닝
getJson('/api/user')
.then(user => getJson(`/api/posts/${user.id}`))
.then(posts => console.log(posts))
.catch(error => console.error(error));
// async/await로 변환
async function getUserPosts() {
try {
const user = await getJson('/api/user');
const posts = await getJson(`/api/posts/${user.id}`);
console.log(posts);
} catch (error) {
console.error(error);
}
}
2. async 함수
await를 사용하는 코드는 항상 비동기적이다. 따라서 async 키워드로 선언된 함수 안에서만 사용해야 한다는 규칙이 있다.
함수를 async로 선언하면 바디에 비동기 관련 코드가 없어도 항상 Promise를 반환한다.
async function simpleAsync() {
return 'Hello';
}
simpleAsync().then(result => console.log(result)); // 'Hello'
어려우면 짤로 보는 자바스크립트 동작 원리(5) - Promises & Async/Await을 참고해보자.
📌 비동기 순회
이때, Promise는 setInterval()이나 클릭 이벤트처럼 여러 번 일어날 수 있는 비동기 작업에는 적합하지 않다.(Promise는 한 번만 일어날 수 있는 비동기 작업에 적합하다.)
이때, ES2018에서 for await...of를 도입하여 비동기 순회를 가능하게 했다.
1. for await...of
비동기 Iterable 객체를 순환하는 루프를 생성시킨다. 물론 동기 Iterable 객체도 순환 할 수 있다.
Promise가 이행(fulfilled)되길 기다렸다가 이행된 값을 루프 변수에 할당하고 루프 바디를 실행시킨다.
async function processUrls(urls) {
for await (const response of urls.map(url => fetch(url))) {
const data = await response.json();
console.log(data);
}
}
2. 비동기 이터레이터
Iterable 한 객체는 [Symbol.iterator]()를 가지고 있었다.
비동기 Iterable 한 객체는 [Symbol.asyncIterator]()를 가진다.
[Symbol.asyncIterator]()의 next() 메서드는 직접적인 순회 결과 객체를 반환하는 것이 아닌 순회 결과 객체로 해석되는 Promise를 반환한다.
앞의 for await...of는 먼저 [Symbol.asyncIterator]()를 호출해보고 없으면 [Symbol.iterator]()를 호출한다.
3. 비동기 제네레이터
이터레이터를 생성하기 제일 쉬운 방법은 제네레이터를 이용하는 것이다. 사용법은 async function*을 사용하면 된다.
// 비동기 제네레이터 예시
async function* fetchPages(urls) {
for (const url of urls) {
const response = await fetch(url);
yield await response.json();
}
}
// 사용
async function processPages() {
const urls = ['/api/page1', '/api/page2', '/api/page3'];
for await (const page of fetchPages(urls)) {
console.log(page);
}
}

