비동기 프로그래밍

javascript 애플리케이션은 단일 스레드에서 동작합니다. 즉, 한 번에 한 가지 일만 할 수 있죠. 여러 가지 일을 동시에 하지 못하는 대신 다른 부수 효과(side effect)에서 자유로울 수 있다는 장점이 있습니다. 하지만 부드러운 자바스크립트 애플리케이션을 만들기 위해서 비동기적 관점이 필요합니다. 이를 좀 더 쉽게 개발자들이 접근할 수 있도록 비동기적 프로그래밍에 필요한 장치들이 추가되었습니다. 이 장치들을 지금부터 알아보겠습니다.

1. 콜백(callback)

  • 자바스크립트에서 가장 오래된 비동기적 매커니즘입니다.
  • 비동기 작업이 끝난 후, 실행해야할 콜백 함수를 넘겨 줍니다.
const timeout = 3 * 1000
console.log('start!')
console.log('=============== (1)')
setTimeout(() => {
  console.log('end!', new Date())
}, timeout)
console.log('=============== (2)')

// [ result ]
// start!
// =============== (1)
// =============== (2)
// end! 2019-11-11T07:00:49.645Z
1.1. 스코프와 비동기적 실행
  • 비동기적 실행에서 가장 혼란스러운 부분은 스코프와 클로저가 비동기적 실행에 영향을 미치는 부분입니다.
  • 함수를 호출하면 항상 클로저가 함께 만들어 지는데, 매겨변수를 포함한 해당 스코프 내의 모든 변수는 무언가가 자신에게 접근할 수 있는한 계속 메모리 위에 존재합니다.
// setTimeout의 로그는 정상적인 시간에 잘 찍힙니다. 하지만 i는 계속 -1로 찍힙니다.
// 이는 setTimeout 자체는 동기적으로 실행됨을 의미합니다. 단지, 콜백으로 넘겨준 함수는 비동기적으로 호출되지요.
// i가 for문 밖에 있기 때문에, setTimeout의 익명함수의 i를 모두 공유하게 됩니다.
// 따라서 연산이 다 끝난 -1이 찍힐 수 밖에 없습니다.
// 우리가 원한대로 5,4,3,2,1,GO가 찍힐 수 있게 하기 위해선 countdown2 함수처럼 수정해야합니다.

function countdown1() {
  let i
  for (i = 5; i >= 0; i--) {
    setTimeout(() => {
      console.log(i === 0 ? 'GO' : i)
    }, (5 - i) * 1000)
  }
}

countdown1()
// -1
// -1
// -1
// -1
// -1
// -1

function countdown2() {
  for (let i = 5; i >= 0; i--) {
    setTimeout(() => {
      console.log(i === 0 ? 'GO' : i)
    }, (5 - i) * 1000)
  }
}

countdown2()
// 5
// 4
// 3
// 2
// 1
// GO
1.2. 콜백 지옥(callback hell!)
  • 1개의 콜백함수를 사용한다면 큰 문제가 생기지 않지만, 실제 로직들은 그리 간단하지 않습니다.
  • 따라서 아래와 같이 콜백함수를 계속 넘겨주는 그런 현상이 발생하는데 이를 ‘콜백 지옥’이라고 부릅니다.
  • 굉장히 복잡하고, 가독성도 떨어지며, 디버깅하기도 수월하지 않습니다.
  • 이를 극복하기 위해 다음에서 알아볼 프라미스(promise)가 등장하였습니다.
$.get('url', function(response) {
  parseValue(response, function(id) {
    auth(id, function(result) {
      display(result, function(text) {
        console.log(text)
      })
    })
  })
})

3. 프라미스(promise)

위에서 언급한 내용처럼 프라미스는 콜백의 단점을 보완하기 위해 만들어졌습니다. 콜백을 대체하는 개념보다는 콜백을 활용하여, 개발자들이 예측할 수 있는 패턴을 만들었다고 이해하는 것이 좋습니다.

3.1. 프라미스의 개념
  • 프라미스의 개념은 간단합니다. 프라미스 기반의 비동기적 함수를 호출하면 Promise 인스턴스를 반환합니다.
  • 또한, 프라미스는 성공(fulfilled) 혹은 실패(rejected)만 합니다. 그외는 결과는 절대 없습니다. 즉, 두 가지 상황만 기대할 수 있고, 그에 따른 처리 로직을 작성해주시면 됩니다.
3.2. 프라미스 만들기
  • 프라미스를 만드는 방법은 크게 어렵지 않습니다. resolve(성공)reject(실패) 콜백이 있는 함수로 새 Promise 인스턴스를 만들기만 하면 됩니다.
function countdown(seconds) {
  return new Promise(function(resolve, reject) {
    for (let i = secondes; i >= 0; i--) {
      setTimeout(function() {
        if (i > 0) console.log(i + '...')
        else resolve(console.log('Go!'))
      }, seconds * i * 1000)
    }
  })
}

countdown(5)
  .then(() => {
    // 성공한 다음 해야할 로직
  })
  .catch(err => {
    // 실패(에러 등) 후, 해야할 로직
  })

junism
Written by@junism
interested in FE esp ReactJS

GitHub