3일에 걸쳐 자바스크립트의 비동기(Asynchronous)에 대한 개념을 공부하고 underbarscore라이브러리의 기능을 구현해보고, 타이머 API,fs모듈,fetch API를 사용해보는 스프린트를 진행했다.
--정리하는 내용은 코드스테이츠의 자료와 PoiemaWEB을 참고하여 공부한 내용입니다.--
자바스크립트 엔진은 한번에 하나의 태스크만을 실행할 수 있는 싱글스레드 방식으로 동작한다.
싱글스레드는 한번에 하나의 태스크만을 실행할 수 있기 때문에 시간이 걸리는 태스크를 실행할 경우 다음 대기중인 태스크는 현재 실행중인 태스크가 완료될 때까지 실행될 수 없다.
이를 블로킹(blocking 작업중단)이라한다.
function sleep(delay,func){
const delayMoment=Date.now()+delay;
while(Date.now()<delayMoment);
func();
}
sleep(2000,()=>{console.log('sleep result')});
console.log('second line');
//sleep result 2초뒤출력
//second line
setTimeout(()=>{console.log('setTimeout result')},2000);
console.log('second line');
//second line
//setTimeout result 2초뒤출력
위의 코드를 보면 setTimeout와 sleep은 동일하게 2초(2000ms)후에 console.log를 실행하지만 sleep을 사용했을 때는 sleep이 처리된후에 second line을 출력하고(blocking) setTimeout을 사용했을 때는 setTimeout이 처리되지 않아도 second line을 출력하는(non-blocking) 차이점이 보인다.
이처럼 현재 실행중인 태스크가 종료되지 않더라도 다음 태스크를 실행하는 방식을 비동기 처리라고 한다.
동기처리방식은 태스크를 하나씩 순서대로 처리하기 때문에 실행순서가 보장된다는 장점이 있지만 처리하는데 시간이 오래걸리는 태스크(예를 들면 서버에 요청을 보내고 응답을 받아 처리하는 태스크)가 있다면 그 태스크가 완료되기 전까지 뒤의 태스크들이 blocking되는 단점이 있다.
그에 반해 비동기 처리방식은 현재 실행중인 태스크가 종료되지 않더라도 기다리지 않고 다음 태스크를 실행하여 블로킹이 발생하지 않는다는 장점이 있지만, 실행순서가 보장되지 않는다는 단점이 있다.
비동기함수: 함수의 호출부에서 실행결과를 기다리지 않아도 되는 함수
동기함수: 함수의 호출부에서 실행결과를 기다려야하는 함수
자바스크립트의 엔진은 싱글스레드로 동작하기 때문에 한번에 하나의 태스크 밖에 실행할 수 없는데 어떻게 setTimeout함수는 실행이 종료되지않고도 다음 태스크를 실행할 수 있었을까?
이는 자바스크립트의 엔진에 내장되어 있지않고 자바스크립트가 동작하는 런타임, 브라우저에 내장되어 있는 이벤트루프라는 기능때문이다.
이벤트루프기능은 자바스크립트의 동시성을 지원하고, 이 기능으로 인해 싱글스레드로 동작되는 자바스크립트도 비동기처리를 할 수 있다.
비동기처리의 주요사례
DOM Element의 이벤트핸들러
타이머 API
서버에 자원을 요청하고 응답받는과정(fetch API)
함수를 바로 호출하지 않고 일정시간이 경과한 이후에 호출할 수 있는 타이머함수와 타이머를 제거할 수 있는 함수를 제공한다.
setTimeout(func,delay)
delay(ms)가 지난후 한번만 콜백함수 func를 실행한다.
함수로 생성된 타이머를 식별할 수 있는 타이머 id를 반환한다.
setInterval(func,delay)
delay마다 반복하여 콜백함수 func를 실행한다.
함수로 생성된 타이머를 식별할 수 있는 타이머 id를 반환한다.
clearTimeout(id),clearInterval(id)
setTimeout,setInterval로 생성된 타이머를 취소한다.
setTimeout의 경우 콜백함수가 실행되기전(delay전)에 타이머가 취소될 경우 콜백함수가 실행되지 않는다.
setInterval의 경우 반복되서 실행되는 콜백함수가 타이머가 취소되면 더 이상 실행되지 않는다.
let i=0;
let temp=setInterval(()=>{console.log(i++)},1000);
setTimeout(()=>{clearInterval(temp)},5000);
//5초간 setInterval의 콜백함수가 실행되다가 타이머를 취소하여 중단된다.
setTimeout과 setInterval은 비동기 처리방식으로 동작한다.
자바스크립트에서 비동기를 처리하는 방법은 대표적으로 콜백함수,Promise,async await이 있다.
setTimeout,setInterval은 인자로 받는 콜백함수를 delay에 맞춰 실행하는 비동기함수이다.
HTML Element의 addEventListener도 콜백함수를 받아 event가 일어날 때마다 콜백함수를 실행하는 비동기 함수이다.
let temp=4;
setTimeout(()=>{temp+=3},100)
console.log(temp)
// 4가 출력된다.
위의 코드에서 temp의 초깃값은 4이고 setTimeout은 비동기함수이기 때문에 100ms가 지나기전(temp에 3이 더해지기전)에 세번째줄이 실행되고 콘솔에는 4가 출력됨을 알 수 있다.
하지만 만약에 우리가 setTimeout의 콜백함수가 실행된후(temp에 3을 더한후)에 temp를 출력하고 싶다면 세번째 줄의 코드를 setTimeout의 콜백함수 안에서 실행해야한다.
let temp=4;
setTimeout(()=>{
temp+=3;
console.log(temp);
},100)
// 7이 출력된다.
만약 비동기함수가 실행된 후에 해야하는 작업이 있다면, 그 작업들은 모두 비동기함수가 인자로 받는 콜백함수안에서 실행되어야 할 것이다.
예를들어 서버에서 데이터를 응답받아 그 데이터를 이용한 작업을 해야한다면 서버에서 데이터를 응답받은 후에 데이터를 이용한 작업이 시작되어야 할 것이다.
하지만 콜백함수가 길어지고 중첩되다보면 계속해서 코드를 들여쓰기 해야하고 가독성이 떨어지는 콜백지옥(Callback hell)이라는 상황을 격게된다.
코드의 가독성이 떨어지고 비동기 처리 중 에러가 발생할 경우 처리가 곤란한 콜백함수의 단점을 보완하기위해 비동기 처리를 위한 새로운 패턴으로 Promise가 도입되었다.
Promise는 ECMA Script6에서 도입되었으며 시간이 걸리는 태스크로 인해 지금은 얻을 수 없는 데이터에 태스크가 종료된후 접근할 수 있는 방법을 제공한다.
Promise객체는 new연산자와 Promise생성자함수를 이용해 생성할 수 있으며 생성자 함수는 비동기처리를 수행할 콜백함수를 전달받는다.
let temp=new Promise((resolve,reject)=>{});
console.log(temp);
// Promise {<pending>}
// [[Prototype]]: Promise
// [[PromiseState]]: "pending"
// [[PromiseResult]]: undefined
위 코드는 new연산자와 Promise생성자 함수를 이용해 Promise객체를 생성한 코드이다.
Promise객체는 PromiseState,PromiseResult라는 프로퍼티를 갖는다.
PromiseState는 현재 비동기 처리가 어떻게 진행되고 있는지에 대한 상태정보를 갖는다.
3가지의 상태정보를 값으로 가질 수 있다.
비동기 처리를 수행할 콜백함수는 다시 resolve와 reject함수를 인수로 전달받는데 만약 비동기 처리가 성공한다면 resolve함수를, 비동기 처리가 실패한다면 reject함수를 호출한다.
pending 상태인 Promise객체의 콜백함수에서 resolve함수를 호출한다면 Promise객체의 PromiseState는 fulfilled로 변하고 PromiseResult는 resolve의 인자값이 된다.
let temp=new Promise((resolve,reject)=>{resolve('success!')});
console.log(temp);
// Promise {<fulfilled>: 'success!'}
// [[Prototype]]: Promise
// [[PromiseState]]: "fulfilled"
// [[PromiseResult]]: "success!"
pending 상태의 Promise객체의 콜백함수에서 reject함수를 호출한다면 Promise객체의 PromiseState는 rejected로 변하고 PromiseResult는 reject의 인자값이 된다.
let temp=new Promise((resolve,reject)=>{reject('fail!')});
console.log(temp);
// Promise {<fulfilled>: 'success!'}
// [[Prototype]]: Promise
// [[PromiseState]]: "rejected"
// [[PromiseResult]]: "fail!"
Promise객체를 생성하고 콜백함수로 비동기처리를 완료한 후 전달받은 데이터를 이용하여 후속처리를 해야할때는 Promise객체의 후속처리 메서드를 사용한다.
Promise객체의 후속처리 메서드는 콜백함수가 비동기처리를 완료하고 PromiseState값이 변한후에 실행된다.
let i=0
let temp=new Promise((resolve,reject)=>{
setTimeout(()=>{
i=i+3;
resolve(i);
},1000);
}).then((data)=>{console.log('then으로 출력한 i값:'+data)})
console.log('Promise밖에서 출력한 i값:'+i)
// Promise밖에서 출력한 i값:0
// then으로 출력한 i값:3 (1초뒤 실행된다.)
//후속처리 메서드 then은 여러번 사용가능,
//catch는 한번만 작성해도 에러처리가능,
//finally는 비동기처리가 성공하든 실패하든 한번 실행된다.
new Promise((resolve,reject)=>{...})
.then((...)=>{...})
.then((...)=>{...})
.then((...)=>{...})
.catch((...)=>{...})
.finally((...)=>{...})
Promise.all 메서드는 여러개의 비동기 처리를 병렬처리할 때 사용한다.
인수로 Promise객체를 요소로 갖는 배열등의 이터러블을 전달받는다.
인수의 Promise객체들은 모두 병렬처리되며 모든 Promise객체들의 상태가 fulfilled가 되면 배열에 Promise객체들의 PromiseResult를 담아 resolve한 Promise객체를 반환한다.
인수로 전달받은 Promise객체들 중 하나라도 상태가 rejected가 된다면 나머지 Promise객체들의 비동기 처리를 기다리지 않고 즉시 종료된다.
let temp1=new Promise((resolve,reject)=>{setTimeout(()=>{resolve(1)},1000)})
let temp2=new Promise((resolve,reject)=>{setTimeout(()=>{resolve(2)},2000)})
let temp3=new Promise((resolve,reject)=>{setTimeout(()=>{resolve(3)},3000)})
let promise=Promise.all([temp1,temp2,temp3]).then((data)=>{console.log(data)});
// 3초뒤 모든 Promise의 비동기 처리가 끝난후 [1,2,3]이 출력된다.
async/await는 ECMA SCript8에서 도입되었다.
Promise를 기반으로 동작하며 Promise의 후속처리 메서드 then,catch,finally를 사용하지 않고 async함수안에서 마치 동기처리처럼 Promise가 처리결과를 반환하도록 사용할 수 있다.
async함수는 async키워드를 이용해 정의하면 async함수의 반환값은 Promise객체이다.
Promise객체의 후속처리 메서드와 마찬가지로 리턴값이 Promise객체가 아니더라도 암묵적으로 Promise객체로 변환해 반환한다.
await키워드는 asunc함수내에서만 사용할 수 있으며 반드시 Promise객체 앞에 사용되어야한다.
await키워드는 뒤에 위치한 Promise객체가 settled상태(fulfilled 혹은 rejected, 즉 비동기 처리가 완료된상태)가 될 때까지 다음 실행을 일시중지시켜 대기하는 기능을 갖고있다.
후속처리 메서드를 사용했던 이유는 Promise객체가 언제 비동기 처리를 완료해야 할지 모르기 때문이었는데, await키워드를 이용하면 비동기 처리를 완료할 때까지(settled상태가 될 때까지)대기하기 때문에 후속처리 메서드를 이용할 필요가 없어진다.
await키워드가 실행을 일시중지시켜 대기하기 때문에 동기처리방식과 유사하게 코드의 실행순서가 보장된다.
await키워드가 붙은 Promise객체는 PromiseResult값을 반환한다.
async function promiseAll(){
const res= await Promise.all([
new Promise((resolve,reject)=>{setTimeout(()=>{resolve(1)},1000)}),
new Promise((resolve,reject)=>{setTimeout(()=>{resolve(2)},2000)}),
new Promise((resolve,reject)=>{setTimeout(()=>{resolve(3)},3000)})
]);
console.log(res)
}
promiseAll();
// 3초뒤 [1,2,3]출력