Immersive Course(이하 IM)는 프로그래밍 교육기관 코드 스테이츠의 웹 개발 심화 코스이다. 아래 내용은 IM에서 배우고, 내가 찾아보고, 다른 수강생들이 전해 준 지식이다.
자바스크립트는 코드를 처리하는 콜스택이 하나다. 한 번에 하나의 일만 처리할 수 있다. 콜스택은 함수를 하나 받아서 처리한 후 값이 리턴되는 시점에 함수를 밖으로 내보낸다.
버튼을 클릭하면 정보를 받아와서 브라우저 화면에 보여주는 웹 페이지가 있다고 가정하자. 만약 정보를 받아오는 데 3분이 걸린다면, 자바스크립트는 그 3분간 다른 일을 할 수 없다. 화면을 지켜보는 사용자는 조용한 화면을 3분간 지켜보고 있어야 한다. 자바스크립트는 이런 현상을 개선하기 위해 - 응답을 받으려고 기다리는 시간 동안에도 다른 일을 할 수 있도록 브라우저의 도움을 받는다. 브라우저에서 제공하는 여러 API를 사용해 코드의 실행 순서나 실행 여부를 계획할 수 있다. 대표적인 Web API로 setTimeout
이 있다.
setTimeout
은 우선 콜스택에 들어갔다가, Web API 실행 공간으로 옮겨가서 타이머를 가동한다. 타이머가 가동이 끝나면 태스크 큐에서 실행될 함수가 대기하다가, 콜스택이 모두 비면 이벤트 루프가 함수를 콜스택으로 옮겨준다. 타이머가 가동되는 시간동안 다른 코드들이 콜스택에 들어갔다 나오면서 실행될 수 있으므로 콜스택이 한 개이더라도 다른 동작이 가능하다. 이를 비동기식 처리 모델이라고 한다.
setTimeout
을 비롯해 서버에 데이터를 요청하는 함수들은 비동기적으로 동작한다.
비동기적으로 요청한 데이터나 결과값을 이용하여 다음 동작을 처리해야 하는 상황이 있다.
아래 예시는 객체의 pet
값을 바꾸는 함수, 그리고 객체의 pet
이 'Cat'
인지 알아보는 함수다. 첫 함수인 changePet()
이 실행되는 데 상당히 시간이 걸리는 함수라고 가정하자. setTimeout
으로 응답 시간을 늦췄다.
const changePet = (target, animal) => {
target.pet = animal;
}
const isCat = (target) => {
console.log(target.pet === 'Cat');
}
const mySister = { pet: 'Dog' };
setTimeout(() => changePet(mySister, 'Cat'), 500);
isCat(mySister);
// false
나는 isCat()
이 true
를 리턴하기를 원하지만, 지금 상황에서는 changePet()
의 동작이 끝나기 전에 isCat()
이 실행되기에 항상 결과는 false
이다. changePet()
이 실행된 후에 순차적으로 isCat()
을 실행하고 싶다면, isCat()
을 changePet()
에 callback으로 넘겨주는 방법을 사용할 수 있다.
callback은 다른 함수에 매개변수로 전달되는 함수이다. changePet()
에 함수 isCat()
을 인자로 전달하고, 동작이 끝난 후에 isCat()
이 실행되도록 했다. 그러면 원하는 답인 true
를 받을 수 있다.
const changePet = (target, animal, callback) => {
target.pet = animal;
callback();
}
const isCat = (target) => {
console.log(target.pet === 'Cat');
}
const mySister = { pet: 'Dog' };
setTimeout(() => {
changePet(mySister, 'Cat', () => isCat(mySister));
}, 500);
// true
이 방식의 단점은, 순서대로 실행되어야 하는 함수가 많을수록 코드 깊이가 깊어진다는 점이다.
// 더 많은 함수들이 실행되어야 한다면?
changePet(mySister, 'Cat', () => {
isCat(mySister, () => {
setWishList(mySister, 'Cat Tower', () => {
nextFunc(arg, () => {
andNextFunc(arg, () => {
// ......
});
})
});
});
});
그리고 만약 실행되는 함수가 실패할 가능성이 있는 경우(데이터 응답 오류 등)에는 아래처럼 에러를 처리해주어야 한다.
setWishList('Cat Tower', function(error, callback) {
if (error) {
// 에러를 처리한다
handleError(error);
} else {
// 에러가 없는 경우 원하는 동작을 수행한다
}
});
에러를 처리하면서 콜백을 받아간다면, 코드 모양은 더욱 복잡해진다.
setWishList('Cat Tower', function(error, callback) {
if (error) {
handleError(error);
} else {
// ...
setWishList('Catnip', function(error, callback) {
if (error) {
handleError(error);
} else {
// ...
setWishList('Cat Ternul', function(error, callback) {
if (error) {
handleError(error);
} else {
// ...그리고 계속된다
}
});
}
});
}
});
이러한 문제점들을 개선하기 위해 그간 라이브러리 형식으로 제공되었던 Promise가, ES6에서 공식적으로 도입되었다.
[ Promise | MDN ]
[ 프로미스 | PoiemaWeb ]
Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체로, new
키워드와 생성자를 사용하여 만든다. 생성자는 매개변수로 '실행 함수'를 받는다. 이 함수는 매개 변수로 두 가지 함수를 가져야 하는데, 첫 번째 함수resolve
는 비동기 작업이 성공적으로 완료되어 결과를 값으로 반환하면 호출되고, 두 번째 함수reject
는 작업이 실패하여 오류의 원인을 반환하면 호출된다. 두 번째 함수는 주로 오류 객체를 받는다.
// Promise 객체 생성 -> 비동기 작업 수행
const promise = new Promise((resolve, reject) => {
if (//...//) {
// 작업 성공 시
resolve('Result');
}
else {
// 작업 실패 시
reject('Error');
}
});
예를 들어 nodejs에서 request
모듈을 사용하여 특정 URL로부터 데이터를 받아온다고 하자. 성공적으로 데이터를 받아왔다면 그 내용을 resolve()
에 전달하고, 에러가 발생했다면 reject()
에 에러를 전달한다.
const getBodyFromGetRequestPromise = url => {
return new Promise((resolve, reject) => {
request(url, function(err, res, body) {
if (err) {
reject(err);
} else {
resolve(JSON.parse(body));
}
});
});
};
Promise의 장점 중 하나는 chaining이다. then()
을 사용하여 여러 개의 콜백을 추가 할 수 있다. 각각의 콜백은 주어진 순서대로 하나씩 실행된다. 그래서 이 then을 이용하여 이전 단계의 비동기 작업이 완료된 후 그 결과값을 이용해 다음 비동기 작업이 실행되도록 만들 수 있다.
아이디 목록을 담은 파일을 가져온 후, 목록 속 아이디 하나를 특정 주소에 붙여 메시지를 받아와 그 내용을 파일에 기록하는 흐름을 만들어보았다.
const fetchUsersAndWriteToFile = (readFilePath, writeFilePath) => {
// 파일을 읽어온다
return getDataFromFilePromise(readFilePath)
// 읽어온 파일에 담긴 id를 이용하여 메시지 데이터를 요청한다
.then(ids => getBodyFromGetRequestPromise("https://fakejson.com/users/" + ids[0]))
// 받아 온 메시지 데이터를 파일로 저장한다
.then(data => writeFilePromise(writeFilePath, data));
}
모든 작업중에 발생한 에러는 catch
메서드를 이용하여 처리한다. 실패한 Promise의 reject()
가 받은 오류가 catch
메서드로 넘어온다.
const fetchUsersAndWriteToFile = (readFilePath, writeFilePath) => {
return getDataFromFilePromise(readFilePath)
.then(ids => getBodyFromGetRequestPromise("https://fakejson.com/users/" + ids[0]))
.then(data => writeFilePromise(writeFilePath, data))
// 에러가 발생하면 catch메서드가 실행된다
.catch(err => console.log(err));
}
[ async function | MDN ]
[ Async/await | JAVASCRIPT.INFO ]
async / await 는 Promise를 보다 편안하게 구현할 수 있는 문법이다. Async function의 기본적인 생김새는 아래와 같다.
async function functionName() {
return 'something';
}
Async 함수의 특징은 언제나 Promise 객체를 리턴한다는 점이다. 리턴되는 객체는 표기되어 있지 않더라도 항상 Promise.resolve()
로 싸여있다. 그래서 then()
을 이용해 해당 Promise를 받아 이어갈 수 있다.
await
는 Async 함수에서만 사용할 수 있는 키워드다. 자바스크립트는 await
가 앞에 붙은 Promise가 resolve()
를 반환할 때까지 기다린다. 그래서 어떤 Promise가 반환하는 값을 이용해 무언가를 하고 싶을 때 await
를 사용한다.
const https = require("https");
// 프로미스 객체를 생성하여 http 엘리먼트를 받아오는 요청을 작성한다.
// 동작 성공 시 resolve에 받은 데이터를 전달하고, 실패 시 reject에 에러를 전달한다.
const getArticle = (url) => {
return new Promise((resolve, reject) => {
https.get(url, res => {
let data = "";
res.on("data", chunk => data += chunk);
res.on("end", () => resolve(data));
res.on("error", e => reject(e));
});
});
}
// try에서는 resolve를 처리하고,
// catch는 reject를 처리한다.
async function retrieveArticle(url) {
try {
const article = await getArticle(url);
// 이 구문은 getArticle에서 데이터가 모두 받아진 후에 동작한다.
console.log(article);
}
catch (e) {
console.log(e);
}
}