비동기의 필요성을 알아야할 필요가 있다.
현대의 소프트웨어는 유저와 많은 상호작용을 수행하기 위해 기본적으로 여러 동작을 동시에 수행할 수 있게 설계된다. 한 가지 동작을 수행하는 동안 다른 유저가 발생시키는 이벤트에 제대로 응답해주지 못한다면 UX가 현저히 떨어질 것이다.
따라서 여러 동작을 동시에 수행하는 점은 중요하고 필수적이기 때문에 이를 구현하기 위해 프로그래밍 언어가 가지는 대표적인 전략이 바로 ‘멀티 쓰레딩’. 이는 프로그램이 연산을 처리할 수 있는 일꾼을 더 고용하는 것이다.
대표적으로 Java가 이러한 멀티 쓰레딩 전략을 취하고 있으며, Go는 쓰레드를 만들고 관리하기가 편하다는 장점으로 유명세를 얻었다(Docker도 Go로 작성, 인프라에서 주로 사용됨)
멀티쓰레딩 전략은 복잡한 연산을 동시에 처리할 수 있다는 장점을 가지고 있지만 그것들을 잘 관리하기 위해 리소스가 많이 든다는 단점을 갖는다.
자바스크립트는 브라우저에서 실행되는 간단한 스크립트 언어로서 만들어졌기에 멀티스레드와 같은 복잡성을 가지지 않아서 싱글스레드로 동작하도록 설계되었다.
자바스크립트에서 일꾼은 한 명인데, 시간이 오래걸릴 것 같으면 외주업체에 이 작업을 맡기는 방식으로 작업을 처리한다. 즉 비동기 프로그래밍을 한다!
외주업체는 검증된 곳이기 때문에 어떤 방식으로 일을 하는지는 직접 관리하지 않는다. 여기서 개발자가 할 일은 오로지 단 한 명의 일꾼, 자바스크립트만 관리해주면 된다.
Node 기준으로 설명
비동기적인 동작을 관리하기 위해 ‘이벤트루프’라는 개념을 사용
이벤트루프에게 동작이 완료되고 나면 다시 연락을 받고, 어떤 처리를 해야하는지 알려줘야한다. 이를 위해 이벤트 루프에 동작을 위임할 때는 콜백함수를 통해 완료된 후 어떤 처리를 해야하는지 함께 전달해줘야 한다.
event queue에 들어가는 애들은 일반 자바스크립트 코드, 이벤트 루프에 들어가려면 비동기 코드를 작성해야 한다.
Node: 자바스크립트의 실행 환경, 즉 엔진과 더불어 동작 상에 필요한 여러가지 API들과 비동기 처리를 할 수 있는 추가적인 구성요소들을 포함한다는 의미, 이 중 노드는 비동기 처리를 하기 위해 libuv라는 라이브러리를 사용하고, C++로 작성되어 있다.
libuv는 기본적인 전략으로 OS단에서 제공하는 API를 사용한다, 따라서 OS에서 이미 비동기적으로 동작할 수 있는 API가 구현되어 있다면 해당 API를 그대로 사용한다.
이벤트 루프 내부에는 6개의 페이즈가 존재하고, 수행할 작업이 남아있는 한 이 페이즈들을 계속해서 순회한다, 각각의 페이즈는 자신만의 콜백 큐를 가지고 있다.
각 페이즈들이 담당하고 있는 동작은 다음과 같다.
한 번 루프를 돌 때 1번부터 순서대로 도는 것을 반복한다.
노드는 더불어 nextTickQueue, microTaskQueue를 추가로 더 관리한다.
nextTickQueue - microTaskQueue - event loop로 수행
event loop는 실행 한도가 있지만, nextTickQueue는 실행 한도가 없다는 것의 차이
콜백 지옥 때문에 Promise라는 개념이 나왔다
모나드(?) 찾아볼 필요 없다!
Promise란 자바스크립트가 미리 만들어둔 하나의 객체이고, then, catch라는 메서드를 가지고 있으며, pending, fulfilled, rejected 세 사지 상태를 가지고 있다.
then()에서 에러를 처리하는 것보다 then().catch() 형태로 에러를 처리하는 것이 일반적이다.
Promise도 근본적으로 then을 이용해서 callback을 깊어지지 않게 만들어졌다는 것 뿐, 콜백 패턴을 벗어나지 못했기에, 그래서 async-await 문법이 나오게 됐다.
setState() 앞에 await 쓰는 것은 의미가 없다, promise 앞에서만 쓰는 것이기 때문
async 함수의 리턴값은 항상 Promise, 함수 내부에서 Promise를 return 하지 않더라고 모든 return value를 암묵적으로 Promise로 감싸서 보내는 동작을 수행한다.
await는 Promise가 settled될 때까지 함수의 실행을 멈추고 대기해준다.
주로 async 함수 내의 resolve, reject는 try-catch로 처리한다.
const delay = (ms) => {
return new Promise((resolve) => {
setTimeout(() => resolve(`completed in ${ms}`), ms)
})
}
async function withoutPromiseAll() {
await delay(1000)
await delay(1000)
await delay(1000)
}
async function withPromiseAll() {
await Promise.all([delay(1000), delay(1000), delay(1000)])
}
console.time()
withoutPromiseAll()
console.timeEnd()
console.time()
withPromiseAll()
console.timeEnd()
동시에 비동기적으로 실행시킨다.
인자로 받은 Promise 중 하나라도 reject 된다면, 동작을 중지하고 reject된 Promise를 리턴한다.
reject된 Promise가 있더라도 전체를 모두 처리해서 배열에 담아준다.
transaction 시 필요하다.
가장 먼저 resolve되거나 reject된 promise를 반환한다.