210830-210901 CodeStates 31-33일차

공윤배·2021년 9월 9일
0

210830-210901 CodeStates 31-33일차

3일에 걸쳐 자바스크립트의 비동기(Asynchronous)에 대한 개념을 공부하고 underbarscore라이브러리의 기능을 구현해보고, 타이머 API,fs모듈,fetch API를 사용해보는 스프린트를 진행했다.
--정리하는 내용은 코드스테이츠의 자료와 PoiemaWEB을 참고하여 공부한 내용입니다.--

비동기(Asynchronous)

자바스크립트 엔진은 한번에 하나의 태스크만을 실행할 수 있는 싱글스레드 방식으로 동작한다.
싱글스레드는 한번에 하나의 태스크만을 실행할 수 있기 때문에 시간이 걸리는 태스크를 실행할 경우 다음 대기중인 태스크는 현재 실행중인 태스크가 완료될 때까지 실행될 수 없다.
이를 블로킹(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)

타이머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가 도입되었다.
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가지의 상태정보를 값으로 가질 수 있다.

  • pending: 비동기 처리가 아직 수행되지 않은 상태를 나타낸다.(Promise객체가 생성된 직후의 기본상태)
  • fulfilled: 비동기 처리가 성공적으로 수행된 상태를 나타낸다.
  • rejected: 비동기 처리를 수행했으나 실패한 상태를 나타낸다.

비동기 처리를 수행할 콜백함수는 다시 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값이 변한후에 실행된다.

  • Promise.prototype.then
    then메서드는 두개의 콜백함수를 인자로 전달받는다.
    첫번째 콜백함수는 fulfilled상태가 되면 호출되고, 두번째 콜백함수는 reject상태가 되면 호출된다.
    두개의 콜백함수 모두 비동기처리가 완료된후 PromiseResult값을 인자로 전달받는다.
    에러에 대한 처리를 하는 두번째 콜백함수는 주로 catch로 대체되어 생략된다.
    항상 Promise객체를 반환하며 return값이 Promise객체가 아닐경우 암묵적으로 그 값을 resolve 또는 reject하여 반환한다.
    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초뒤 실행된다.)
  • Promise.prototype.catch
    catch메서드는 한개의 콜백함수를 인수로 전달받는다.
    catch메서드의 콜백함수는 Promise의 상태가 rejected일때 호출된다.
    주로 Promise의 비동기 처리가 실패했을 때 에러를 처리하기 위해 사용한다.
    후속처리메서드 then이 여러개있더라도 마지막에 한번만 작성해주면 도중에 reject상태가 되면 바로 콜백함수가 호출된다.
    then과 마찬가지로 Promise객체를 반환한다.
  • Promise.prototype.finally
    finally메서드는 한 개의 콜백함수를 인수로 전달받는다.
    finally메서드의 콜백함수는 Promise의 상태가 fulfilled,reject여부와는 관계없이 무조건 한번 호출된다.
    finally메서드 또한 Promise객체를 반환한다.
//후속처리 메서드 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를 이용한 비동기 처리

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]출력

0개의 댓글