자바스크립트의 비동기 처리 방식은 총 세 가지다.
1. 콜백(callback) : 파라미터로 함수를 전달받아 함수 내부에서 실행하는 함수
2. Promise : 자바스크립트에서 제공하는 비동기를 간편하게 처리할 수 있도록 도와주는 객체
3. async/await : 기존의 Promise보다 더 간결하게 작성할 수 있는 문법
setTimeout(() => {
console.log('This is callback');
}, 1000);
콜백함수는 함수의 파라미터로 함수 자체를 넘겨, 함수 내에서 매개변수 함수를 실행하는 기법이다.
const fs = require('fs');
fs.writeFile('test.txt', 'async function', (err) => {
if(!err) {
fs.readFile('test.txt', (err, data) => {
if(!err) {
console.log(data.toString()); // async function
}
});
}
});
fs
는 NodeJS에서 파일 입출력 처리를 담당하는 모듈이다. fs
모듈은 비동기 api와 동기 api를 모두 제공한다. 위의 코드의 writeFile
과 readFile
은 대표적인 비동기 api다.
readFile
은 writeFile
의 파라미터로 넘겨 writeFile
의 콜백함수로 사용된다. 즉, writeFile
의 실행이 끝난 뒤에 readFile
이 실행된다.
💥콜백 지옥?
fs.writeFile('test.txt', 'data', (err) => {
if(!err) {
fs.readFile('test.txt', (err, data) => {
if(!err) {
fs.writeFile('test.txt', data.toString(), (err) => {
if(!err) {
fs.readFile('test.txt', (err, data) => {
if(!err) { ... }
});
}
});
}
});
}
});
콜백 함수를 반복적으로 사용하면 콜백 지옥에 빠지게 된다. 콜백 지옥은 콜백 함수를 중첩하여 깊이가 깊어져 가독성이 떨어지는 현상을 말한다. 그렇다면 비동기 작업을 좀 더 간단하게 처리하는 방법은 없을까?
프로미스는 자바스크립트에서 비동기 처리를 위해 제공하는 객체로, 비동기 작업의 성공/실패의 결과값을 나타낸다. 프로미스를 사용하면 앞서 언급한 콜백 지옥 현상을 해결할 수 있다.
const promise = new Promise((resolve, reject) => { });
Promise
객체는 new
키워드와 생성자를 통해 생성한다. 파라미터로 resolve
, reject
를 가질 수 있다. resolve
와 reject
는 자바스크립트에서 제공하는 콜백함수다.
resolve(val)
: 작업을 성공적으로 처리한 후 val
와 함께 호출reject(err)
: 에러가 발생하는 경우 에러 객체를 나타내는 err
과 함께 호출프로미스는
resolve()
나reject()
중 하나를 반드시 호출해야 한다. 변경된 상태는 다시 수정할 수 없다.
Promise 상태
프로미스는 비동기 작업의 결과를 약속하므로 다음 세 가지 상태 중 하나를 가진다.
const promise = new Promise((resolve, reject) => {
setTimeout(() => { // (asynchronous function)
resolve(true); // success
}, 1000);
});
promise
는 비동기 함수 setTimeout()
를 포함한 비동기 함수다. 1초가 지나기전에 promise
를 출력하면 pending
상태를 갖는다. 1초가 지나면 resolve()
메서드에 의해 fulfilled
상태로 변한다. reject()
메서드를 사용하면rejected
상태로 만들 수 있다.
Promise 핸들러
프로미스는 핸들러를 이용하여 콜백 지옥을 해결한다. 프로미스 객체의 작업 성공 유무를 핸들러를 통해 받아 후속 작업을 처리한다.
.then()
: 프로미스가 fulfilled 되었을 때 실행.catch()
: 프로미스가 rejected 되었을 때 실행.finally()
: 프로미스 상태와 무관하게 실행promise
.then((val) => { console.log(val); }); // fulfilled
.catch((err) => { console.log(err); }); // rejected
.finally(() => { });
Promise 정적 메서드
프로미스 객체는 생성자 함수 외에도 여러 정적 메서드를 제공한다. 정적 메서드는 객체를 초기화하거나 생성하지 않고 바로 사용할 수 있으므로 비동기 작업을 수행하지 않는 함수에서도 프로미스를 활용할 수 있다.
Promise.resolve()
Promise.reject()
Promise.all()
: 여러 개의 프로미스 요소들을 한꺼번에 처리해야 할 때 사용한다. 모든 프로미스가 완료될 때까지 기다린 후, 모든 프로미스가 완료되면 then
핸들러를 실행한다.const promise = (sec) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(sec);
}, sec);
});
}
const promises = [promise(1000), promise(2000), promise(5000)];
Promise.all(promises)
.then((data) => {
console.log(data); // [1000, 2000, 5000]
});
Promise.allSettled()
: Promise.all()
의 업그레이드 버전으로, 모든 프로미스가 처리되면 각각의 상태와 값을 모아놓은 배열을 반환한다.Promise.allSettled(promises)
.then((data) => {
console.log(data);
});
/*
[
{ status: 'fulfilled', value: 1000 },
{ status: 'fulfilled', value: 2000 },
{ status: 'fulfilled', value: 5000 }
]
*/
Promise.any()
: Promise.all()
의 반대 버전으로, 주어진 프로미스 중 하나라도 fulfilled 되면 바로 결과를 반환한다. 만약 모든 프로미스가 rejected 된다면 거부 프로미스를 반환한다.
Promise.race()
: Promise.any()
와 유사하지만 작업의 성공 여부와 상관없이 무조건 처리가 끝난 프로미스 결과를 반환한다.
💥 콜백 지옥 해결하기
앞에 등장했던 콜백 지옥 코드를 핸들러를 사용해 해결해보자.
const write = (path, data) => {
return new Promise((resolve, reject) => {
fs.writeFile(path, data, (err) => {
if(err) {
reject();
}
resolve();
});
});
};
const read = (path) => {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if(err) {
reject();
}
resolve(data.toString());
});
});
}
write(path, data).then(() => {
read(path).then((data) => {
write(path, data).then(() => {
read(path).then(...);
});
});
});
writeFile
과 readFile
을 프로미스 객체를 반환하는 함수로 분리하고 then
핸들러를 이용하여 후속 작업을 수행한다.
하지만 위의 코드도 then
핸들러를 중첩적으로 사용하여 코드의 가독성이 떨어진다. 콜백 지옥에 이은 프로미스 지옥이 등장했다. 프로미스 지옥을 해결할 방법은 없을까?
참고자료
자바스크립트 비동기
자바스크립트 Promise