자바스크립트 비동기 함수 알아보기

2020-06-26
해당 글보다 2022.2.1에 새로 작성한 자바스크립트의 비동기 프로그래밍 글을 봐주세요!

비동기(Asynchronous) 함수란?

자바스크립트는 싱글스레드이기 때문에 한 번에 하나의 작업만 수행할 수 있다. 이를 해결하기 위해 비동기가 생겼다.

비동기란 특정 코드의 처리가 끝나기 전에 다음 코드를 실행할 수 있는 것을 뜻한다.

카페에 비유하자면, 만약 알바생이 한 명이라면 주문과 음료 제조를 동시에 하지 못한다. 하지만 여러 명이 있다면, 한명이 주문을 받으면 그 순서에 맞게 다른 사람이 음료 제조를 하여 주문과 음료 제조를 동시에 할 수 있다.

자바스크립트는 즉시 처리하지 못하는 이벤트들을 이벤트 루프에 모아 놓고, 먼저 처리해야하는 이벤트를 실행한다.

자바스크립트에서 가장 대표적인 비동기 처리 사례에는 setTimeout()이 있으며 일정 시간 뒤에 함수를 실행시키는 것이다.

console.log('Start')

setTimeout(function() {
  console.log('5초 후 실행')
}, 5000)

console.log('End')

위와 같은 코드가 있다면 console에는 'Start', 'End'가 바로 찍히고 그 다음에 '5초 후 실행'이 찍힌다.

비동기 방식

자바스크립트에는 콜백 함수, Promise, async await 이렇게 크게 3가지 비동기 방식이 존재한다.


콜백(Callback) 함수

자바스크립트 비동기하면 '콜백 지옥'이라는 단어를 몇 번 봤을 것이다.

콜백 함수는 하나만 썼을 때는 간단하지만, 비동기로 함수의 매개 변수에 다른 콜백 함수가 중첩되어 사용된다면 그 코드를 보기에 굉장히 어렵고 유지보수도 힘들어진다.

step1(function(value1) {
  step2(function(value2) {
    step3(function(value3) {
      step4(function(value4) {
        step5(function(value5) {
          step6(function(value6) {
            // Do something with value6
          })
        })
      })
    })
  })
})

이 코드에서 에러를 잡기 위해서는 각 콜백 함수마다 에러 처리를 써야하는데 그럼 코드량도 어마어마해진다.

step1(function(err, value1) {
  if (err) {
    console.log(err)
    return
  }
  step2(function(err, value2) {
    if (err) {
      console.log(err)
      return
    }
    step3(function(err, value3) {
      if (err) {
        console.log(err)
        return
      }
      step4(function(err, value4) {
        // ,...
      })
    })
  })
})

Promise

위의 콜백 문제를 해결하기 위해 ES2015에 Promise가 도입되었다. 콜백 함수의 error, success의 처리를 보다 간단하게 하기 위해 Promise가 생겼다.

Promise는 latency, delay(지연) 때문에 현재 당장 얻을 수 없지만 가까운 미래에 얻을 수 있는 데이터에 접근하기 위한 방법을 제공한다. Promise로 비동기 작업이 완료된 후 결과 값을 받을 수 있다.

Promise 생성 및 상태

Promise는 new Promise()로 생성할 수 있으며, 종료될 때 세 가지 상태를 갖는다.

  • Pending(대기): 이행하거나 거부되지 않은 초기 상태
  • Fulfilled(이행): 완료되어 프로미스가 결과 값을 반환해준 상태
  • Rejected(거부): 실패하거나 오류가 발생항 상태
Promise

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise

Promise는 resolvereject 2개의 인자를 받으며, 비동기 처리가 성공하면 resolve가 호출되고, 실패하면 reject가 호출된다.

const promise = new Promise((res, rej) => {
  setTimeout(() => {
    res('성공')
  }, 1000)
})

Promise Chaining

Promise의 또 다른 특징은 체이닝(chaining)인데, .then()을 이용해서 여러개의 Promise를 이을 수 있다는 것이다.

new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve(0)
  }, 2000)
})
  .then(function(result) {
    console.log(result) // output: 0
    return result + 10
  })
  .then(function(result) {
    console.log(result) // output: 10
    return result + 20
  })
  .then(function(result) {
    console.log(result) // output: 30
  })

.then()으로 앞의 return 값을 받아서 최종 console 값이 30이 된 걸 볼 수 있다.

이 외, Promise에서 에러 처리를 할 수도 있는데 보통 catch()를 사용한다.

const promise = new Promise((res, rej) => {
  setTimeout(() => {
    rej('에러 발생')
  }, 1000)
})

promise.then(res => console.log(res)).catch(err => console.error(err))
// output: 에러 발생

그러나 Promise도 중첩으로 인해 콜백과 동일한 hell이 발생할 수 있다.


Quiz: 어떤게 먼저 실행될까?

console.log('hi')

setTimeout(function() {
  console.log('0.1')
}, 100)

Promise.resolve()
  .then(function() {
    console.log('first')
  })
  .then(function() {
    console.log('second')
  })

setTimeout(function() {
  console.log('0')
}, 0)

console.log('end')

정답은

hi
end
first
second
0
0.1

순 이다.

setTimeout()Promise는 모두 비동기 함수이지만, setTimeout은 태스크 큐지만, Promise는 비동기 중에서도 먼저 실행되는 마이크로태스크 큐에 들어있기 때문에 먼저 실행이 된다. 이벤트 루프는 콜 스택이 비면 먼저 마이크로태스크 큐에서 대기하고 있는 함수를 실행 하고, 이후에 태스크 큐에서 대기하고 있는 함수를 가져와 실행한다.


async await

async await를 ES2017에 등장한 것인데, Promise의 메서드 체이닝을 더 깔끔한 코드를 작성할 수 있게끔 만들어진 것이다. 콜백 hell을 없애줄 수 있게 되었다.

async await의 기본 구조를 아래와 같다.

const getSomthing = async () => {
  await doSomething()
}

함수 앞에 async를 붙이고, 비동기 처리할 코드 앞에 await를 붙인다.

MDN 예문을 가져와 보겠다.

const asyncFunc = async () => {
  console.log('calling')
  await setTimeout(() => console.log('resolved'), 2000)
}

asyncFunc()

이렇게 실행하면 먼저 calling이 찍히고, 2초 뒤에 resolved가 찍히는 것을 볼 수 있다.


async await도 trycatch로 성공 및 에러 여부를 감지할 수 있다.

Promise와 async await의 코드 차이를 한번에 보자.

Promise

const promise = new Promise((res, rej) => {
  console.log('first')
  setTimeout(() => {
    res('2초 후')
  }, 2000)
  console.log('end of function')
})

promise.then(res => console.log(res)).catch(err => console.error(err))

async await

const asyncFunc = async () => {
  try {
    console.log('first')
    await setTimeout(() => console.log('2초 후'), 2000)
  } catch (err) {
    console.log(err)
  }
  console.log('end of function')
}

asyncFunc()

두 함수의 결과 모두 first, end of function이 바로 찍히고, 2초 후에 2초 후가 console에 찍힌다.

같은 비동기 함수임에도 async await을 사용하면 코드를 더 깔끔하게 작성할 수 있다.


참고