동기적이라는 것은 시작 시점과 완료 시점이 같은 상황을 말한다.
예를 들어, 동기적인 카페에서 직원 1명이 주문을 받았다.
👧🏻 손님 A: 아이스 카페라떼 주문이요!
라고 했을 때, 직원은 손님 A에게 ☕️ 카페라떼를 만들어 주었다.
그리고 손님 A의 주문에 대한 동작이 끝남과 동시에 손님 B의 주문을 받는다.
🧑🏻 손님 B: 아이스 카페모카 주세요
직원은 한명이기 때문에 동시에 카페라떼와 카페모카를 만들 수 없다. 이런 식으로 동작이 순차적으로 처리되는 것을 동기적이라고 한다.
동기적인 것은 blocking 하다. 직원이 손님 A의 카페라떼를 만드는 동안 손님 B의 주문을 받을 수 없기 때문이다.
비동기적인 것은 동기적인 것과 반대로 시작 시점과 완료 시점이 같지 않고, 요청에 대한 결과가 동시에 일어나지 않는다.
☕️ 비동기적인 카페에서, 직원은 손님 A의 주문과 손님 B의 주문을 모두 받고 한꺼번에 카페라떼와 카페모카를 만든다. 어떤 것이 먼저 완성될지는 예측할 수 없다. 나중에 주문한 손님 B가 먼저 커피를 받을 수도 있다!
따라서 비동기적인 것은 non-blocking 하다. 손님 A의 주문을 받았다고 손님 B의 주문을 처리하지 못하는 것이 아니다.
javascript의 런타임인 node.js은 non-blocking하고 비동기적이다.
💭 유튜브 화면에서, 영상이 비동기적으로 로딩되는 동안 다른 버튼을 클릭할 수도있고, 다른 페이지로 이동할 수도 있다. 만약 동기적으로 로딩된다면 사용자는 영상이 로딩되는 동안 다른 작업을 하지 못하고 꼼짝없이 기다려야 한다.
🧐 그렇다면 javascript에서 어떻게 비동기적으로 작업을 수행할 수 있을까?
→ 콜백 함수를 통해 구현할 수 있다!
✏️ callback function
다른 함수의 전달인자(argument)로 넘겨주는 함수
callback 함수를 넘겨 받은 함수에게는 선택지가 있다.
1. callback 함수를 즉시 실행할 수도 있고,
2. 나중에 비동기적으로(asynchronously) 실행할 수도 있다.
document.querySelector('#btn').addEventListener('click', function (e) {
console.log('button clicked');
});
버튼 요소에 이벤트 핸들러를 연결해주는 코드이다. addEventListner는 콜백 함수를 두번째 인자로 받고 있다.
이 때, 함수 자체를 연결하는 것이지, 함수 실행을 연결하는 것이 아니다.
const handleBtnClick = (e) => { console.log('button clicked'); }
document.querySelector('#btn').addEventListenr('click', handleBtnClick);
따라서 위 코드처럼 이벤트 핸들러가 따로 구현되어 있으면 전달할 때 함수 자체를 전달해주어야 한다.
const printString = (string) => {
setTimeout(
() => {
console.log(string)
},
Math.floor(Math.random() * 100) + 1
)
}
const printAll = () => {
printString('A')
printString('B')
printString('C')
}
printAll()
위 코드에서, setTimeout 함수는 인자로 콜백 함수를 받고 있다.
콜백 함수는 비동기적으로 실행되기 때문에, 비록 printString의 인자로 'A', 'B', 'C'를 차례로 넘겨주었다고 해도, random 함수에 의해 기다리는 시간이 결정되고 실행 결과를 예측할 수 없다.
이렇게 asynchronously 하게 처리하면 결과를 예측할 수 없다는 문제가 생긴다.
위 코드에서 A, B, C를 차례대로 출력하고 싶다면 어떻게 해야 할까? callback 함수를 이용하면 된다.
const printString = (string, callback) => {
setTimeout(
() => {
console.log(string)
callback()
},
Math.floor(Math.random() * 100) + 1
)
}
const printAll = () => {
printString('A', () => {
printString('B', () => {
printString('C', () => {})
})
})
}
printAll()
마찬가지로 random 함수에 의해 기다릴 시간이 결정되지만 순차적으로 실행된다.
🧐 그런데 비동기적으로 관리해야 할 일들이 많아진다면?
위와 같은 방식으로는 무한 callback의 굴레에 빠질 수 있다. 그래서 등장한 것이 Promise이다. Promise는 callback chain을 관리한다.
MDN: Promise
공식 문서에 따르면
✏️ Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타낸다.
또한, Promise는 세가지 상태를 가질 수 있다.
1.대기(pending): 이행하지도, 거부하지도 않은 초기 상태
2.이행(fulfilled): 연산이 성공적으로 완료됨
3.거부(rejected): 연산이 실패함
Promise를 사용하면 비동기 연산을 동기 연산처럼 사용할 수 있다.
위에서 봤듯이 async 하다는 것은 요청에 대한 결과가 동시에 일어나지 않기 때문에 완료 시점을 예측할 수 없다는 말인데, Promise는 이 비동기 작업이 미래의 어떤 시점에 결과를 제공해줄 것이라고 약속(Promise)을 해준다.
자세한 문법에 대한 설명은 생략하고, 앞서 계속 봤던 A, B, C 출력하기를 Promise를 통해 해보자.
const printString = (string) => {
return new Promise((resolve, reject) => {
setTimeout(
() => {
console.log(string)
resolve()
},
Math.floor(Math.random() * 100) + 1
)
})
}
const printAll = () => {
printString('A')
.then(() => {
return printString('B')
})
.then(() => {
return printString('C')
})
}
printAll()
.then() 이라는 메서드를 사용해 순차적으로 함수를 실행할 수 있다.
하지만 마찬가지로 처리해야할 작업이 많아진다면 무한 .then()의 굴레에 빠질 수 있다 🥲
그래처 최종적으로 async와 await가 등장한다.
✏️ async function
async 키워드를 통해 AsyncFunction 객체인 비동기 함수를 정의할 수 있다. 이 async function은 Promise를 사용해 결과를 반환한다.
다음과 같은 형태로 선언할 수 있다.async function name([param..]) { statements }
✏️ await
async 함수는 await 식을 포함할 수 있다.
await 식은 async 함수의 실행을 일시 중지하고 전달된 Promise의 해결을 기다린 다음 async 함수의 실행을 다시 시작한다.
차례대로 A, B, C를 출력하는 작업을 async/await로 해보자.
const printString = (string) => {
return new Promise((resolve, reject) => {
setTimeout(
() => {
console.log(string)
resolve()
},
Math.floor(Math.random() * 100) + 1
)
})
}
const printAll = async () => {
const a = await printString('A');
const b = await printString('B');
const c = await printString('C');
}
printAll()
callback 감옥에서 벗어날 수 있다!