Today I Learned
- 비동기
- 동기와 비동기
- Callback
- Promise
- Async/Await
( 이미지 출처 : https://poiemaweb.com/js-async )
JavaScript의 동기 처리란 ‘특정 코드의 실행이 완료될 때까지 기다리고 난 후 다음 코드를 수행하는 것’을 의미한다. 코드는 순차적으로 실행되며 어떤 작업이 수행 중이면 다음 작업은 대기하게 된다.
( 이미지 출처 : https://poiemaweb.com/js-async )
예를 들어 서버에서 데이터를 가져오는 작업을 동기적으로 처리하면, 서버로부터 데이터를 응답 받을 때까지 작업이 중단(Blocking)된다.
동기식 처리 모델 예시
function func1() {
console.log('func1');
func2();
}
function func2() {
console.log('func2');
func3();
}
function func3() {
console.log('func3');
}
func1();
// 'func1'
// 'func2'
// 'func3'
JavaScript의 비동기 처리는 ‘특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드들을 수행하는 것’을 의미한다. 코드가 종료되지 않은 상태라 하더라도 대기하지 않고 다음 코드를 실행한다.
( 이미지 출처 : https://poiemaweb.com/js-async )
예를 들어 서버에서 데이터를 가져오는 작업을 비동기적으로 처리하면, 서버에 데이터를 요청한 이후 서버로부터 데이터가 응답될 때까지 대기하지 않고(Non-Blocking) 즉시 다음 태스크를 수행한다. 이후 서버로부터 데이터가 응답되면 이벤트가 발생하고 이벤트 핸들러가 데이터를 가지고 수행할 태스크를 계속해 수행한다.
비동기식 처리 모델 예시
function func1() {
console.log('func1');
func2();
}
function func2() {
setTimeout(function() {
console.log('func2');
}, 0);
func3(); // setTimeout을 기다리지 않고 실행됨
}
function func3() {
console.log('func3');
}
func1();
// 'func1'
// 'func3'
// 'func2'
( 이미지 출처 : https://poiemaweb.com/js-async )
setTimeout 메소드는 비동기 함수이다. 따라서 setTimeout을 실행한 뒤 delay만큼 대기하지 않고 즉시 다음 작업을 수행한다.
비동기적으로 처리되는 대표적인 작업
- 대부분의 DOM 이벤트 핸들러
- Timer 함수(setTimeout, setInterval)
- Ajax 요청
Callback
함수를 통해 비동기 코드의 순서를 제어할 수 있지만 여러 개의 콜백함수가 중첩됨으로써 가독성이 떨어지는 이른바 Callback Hell 현상이 발생하기 쉽다.
Callback Hell
step1(function(value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
step5(value4, function(value5) {
// value2를 사용하는 작업
console.log(value5)
});
});
});
});
});
전통적인 콜백 패턴의 단점
- 콜백 헬(Callback Hell)로 인한 가독성 악화
- 비동기 처리 중 발생한 에러를 처리하기 어려움
- 여러 개의 비동기 처리를 한번에 처리하기에 한계가 있음
프로미스(Promise)는 ES6에서 비동기 처리를 위해 도입한 또 다른 패턴으로, 콜백 패턴의 단점을 보완한다.
Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타낸다. Promise는 Promise 생성자 함수를 통해 인스턴스화하므로, new
키워드를 통해 인스턴스 객체를 생성해야 한다. Promise 생성자 함수는 코드가 정상적으로 처리됐을 때 호출될 resolve
함수와 에러가 발생했을 때 호출될 reject
함수를 콜백 함수로 전달 받는다.
const promise = new Promise((resolve, reject) => {
// 비동기 작업을 수행한다.
if (/* 비동기 작업 수행 성공 */) {
resolve('성공');
}
else { /* 비동기 작업 수행 실패 */
reject('실패');
}
});
state
: 비동기 처리가 성공(fulfilled)했는지 실패(rejected)하였는지 등에 관한 상태(state) 정보
result
Promise로 구현된 비동기 함수는 Promise 객체를 반환한다. 이는 Promise의 후속 처리 메소드인 .then
, .catch
, .finally
메서드를 사용해야 접근이 가능하다.
then
비동기 처리가 성공하면 resolve 함수를 호출하고 .then 메소드로 접근할 수 있다.
.then 안에서 리턴한 값이 Promise면 Promise의 내부 프로퍼티 result를 다음 .then 의 콜백 함수의 인자로 받아올 수 있다.
const promise = new Promise((resolve, reject) => {
resolve("성공");
});
promise.then((value) => {
console.log(value); // "성공"
})
catch
비동기 처리가 실패하면 reject 함수를 호출하고 .catch 메소드로 접근할 수 있다.
const promise = new Promise(function(resolve, reject) {
reject(new Error("실패"))
});
promise.catch(error => {
console.log(error); // Error: 실패
})
finally
비동기 처리의 성공/실패 여부와 상관없이 .finally 메소드를 통해 반환된 Promise에 접근할 수 있다.
const promise = new Promise(function(resolve, reject) {
if (/* 비동기 작업 수행 성공 */) {
resolve('성공');
}
else { /* 비동기 작업 수행 실패 */
reject('실패');
}
});
promise
.then(value => {
console.log(value); // "성공"
})
.catch(error => {
console.log(error);
})
.finally(() => {
console.log("성공이든 실패든 작동!"); // "성공이든 실패든 작동!"
})
비동기 작업을 순차적으로 진행할 때, Promise chaining이 필요하다. Promise의 후속 처리 메소드들이 Promise를 리턴하기 때문에 체이닝이 가능하다.
const promise = new Promise(function(resolve, reject) {
if (true) {
resolve('성공');
}
else {
reject('실패');
}
});
promise
.then((value) => {
console.log(value);
return '성공1';
})
.then((value) => {
console.log(value);
return '성공2';
})
.then((value) => {
console.log(value);
return '성공3';
})
.catch((error) => {
console.log(error);
return '실패';
})
.finally(() => {
console.log('성공이든 실패든 작동!');
});
// 성공
// 성공1
// 성공2
// 성공이든 실패든 작동!
Promise.all()
은 여러 개의 비동기 작업을 동시에 처리하고 싶을 때 사용한다.
const promiseOne = () => new Promise((resolve, reject) => setTimeout(() => resolve('1초'), 1000));
const promiseTwo = () => new Promise((resolve, reject) => setTimeout(() => resolve('2초'), 2000));
const promiseThree = () => new Promise((resolve, reject) => setTimeout(() => resolve('3초'), 3000));
Promise.all([promiseOne(), promiseTwo(), promiseThree()])
.then((value) => console.log(value))
// ['1초', '2초', '3초']
.catch((err) => console.log(err));
위 코드를 Promise chaining으로 구현하면 코드들이 순차적으로 작업이 수행돼 총 6초가 걸리지만, Promise.all()을 사용하면 3초 내에 모든 작업을 수행할 수 있다.
또한 Promise.all()은 인자로 받는 배열에 있는 Promise 중 하나라도 에러가 발생하게 되면 그 즉시 실행을 중단하고 에러를 반환한다.
async
/await
는 ES8에서 도입된 문법으로, 해당 문법을 사용하면 Promise를 좀 더 편하고 간결하게 사용하고 가독성을 향상시킬 수 있다. try..catch
를 사용해 에러 핸들링도 할 수 있다.
async
await
promise.then
의 역할을 수행한다.async function f() {
try {
const response = await fetch('http://유효하지-않은-주소');
const user = await response.json();
} catch(err) {
alert(err);
}
}
f();
참고