[JS] 9. 비동기 처리 (Asynchronous Processing)
비동기 처리 (Asynchronous Processing) : 프로세스의 완료를 기다리지 않고 다른 작업을 진행
- 싱글 쓰레드 (
Single Thread) : 한번에 하나의 함수만 실행 → 동기 처리 (Synchronous Processing)과 동일- 자바스크립트는 콜 스택이 하나 → 콜 스택에 쌓인 함수나 코드를 위에서 아래로 차례대로 실행
- 논블로킹 (
Non-Blocking) :I/O를 수행하는 비동기 함수는 백그라운드에 넘김 - 멀티 프로세스 (
Multi Processes) : 백그라운드는OS프로세스에 의존
비동기 처리의 순서?
- 런타임 (
Runtime) → 실행 컨텍스트 (Execution Context) → 콜 스택 (CallStack)- 백그라운드 (
Background) → 운영체제 (OS)- 테스크 큐 (
Task Queue) → 콜 스택 (Call Stack)
자바스크립트 런타임 (JavaScript Runtime) : 자바스크립트가 실행되는 환경
Web API: 브라우저에서 제공하는API(setTimeout,HTTP요청 메소드,DOM이벤트)- 테스트 큐 (
Task Queue) : 이벤트가 발생한 뒤에 호출되어야 할 콜백 함수들이 대기하는 공간 - 이벤트 루프 (
Event Loop) : 이벤트 발생 시 콜백 함수들을 관리, 호출된 콜백 함수의 실행 순서 결정
비동기 콜백 패턴 (Asynchronous Callback Pattern) : 비동기 작업의 완료를 다루는 전통적인 방식
- 현재 실행되고 있는 함수가 끝난 뒤에 실행되는 콜백 함수를 통해 실행 순서를 지정
setTimeout(function() {
console.log('task1', new Date());
setTimeout(function() {
console.log('task2', new Date());
setTimeout(function() {
console.log('task3', new Date());
console.log('END>>', new Date());
}, 1000 );
}, 2000);
}, 3000);
console.log('START', new Date());
콜백 함수를 여러 개 중첩하면, 코드의 가독성이 떨어지는 콜백 지옥 (
Callback Hell)이 발생!
프로미스 (Promise) : 비동기 작업의 성공 및 실패에 대한 완료 결과를 처리하는 객체
- 콜백 지옥을 피하고 비동기 처리를 쉽게 처리할 수 있도록
ES6부터then도입 - 프로미스를 호출하면 프로미스 인스턴스 (
Promise Instance)를 반환한 후Resolve&Reject
프로미스의 3가지 상태 (
Status)
- 대기 (
Pending) : 비동기 처리 로직이 미완료된 초기 상태, 성공 또는 실패할 때까지 대기- 이행 (
Fulfilled): 비동기 처리가 완료되어, 프로미스가 결과 값을 반환한 상태- 거부 (
Rejected) : 비동기 처리가 실패하거나 오류가 발생한 상태
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const now = Date.now();
if (now % 2 === 0) {
resolve(console.log('[Fulfilled]'), now);
} else reject('Rejected');
}, 1000);
setTimeout(() => {
reject(new Error('[TimeExceeded]'),);
}, 1001);
});
promise.then(
success => console.log('[Resolved]'),
fail => console.log('[Rejected]')
)
// 프로미스를 클래스로 표현한다면?
class Promise {
constructor(callback) {
console.log('[Promise 생성자]')
callback (this.resolve.bind(this), this.reject.bind(this));
}
then(resolve) {
console.log('[then 메소드 실행]')
this.success = callback;
}
catch(x) {
console.log('[catch 메소드 실행]')
this.failure = callback;
}
success(x) {
console.log('[success 메소드 실행] ' + x)
}
failure(x) {
console.log('[failure 메소드 실행] ' + x)
}
resolve(x) {
console.log('[resolve 메소드 실행]')
return this.success(x);
}
reject(x) {
console.log('[reject 메소드 실행]')
return this.failure(x);
}
callback(resolve, reject) {
console.log('[callback 메소드 실행]')
}
};
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
const now = Date.now();
console.log('이행 :', now)
if (now % 2 === 0)
resolve(now)
else
reject(new Error("실패"))
console.log("[setTimeout 메소드 실행]")
}, 1000)
}
);
프로미스 클래스 메소드 (Promise Class Method) : 프로미스에서 비동기 작업을 다루기 위해 제공
Promise.resolve: 주어진 값을 성공 상태의 프로미스로 반환Promise.resolve(x).then(val => console.log(val));
Promise.reject: 주어진 값을 실패 상태의 프로미스로 반환Promise.reject(new Error('...')).catch(console.error);
Promise.all: 여러 프로미스가 모두 성공 시 시간과 무관하게 순서를 보장하여 프로미스들을 모두 반환, 하나라도 실패하면 첫번째로 실패한 프로미스 반환Promise.all(iterables).then().catch(...)
Promise.race: 여러 프로미스 중에서 가장 빠른 것을 반환, 하나라도 실패하면 첫번째로 실패한 프로미스 반환Promise.race(iterables).then().catch(...)
Promise.any: 여러 프로미스 중에서 제일 빨리 성공한 것을 반환Promise.any(iterables).then().catch(...)
Node.js모듈의util.promisify: 콜백 함수 기반의 비동기 함수를 프로미스 기반으로 변환function promisify(fn) { return new Promise( (resolve, reject) => { try { const ret = fn(); resolve(ret); } catch(err) { reject(err); } }) } const exec = util.promisify(execute); exec.then(...).catch(...)
async, await : 프로미스를 생성하고 소비하기 위한 문법적 설탕
문법적 설탕 (
Syntax Sugar) : 문법적 기능은 그대로인데, 사람이 직관적으로 읽을 수 있게끔 만드는 것
- 비동기 함수에서 콜백을 사용하는 대신에, 단순한 논리적 흐름을 작성
- 프로미스의
then,catch,finally를 사용할 필요 없음
- 프로미스의
async는 프로미스를 반환하고,await는resolve및reject와 매핑- 성공 :
return→resolve→result - 실패 :
error→reject→throw
- 성공 :
// const fn = async() => {...}
async function fn() { // Promise 반환
...
result = await fetch(url); // fetch.then().catch()
}
console.log(await fn());
Promise&then: 각각이 별도의 쓰레드로 실행되므로 병렬async,await: 단일 쓰레드를 차례로 실행하므로 직렬→ 연관이 없는 비동기 함수 실행에
async,await을 남발하지 말자!
// promise, async, await을 활용하여 페치한 뒤에 2초간 sleep 구현
const f = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
if (!res.ok) throw new Error("Fail to Fetch!!");
console.log(Date.now())
await new Promise((resolve) => {setTimeout(resolve, 2000)});
const data = await res.json();
return data.name;
};
console.log(await f());
console.log(Date.now())
// promise, async, await을 활용하여 1초 간격으로 3번 출력하는 depthTimer 구현
let depthTimer = async (str) => {
console.log(str, new Date());
await new Promise((resolve) => setTimeout(resolve, 1000));
};
(async function () {
await depthTimer('START!');
await depthTimer('depth1');
await depthTimer('depth2');
await depthTimer('depth3');
console.log('Already 3-depth!!');
})();
for-await-of : 비동기적으로 이터레이터 (Iterator)를 반복하는 문법
async함수에서 비동기적으로 값을 가져와 처리할 때 활용
const afterTime = sec => new Promise(
resolve => setTimeout(resolve, sec * 1000, sec
));
console.time('for-await-of ');
const arr = [afterTime(1), afterTime(2)];
for (const fo of arr.values()) {
console.log('fo =', fo);
}
for await (const fao of arr.values()) {
console.log('fao =', fao);
}
console.timeEnd('for-await-of ');
// > fo = Promise { <pending> }
// > fo = Promise { <pending> }
// > fao = 1
// > fao = 2
// > for-await-of : 2.003s