본문 바로가기

Frontend Study

[FE_Bootcamp] 25일차_비동기

비동기,..,,.약 5주정도 부트캠프를 들으면서 그냥 이해부터 어려웠던 단원은 처음이었다,..,.,

페어 프로그래밍을 같이 하셨던 분도 비동기 하면서 뇌정지가 오셨다고.,.,,.,,

교육 엔지니어님이 여기 조금 어려울 것이라 하셨을땐 얼마나 어렵겠어~ 라고 했지만

오늘의 교훈 : 아는사람이 하는 말은 웬만하면 듣자.


 

1. 동기와 비동기

락 역사상 최고의 명곡중 하나인 Guns N' Roses의 'Sweer Child O' Mine'이라는 노래가 있다.

'나 음악 좀 듣는다' 하는 사람들은 절대 모를 수 없는 유명한 기타 리프로 시작해서, 베이스와 리듬 기타가 들어오고, 드럼까지 들어온 후에야 보컬이 튀어나오는 구성을 가지고 있다. 

 

그런데, 만약 이 노래에서 리드 기타 → 베이스 → 리듬 기타 → 드럼 → 보컬 순서대로 곡이 진행된다면 어떻게 될까?

리드 기타의 리프 연주 및 솔로가 전부 끝난 다음 베이스가 나오고, 베이스 연주가 다 끝나야 리듬 기타가 나오고, 드럼이 나오고, 보컬이 노래를 하고,..,,.,

이게 무슨 음악이지? 싶을 정도로 노래가 이상해질 뿐더러, 러닝타임도 악기의 개수만큼 늘어날 것이다.

SCOM 같은 경우에는 러닝타임이 6분에 육박하므로, 한곡을 들으려면 30분이 필요한 것이다. 

 

이처럼 모든 작업들이 '순서대로' 진행되며, 앞선 작업이 끝나야 다음 작업이 실행되는 것을 '동기적 작동'이라고 한다.

 

동기적 작동

 

우리가 흔히 아는 것 처럼 모든 세션이 구성에 따라 조화롭게 연주되고, 러닝타임에 딱 맞게 끝나기 때문에 시간 낭비도 일어나지 않는다. 

특정 작업이 끝날 때 까지 기다리지 않고 다음 작업을 시작하는 것을 '비동기적 작동'이라고 한다.

 

비동기적 작동

 

어떤 웹사이트를 들어갔는데, 동기적으로 작동하는 사이트라면 HTML을 받아와 뼈대를 만들고, CSS로 스타일을 하나하나 입힌 뒤 자바스크립트 기능이 구현될 것이다. 물론 각 element나 텍스트도 위에서부터 하나하나 나타난다.

간단한 사이트면 몰라도 복잡한 사이트는 HTML을 로딩하는데만도 한세월이 걸릴 것이다. 

이러한 불편함을 해결하고자 비동기적 작동을 사용하는 것이다.  

자바스크립트는 '싱글 스레드' 기반 언어이기 때문에, 동기적 작동이 이루어진다. 하지만 런타임에서 비동기 작동 처리를 도와주기 때문에 비동기적 작동을 사용할 수 있다.


2. 비동기의 예시

비동기를 알아보기 위해, setTimeout() 함수를 사용해보자.

setTimeout()은 일정 시간 뒤에 콜백함수가 실행되게 하는 함수이다.

 

//setTimeout(콜백함수, 실행 시간)

function burger(){
  console.log('햄버거를 주문한다')
  setTimeout(function () {
    console.log('돈을 낸다')
  }, 3000);
  console.log('햄버거를 가져간다')
}

 

이 코드의 출력은 어떻게 될까?

  1. 햄버거를 주문한다
  2. 돈을 낸다
  3. 햄버거를 가져간다

많은 사람들은 이렇게 생각할 것이다.

결과는?

 

순식간에 도둑이 될뻔

 

이상하게도, 햄버거를 주문한 뒤 돈을 내지 않고 가져가는 작업이 실행된다. 어찌 된 일일까!

이것이 바로 비동기적 작동 때문이다. 

 

코드가 실행되기 전 상태

 

앞서 설명하길, 비동기적 작동은 작업이 끝날 때 까지 기다리지 않고 다음 작업을 실행하는 것을 말한다고 했다.

햄버거 주문과 수령은 실행과 동시에 바로 출력이 나오지만, 돈을 내는 것은 실행하고 3초 후에 출력이 나온다.

'햄버거를 가져간다'는 이 3초를 기다리지 않는다는 것이다. 

 

'햄버거를 가져간다'는 '돈을 낸다'를 기다려주지 않는다.


3. 비동기의 순서 제어

비동기는 효율적이지만, 가끔 문제가 발생할 수도 있다.

당장 예시만 봐도, '돈을 낸다'가 우선이 되어야 하지만 '햄버거를 가져간다'가 먼저 출력되어 갑자기 도둑이 되어버렸다.

코드 작성시에는 '예측 가능한 코드'를 작성해야 하는데, 비동기를 사용하다보면 예측 불가능한 코드가 나오는 경우도 있을 수 있다.

이러한 문제점들을 해결하기 위해 우리는 비동기의 순서를 제어할 수 있다.


A. 콜백함수 이용

콜백함수를 이용한 비동기 제어는 '콜백함수가 호출되는 시점에 코드를 실행한다'라고 이해할 수 있다.

햄버거를 사러 갔을 때, 주문을 하고 대기번호를 받았다면 햄버거가 나올 때 까지 무엇을 하건 아무도 신경쓰지 않는다.

그리고 대기번호가 불렸을 때 가서 맛있는 햄버거를 가져오면 되는 것이다.

콜백함수의 원리는 이것과 놀라울 정도로 같다.

  • 주문 = 코드
  • 대기번호 = 콜백함수
  • 대기번호가 불림 = 콜백함수 실행
function order(callback){
  setTimeout(function(){
    console.log('햄버거를 주문한다')
    callback()
  }, 1000)
}

function pay(callback){
  setTimeout(function(){
    console.log('돈을 낸다')
    callback()
  }, 3000)
}

function take(callback){
  setTimeout(function(){
    console.log('햄버거를 가져간다')
    callback()
  }, 1000)
}

 

이 코드는 주문, 지불, 수령 각각 1, 3, 1초의 지연 시간을 가지고 있기 때문에, 아까와 같은 결과를 출력한다.

 

또 도둑이 되었다

 

여기서 콜백함수를 사용한다면

주문 완료 → 콜백함수를 통해 지불 코드 실행 → 지불을 완료 → 콜백함수를 통해 수령 코드 실행

라는 과정을 거쳐 순서대로 출력할 수 있게 된다.

 

function printAll(){
  order(() =>{
    pay(() =>{
      take(() => {})
    })
  })
}

 

order의 callback 함수 자리에 pay()가 들어가고, pay()의 콜백함수 자리에 take()가 들어가는 것이다!!

따라서 order가 실행되면 callback( =pay())가 실행된다. 

 

단, 콜백함수는 편리하고 쓰기 쉽지만 제어해야 할 코드가 많으면 무수히 많은 콜백함수를 써야 할 수도 있다는 단점이 있다.

이를 콜백 지옥(Callback Hell)이라고 한다.

당장 예시 코드만 봐도, 주문, 지불, 수령 세개의 함수밖에 없기 때문에 콜백함수도 3개 뿐이지만, 저 함수들이 100개로 늘어난다면 콜백함수도 100개를 써야 하는 것이다. 

 

Callback Hell을 방지하기 위해 promise를 이용한다


B. Promise

promise는 비동기 처리를 위해 사용되는 클래스이다.

클래스이기 때문에 new 키워드를 이용해 생성하고, 매개변수로 콜백함수를 갖는다.

이 콜백함수는 resolve, reject라는 두개의 함수를 매개변수로 가지며, 이 매개변수는 코드의 정상 처리 여부 확인에 쓰인다.

 

let promise = new Promise(function(resolve, reject){
  //콜백함수가 정상적으로 처리된 경우
  value = 'hi'
  resolve(value)
  
  //콜백함수에서 에러가 발생한 경우
  err = 'error'
  reject(err)
})

 

콜백함수가 정상적으로 처리된 경우 resolve 함수를 호출, 에러가 난 경우 reject 함수를 호출한다.

promise는 클래스이므로, 객체의 성질을 가지고 있다.

이 promise 객체는 현재 상태를 나타내는 'state'콜백함수의 결과를 나타내는 'result' 2개의 프로퍼티를 가진다.

 

promise 객체

 

state의 초기값은 pending(대기), result의 초기값은 undefined이다.

만약 promise 함수의 콜백함수가 정상적으로 처리 되었다면 state는 'fulfilled', result는 resolve의 매개변수(resolve(value)라면 value)로 바뀐다.

 

resolve 함수가 호출되었을 때

 

반대로 콜백함수가 제대로 처리되지 않았다면 state는 'rejected', result는 reject의 매개변수로 바뀐다.

 

reject 함수가 호출되었을 때

 

 

promise 객체의 작동원리

 

 

promise 객체에 전달되는 값들은 then, catch, finally 메서드를 통해 접근할 수 있다.

 

then()

then()은 promise의 콜백함수가 정상적으로 처리되었을 때, value에 접근 가능하게 해주는 메서드이다.

resolve 함수를 통해 전달된 value는 then 메서드의 전달인자가 된다.

이때 then의 retun값은 'result' 프로퍼티의 값이 된다.

 

let promise = new Promise(function(resolve, reject){
  value = 'hi'
  resolve(value)
})

promise.then(str =>{
  console.log(str)
})

 

 

resolve 함수를 통해 value 'hi'가 전달되었고, 이 'hi'는 then 메서드의 전달인자가 되어 전달인자 str을 출력하니 value 'hi'가 출력되었다. 

이 then을 통해 콜백함수처럼 비동기의 순서를 제어할 수 있다.

 

function order(){
  return new Promise(function(resolve, reject){
    setTimeout(function(){
      resolve()
      console.log('햄버거를 주문한다')
    }, 1000)
  })
}

function pay(){
  return new Promise(function(resolve, reject){
    setTimeout(function(){
      resolve()
      console.log('돈을 낸다')
    }, 3000)
  })
}

function take(){
  return new Promise(function(resolve, reject){
    setTimeout(function(){
      resolve()
      console.log('햄버거를 가져간다')
    }, 1000)
  })
}

order().then(() =>{
  return pay().then(() =>{
    return take().then(() => {})
  })
})

 

하지만 콜백함수와 비슷해서인지, Promise Hell에 걸릴 수 있다.

catch()

catch는 then과 반대로 state가 rejected일때, reject의 전달인자를 가져온다.

 

let promise = new Promise(function(resolve, reject){
  err = 'error'
  reject(value)
})

promise.catch(str =>{
  console.log(str)
})

 

 

 


만약 promise 객체가 너무 많아 한번에 처리가 어렵다면, Promise.all을 사용하는 것도 하나의 방법이다.

Promise.all은 여러개의 비동기 작업을 한번에 처리할 수 있다.

위의 햄버거 코드 같은 경우, 1초 + 3초 + 1초 총 5초의 시간이 걸려야 코드가 모두 작동된다.

또한 코드가 반복되고 겹쳐서 가독성이 떨어지기도 한다.

이런 불편함을 해결하고자 Promise.all을 사용한다.

 

Promise.all([order(), pay(), take()])

 

Promise.all은 배열의 형태로 입력하며, 배열 안에는 promise 객체가 들어가야 한다.

만약 value가 존재한다면, 출력 역시 배열의 형태로 나온다.

 


4. async, await

사실 promise는 복잡할 뿐더러, 사용법도 개념도 처음 보는 사람이 쓰기에는 조금 어렵다. 

그래서 ES8부터 async, await라는 키워드를 통해 아주 간단하게 비동기를 구현할 수 있게 되었다.

 

사용법은 아주 간단하다. 

비동기 작업을 실행할 함수들은 promise를 이용해 작성하고, 이 함수들을 실행하는 함수 앞에 async를 붙인 뒤 함수 내에서 await를 붙여 작업을 실행하기만 하면 된다.

 

function order(){
  return new Promise(function(resolve, reject){
    setTimeout(function(){
      resolve()
      console.log('햄버거를 주문한다')
    }, 1000)
  })
}

function pay(){
  return new Promise(function(resolve, reject){
    setTimeout(function(){
      resolve()
      console.log('돈을 낸다')
    }, 3000)
  })
}

function take(){
  return new Promise(function(resolve, reject){
    setTimeout(function(){
      resolve()
      console.log('햄버거를 가져간다')
    }, 1000)
  })
}

async function burger(){
  await order()
  await pay()
  await take()
}

 

아주 간단


어렵고 어려운 비동기를 드디어 끝마쳤다.

처음에는 진짜 어려웠는데, 계속 찾아보고 블로그를 직접 써나가니까 그래도 어느정도 이해가 되는 것 같아 다행이다.

내일까지 비동기 단원을 계속 나갈텐데, 뒤쳐지지 않도록 열심히 해야겠다,..,,.