비동기적의 뜻은 데이터가 들어오거나 어떤 이벤트가 일어날 때까지 계산을 멈추고 대기하는 일이 잦다는 뜻. 웹 브라우저의 JS 프로그램은 일반적으로 이벤트 주도적이다. 즉, 프로그램이 실제로 무언가를 실행하기 전에 사용자가 뭔가 클릭하거나 탭하기를 기다린다.
일반적으로 서버는 네트워크를 통해 클라이언트 요청이 들어온 후에야 작업을 시작한다.
ES6에서 도입한 Promise, ES7 async와 await 를 도입했다. ES20218에서는 비동기 이터레이터와 for/await 루프를 도입 해서 동기적인 것처럼 보이는 단순한 루프에서 비동기 이벤트 스트림을 다룰 수 있게 된다.
프라미스, async, await, for/await를 이해하기 위해서 클라이언트 사이드, 서버 사이드를 보면서 웹 브라우저와 노드의 비동기적 기능에 대해 이해 해야한다.
setTimeout(checkForUpdates, 60000);
//checkForUpdates를 1분 뒤에 호출하고 1분마다 다시 호출
let updateIntervalId = setInterval(checkForUpdates, 60000);
// setInterval()이 반환하는 값을 clearInterval()에 넘겨 반복 호출을 중단할 수 있따.
function stopCheckingForUpdates() {
clearInterval(updateIntervalId);
}
let okey = document.querySelector("#confirmUpdateDialog button.okay");
// 사용자가 버튼을 클릭하며 호출될 콜백 함수를 등록한다.
okey.addEventListener("click", applyUpdate);
function getCurrentVersionNumber(versionCallback) {// 콜백을 인자로 받는다.
// 백엔드의 버전 API에 HTTP 요청은 한다.
let request = new XMLHttpRequest();
request.open("GET", "http://~");
request.send();
// 응답을 받았을 때 호출할 콜백을 등록
request.onload = function () {
if (request.status === 200) {
// HTTP 상태가 Ok 이면 버전 번호를 가져와서 콜백을 호출
let currentVersion = parseFloat(request.responseText);
versionCallback(null, currentVersion);
} else {
versionCallback(this.response.statusText, null);
}
};
// 네트워크 에러가 생겼을 때 호출할 다른 콜백을 등록한다.
request.onerror = request.ontimeout = function (e) {
versionCallback(e.type, null);
};
}
const fs = require("fs");
let options = {
// 기본 옵션
};
fs.readFinle("config.json", "utf-8", (err, text) => {
if (err) {
// 에러가 있으면 경고를 표시하고 계속 진행
console.warn("Err", err);
} else {
// 에러가 없으면 파일 콘텐츠를 분석하고 옵션 객체에 할당
Object.assign(options, JSON.parse(text));
}
// 어느 쪽이든 이제 프로그램을 실행할 수 있다.
});
const https = require("https");
//URL의 텍스트 콘텐츠를 읽고 비동기적으로 콜백에 전달한다.
function getText(url, callback) {
// URL에 HTTP GET 요청을 시작
request = https.get(url);
request.on("response", (response) => {
// 응답 이벤트가 있다는 것은 응답 헤더를 받았다는 의미
let httpStatus = response.statusCode;
// HTTP 응답의 바디는 아직 받지 못했으므로
// 바디를 받았을 때 호출할 이벤트 핸들러를 등록
response.setEncoding("utf-8");
let body = "";
// 바디의 텍스트 덩어리를 사용할 수 있게 되면 이 이벤트 핸들러를 호출
response.on("data", (chunk) => {
body += chunk;
});
// 응답이 완료되면 이 이벤트 핸들러르 호출
response.on("end", () => {
if (httpStatus === 200) {
callback(null, body);
} else {
callback(httpStatus, null);
}
});
});
// 저수준 네트워크 에러를 처리할 이벤트 핸들러도 등록
request.on("error", (err) => {
callback(err, null);
});
}
비동기 프로그래밍을 단순화하도록 설계된 코어 기능인 프라미스.
프라미스는 비동기 작업 하나가 앞으로 어떤 결과를 보일지 나타낸다.
앞선 getText()함수를 변형해서 JSON인 HTTP 응답 바디를 분석하고 콜백 인자를 받는 대신 프라미스를 반환하는 getJSON() 함수를 만든다.
getJSON(url).then(jsonData => {
// JSON 값을 받아 분석하면 비동기적으로 호출될 콜백 함수
})
getJSON()은 URL에 비동기 HTTP 요청을 보내고 응답을 대기하면서 프라미스 객체를 반환한다. 이 객체에는 then() 인스턴스 메서드가 있다. 콜백 함수는 getJSON()에 직접 전달하지 않고 then() 메서드에 전달한다. HTTP 응답이 도착하면 응답 바디를 JSON()으로 분석하고 분석된 값을 then()에 전달한 함수에 전달
// 사용자 프로필을 표시하는 함수
function displayUserProfile(profile) {
}
getJSON('/api/user/profile').then(displayUserProfile)
getJSON("/api/user/profile").then(displayUserProfile, handleProfileError);
getJSON("/api/user/profile").then(displayUserProfile).catch(handleProfileError);
프라미스의 가장 중요한 장점 중 하나인 비동기 작업 시퀀스를 then()의 체인으로 이어서 콜백 헬을 방지한다는 점입니다.
fetch(documentURL)
.then((response) => response.json())
.then((document) => {
return render(document);
})
.then((rendered) => cacheInDatabase(rendered))
.catch((error) => handle(error));
fetch().then().then() 같은 표현식 하나에 메서드를 하나 이상 호출하는 것을 메서드 체인이라 부른다.
function c1(response) {
// 콜백1
let p4 = response.json();
return p4; // 프라미스 4를 반환
}
function c2(profile) {
// 콜백2
displayUserProfile(profile);
}
let p1 = fetch("/api/user/profile"); // 프라미스 1, 작업 1
let p2 = p1.then(c1); // 프라미스 2, 작업 2
let p3 = p2.then(c2); // 프라미스 3, 작업 3
프라미스의 .catch() 메서드는 null을 첫 번째 인자로, 에러 처리 콜백을 두 번째 인자로 전달하여 .then()을 호출하는 것을 축약한 형태.
p.then(null, c);
p.catch(c);
프라미스 체인에 .finally()를 추가하면 호출한 프라미스가 완료될 때 .finally()가 호출 된다.
프라미스 이행 여부와 관계없이 파일이나 네트워크 연결을 닫는 것과 같은 정리 작업을 해야한다면 .finally() 콜백이 이상적.
fetch("/api/user/profile") // HTTP 요청 시작
.then((response) => { // 상태와 헤더를 받으면 호출
if (!response.ok) { // 404또는 비슷한에러라면
return null; // 사용자가 로그인 했을 수도. 빈 프로필 반환
}
// 헤더를 체크해 서버가 JSON을 보냈는지 확인
// 그렇지 않다면 서버에서 뭔가 잘못된 심각한 에러 상황
let type = response.headers.get("content-type");
if (type !== 'application/json') {
throw new TypeError(`Expected JSON, got ${type}`);
}
// 여기 도달했다면 2xx 상태와 함께 JSON 콘텐츠 타입을 받은 것 이므로
// 응답 바디를 JSON 객체로 파싱하는 프라미스를 반환해도 안전.
return response.json()
})
.then(profile => {
if (profile) {
displayUserProfile(profile)
} else {
displayLoggedOutProfilePage();
}
})
.catch(e => {
if (e instanceof NetworkError) {
// 인터넷 연결이 끊겼다면 fetch()가 이런 식으로 실패할 수 있다.
displayErrorMessage("Check your internet connection.");
} else if (e instanceof TypeError) {
// 위에서 TypeError를 일으킨 경우
displayErrorMessage("Something is wrong with our server")
} else {
// 예상치 못한 에러를 잡는 용도로만 사용
console.log(e)
}
})
.catch((e) => wait(500).then(queryDatabase))
.catch((e) => {wait(500).then(queryDatabase)});
위에 코드에서 첫번째 코드는 함수 바디가 표현식 하나이므로 표현식을 감싼 괄호를 생략할 수 있다. 표현식의 값이 함수의 반환 값이므로 정확한코드이다.
하지만 두번째 코드는 첫 번째 코드와 비슷해 보이지만 괄호로 감싼 형태이므로 어떤 값도 반환하지 않는다.
이를 주의하여 코드를 짜야한다.
const urls = [
/* 0개 이상의 URL */
];
// 프라미스 객체의 배열로 변환
promises = urls.map((url) => fetch(url).then((r) => r.text()));
Promise.all(promises)
.then((bodies) => {
/* 문자열 배열을 사용할 코드 */
})
.catch((e) => console.error(e));
function getJSON(url) {
return fetch(url).then((response) => response.json());
}
function getHighScore() {
return getJSON('/api/user/profile').then(profile => profile.highScore);
}
function wait(duration) {
return new Promise((resolve, reject) => {
if (duration < 0) {
reject(new Error("Time travel not yet implemented"));
}
// 인자가 유효하면 비동기적으로 대기했다가 프로미스를 해석.
// setTimeout은 reslove()를 인자 없이 호출
// 이 프라미스는 정의되지 않는 값으로 이행
setTimeout(resolve, duration);
});
}
function fetchSequentially(urls) {
const bodies = [];
function fetchOne(url) {
return fetch(url)
.then((response) => response.text())
.then((body) => {
bodies.push(body);
});
}
let p = Promise.resolve(undefined);
for (url of urls) {
p = p.then(() => fetchOne(url));
}
return p.then(() => bodies);
}
fetchSequentially(urls)
.then((bodies) => {
/* 문자열 배열을 사용할 코드 */
})
.catch((e) => console.error(e));
다른 방법도 있다. 프라미스를 미리 생성하지 않고 각 프라미스 콜백이 다음 프라미스를 생성해 반환하게 할 수 있다.
// 이 함수는 입력 값 배열과 함께 'promiseMaker' 함수를 받는다.
// 배열에 포함된 값 x에 대해 promiseMaker(x)는 다른 값으로 이행되는 프라미스를 반환
// 이 함수는 계산된 출력 값 배열로 이행되는 프라미스를 반환.
// 하지만 promiseSequence()는 프라미스를 한꺼번에 생성해서 병렬로 실행하지 않고
// 한 번에 프라미스 하나만 실행하며 이전 프라미스가 이행되기 전에는
// promiseMaker()를 호출하지 않습니다.
function promiseSequence(inputs, promiseMaker) {
// 배열의 수정 가능한 비공개 사본을 만든다.
inputs = [...inputs];
function handleNextInput(outputs) {
if (inputs.length === 0) {
// 입력이 더 없으면 출력 배열을 반환하면서
// 이 프라미스와 함께, 해석됐지만 미이행된 이전 프라미스를 모두 이행한다.
return outputs;
} else {
// 처리할 입력이 남았으면 프라미스 객체를 ㅂ나환한다.
// 이 객체는 현재 프라미스를 새 프라미스의 미래 값으로 해석
let nextInput = inputs.shift(); // 다음 입력 값을 가져온다.
return promiseMaker(nextInput)
.then((output) => outputs.concat(output))
.then(handleNextInput);
}
}
// 빈배열로 이행되는 프라미스로 시작하고 위 함수를 콜백으로 사용.
return Promise.resolve([]).then(handleNextInput);
}
let response = await fetch("~")
let profile = await response.json()
await는 async 키워드로 선언된 함수에만 사용할 수 있는 규칙이 있다.
async function getHighScore() {
let response = await fetch("~");
let profile = await response.json();
return profile.highScore;
}
displayHighScore(await getHighScore());
getJSON()함수를 async로 고쳐 썼을 떄
async function getJSON(url) {
let response = await fetch(url);
let body = await response.json();
return body;
}
로 변환 된다.
let value1 = await getJSON(url1);
let value2 = await getJSON(url2);
JSON 값 두개를 가져오기위해 위의 코드를 썼을 때 불필요하게 연속적인 문제가 있다.
let [value1, value2] = await Promise.all([getJSON(url1), getJSON(url2)]);
로 바꿔쓴다.
async function f(x) {
}
// ==>
function f(x) {
return new Promise(function (reslove, reject) {
try {
resolve((function (x) {
}))
} catch (e) {
reject(e)
}
})
}
프라미스는 setInterval()이나 웹 브라우저의 ‘클릭’ 이벤트 처럼 여러 번 일어날 수 있는 비동기 작업에는 적합하지 않다.
하지만 ES2018 부터 해결책이 나왔다. 비동기 이터레이터는 for/await로 사용 할 수 있다.
노드 12는 리더블(readable) 스트림을 비동기적으로 이터러블로 만든다. 따라서 for/await 루프로 스트림의 연속적인 데이터 덩어리를 읽을 수 있다.
const fs = require("fs");
async function parseFile(filename) {
let stream = fs.createReadStream(filename, { encoding: "utf-8" });
for await (let chunk of stream) {
parseChunc(chunk);
}
}
function elapsedTime(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function* clock(interval, max=Infinity) {
for (let count = 1; count <= max, count++){
await elapsedTime(interval);
yield(count)
}
}
async function test() {
for await (let tick of clock(300, 100)) {
console.log(tick)
}
}