1. 동기와 비동기

동기란 어떤 코드가 위에서부터 아래까지 흐름대로 자연스럽게 실행됨을 의미합니다.
좀 더 정확한 예시로 함수A를 실행하면 함수A의 작업이 완전히 끝난 후 다음 작업을 실행하는 것입니다.

function syncFunction1() {
  console.log(1)
}

function syncFunction2() {
  console.log(2)
}

function syncFunction3() {
  console.log(3)
}

syncFunction1()
syncFunction2()
syncFunction3()

위 코드는 함수를 실행한 순서대로 1, 2, 3을 출력합니다.

흐름이 위에서 아래로 동기적입니다.

function asyncFunction1() {
  setTimeout(function() {
    console.log(1)
  }, 1000)
}

function syncFunction1() {
  console.log(2)
}

function syncFunction2() {
  console.log(3)
}

asyncFunction1()
syncFunction1()
syncFunction2()
// 2 3 1

위 코드는 asyncFunction1을 먼저 실행했음에도 1이 제일 마지막에 출력됩니다.

흐름이 실행 순서와 다르게 비동기적입니다.
asyncFunction1의 작업이 끝나기 전에 다음 작업이 실행되었습니다.

setTimeout 함수가 1초 뒤로 실행을 지연시켰기 때문입니다.

비동기적인 작업은 서버에 요청을 주고 받을 때, 파일 입출력을 할 때 등 종종 쓰입니다.

주의: 엄밀하게 setTimeout 함수는 지연시간을 보장하지 않고 다른 동작 방식을 가집니다. 이 글에선 콜스택이나 작업 큐, Web api에 따른 setTimeout 함수 동작 방식은 설명하지 않습니다.

2. 콜백 함수

위에 코드에서 순차적으로 1, 2, 3을 출력하게 하려면 어떻게 해야할까요?

초기 자바스크립트는 콜백 함수라는 방법을 사용했습니다.

function asyncFunction1(cb) {
  setTimeout(function() {
    console.log(1)
    cb()
  }, 1000)
}

function syncFunction1(cb) {
  console.log(1)
  cb()
}

function syncFunction2() {
  console.log(2)
}

asyncFunction1(function(asyncFunction1Result) {
  syncFunction1(function(syncFunction1Result) {
    syncFunction2()
  })
})
// 1 2 3

콜백 함수를 받아서 비동기적 작업 이후에 콜백함수를 실행하도록 했습니다.

비동기 함수가 return하는 값이 있다면, 콜백함수의 파라미터로 전달됩니다.

3. Callback hell

콜백 함수는 문제점이 있습니다.

바로 비동기적인 작업이 길어질수록 콜백이 깊어집니다.

또한 콜백 내에서 if문 분기와 에러 핸들링을 어렵게 합니다.

function asyncFunction1(cb) {
  setTimeout(function() {
    console.log(1)
    cb()
  }, 100)
}

// 중략

function asyncFunction9(cb) {
  setTimeout(function() {
    console.log(9)
    cb()
  }, 100)
}

function asyncFunction10(cb) {
  setTimeout(function() {
    console.log(10)
  }, 100)
}

asyncFunction1(function(result) {
  asyncFunction2(function(result) {
    asyncFunction3(function(result) {
      asyncFunction4(function(result) {
        asyncFunction5(function(result) {
          asyncFunction6(function(result) {
            asyncFunction7(function(result) {
              asyncFunction8(function(result) {
                asyncFunction9(function(result) {
                  asyncFunction10()
                })
              })
            })
          })
        })
      })
    })
  })
})

// 1 2 3 4 5 6 7 8 9 10

4. Promise

콜백 헬을 해결하기 위해서 Promise 패턴이 등장했습니다.

비동기 작업을 콜백이 아닌 then으로 연결하고,

catch로 에러 핸들링을 편하게 할 수 있습니다.

function asyncFunction1() {
  return new Promise(function (resolve) {
    setTimeout(function() {
      console.log(1)
      resolve()
    }, 300)
  })
}

function asyncFunction2() {
  return new Promise(function (resolve) {
    setTimeout(function() {
      console.log(2)
      resolve()
    }, 100)
  })
}

function asyncFunction3() {
  return new Promise(function (resolve) {
    setTimeout(function() {
      console.log(3)
      resolve()
    }, 100)
  })
}

asyncFunction1()
asyncFunction2()
asyncFunction3()
// 2 3 1

위는 2 3 1을 출력하는 비동기 코드입니다.

Promise 패턴을 사용하기 위해서 return new Promise()로 Promise 객체를 반환하게 했습니다.

Promise 객체는 thencatch 함수를 가집니다.

asyncFunction1()
  .then(function(result) {
    return asyncFunction2()
  })
  .then(function(result) {
    return asyncFunction3()
  })
  .catch(function(error) {
    console.log(error) // promise 체이닝 과정 중에 에러가 발생하면 catch 블록으로 옵니다.
  })
// 1 2 3

Promise 객체는 then 함수를 가지고 있습니다.

then을 이용해서 콜백을 대체할 수 있습니다.

콜백과 마찬가지로 then 함수의 파라미터로 결과값을 전달할 수 있습니다.

이처럼 promisethen 체이닝을 이용해서 비동기 작업을 컨트롤할 수 있습니다.

Promise 패턴은 현재(2019년 5월)도 상당히 많이 사용하고 있습니다.

5. Promise hell

asyncFunction1()
  .then(function(result) {
    if (result === 'A') {
      return asyncSuccess()
    }
    return asyncFunction2()
  })
  .then(function() {
    return asyncFunction3()
  })
  .then(function() {
    return asyncFunction4()
  })
  .then(function() {
    return asyncFunction5()
      .catch(function(specificError) {
        console.log(specificError)
      })
  })
  .then(function() {
    return asyncFunction6()
  })
  .then(function() {
    return asyncFunction7()
  })
  .catch(function(error) {
    console.log(error)
  })

하지만 Promise 패턴도 만능은 아니었습니다.

잘못 사용하면 여전히 then이 깊어질 수 있고,

if 문 분기와 특정 에러 핸들링은 여전히 어렵습니다.

또한 코드가 then 체이닝에 갇혀야 하지요.

6. Async await

이를 해결하기 위해 async await이 ES7에 등장했습니다.

async function async1() { // 함수 앞에 async 키워드가 붙습니다.
  return 1
}

console.log(async1() instanceof Promise) // async 함수는 무조건 Promise 객체를 반환합니다.
const asyncReturn = async1()
asyncReturn.then() // 따라서 async 함수의 리턴값에도 then이 있습니다.

async 함수 내부에서 await을 사용할 수 있습니다.

function asyncFunction1() {
  return new Promise(function (resolve) {
    setTimeout(function() {
      resolve(111)
    }, 300)
  })
}

async function async1() {
  const result = await asyncFunction1()
  // await 키워드로 다른 promise를 반환하는 함수의 실행 결과값을 변수에 담을 수 있습니다.
  console.log(result) // 111
}

async function async2() {
  let result
  try {
    result = await asyncFunction1()
  } catch (error) { // await에서 발생한 에러는 모두 아래 catch 블록에 걸립니다.
    console.log(error)
  }
  if (result === 'AAA') { // if문 분기도 일반 동기함수처럼 작성 가능합니다.
    doSomething()
  }
  return result
}

async 함수 내부에선 await을 사용해 마치 동기적으로 코드를 작성할 수 있습니다.

에러 핸들링과 if 문 분기 또한 동기적으로 작성 가능합니다.

프로미스와 다르게 에러 스택 추적도 용이합니다.

주의해야 할 점은 최종 async 함수의 반환값은 항상 promise 객체라는 것입니다.