일반적으로 웹 브라우저의 자바스크립트 프로그램은 이벤트 주도적이다. 따라서 일반적으로 서버는 네트워크를 통해 클라이언트 요청이 들어온 후에야 작업을 시작한다.
이번 장에서 배울 것:
1. 비동기 코드를 쉽게 만드는 중요한 기능: promise, async, await
2. 단순한 루프에서 비동기 이벤트 스트림 다루기: ES2018 비동기 이터레이터, for/await 루프 도입
setTimeout(fn, ms)
, setInterval(fn, ms)
일정 시간이 지나면 코드를 실행하는 단순한 비동기 프로그램 유형 중 하나.
지정된 컨텍스트에 지정된 이벤트가 일어날 때마다 함수(이벤트 핸들러, 이벤트 리스너)를 호출.
XMLHttpRequest 클래스와 콜백 함수를 사용해 HTTP 요청을 보내고 서버의 응답을 비동기적으로 처리
노드(서버사이드 JS 환경)는 비동기적으로 만들어져 있으며 많은 API가 콜백과 이벤트를 사용한다. (ex. 파일 콘텐츠 읽고 쓰기 등)
노드에서는 on() 메서드로 이벤트 핸들러를 등록한다.
https 내장 모듈을 이용해 http 통신하는 예제
const https = require('https');
// URL의 텍스트 콘텐츠를 읽고 비동기적으로 콜액에 전등립니다.
function getText(url, callback) {
// URL에 HTIP GET 요청을 시작합니다.
const request = https.get(url);
// 응답 이벤트를 처리할 함수를 등록합니다.
request.on('response', response => {
// 응답 이벤트가 있다는 것은 응답 헤더를 받았다는 의미입니다.
let httpStatus = response.statusCode;
// HTIP 응답의 바디는 아직 받지 못했으므로
// 바디를 받았을 때 호출할 이벤트 핸들러를 등록합니다.
response.setEncoding('utf-8'); // 유니코드 텍스트를 예상합니다.
let body = ''; // 텍스트는 이 변수에 누적됩니다.
// 바디의 텍스트 덩어리를 사용할 수 있게 되면 이 이벤트 핸들러를 호출합니다.
response.on('data', chunk => {
body += chunk;
});
// 응답이 완료되면 이 이벤트 핸들러를 호출합니다.
response.on('end', () => {
if (httpStatus === 200) {
// HTIP 응답이 OKi라면
callback(null, body); // 응답 바디를 콜백에 전달합니다.
} else {
// 그렇지 않다면 에러를 전달합니다.
callback(httpStatus, null);
}
});
});
// 저수준 네트워크 에러를 처리할 이벤트 핸들러도 등록합니다.
request.on('error', err => {
callback(err, null);
});
}
Promise : 비동기 작업의 결과를 나타내는 객체
콜백의 문제점 개선
1. 가독성: 콜백의 중첩(콜백 헬)을 좀 더 선형에 가까운 프라미스 체인으로 바꿈
2. 에러 처리: 최초 실행자에게 에러를 전달할 수 없던 콜백의 문제 해결
then()
에러 처리
용어 정리
fulfill
(이행) 혹은 reject
(거부), : 작업의 성공 여부pending
(대기) 혹은 settled
(완료) : 작업의 완료 여부fetch (" /api/user/profi le")
.then(response => { return response.json(); })
.then(profile => { displayUserProfile(profile); }) ;
fetch(theURL) // 작업 1. 프라미스 을 반환
.then(callbackl) // 작업 2. 프라미스 를 반환
.then(callback2); // 작업 3. 프라미스 을 반환
프라미스는 중간 콜백 함수의 반환 값을 담은 프로미스 객체를 전달한다. 다음 프라미스로 잘 전달되면 해석, 아니면 거부된다.
모든 프라미스 체인은 연결되어 있다.
startAsyncOperation( )
.then(doStageTwo)
.catch (recoverFromStageToError)
.then(doStageThree)
.then(doStageFour)
.catch(logStageThreeAndFourErrors);
ES2017
" async, await는 효율적인 프라미스 코드에서 프라미스를 숨겨 (비효율적이지만) 읽기 쉽고 이해하기 쉬운 동기적 코드와 비슷하게 만듭니다. " 420p
이행된 프라미스 값 = 동기적 함수의 반환 값.
거부된 프라미스의 값 = 동기적 함수에서 일으킨 에러
await는 async 키워드로 선언된 함수 안에서만 사용할 수 있다.
ES2022 에서는 모듈의 최 상단에 바로 awiat 사용할 수 있음!
- Ecma International approves ECMAScript 2022: What’s new?
- 27.14 Top-level await
in modules [ES2022] (advanced)
비동기적으로 데이터를 로드하는 모듈을 초기화할 수 있게 한다.
const params = new URLSearchParams(location.search);
const language = params.get('lang');
const messages = await import(`./messages-${language}.mjs`); // (A)
console.log(messages.welcome);
let lodash;
try {
lodash = await import('https://primary.example.com/lodash');
} catch {
lodash = await import('https://secondary.example.com/lodash');
}
const resource = await Promise.any([
fetch('http://example.com/first.txt')
.then(response => response.text()),
fetch('http://example.com/second.txt')
.then(response => response.text()),
]);
탑레벨 await 모듈을 임포트 해오는 예시
first.mjs
:
const response = await fetch('http://example.com/first.txt');
export const first = await response.text();
main.mjs
:
import {first} from './first.mjs';
import {second} from './second.mjs';
assert.equal(first, 'First!');
assert.equal(second, 'Second!');
대충 이런 느낌으로 동작한다.
1. 각각의 비동기 모듈은 fulfilled된 프로미스 객체를 반환한다.
2. 비동기 모듈을 불러온 main.mjs에서는 비동기 모듈들을 Promise.all로 모두 fulfilled상태로 둔 다음 관련 작업들을 실행한다.
3. 동기 모듈은 원래대로 동작
first.mjs
:
export let first;
export const promise = (async () => { // (A)
const response = await fetch('http://example.com/first.txt');
first = await response.text();
})();
main.mjs
:
import {promise as firstPromise, first} from './first.mjs';
import {promise as secondPromise, second} from './second.mjs';
export const promise = (async () => { // (B)
await Promise.all([firstPromise, secondPromise]); // (C)
assert.equal(first, 'First content!');
assert.equal(second, 'Second content!');
})();
The two most important benefits of top-level await
are:
On the downside, top-level await
delays the initialization of importing modules. Therefore, it‘s best used sparingly. Asynchronous tasks that take longer are better performed later, on demand.
However, even modules without top-level await
can block importers (e.g. via an infinite loop at the top level), so blocking per se is not an argument against it.
await는 프라미스에 기반하므로 직접 사용할 때와 마찬가지로 Promise.all()을 사용하면 된다. (getJSON 은 async로 감싸인 비동기 함수)
let [valuel, value2] = await Promise.all([getJSON(urll) , getJSQN(url2)]);
await 키워드를 함수 바디를 동기적 덩어리로 구분하는 일종의 표식이라고 생각하면 편하다.
프라미스 기반으로 만들어졌고 for/of 루프의 새 형태인 for/await 와 함께 사용하도록 설
일반적인 이터레이터와의 차이
Symbol.asyncIterator
)를 가진 메서드 존재async function*
을 사용한다.// aait를 사용할 수 있도록 setTimeout ( )을 감싸는 프라미스 기반 래퍼 함수입니다.
// 밀리초 단위로 지정된 시간이 지나면 이행되는 프라미스를 반환합니다.
function elapsedTime(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 지정된 횟수만큼 (무한히 반복할 수도 있습니다) 지정된 시간마다
// 차운터를 증가시켜 전달하는 비동기 제너레이터 합수
async function* clock(interval, max=Infinity) {
for(let count=1; count <= max; count++) {
// 일반적인 for 루프
await elapsedTime(interval); // 지정된 시간만큼 대기하고
yield count;
}
}
// 비동기 제너레이터와 for/await를 사용하는 테스트 합수
// for/await를 쓸 수 있어야 하므로 비동기로 만들었습니다.
async function test() { // 카운터를 전달합니다.
for await (let tick of clock(300, 100)) { //300밀리초마다 100번 반복
console.log(tick);
}
}
비동기 제너레이터를 사용하지 않고 비동기 이터레이터 직접 만들기. 왜...
(일단 패스)