🔥 학습목표
- 비동기/동기의 차이를 설명할 수 있다.
- 비동기 함수를 조건에 따라 (부분적으로) 동기적으로 사용할 수 있다.
- 어떤 경우에 중첩된 콜백이 발생하는지 이해할 수 있다.
- callback, promise, async/await 의 차이를 설명할 수 있다.
특정 코드의 실행이 완료될 때까지 기다리고 난 후 다음 코드를 수행하는 것
특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 수행하는 것
JavaScript는 싱글 스레드 기반으로 동작하는 언어이므로 동기적으로 작동한다.
그러나 JavaScript에서는 비동기 처리 또한 가능하다.
왜 그럴까?
그 이유는 JavaScript가 작동하는 환경(런타임)에서 비동기 처리를 도와주기 때문이다.
더 자세한 내용은 후에 학습하고 기록한다.
당장은 특별한 작업 없이 비동기 처리를 할 수 있다는 것만 알고 비동기 코드 작성에 집중하자!
타이머 API는 브라우저에서 제공하는 Web API이며 비동기로 작동하도록 구성되어 있다.
<타이머 관련 API>
setTimeout(callback, millisecond)
: 일정 시간 후에 함수를 실행한다.
- (실행할 콜백 함수, 콜백 함수 실행 전 기다리는 시간)
- return 값: 임의의 타이머 ID
setTimeout(function () { console.log('1초 후 실행'); }, 1000); // 123
clearTimeout(timerID)
:setTimeout
타이머를 종료
- return 값: 없음
const timer = setTimeout(function () { console.log('10초 후 실행'); }, 10000); clearTimeout(timer); // setTimeout이 종료됨.
setInterval(callback, millisecond)
: 일정 시간의 간격을 가지고 함수를 반복적으로 실행한다.
- (실행할 콜백 함수, 반복적으로 함수를 실행시키는 시간 간격)
- return 값: 임의의 타이머 ID
setInterval(function () { console.log('1초마다 실행'); }, 1000); // 345
clearInterval(timerID)
:setInterval
타이머를 종료
- return 값: 없음
const timer = setInterval(function () { console.log('1초마다 실행'); }, 1000); clearInterval(timer); // setInterval이 종료됨.
그러나 타이머API를 활용하여 코드를 작성하면 문제가 발생한다.
비동기 코드는 코드가 '코드가 작성 된 순서대로 작동'되는 것이 아닌, 동작이 완료되는 순서대로 작동하게 된다. 코드의 순서를 예측할 수가 없다.
node index.js
명령어로 위 코드를 실행시켜 보면 코드를 실행할 때마다 순서가 바뀌는 걸 확인할 수 있다.
개발자는 제어 가능한, 예측 가능한 코드를 작성해야 하는데, 타이머 API와 같이 비동기로 작동하는 코드는 어떻게 제어해야 할까?
비동기로 작동하는 코드를 제어할 수 있는 방법은 대표적으로 세 가지가 있다.
코드가 길어질 수록 복잡해지고 가독성이 낮아지는 Callback Hell이 발생하는 단점이 있다.
비동기 코드를 제어할 수 있는 다른 방법으로 Promise
클래스가 있다. Callback Hell을 방지하는 역할을 수행한다.
let promise = new Promise((resolve, reject) => {
// 1. 정상적으로 처리되는 경우
// resolve의 인자에 값을 전달할 수도 있습니다.
resolve(value);
// 2. 에러가 발생하는 경우
// reject의 인자에 에러메세지를 전달할 수도 있습니다.
reject(error);
});
프로미스가 정상적으로 처리 된 경우 프로미스 객체는 아래와 같다.
프로미스가 에러가 발생한 경우 프로미스 객체는 아래와 같다.
new Promise
가 반환하는 Promise
객체는 state
, result
내부 프로퍼티를 갖는다. 두 프로퍼티에 직접 접근은 불가능 하지만, .then
, .catch
, .finally
메서드를 사용하면 접근이 가능하다.
state
pending
(대기) : 기본 상태fufilled
(이행) : 비동기 처리를 수행할 콜백 함수(excutor
)가 성공적으로 작동한 경우rejected
(거부) : 에러 발생result
undefined
: 처음 상태value
: 콜백 함수(excutor
)가 성공적으로 작동하여 resolve(value)
가 호출된 경우error
: 에러가 발생하여 reject(error)
가 호출된 경우
excutor
에 작성한 코드들이 정상적으로 처리 →resolve
함수 호출 →.then
메서드로 접근
.then
안에서 리턴한 값이 Promise
면 Promise
객체의 내부 프로퍼티인 result
를 다음 .then
의 콜백 함수 인자로 받을 수 있다.Promise
가 아니라면 리턴한 값을 .then
의 콜백 함수의 인자로 받아올 수 있다..then
, .catch
, .finally
메서드는 Promise
를 리턴한다.(Promise chaining
가능)let promise = new Promise((resolve, reject) => {
resolve("성공");
});
promise.then(value => {
console.log(value);
// "성공"
})
executor
에 작성했던 코드들이 에러 발생 → →reject
함수를 호출 →.catch
메서드로 접근
let promise = new Promise(function(resolve, reject) {
reject(new Error("에러"))
});
promise.catch(error => {
console.log(error);
// Error: 에러
})
executor
에 작성했던 코드들의 정상 처리 여부와 상관없이.finally
메서드로 접근 가능
let promise = new Promise(function(resolve, reject) {
resolve("성공");
});
promise
.then(value => {
console.log(value);
// "성공"
})
.catch(error => {
console.log(error);
})
.finally(() => {
console.log("성공이든 실패든 작동!");
// "성공이든 실패든 작동!"
})
여러 개의 비동기 작업을 동시에 처리하고 싶을 때 사용. 인자로 배열을 받는다. 배열에 있는 모든
Promise
에서 동작이 정상 처리 된다면 그 결과를 새로운 배열에 저장해 새로운Promise
객체를 반환한다.
Promise.all([
new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러1'))), 1000),
new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러2'))), 2000),
new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러3'))), 3000),
])
.then((value) => console.log(value))
.catch((err) => console.log(err));
// Error: 에러1
🔴
callback
을 사용할 경우, 로직의 결과값을 처리하기 위해선callback
안에서만 동작이 가능하고, 그 밖에서는 비동기 계산 값을 알 수가 없다.
🔵
promise
를 사용할 경우, 비동기 계산 값이promise
객체에 저장되기 때문에.then
,.catch
,.finally
와 같은 메서드로 접근이 가능하다.
하지만 Promise
도 코드가 길어질 수록 복잡해지고 가독성이 낮아지는 Promise Hell이 발생한다.
JavaScript ES8은 async
/awiat
키워드를 제공하여 이러한 문제를 해결했다.
함수 앞에 async
키워드를 사용하고 async
함수 내에서만 await
키워드를 사용한다.
await
키워드가 작성된 코드가 동작하고 나서야 다음 순서의 코드가 동작한다.
// 함수 선언식
async function funcDeclarations() {
await 첫 번째 수행 코드
await 첫 번째 코드가 끝난 뒤 수행되는 코드
...
}
// 함수 표현식
const funcExpression = async function () {
await 마찬가지
...
}
// 화살표 함수
const ArrowFunc = async () => {
await 마찬가지
...
}
node index.js
명령어를 실행하면 동기적으로 출력되는 결과를 확인할 수 있다.
promise
는 .then
메서드를 통해 접근해야만 결과 값을 반환받을 수 있어 코드가 길어질 경우 가독성이 좋지 않았지만,
async
/await
는 await
를 통해 Promise
반환 값을 바로 받아올 수 있어 코드 가독성이 좋다.