동기식 : 응답을 받아야만 다음 동작을 이어가는 방식
비동기식 : 응답 여부와 관계 없이 다음 동작을 이어가는 방식
자바스크립트는 기본적으로 동기식으로 작동한다. 하나의 Call Stack만 사용하기 때문에, JS만으로는 비동기식 동작을 구현할 수 없다. 따라서 브라우저에서 지원하는 Web API를 이용해서 비동기식 동작을 구현하게 된다. setTimeout
이나 axois를 이용해 요청을 보내고 응답을 기다려야 할 경우, 이 함수는 Web API로 넘어가게 되고 다른 함수가 Call Stack으로 들어와 실행되는데, 이를 오히려 방지해야 하는 경우가 있다.
function a() {
setTimeout(() => {
console.log('A')
}, 1000);
}
function b() {
console.log('B')
}
function test() {
a()
b()
}
test()
// B
// A
함수의 실행의 순서를 보장하고 싶을 때 사용할 수 있는 방법 중에 하나로 callback이 있다.
function a(callback) {
setTimeout(() => {
console.log('A')
callback()
}, 1000);
}
function b(callback) {
console.log('B')
callback()
}
function c(callback) {
setTimeout(() => {
console.log('C')
callback()
}, 1000);
}
function d(callback) {
console.log('D')
callback()
}
function test() {
a(() => {
b(() => {
c(() => {
d(() => {
})
})
})
})
}
test()
// A
// B
// C
// D
callback을 사용하지 않고 실행하면 실행하는데 딜레이가 있는 a
함수보다 b
함수가 먼저 실행되겠지만, 비동기 실행을 했기 때문에 순서대로 실행이 되었다. 하지만 callback을 여러 개 중첩해서 사용할 경우 코드가 복잡해지고 관리가 어려워진다. 위 코드처럼 순차적으로 되어 있지 않고 조금만 복잡해도 알아보기 힘들 것이다.
callback 함수를 사용해 순서를 보장해 주는 것처럼 함수 실행의 순서를 보장할 수 있는 방법으로 Promise 객체를 사용하는 방법이 있다.
function a() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('A')
resolve()
}, 1000)
})
}
function b() {
return new Promise((resolve) => {
console.log('B')
resolve()
})
}
function c() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('C')
resolve()
}, 1000)
})
}
function d() {
return new Promise((resolve) => {
console.log('D')
resolve()
})
}
function test() {
a().then(() => {
b().then(() => {
c().then(() => {
d().then(() => {
console.log('Done')
})
})
})
})
}
test()
a
, b
, c
, d
함수는 각각 Promise 객체를 반환한다. Promise 객체의 매개변수로 하나의 함수가 들어가고, 해당 함수의 매개변수로 resolve
가 할당된다. 이 resolve
는 callback과 대응한다고 볼 수 있다.
a
함수 내부에서 resolve
매개변수가 실행될 때까지 앞의 코드가 실행되고, resolve
가 실행되면 Promise 객체를 반환한 뒤 then
메서드를 실행한다.
하지만, 위 코드를 보면 callback을 사용했을 때와 유사하게 복잡하다. 이를 return을 이용해서 수정하면 아래처럼 만들 수 있다.
function test() {
a().then(() => {
return b()
}).then(() => {
return c()
}).then(() => {
return d()
}).then(() => {
console.log('Done')
})
}
a
함수를 실행하면 Promise 객체 하나를 반환한다. then
메서드를 실행하여 a
함수의 실행이 끝난 뒤 b
함수를 실행하여 반환한다. 즉, b
함수의 Promise 객체가 반환된다. 마지막에 반환값 없이 함수를 실행하고 끝난다.
Promise 객체는 catch
와 finally
메서드를 활용해서 예외처리가 가능하다.
function a(number) {
return new Promise((resolve, reject) => {
if (number > 4) {
reject()
return
}
setTimeout(() => {
console.log('A')
resolve()
}, 1000)
})
}
function test() {
a([인수])
.then(() => {
console.log('resolve')
})
.catch(() => {
console.log('reject')
})
.finally(() => {
console.log('done')
})
}
a
함수부터 살펴 보면 매개변수로 reject
가 추가되었다. reject
는 resolve
가 실행될 수 없는 상황에서 실행되는 매개변수로 catch
메서드를 실행시킨다. 위 코드를 예로 살펴보면, a
함수의 number
매개변수로 4이하의 값이 할당되면 resolve
매개변수가 실행되어 결과는 다음 과 같다.
A
resolve
done
반대로 a
함수의 number
매개변수로 4보다 큰 값이 할당되면 reject
매개변수가 실행되어 결과는 다음 과 같다.
reject
done
결과를 보면 finally
라는 메서드는 resolve
와 reject
가 실행됐을 때 모두 동작하는 것을 알 수 있다. finally
는 항상 실행되는 메서드이다.
최신의 자바스크립트는 async
함수와 await
를 제공한다. 이 또한 함수 실행 순서를 보장해주는 역할을 하는데 callback이나 Promise 객체의 메서드를 사용할 때 보다 훨씬 직관적인 코드를 작성할 수 있다.
function a() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('A')
resolve()
}, 1000)
})
}
function b() {
return new Promise((resolve) => {
console.log('B')
resolve()
})
}
function c() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('C')
resolve()
}, 1000)
})
}
function d() {
return new Promise((resolve) => {
console.log('D')
resolve()
})
}
async function test() {
await a()
b()
await c()
d()
}
결과는 Promise의 then
메서드를 사용했을 때와 똑같다. 하지만 test
함수의 내부가 훨씬 직관적이고 깔끔하다. async는 비동기 실행을 할 함수를 나타내고 await는 해당 함수가 실행이 완료될 때까지 대기한 뒤에, 다음 코드를 실행한다는 의미이다.
function a(number) {
// resolve가 실행될 수 없을 때 reject
return new Promise((resolve, reject) => {
if (number > 4) {
reject()
return
}
setTimeout(() => {
console.log('A')
resolve()
}, 1000)
})
}
async function test() {
try {
await a(11)
console.log('resolve')
} catch (err) {
console.log('reject')
} finally {
console.log('done')
}
}