// 기존
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.text())
.then((result) => { console.log(result); });
// async, await
async function fetchAndPrint() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const result = await response.text();
console.log(result);
}
fetchAndPrint();
async(asynchronous)는 비동기를 의미한다. 함수 안에 비동기적으로 실행되는 부분이 있다는 것을 알려준다. 이 때 await는 뒤의 코드를 리턴하고 리턴하는 Promise 객체를 fulfilled 상태 또는 rejected 상태가 될 때까지 기다려준다. fulfilled 상태가 되면 작업 성공 결과를 리턴해준다. await 키워드는 async 함수 안에서만 사용할 수 있다.
async function fetchAndPrint() {
console.log(2);
const response = await fetch('https://jsonplaceholder.typicode.com/users'); // fulfilled가 될 때까지 기다린다.
console.log(7);
const result = await response.text();
console.log(result);
}
console.log(1);
fetchAndPrint();
console.log(3);
console.log(4);
console.log(5);
console.log(6);
await을 만나는 순간 함수 바깥으로 나가 코드를 수행하고 다시 돌아오게 된다.
위의 코드를 참고하면 fetchAndPrint 함수는 비동기 실행 함수처럼 생기지 않았다. 왜냐하면 async/await 구문 자체가 기존의 Promise 객체를 사용하는 코드(Promise Chaining)를 개발자가 더 편하게 작성할 수 있도록 하고 코드의 가독성을 높이기 위해서 도입된 일종의 Syntactic sugar(기존 문법을 더 편하게 사용할 수 있도록 하는 문법적 장치)에 해당하기 때문이다.
코드에서 async/await가 보인다면 사실 비동기 실행되는 코드가 있다는 걸 기억해야 한다. 위 코드의 원래 모습은 아래와 같다.
/* fetch('https://www.google.com')
.then((response) => response.text())
.then((result) => { console.log(result); }); */
function fetchAndPrint() {
console.log(2);
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => {
console.log(7);
return response.text();
})
.then((result) => { console.log(result); });
}
console.log(1);
fetchAndPrint();
console.log(3);
console.log(4);
console.log(5);
console.log(6);
하지만 이런 편안함을 얻은 대신 한 가지 주의해야 할 점이 있다. 바로 이 async/await 구문 중에 비동기 실행되는 부분이 있다는 사실에 유의하며 코드를 작성 및 해석해야 한다는 점이다. 아무리 async/await 구문이 동기 실행 코드처럼 생겼다고 해도 그 속에는 Promise 객체가 존재함을 절대 잊어서는 안된다.
마지막으로 정리하자면, async 함수 안의 코드가 실행되다가 await를 만나면, 일단 await 뒤의 코드가 실행되고, 코드의 실행 흐름이 async 함수 바깥으로 나가서 나머지 코드를 다 실행한다. 물론 함수 바깥에서 더 이상 실행할 코드가 없을 수도 있다. 어느 경우든 그 이후로는, await 뒤에 있던 Promise 뒤에 있던 Promise 객체가 fulfilled 상태가 되기를 기다린다. 그리고 기다리던 Promise 객체가 fulfilled 상태가 되면 await이 Promise 객체의 작업 성공 결과를 리턴하는 것이다.
async function fetchAndPrint() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const result = await response.text();
console.log(result);
} catch (error) { // rejected 상태가 되면 catch문으로 넘어오게 된다.
console.log(error);
} finally { // 항상 동작
console.log('exit');
}
}
fetchAndPrint();
async function fetchAndPrint() {
return 3;
}
fetchAndPrint(); // Promise {<fulfilled>: 3} -> 숫자 3만 리턴했지만 Promise 객체까지 리턴한다.
// -------------------- //
async function fetchAndPrint() {
return fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.text());
fetchAndPrint(); // Promise {<pending>}
async 함수는 그 안에서 리턴하는 값에 따라 그에 맞는 Promsie 객체를 리턴한다.
async 함수 안에서 Promise 객체를 리턴하는 경우에는 해당 Promise 객체와 동일한 상태와 작업 성공 결과(또는 작업 실패 정보)를 가진 Promise 객체를 리턴한다. (그냥 해당 Promise 객체를 리턴한다고 봐도 무방하다.)
// pending 상태의 Promise 객체
async function fetchAndPrint() {
return new Promise((resolve, reject)=> {
setTimeout(() => { resolve('abc'); }, 4000);
});
}
fetchAndPrint();
// fulfilled 상태의 Promise 객체
async function fetchAndPrint() {
return Promise.resolve('Success');
}
fetchAndPrint();
// rejected 상태의 Promise 객체
async function fetchAndPrint() {
return Promise.reject(new Error('Fail'));
}
fetchAndPrint();
async 함수 내부에서 Promise 객체 이외에 숫자나 문자열, 일반 객체 등을 리턴하는 경우에는, fulfilled 상태이면서, 리턴된 값을 작업 성공 결과로 가진 Promise 객체를 리턴한다.
// 숫자
async function fetchAndPrint() {
return 3;
}
fetchAndPrint();
// 문자열
async function fetchAndPrint() {
return 'Hello';
}
fetchAndPrint();
// 객체
async function fetchAndPrint() {
const member = {
name: 'Jerry',
email: 'jerry@codeitmall.kr',
department: 'sales',
};
return member;
}
fetchAndPrint();
async function fetchAndPrint() {
console.log('Hello Programming!');
}
fetchAndPrint();
fulfilled 상태이면서, undefined를 작업 성공 결과로 가진 Promise 객체가 리턴된다.
async function fetchAndPrint() {
throw new Error('Fail');
}
fetchAndPrint();
async 함수 안에서 에러가 발생하면, rejected 상태이면서, 해당 에러 객체를 작업 실패 정보로 가진 Promise 객체가 리턴된다.
const applyPrivacyRule = async function (users) {
const resultWithRuleApplied = users.map(user) => {
const keys = Object.keys(user);
const userWithoutPrivateInfo = {};
keys.forEach((key) => {
if (key !== 'address' && key !== 'phone') {
userWithoutPrivateInfo[key] = user[key];
}
});
return userWithoutPrivateInfo;
});
const p = new Promise((resolve, reject) => {
setTimeout(() => { resolve(resultWithRuleApplied); }, 2000);
});
return p;
};
async function getUsers() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const result = await response.text();
const users = JSON.parse(result);
const resultWithPrivacyRuleApplied = await applyPrivacyRule(users); // async 함수안에서 async 함수 사용(await을 붙여서 사용한다.)
return resultWithPrivacyRuleApplied;
} catch (error) {
console.log(error);
} finally {
console.log('exit');
}
}
getUsers().then((result) => { console.log(result); });
자바스크립트에서 함수를 표현하는 방법에는 '함수 선언식(Function Declaration)', '함수 표현식(Function Expression)', '화살표 함수(Arrow Function)' 등이 있다. 그리고 함수 표현식은 또 '기명 함수 표현식(Named Function Expression)'과 '익명 함수 표현식(Anonymous Function Expression)'으로 나눌 수 있고, 화살표 함수의 경우 함수 내부의 내용에 따라 더 축약(shorten)해서 나타낼 수 있다.
// 1) Function Declaration
async function example1(a, b) {
return a + b;
}
// 2-1) Function Expression(Named)
const example2_1= async function add(a, b) {
return a + b;
};
// 2-2) Function Expression(Anonymous)
const example2_2 = async function(a, b) {
return a + b;
};
// 3-1) Arrow Function
const example3_1 = async (a, b) => {
return a + b;
};
// 3-2) Arrow Function(shortened)
const example3_2 = async (a, b) => a + b;
자바스크립에는 함수를 정의하면서 동시에 실행하는 즉시실행함수(IIFE, Immediately-invoked function expression)라는 개념도 있다.
(function print(sentence) {
console.log(sentence);
return sentence;
}('I love JavaScript!'));
(function (a, b) {
return a + b;
}(1, 2));
((a, b) => {
return a + b;
})(1, 2);
((a, b) => a + b)(1, 2);
(async function print(sentence) {
console.log(sentence);
return sentence;
}('I love JavaScript!'));
(async function (a, b) {
return a + b;
}(1, 2));
(async (a, b) => {
return a + b;
})(1, 2);
(async (a, b) => a + b)(1, 2);
이런 식으로 async를 붙여 사용할 수 있다.
async function getResponses(urls) {
for(const url of urls){
const response = await fetch(url);
console.log(await response.text());
}
}
getResponses 함수는 urls라는 파라미터로, 여러 개의 URL들이 있는 배열을 받아서, 순서대로 각 URL에 request를 보내고, response의 내용을 출력하는 함수이다. 하지만 이전 URL에 request를 보내고 response를 받아서 출력하고 나서야, 다음 URL에 대한 request를 보낼 수 있다는 문제가 있다. 즉, 순차적인 작업 처리를 한다는 점이다. 왜냐하면 이전 URL에 대해서 await문이 붙은 Promise 객체가 fulfilled 상태가 될 때까지는 그 다음 URL에 대한 작업들이 시작될 수 없기 때문이다.
만약 response의 내용의 순서가 중요하지 않은 경우라면 다음과 같이 작성할 수 있다.
async function fetchUrls(urls){
for(const url of urls){
(async () => { // 추가된 부분!
const response = await fetch(url);
console.log(await response.text());
})(); // 추가된 부분!
}
}
이렇게 코드를 고치면 일단 각 URL에 대해서 fetch 함수를 실행해서 request를 보내느 것을 순서대로 바로 실행해버린다. 일단 모든 URL에 대한 request를 쭉 보내버리고, 먼저 response가 오는 순서대록 그 내용이 출력된다.
async 함수 안에서는 무언가를 반복 처리하는 코드를 쓸 때 유의해야 한다. 왜냐하면 의도치 않게 순차 처리를 수행하는 비효율적인 코드를 짜는 실수를 하기 쉽게 때문이다. 만약 순차적인 처리가 필요한 경우가 아니라면 각 작업을 다시 async 함수로 묶어주면 된다.
비동기 실행의 정의는 다음과 같이 표현할 수 있다.
setTimeout, setInterval 함수, DOM 객체의 addEventListener 메소드 등이 여기에 해당한다. 여러 비동기 작업의 순차적인(sequential) 처리가 필요한 경우에는 다음과 같이 작성하면 된다.
fs.readFile('file1.txt', 'utf8', (error1, data1) => {
if (error1) {
console.log(error1);
} else {
console.log(data1);
fs.readFile('file2.txt', 'utf8', (error2, data2) => {
if (error2) {
console.log(error2);
} else {
console.log(data2);
fs.readFile('file3.txt', 'utf8', (error3, data3) => {
if (error3) {
console.log(error3);
} else {
console.log(data3);
}
});
}
});
}
});
fetch('https://www.google.com')
.then((response) => response.text())
.then((result) => { console.log(result); })
.catch((error) => { console.log(error); })
.finally(() => { console.log('exit'); });
function readFile_promisified(filename) {
const p = new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (error, data) => {
if (error) {
reject(error); // 에러 발생 시 -> rejected
} else {
resolve(data); // 파일 내용 읽기 완료 -> fulfilled
}
});
});
return p;
}
Promise 객체를 사용하면 콜백 헬 문제를 방지하면서, 여러 비동기 작업을 순차적으로 처리할 수 있다. 그리고 전통적인 비동기 실행 함수들 중에서도 그 콜백이 단 한번 실행되는 함수들은 Promisify에서 콜백 헬의 가능성을 없애고, Promise Chain 안에서 그 콜백의 리턴값을 사용할 수 있다.
async function fetchAndPrint() {
try {
const response = await fetch('https://www.google.www');
const result = await response.text();
console.log(result);
} catch(error) {
console.log(error);
} finally {
console.log('exit');
}
}
fetchAndPrint();
async/await 구문은 Promise 객체를 다루는 코드(Promise Chaining 코드 등)를 조금 더 익숙하게 느끼는 동기 실행 스타일의 코드로 작성할 수 있게 해주는 Syntactic sugar이다. async 함수 안의 내용이 순차적으로 실행되다가도, await 문을 만나면 await 바로 뒤에 붙은 코드를 실행해두고, 일단은 함수 바깥으로 코드 흐름이 바뀐다.
2021년 1월을 기준으로 아직 위의 3가지 비동기 실행 관련 문법들은 서로 상호보완적인 것들이라고 할 수 있다. 왜냐하면 아직 아래와 같이 하나가 다른 하나를 완벽히 대체하지 못하는 측면이 있기 때문이다.
콜백을 함수의 파라미터로 바로 전달하는 전통적인 방식의 비동기 실행 함수들 중에서도 setInterval, addEventListener처럼 그 콜백이 단 한번이 아니라 여러 번 실행되어야 하는 것들은 Promisify해서 사용하면 안 된다. Promise 객체는 한번 fulfilled 또는 rejected 상태가 되고나면 그 상태와 결과가 다시는 바뀌지 않기 때문이다.
async/await 구문의 경우, await은 async 함수 안에서만 사용할 수 있고, 코드의 top-level(어떤 함수 블록 안에 포함된 것이 아닌 코드의 최상위 영역)에서는 사용될 수는 없다. 그래서 코드의 top-level에서 async 함수가 리턴한 Promise 객체의 작업 성공 결과를 가져오려면 await을 사용할 수는 없고, 여전히 then 메소드를 사용해야 한다.
자바스크립트에서 콜백은 어떤 함수의 파라미터로 전달되는 모든 함수를 의미하는 개념이다. 그러니까 어떤 함수의 파라미터로 전달되기만 한다면 해당 함수는 그 함수의 콜백이 되는 것이다.
이제까지 본 콜백들은 '비동기 실행되는 콜백'에 해당한다. 그렇다면 '동기 실행되는 콜백'에는 어떤게 있을까? filter 메소드를 생각해보자.
const arr = [1, 2, 3, 4, 5, 6];
const newArr = arr.filter(function isOdd(num) {
return (num % 2 === 1);
});
console.log(newArr); // [1, 3, 5]
arr라는 배열에서 홀수만을 추출해서 해당 홀수들만으로 이루어진 새로운 배열을 리턴한다. filter 함수의 파라미터 부분을 보면 isOdd라는 함수가 들어있는 것을 알 수 있다.filter 함수는 arr 배열에서 각 요소를 하나씩 순회하면서 매 요소마다 isOdd 함수를 실행하고, 해당 함수가 true를 리턴하는 요소들만을 추출한다. 화살표 함수로 작성하면 다음과 같이 표현할 수 있다.
const arr = [1, 2, 3, 4, 5, 6];
const newArr = arr.filter((num) => num % 2);
console.log(newArr); // [1, 3, 5]
filter 메소드 안의 콜백은 앞서 말했듯이 '동기 실행되는 콜백'이다. 즉, 이 콜백은 '비동기 실행되는 콜백'과 달리 아주 정직하게 순서대로 실행된다.
filter 함수는 파라미터로 받은 콜백을, 매 요소를 순회하면서, 매 요소를 대상으로 실행한다. 어떤 처리를 나중에 특정 조건이 만족되었을 때 남은 작업을 처리하기 위해서 사용하는 것이 아니다.