이미지 출처: What every programmer should know about Synchronous vs. Asynchronous Code
동기 방식은 서버에서 요청을 보냈을 때 응답이 돌아와야 다음 동작을 수행할 수 있다. 즉 A 작업이 모두 진행될 때까지 B 작업은 대기해야한다.
비동기 방식은 반대로 요청을 보냈을 때 응답 상태와 상관없이 다음 동작을 수행 할 수 있다. 즉 A작업이 시작하면 동시에 B작업이 실행된다. A작업은 결과값이 나오는대로 출력된다.
동기는 '직렬적'으로 작동하는 방식이고 비동기는 '병렬적'으로 작동하는 방식이다. 즉, 비동기란 특정 코드가 끝날때 까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행하는 것을 의미한다. 비동기 처리를 예로 Web API, Ajax, setTimeout 등이 있다.
console.log("1st");
console.log("2nd");
console.log("3rd");
위와 같은 코드를 작성하고 실행해 보면 예상되는 결과는 1st, 2nd, 3rd일 것이다.
1st
2nd
3rd
예상했던 것처럼 1st, 2nd, 3rd가 차례대로 찍히는 것을 알 수 있다.
이처럼 코드가 위에서부터 아래로 내려오면서 하나가 끝나면 다음 코드가 실행되는 방식을 동기적 처리 (Synchronous)라고 한다.
이미지 출처: 동기식 처리 모델(Synchronous processing model)
비동기적 처리는 어떤것을 의미하는지 코드로 직접 보면
console.log("1st");
setTimeout(() => {
console.log("2nd");
}, 0)
console.log("3rd");
setTimeout() 메소드를 사용했다.
setTimeout()메소드의 첫번째 인자는 콜백함수를 사용하였고, 두번째 인자는 지연시간이다.
두번째 인자가 0이기 때문에 바로 실행될거라 예상하고,
1st, 2nd, 3rd가 찍힐것이라 예상하였다.
하지만 결과값은
1st
null
3rd
2nd
이는 setTimeout()
메소드가 비동기적 API이기 때문이다.
위의 코드를 컴퓨터의 입장에서 해석해보면 다음과 같다.
첫번째 줄에서 console.log("1st");를 만나고 콘솔에 1st를 찍는다.
두번째 줄에서 setTimeout() 메소드를 만나서 해당 매소드가 비동기적 매소드이기 때문에 이를 처리하는 다른 프로그램에 맡긴다.
그러고 나서 곧바로 console.log("3rd")를 콘솔에 찍는다.
setTimeOut() 메소드를 처리하는 프로그램은 비동기적 API를 제외한 모든 코드가 실행 된 이후 결과를 콘솔에 찍는다.
병렬적으로 태스크를 수행하는 방식
요청을 보낸 후 응답의 수락 여부와는 상관없이 다음 태스크가 동작하는 방식이다. a 태스크가 실행되는 시간 동안 b 태스크를 할 수 있으므로 자원을 효율적으로 사용할 수 있다.
이때, 비동기 요청시 응답 후 처리할 '콜백 함수'를 함께 알려준다. 따라서 해당 태스크가 완료되었을 때, '콜백 함수'가 호출된다.
이미지 출처: 비동기식 처리 모델(Asynchronous processing model)
step1(function(value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
step5(value4, function(value5) {
// value5를 사용하는 처리
});
});
});
});
});
비동기적 코드의 실행 결과는 동기적 코드가 전부 실행 되고나서 값을 반환한다.
Concurrency : 동시성(병행성)
Parallelism : 병렬성
동기
는 디자인이 비동기
보다 간단하고 직관적일수 있지만 결과가 주어질 때 까지 아무것도 못하고 대기해야하는 문제가 있다. 비동기
는 동기
보다 복잡하지만 결과가 주어지는데 시간이 걸려도 그 시간동안 다른 작업을 할 수 있어서 보다 효율적일 수 있다.
resolve
와 reject
함수를 인자로 전달받는다.// Promise 객체의 생성
const promise = new Promise((resolve, reject) => {
// 비동기 작업을 수행한다.
if (/* 비동기 작업 수행 성공 */) {
resolve('result');
}
else { /* 비동기 작업 수행 실패 */
reject('failure reason');
}
});
Promise는 비동기 처리가 성공(fulfilled)하였는지 또는 실패(rejected)하였는지 등의 상태(state) 정보를 갖는다.
pending
: 비동기 처리가 아직 수행되지 않은 상태fulfilled
: 비동기 처리가 수행된 상태 (성공)rejected
: 비동기 처리가 수행된 상태 (실패)settled
: 비동기 처리가 수행된 상태 (성공 또는 실패)resolve
메소드를 호출한다.reject
메소드를 호출한다. 이때 reject 메소드의 인자로 에러 메시지를 전달한다. 이 에러 메시지는 Promise 객체의 후속 처리 메소드로 전달된다.후속 처리 메소드에는 대표적으로 then
(Promise 반환)과 catch
(예외)가 있다.
then
then 메소드는 두 개의 콜백 함수를 인자로 전달 받는다. 첫 번째 콜백 함수는 성공(fulfilled, resolve 함수가 호출된 상태) 시 호출되고 두 번째 함수는 실패(rejected, reject 함수가 호출된 상태) 시 호출된다. then 메소드는 Promise를 반환한다.
catch
예외(비동기 처리에서 발생한 에러와 then 메소드에서 발생한 에러)가 발생하면 호출된다. catch 메소드는 Promise를 반환한다.
비동기 함수의 처리 결과를 가지고 다른 비동기 함수를 호출해야 하는 경우, 함수의 호출이 중첩(nesting)이 되어 복잡도가 높아지는 콜백 헬이 발생한다. 프로미스는 후속 처리 메소드인 then
이나 catch
로 메소드를 체이닝(chainning)하여 여러 개의 프로미스를 연결하여 사용할 수 있다. 이로써 콜백 헬을 해결한다.
따라서, then 메소드가 Promise 객체를 반환하도록 하면(then 메소드는 기본적으로 Promise를 반환한다.) 여러 개의 프로미스를 연결하여 사용할 수 있다.
// 포스트 id가 1인 포스트를 검색하고 프로미스를 반환한다.
promiseAjax('GET', `${url}/1`)
// 포스트 id가 1인 포스트를 작성한 사용자의 아이디로 작성된 모든 포스트를 검색하고 프로미스를 반환한다.
.then(res => promiseAjax('GET', `${url}?userId=${JSON.parse(res).userId}`))
.then(JSON.parse)
.then(render)
.catch(console.error);
async와 await는 자바스크립트의 비동기 처리 패턴 중 가장 최근에 나온 문법이다. 기존의 비동기 처리 방식인 콜백 함수와 프로미스의 단점을 보완하고 개발자가 읽기 좋은 코드를 작성할 수 있게 도와준다. 특히, 복잡했던 Promise를 조금 더 편하게 사용할 수 있다. async await 의 기본 문법은 아래와 같다.
async function 함수명() {
await 비동기_처리_메서드_명();
}
async function f() {
return 1;
}
f().then(alert); // 1
위 함수에서 1을 Promise.resolve
로 감싸도 같은 결과를 반환한다.
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
즉, async
가 붙은 함수는 반드시 프라미스를 반환하고, 프라미스가 아닌 것은 프라미스로 감싸 반환한다.
await는 async 함수 안에서만 동작한다. await는 ‘기다리다'라는 뜻을 가진 영단어 인데, 프라미스가 처리될 때 까지 기다리는 역할을 한다. 그리고 결과는 그 이후 반환된다.
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000)
});
let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)
alert(result); // "완료!"
}
f();
(*)
로 표시한 줄에서 실행이 잠시 '중단’되었다가 프라미스가 처리되면 실행이 재개된다.result
값이 변수 result에 할당된다. 따라서 위 예시를 실행하면 1초 뒤에 '완료!'가 출력된다.await
는 말 그대로 프라미스가 처리될 때까지 함수 실행을 기다리게 만든다. 프라미스가 처리되면 그 결과와 함께 실행이 재개된다. 프라미스가 처리되길 기다리는 동안엔 엔진이 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않는다.
await
를 사용하지 않았다면 데이터를 받아온 시점에 콘솔을 출력할 수 있게 콜백 함수나 .then()
등을 사용해야 했을 것이다. 하지만 async await 문법덕에 비동기에 대한 사고를 하지 않아도 된다.await
는 promise.then
보다 좀 더 세련되게 프라미스의 result
값을 얻을 수 있도록 해주는 문법이다. promise.then
보다 가독성 좋고 쓰기도 쉽다.async await 를 사용한 깃헙의 프로필 사진을 보여주는 코드 예시
async function showAvatar() {
// JSON 읽기
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// github 사용자 정보 읽기
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// 아바타 보여주기
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// 3초 대기
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
await
가 던진 에러는 throw
가 던진 에러를 잡을 때처럼 try..catch
를 사용해 잡을 수 있다.
async function f() {
try {
let response = await fetch('http://유효하지-않은-주소');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
에러가 발생하면 제어 흐름이 catch 블록으로 넘어간다. 또한, 여러 줄의 코드를 try로 감쌀 수 있다.
async/await
을 사용하면 await
가 대기를 처리해주기 때문에 .then
이 거의 필요하지 않다. 또한, .catch
대신 일반 try..catch
를 사용할 수 있다는 장점도 있다. 항상 그러한 것은 아니지만, promise.then
을 사용하는 것보다 async/await
를 사용하는 것이 대개는 더 편리하다async
함수 바깥의 최상위 레벨 코드에선 await
를 사용할 수 없다. 그렇기 때문에 관행처럼 .then/catch
를 추가해 최종 결과나 처리되지 못한 에러를 다룬다.