비동기 자바스크립트(Asynchronous JavaScript)는 자바스크립트라는 언어에서 시간이 걸리는 작업
을 처리하는 중요한 부분을 담당합니다.
비동기 자바스크립트를 이해하는 과정은 동기와 비동기의 차이점을 인지하는 것으로부터 시작합니다.
동기적 작업은 요청을 보냈다면, 응답을 받아야 다음 동작이 이루어집니다. 응답을 받아야만 다음 동작이 이루어지기 때문에 순차적 실행
을 의미합니다.
순차적 실행
에서는 앞 작업이 빠르게 끝나면 뒷작업을 빠르게 할 수 있고, 앞 작업이 느리게 끝나면 뒷 작업의 실행 시점이 느려집니다. 이를 블로킹(Blocking)이라고 합니다.
폭포수 모델
동기적으로 코드를 실행하는 것은 마치 소프트웨어 개발 방법론 중 하나인 폭포수 모델과 비슷합니다. 폭포수 모델에서는 마치 폭포수가 흐르는 것처럼 이전단계가 다음단계에 순차적으로 영향을 미칩니다.
반면에 비동기적으로 실행하는 것은 요청을 보내고 응답이 돌아오지 않아도 다음 코드를 실행한다는 의미입니다. 이는 응답을 기다리지 않으므로 실행 순서를 보장시키지 않습니다. 따라서 동기와는 반대로 블로킹이 발생하지 않습니다.(Non-Blocking)
주문하기
카페나 음식점을 가서 줄을 서서 메뉴를 주문했다고 가정해보겠습니다. 손님 여러명이 주문을 하게되고 직원들은 그 주문들을 처리합니다. 이제 많은 주문들은 주방 직원들에게 전달되고 동시에 제조됩니다. 메뉴의 제조 시간에 따라 음식을 먼저 받는 손님이 있을 수 있고, 늦게 받는 손님이 있을 수 있겠죠. 이는 비동기 자바스크립트의 동작 방식과 동일합니다.
비동기적으로 처리한다면 실행 흐름이 멈추지 않기 때문에 동시에 여러가지 작업을 처리할 수도 있고, 기다리는 과정에서 다른 함수를 호출할 수도 있습니다.
자바스크립트는 싱글스레드인데 어떻게 동시에 여러가지 작업을 처리할 수 있을까요?
웹 브라우저는 다양한 함수와 API를 제공하여 비동기적으로 여러작업(setTimeout, addEventListener에 전달되는 콜백함수, 네트워크 요청등)이 실행될 수 있습니다.
클라이언트 사이드 자바스크립트 프로그램은 거의 대부분 이벤트 주도적(Event Driven)
입니다. 미리 지정된 계산을 실행하기보다는 사용자가 뭔가 하기를 기다렸다가 그 행동에 반응합니다. 웹 브라우저는 사용자가 키를 누르고, 마우스를 움직이고, 마우스 버튼을 클릭하고, 터치 스크린을 터치할 때 이벤트를 일으킵니다. 즉, 웹 브라우저는 지정된 이벤트가 일어날 때마다 함수를 호출합니다.
이런 이벤트 주도적인 클라이언트 자바스크립트에서는 특정 이벤트가 발생했을 때 어떤 동작을 실행하기 위해 콜백함수를 활용합니다.
callback
은 "부르다", "호출하다"의 의미인 call
과 "되돌아오다"는 의미의 back
의 합성어로 되돌아 호출해달라는 명령입니다.
콜백은 다른 함수에게 전달하는 함수입니다.(자바스크립트에서 함수는 일급 객체
)
어떤 함수 X를 호출하면서 특정 조건일 때 함수 Y를 실행해서 나에게 알려달라는 요청을 함께 보내는 것입니다. 요청을 받은 함수 X의 입장에서는 해당 조건이 갖추어졌는지 여부를 스스로 판단하고 Y를 직접 호출합니다.
위 예제에서 알 수 있는 사실은 X 함수가 Y를 언제 실행할지 결정할 수 있다는 것입니다. 이를 제어권을 위임
한다고 합니다.
콜백 함수를 위임 받은 함수는 자체적인 내부 로직에 의해 콜백함수를 적절한 시점
에 실행합니다.
document.getElementById("someElement").addEventListener("click", handleClick)
예를 들어보면, 위와 같이 HTML 요소에 클릭이벤트를 등록하고 handleClick
이라는 함수를 전달함으로써 addEventListener
에게 여러분이 만든 handleClick
이라는 함수의 호출 제어권을 넘겨주는 것(제어권 위임)이라고 할 수 있습니다.
콜백은 Synchronous Callback
과 Asynchronous Callback
으로 나뉩니다.
function greet(name) {
console.log(`Hello ${name}`);
}
function greetHyunjin(callback) {
const name = 'hyunjin'
callback(name)
}
greetHyunjin(greet);
위 함수에서 greetHyunjin
에 greet
콜백 함수를 넘겨서 호출하면 코드가 순차적으로 실행됩니다.
const numbers = [1,2,3,4,5,6]
numbers.sort((a,b) => a - b)
numbers.map(n => n * 2)
위와 같이 배열의 메서드인 sort와 map에 전달되는 콜백 함수도 동기적 콜백
입니다.
반면에 비동기 콜백
은 보통 시간이 걸리는 비동기 요청이 끝난 후에 콜백함수를 호출합니다. 즉 비동기 콜백은 어떤 작업이 실행될 동안 기다렸다가 콜백함수를 호출하는 것입니다.
function greet(name) {
console.log(`Hello ${name}`)
}
setTimeout(greet, 2000, 'hyunjin') // 세번째 인자는 콜백 함수의 인자로 들어갑니다.
function callback() {
document.getElementById("greet").innerHTML = "Hello hyunjin"
}
document.getElementById("btn").addEventListener("click", callback)
위의 setTimeout
함수나 addEventListener
에게 전달하는 콜백 함수는 비동기 콜백함수로 활용됩니다.
어떤 비동기 작업이 마무리된 후에 다음코드를 실행하기위해서 콜백함수를 넘겼는데 그 콜백함수에서 또한 비동기 요청을 하고 그 요청이 끝난 후에 실행될 콜백함수를 받는다고 생각해보겠습니다.
fetchCurrentUser('/api/user', function(result) {
fetchFollowersByUserId(`/api/followers/${result.id}`, function (result) {
fetchFollowersInterest(`/api/interests/${ersult.followerId}`, function(result) {
console.log(result)
})
}
})
이전 요청의 결과가 다음 실행에 필요한 데이터를 전달해줍니다. 또한 비동기 콜백패턴을 사용했을 때 Callback Hell
은 자연스럽게 발생합니다.
ES6에서는 비동기 처리를 위한 다른 패턴으로 Promise
를 도입했습니다. Promise
는 Callback Hell
과 같은 콜백이 갖은 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있습니다.
Promise가 와닿지 않는다면 읽어보세요.
현진과 엘리스가 집에 있다고 해보겠습니다.
현진은 배가 고파졌고 엘리스에게 밥을 먹자고 합니다. 현진은 엘리스에게 집 앞에서 치킨을 사오라고 합니다. (fetchChicken -> 비동기) 근데 치킨집이 닫았을 수 있어서 치킨집에 도착하면 치킨을 사올 수 있는지 치킨을 사오지 못하는지 문자를 보내서 알려달라고 부탁합니다. (Promise가 fulfilled 또는 reject)
현진은 집에서 치킨과 같이 먹을 맥주를 준비합니다. (비동기로 엘리스에게 일 시켜놓고 현진은 다른 일을 진행)
다음으로 이어질 상황은 두가지가 있습니다.
첫번째로는 엘리스가 치킨을 사올 수 있다고 문자를 보낸 것 입니다. 이때 Promise가 fulfilled(이행)되었다고 표현하고 Success callback을 실행합니다. Success Callback으로는 치킨을 먹기위한 테이블 세팅을 진행합니다. (then 구문)
두번째 경우로는 엘리스가 치킨을 사올 수 없다고 문자를 보냅니다. 이때 Promise가 reject(거부)되었다고 표현하고 Failure Callback을 실행합니다. Failure Callback으로는 엘리스에게 피자를 사오라고 문자를 보낼 수 있습니다.(catch 구문)
우선 Promise는 new Promise
를 호출하는 것으로 만들 수 있습니다.
new Promise
에 전달되는 함수는 executor(실행자 또는 실행함수)
라고 부릅니다. executor
는 new Promise
가 만들어질 때 자동으로 실행됩니다.
executor
의 인수 resolve
와 reject
는 자바스크립트에서 자체 제공하는 콜백함수
입니다. 개발자는 resolve
와 reject
를 신경 쓰지 않고 executor안 코드만 작성
하면 됩니다.
executor 함수안에서 결과를 즉시 얻든 늦게 얻든 상관없이 상황에 따라 인수로 넘겨준 콜백 중 하나를 반드시 호출해야합니다. 즉, resolve
또는 reject
를 반드시 호출해야합니다.
정리해보면 executor
함수는 자동으로 실행되는데 여기서 원하는 일이 처리됩니다. 처리가 끝나면 executor 함수는 성공 여부에 따라 resolve
또는 reject
를 호출합니다.
resolve와 reject 콜백함수는 자바스크립트 엔진이 미리 정의한 함수이므로 개발자가 만들 필요가 없습니다.
resolve 또는 reject를 호출했을 때 Promise 내부 프로퍼티가 어떻게 바뀌는지 보겠습니다. Promise 객체는 두가지 프로퍼티를 갖습니다.
첫째로는 state
, 둘째로는 result
입니다.
이 객체는 처음에 state
가 pending
즉 대기 상태였다가 resolve
가 호출되면 fulfilled
상태, 이행상태로 변합니다.(이행된 프라미스)
반면에 reject
가 호출되면 state가 rejected
로 변합니다.(거부된 프라미스)
result
는 처음에 undefined
상태를 갖다가 resolve
가 호출되면 그 값으로, reject
가 호출되면 error
로 바뀝니다.
Resolve 또는 rejected 상태의 프라미스는 처리된 프라미스
(Settled Promise)라고 부릅니다. 이와 반대되는 프라미스로 pending
상태의 프라미스가 있습니다.
Promise의 후속 처리 메서드인 then, catch, finally는 모두 Promise를 반환합니다.
Promise를 바탕으로 async / await 개념에 대해 알아보겠습니다. function 앞에 async를 붙이면 해당 함수는 항상 프라미스를 반환합니다.
프라미스가 아닌 값을 반환하더라도 이행 상태의 프라미스로 값을 감싸 이행된 프라미스가 반환되도록 합니다.
예시의 함수를 호출하면 결과가 elice 인 프라미스가 반환됩니다.
async 함수에서도 명시적으로 Promise를 반환할 수 있습니다. 결과는 동일합니다.
await은 기다리다
라는 뜻을 가진 영단어로 await 키워드를 만나면 프라미스가 처리될 때까지 기다립니다.
프라미스가 처리되면 그 결과와 함께 실행이 재개됩니다. 프라미스가 처리되길 기다리는 동안엔 엔진이 다른 일을 할 수 있기 때문에 CPU 리소스가 낭비되지 않습니다.
(다른일 : 스크립트 실행, 이벤트 처리 등)
await은 promise의 then 보다 좀더 가독성 좋게 프라미스의 결과값을 얻을 수 있도록 해주는 문법입니다.
이 예제를 보겠습니다. 1초 후에 resolve
되는 promise
가 있고 다음줄에서 resolve
결과를 기다리기 위해 await
를 붙여서 promise
를 기다립니다.
그러면 콘솔에는 resolve
된 프라미스의 결과값인 완료가 표시됩니다.
자바스크립트에서 코드의 흐름을 제어(Flow Control)하는 것은 어렵습니다. 자바스크립트의 Callback 패턴, Promise, async / await과 같은 개념을 숙지해야합니다.
자바스크립트는 싱글 스레드, Non Blocking I/O라고 부릅니다. 자바스크립트가 싱글 스레드라면 도대체 어떻게 코드를 비동기적으로 실행할 수 있는 걸까요?
JavaScript Engine(V8, Spidermonkey, JavaScript Core)이 있기에 가능합니다. 자바스크립트 엔진에 내장된 Web API를 활용하면 여러 비동기 작업을 백그라운드에서 수행할 수 있습니다.
Web API에는 ajax, event listeners, fetch API, setTimeout과 같은 기능들이 있고 이들을 활용하면 비동기 작업을 할 수 있습니다. 즉, 자바스크립트의 콜스택에서 Web API와 관련된 함수를 인지하면 그 처리를 브라우저에게 넘기고 브라우저가 그 작업을 끝내면 stack에 콜백 형태로 반환합니다.
자바스크립트 자체는 싱글 스레드 Non Blocking I/O가 맞습니다. 하지만 브라우저에 내장된 Web API를 활용함으로써 C++ 언어의 기능을 활용할 수 있습니다.(C++ 에서는 멀티스레드를 활용)
이 글은 엘리스 SW 엔지니어 트랙 3기 수업(22.10.06)때 진행한 자료입니다.
짧은 기간 코치를 맡았지만 웹사이트로 롤링 페이퍼까지 만들어주시고 잊지못할 경험을 하게해주신 모든 레이서분들에게 감사합니다.
지시안님
, 박우찬님
, 강민희님
, 서윤지님
, 이수호님
, 박재인님
, 조가영님
, 곽지우님
, 김상현님
, 서아름님
, 조욱현님
, 홍희선님
, 조건형님
, 김유정님
, 김민경님
, 허지윤님
, 송주혜님
, 이재웅님
, 이수호님
, 서아름님
, 김건우님
, 주갑열님
, 홍화낙님
, 설지윤님
, 김선은님
, 곽지우님
, 권규리님
, 김보라님
, 신현규님
, 김유정님
, 장은영님
, 고나현님
, 성지안님
, 백소라님
, 김동준님
, 손형석님
, 정지훈님
, 박재현님
, 권민영님
, 전세현님
, 장소진님
외 모든 레이서분들께 감사합니다.
잘읽었습니다