JavaScript의 이벤트 루프, 콜백, 프로미스 및 Async/Await 이해

저자는 코로나19 구호 기금기부를 위한 쓰기 프로그램의 일환으로 선택했습니다.

소개

인터넷 초기에는 웹사이트가 종종 정적 데이터로 이루어진 HTML 페이지로 구성되었습니다. 그러나 이제 웹 애플리케이션이 보다 인터랙티브하고 동적으로 변하면서, 외부 네트워크 요청을 하여 API 데이터를 검색하는 등의 강력한 작업을 수행하는 것이 점점 더 필요해졌습니다. 이러한 작업을 JavaScript에서 처리하기 위해서는 비동기 프로그래밍 기술을 사용해야 합니다.

JavaScript는 한 번에 한 번씩 작업을 처리하는 단일 스레드 실행 모델을 가진 동기식 프로그래밍 언어이기 때문에 한 번에 하나의 문을 처리할 수 있습니다. 그러나 API에서 데이터를 요청하는 것과 같은 작업은 요청되는 데이터의 크기, 네트워크 연결 속도 및 기타 요인에 따라 결정되는 불확실한 시간이 걸릴 수 있습니다. API 호출이 동기적으로 수행된다면, 브라우저는 해당 작업이 완료될 때까지 스크롤링이나 버튼 클릭과 같은 사용자 입력을 처리할 수 없게 됩니다. 이를 차단이라고 합니다.

브라우저 환경에서는 차단 동작을 방지하기 위해 JavaScript가 액세스할 수 있는 여러 웹 API가 있으며, 이는 비동기로 작동하여 다른 작업과 병렬로 실행될 수 있습니다. 이는 비동기 작업이 처리되는 동안 사용자가 브라우저를 정상적으로 계속 사용할 수 있도록하는 데 유용합니다.

JavaScript 개발자로서 비동기 웹 API를 다루고 이러한 작업의 응답 또는 오류를 처리하는 방법을 알아야합니다. 이 기사에서는 이벤트 루프, 콜백을 통한 비동기 동작의 원래 처리 방식, 프로미스의 업데이트된 ECMAScript 2015 추가, 그리고 async/await의 현대적인 사용 방법에 대해 알아보겠습니다.

참고: 이 기사는 브라우저 환경의 클라이언트 측 JavaScript에 중점을 두고 있습니다. 동일한 개념은 일반적으로 Node.js 환경에서도 유효하지만, Node.js는 브라우저의 Web API 대신 자체 C++ API를 사용합니다. Node.js에서 비동기 프로그래밍에 대한 자세한 정보는 Node.js에서 비동기 코드 작성 방법을 참조하십시오.

이벤트 루프

이 섹션에서는 이벤트 루프로 JavaScript가 비동기 코드를 처리하는 방법을 설명합니다. 먼저 작업하는 이벤트 루프를 데모로 실행한 다음 이벤트 루프의 두 가지 요소, 즉 스택과 큐를 설명합니다.

비동기 Web API를 사용하지 않는 JavaScript 코드는 동기적으로 실행됩니다. 즉, 하나씩 순차적으로 실행됩니다. 다음은 각각 숫자를 출력하는 세 개의 함수를 호출하는 예제 코드입니다.

// 세 개의 예제 함수 정의
function first() {
  console.log(1)
}

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

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

이 코드에서는 세 개의 함수를 정의하여 console.log()로 숫자를 출력합니다.

다음으로 함수를 호출합니다:

// 함수 실행
first()
second()
third()

출력은 함수가 호출된 순서에 따라 결정됩니다. first(), second(), 그런 다음 third()입니다:

Output
1 2 3

비동기 Web API를 사용하면 규칙이 더 복잡해집니다. 테스트할 수 있는 내장 API는 setTimeout입니다. 이 API는 타이머를 설정하고 지정된 시간 후에 작업을 수행합니다. 그렇지 않으면 전체 브라우저가 대기하는 동안 동결되어 사용자 경험이 저하됩니다. setTimeout은 비동기적이어야 합니다.

second 함수에 setTimeout을 추가하여 비동기 요청을 모의합니다:

// 세 개의 예제 함수를 정의하지만 하나는 비동기 코드가 포함됩니다
function first() {
  console.log(1)
}

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

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

setTimeout은 두 개의 인수를 사용합니다: 비동기적으로 실행할 함수와 해당 함수를 호출하기 전에 기다릴 시간입니다. 이 코드에서는 console.log를 익명 함수로 래핑하고 setTimeout에 전달하여 함수를 0 밀리초 후에 실행하도록 설정했습니다.

이제 이전과 같이 함수를 호출하세요:

// 함수 실행
first()
second()
third()

setTimeout0으로 설정했으므로 이 세 개의 함수를 실행하더라도 숫자가 순차적으로 인쇄될 것으로 예상할 수 있습니다. 그러나 비동기적인 함수 때문에 타임아웃이 있는 함수가 마지막에 인쇄됩니다:

Output
1 3 2

setTimeout을 0초로 설정하든 5분으로 설정하든 상관없이 비동기 코드에 의해 호출된 console.log는 동기적인 최상위 함수 이후에 실행됩니다. 이는 JavaScript 호스트 환경, 이 경우 브라우저가 동시성 또는 병렬 이벤트를 처리하기 위해 이벤트 루프라는 개념을 사용하기 때문에 발생합니다. JavaScript는 한 번에 한 문장만 실행할 수 있기 때문에 이벤트 루프에게 특정 문장을 언제 실행할지 알려주어야 합니다. 이 이벤트 루프는 스택의 개념을 사용하여 이를 처리합니다.

스택

스택 또는 호출 스택은 현재 실행 중인 함수의 상태를 유지합니다. 스택의 개념에 익숙하지 않다면, “마지막에 추가된 것이 먼저 제거됨” (LIFO) 속성을 가진 배열로 상상할 수 있습니다. 이것은 스택의 끝에만 항목을 추가하거나 제거할 수 있다는 것을 의미합니다. JavaScript는 현재 스택에서 현재 프레임 (또는 특정 환경에서의 함수 호출)을 실행한 다음 해당 프레임을 제거하고 다음으로 넘어갑니다.

동기 코드만 포함된 예제의 경우, 브라우저는 다음과 같은 순서로 실행을 처리합니다:

  • first()를 스택에 추가하고, first()를 실행하여 콘솔에 1을 기록한 다음 first()를 스택에서 제거합니다.
  • second()를 스택에 추가하고, second()를 실행하여 콘솔에 2를 기록한 다음 second()를 스택에서 제거합니다.
  • third()를 스택에 추가하고, third()를 실행하여 콘솔에 3을 기록한 다음 third()를 스택에서 제거합니다.

setTimeout을 사용한 두 번째 예제는 다음과 같습니다:

  • first()를 스택에 추가하고, first()를 실행하여 콘솔에 1을 기록한 다음 first()를 스택에서 제거합니다.
  • second()를 스택에 추가하고, second()를 실행합니다.
    • setTimeout()을 스택에 추가하고, setTimeout() Web API를 실행하여 타이머를 시작하고 익명 함수를 대기열에 추가한 다음 setTimeout()을 스택에서 제거합니다.
  • 스택에서 second()를 제거합니다.
  • 스택에 third()을 추가하고, third()을 실행하여 콘솔에 3을 기록한 다음, third()을 스택에서 제거합니다.
  • 이벤트 루프는 대기 중인 메시지를 확인하고 setTimeout()에서 익명 함수를 찾아 스택에 추가하고, 콘솔에 2를 기록한 다음 스택에서 제거합니다.

setTimeout을 사용하여 비동기 웹 API를 통해 대기열의 개념을 소개하며, 이 튜토리얼에서 다룰 것입니다.

대기열

대기열 또는 메시지 대기열 또는 작업 대기열로도 불리는 대기열은 함수의 대기 영역입니다. 호출 스택이 비어 있을 때마다 이벤트 루프는 가장 오래된 메시지부터 대기열을 확인하여 대기 중인 메시지를 찾습니다. 메시지를 찾으면 해당 메시지를 스택에 추가하고 해당 함수를 실행합니다.

setTimeout 예제에서 익명 함수는 타이머가 0초로 설정되어 있기 때문에 나머지 최상위 실행 후 즉시 실행됩니다. 타이머가 정확히 0초나 지정된 시간에 코드가 실행될 것이라는 것을 기억하는 것이 중요하지만, 그것은 익명 함수를 해당 시간만큼 대기열에 추가한다는 것을 의미합니다. 이 대기열 시스템은 타이머가 타이머가 종료되면 익명 함수를 스택에 직접 추가하면 현재 실행 중인 함수를 중단시킬 수 있으며, 이는 의도하지 않은 예측할 수 없는 효과를 초래할 수 있기 때문에 존재합니다.

참고: 약속을 처리하는 작업 대기열 또는 마이크로태스크 대기열이라는 다른 대기열도 있습니다. 프라미스와 같은 마이크로태스크는 setTimeout과 같은 매크로태스크보다 더 높은 우선 순위로 처리됩니다.

이제 이벤트 루프가 스택과 대기열을 사용하여 코드의 실행 순서를 처리하는 방법을 알게 되었습니다. 다음 작업은 코드의 실행 순서를 제어하는 방법을 알아내는 것입니다. 이를 위해 먼저 이벤트 루프가 비동기 코드를 올바르게 처리하는 원래 방법에 대해 알아보겠습니다: 콜백 함수입니다.

콜백 함수

setTimeout 예제에서는 시간이 지난 후에 함수가 실행되었습니다. 그러나 third 함수와 같은 함수 중 하나가 시간이 지난 후에 실행되도록 보장하려면 비동기 코딩 방법을 사용해야 합니다. 여기서의 시간 초과는 데이터를 포함하는 비동기 API 호출을 나타낼 수 있습니다. API 호출에서 데이터를 처리하려고 하지만 데이터가 먼저 반환되었는지 확인해야 합니다.

이 문제에 대한 원래 해결책은 콜백 함수를 사용하는 것입니다. 콜백 함수에는 특별한 구문이 없으며, 다른 함수의 인수로 전달된 함수일 뿐입니다. 다른 함수를 인수로 사용하는 함수를 고차 함수라고 합니다. 이 정의에 따르면 인수로 전달된다면 어떤 함수든지 콜백 함수가 될 수 있습니다. 콜백은 본질적으로 비동기적이지 않지만 비동기 목적으로 사용할 수 있습니다.

다음은 고차 함수와 콜백의 구문적 코드 예입니다:

// 함수
function fn() {
  console.log('Just a function')
}

// 다른 함수를 인수로 사용하는 함수
function higherOrderFunction(callback) {
  // 인수로 전달된 함수를 호출할 때 콜백이라고 합니다.
  callback()
}

// 함수 전달
higherOrderFunction(fn)

이 코드에서는 함수 fn을 정의하고, 함수 higherOrderFunction을 정의하여 함수 callback을 인수로 사용하며, fnhigherOrderFunction에 콜백으로 전달합니다.

이 코드를 실행하면 다음이 표시됩니다:

Output
Just a function

첫 번째, 두 번째 및 세 번째 함수에 setTimeout을 사용하려면 이제까지 한 것입니다:

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

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

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

태스크는 두 번째 함수에서 비동기 작업이 완료될 때까지 항상 세 번째 함수를 지연시키는 것입니다. 여기서 콜백이 필요합니다. 실행 레벨에서 첫 번째, 두 번째세 번째를 실행하는 대신 세 번째 함수를 두 번째에 인수로 전달할 것입니다. 두 번째 함수는 비동기 작업이 완료된 후에 콜백을 실행합니다.

콜백이 적용된 세 가지 함수는 다음과 같습니다:

// 세 함수 정의
function first() {
  console.log(1)
}

function second(callback) {
  setTimeout(() => {
    console.log(2)

    // 콜백 함수 실행
    callback()
  }, 0)
}

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

이제, 첫 번째두 번째를 실행한 후 세 번째두 번째에 인수로 전달합니다:

first()
second(third)

이 코드 블록을 실행한 후에는 다음 출력이 표시됩니다:

Output
1 2 3

첫 번째로 1이 출력되고 타이머가 완료된 후(이 경우 0초, 그러나 어떤 시간으로 변경할 수 있음) 2 다음 3이 출력됩니다. 함수를 콜백으로 전달함으로써 비동기 웹 API (setTimeout)가 완료될 때까지 함수 실행을 성공적으로 지연시켰습니다.

여기서 주요 포인트는 콜백 함수가 비동기가 아니라는 것입니다 – setTimeout은 비동기 작업을 처리하는 비동기 웹 API입니다. 콜백은 단순히 비동기 작업이 완료되었을 때 알려주고 작업의 성공 또는 실패를 처리할 수 있도록 합니다.

이제 콜백을 사용하여 비동기 작업을 처리하는 방법을 배웠으므로, 다음 섹션에서는 너무 많은 콜백을 중첩하고 “파이라미드 오브 둠(pyramid of doom)”을 생성하는 문제를 설명합니다.

중첩된 콜백과 파이라미드 오브 둠

콜백 함수는 다른 함수가 완료되고 데이터를 반환할 때까지 함수의 지연 실행을 보장하는 효과적인 방법입니다. 그러나 콜백의 중첩 구조 때문에 서로에게 의존하는 많은 연속적인 비동기 요청이 있는 경우 코드가 혼란스러워질 수 있습니다. 이것은 초기에 JavaScript 개발자들에게 큰 좌절감을 주었으며, 결과적으로 중첩된 콜백을 포함하는 코드는 종종 “파이라미드 오브 둠(pyramid of doom)” 또는 “콜백 지옥(callback hell)”이라고합니다.

다음은 중첩된 콜백의 예시입니다:

function pyramidOfDoom() {
  setTimeout(() => {
    console.log(1)
    setTimeout(() => {
      console.log(2)
      setTimeout(() => {
        console.log(3)
      }, 500)
    }, 2000)
  }, 1000)
}

이 코드에서 각 새로운 setTimeout은 더 높은 순서 함수 내에 중첩되어 있어서 더 깊고 더 깊은 콜백의 피라미드 모양을 만듭니다. 이 코드를 실행하면 다음과 같은 결과가 나옵니다:

Output
1 2 3

실제로 실제 비동기 코드를 사용하면 훨씬 복잡해질 수 있습니다. 아마도 비동기 코드에서 오류 처리를 해야 하고, 그런 다음 각 응답에서 일부 데이터를 다음 요청으로 전달해야 할 것입니다. 이를 콜백으로 수행하면 코드가 해석하기 어려워지고 유지 보수하기 어려워집니다.

다음은 더 현실적인 “둑대의 피라미드”의 실행 가능한 예제입니다:

// 예제 비동기 함수
function asynchronousRequest(args, callback) {
  // 인수가 전달되지 않으면 오류를 throw합니다
  if (!args) {
    return callback(new Error('Whoa! Something went wrong.'))
  } else {
    return setTimeout(
      // 단지 임의의 숫자를 추가해서 조작된 비동기 함수처럼 보이게합니다
      // 서로 다른 데이터가 반환되는 것처럼
      () => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
      500,
    )
  }
}

// 중첩된 비동기 요청
function callbackHell() {
  asynchronousRequest('First', function first(error, response) {
    if (error) {
      console.log(error)
      return
    }
    console.log(response.body)
    asynchronousRequest('Second', function second(error, response) {
      if (error) {
        console.log(error)
        return
      }
      console.log(response.body)
      asynchronousRequest(null, function third(error, response) {
        if (error) {
          console.log(error)
          return
        }
        console.log(response.body)
      })
    })
  })
}

// 실행
callbackHell()

이 코드에서는 각 함수가 가능한 responseerror를 처리하도록 만들어야 하므로 함수 callbackHell은 시각적으로 혼란스럽습니다.

이 코드를 실행하면 다음이 표시됩니다:

Output
First 9 Second 3 Error: Whoa! Something went wrong. at asynchronousRequest (<anonymous>:4:21) at second (<anonymous>:29:7) at <anonymous>:9:13

이러한 방식으로 비동기 코드를 처리하는 것은 이해하기 어렵습니다. 결과적으로 ES6에서는 프로미스의 개념이 소개되었습니다. 다음 섹션에서 이에 대해 자세히 알아보겠습니다.

프로미스

A promise represents the completion of an asynchronous function. It is an object that might return a value in the future. It accomplishes the same basic goal as a callback function, but with many additional features and a more readable syntax. As a JavaScript developer, you will likely spend more time consuming promises than creating them, as it is usually asynchronous Web APIs that return a promise for the developer to consume. This tutorial will show you how to do both.

Promise 생성

약속은 new Promise 구문으로 초기화할 수 있으며, 함수로 초기화해야 합니다. 약속에 전달되는 함수에는 resolvereject 매개변수가 있습니다. resolvereject 함수는 각각 작업의 성공과 실패를 처리합니다.

다음 줄을 사용하여 약속을 선언하세요:

// 약속 초기화
const promise = new Promise((resolve, reject) => {})

웹 브라우저의 콘솔에서 이 상태의 초기화된 약속을 검사하면 pending 상태이고 undefined 값이 있는 것을 알 수 있습니다:

Output
__proto__: Promise [[PromiseStatus]]: "pending" [[PromiseValue]]: undefined

지금까지 약속을 위한 설정이 아무것도 이루어지지 않았으므로 계속해서 pending 상태로 남아 있습니다. 약속을 테스트하기 위해 먼저 약속을 충족시켜 값을 해결할 수 있습니다:

const promise = new Promise((resolve, reject) => {
  resolve('We did it!')
})

이제 약속을 검사하면 fulfilled 상태이고 resolve에 전달한 값으로 value가 설정된 것을 찾을 수 있습니다:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: "We did it!"

이 섹션의 시작에서 언급했듯이 약속은 값이 반환될 수 있는 객체입니다. 성공적으로 충족된 후에는 valueundefined에서 데이터로 채워집니다.

A promise can have three possible states: pending, fulfilled, and rejected.

  • 보류 중 – 해결되거나 거부되기 전의 초기 상태
  • 이행됨 – 성공적인 작업, 약속이 해결됨
  • 거부됨 – 실패한 작업, 약속이 거부됨

약속이 이행되거나 거부된 후에는 해결됩니다.

이제 약속이 생성되는 방법에 대한 개념을 가졌으므로, 개발자가 이러한 약속을 어떻게 소비할 수 있는지 살펴보겠습니다.

약속 소비하기

마지막 섹션의 약속은 값으로 이행되었지만, 해당 값을 액세스할 수도 있어야 합니다. 약속에는 코드에서 resolve에 도달한 후 실행되는 then이라는 메서드가 있습니다. then은 약속의 값을 매개변수로 반환합니다.

다음은 예제 약속의 value를 반환하고 로그에 기록하는 방법입니다:

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

만든 약속의 [[PromiseValue]]We did it!입니다. 이 값이 익명 함수로 response로 전달됩니다:

Output
We did it!

지금까지 만든 예제는 비동기 Web API를 포함하지 않았습니다. 이 예제는 네이티브 JavaScript 약속의 생성, 해결 및 사용 방법만 설명했습니다. setTimeout을 사용하여 비동기 요청을 테스트할 수 있습니다.

다음 코드는 약속으로 반환된 비동기 요청에서 데이터를 시뮬레이션합니다:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Resolving an asynchronous request!'), 2000)
})

// 결과를 기록합니다
promise.then((response) => {
  console.log(response)
})

then 구문을 사용하면 setTimeout 작업이 2000 밀리초 후에 완료될 때만 response가 로그에 기록됩니다. 이 모든 작업은 콜백을 중첩하지 않고 수행됩니다.

이제 두 초 후에 약속 값을 해결하고 then에 기록됩니다:

Output
Resolving an asynchronous request!

약속은 데이터를 여러 비동기 작업으로 전달하기 위해 연결될 수도 있습니다. then에서 값이 반환되면 이전 then의 반환 값을 충족하는 다른 then을 추가할 수 있습니다:

// 약속 연결
promise
  .then((firstResponse) => {
    // 다음 then을 위한 새 값 반환
    return firstResponse + ' And chaining!'
  })
  .then((secondResponse) => {
    console.log(secondResponse)
  })

두 번째 then에서 충족된 응답은 반환 값을 기록합니다:

Output
Resolving an asynchronous request! And chaining!

then이 연결될 수 있기 때문에 약속을 사용하는 것이 콜백보다 동기적으로 더 나오게 할 수 있습니다. 이렇게 함으로써 유지 및 검증이 더 쉬운 더 읽기 쉬운 코드를 작성할 수 있습니다.

오류 처리

지금까지 당신은 성공적인 resolve로만 처리했습니다. 이로 인해 프로미스는 fulfilled 상태가 됩니다. 그러나 비동기 요청에서는 종종 오류를 처리해야 합니다. API가 다운된 경우, 잘못된 형식 또는 인증되지 않은 요청이 전송된 경우 등. 프로미스는 두 가지 경우 모두 처리할 수 있어야 합니다. 이 섹션에서는 프로미스를 만들고 사용하는 성공 및 오류 케이스를 테스트하는 함수를 만듭니다.

getUsers 함수는 프로미스에 플래그를 전달하고 프로미스를 반환합니다:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 비동기 API에서 resolve 및 reject 처리
    }, 1000)
  })
}

코드를 설정하여 onSuccesstrue인 경우 타임아웃이 일부 데이터로 완료됩니다. false인 경우 함수는 오류로 거부됩니다:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 비동기 API에서 resolve 및 reject 처리
      if (onSuccess) {
        resolve([
          {id: 1, name: 'Jerry'},
          {id: 2, name: 'Elaine'},
          {id: 3, name: 'George'},
        ])
      } else {
        reject('Failed to fetch data!')
      }
    }, 1000)
  })
}

성공한 결과의 경우 샘플 사용자 데이터를 나타내는 자바스크립트 객체를 반환합니다.

오류를 처리하기 위해 catch 인스턴스 메서드를 사용합니다. 이것은 error를 매개변수로 사용하는 실패 콜백을 제공합니다.

getUser 명령을 onSuccessfalse로 설정하여 실행하고, 성공 케이스에는 then 메서드를 사용하고 오류에는 catch 메서드를 사용합니다:

// 오류를 트리거하기 위해 false 플래그를 사용하여 getUsers 함수 실행
getUsers(false)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

오류가 발생했기 때문에 then은 건너뛰고 catch가 오류를 처리할 것입니다:

Output
Failed to fetch data!

플래그를 바꿔서 resolve 대신에 사용하면, catch가 무시되고 데이터가 반환됩니다:

// true 플래그를 사용하여 getUsers 함수를 성공적으로 해결합니다
getUsers(true)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

이렇게 하면 사용자 데이터가 생성됩니다:

Output
(3) [{…}, {…}, {…}] 0: {id: 1, name: "Jerry"} 1: {id: 2, name: "Elaine"} 3: {id: 3, name: "George"}

참고로, Promise 객체의 핸들러 메서드가 있는 테이블이 여기 있습니다:

Method Description
then() Handles a resolve. Returns a promise, and calls onFulfilled function asynchronously
catch() Handles a reject. Returns a promise, and calls onRejected function asynchronously
finally() Called when a promise is settled. Returns a promise, and calls onFinally function asynchronously

Promise는 새로운 개발자들뿐만 아니라 비동기 환경에서 작업한 적이 없는 경험이 풍부한 프로그래머들에게 혼란스러울 수 있습니다. 그러나 언급한 대로, Promise를 생성하는 것보다 소비하는 것이 훨씬 더 일반적입니다. 일반적으로 브라우저의 웹 API나 서드파티 라이브러리에서 Promise를 제공하고, 당신은 그것을 소비하기만 하면 됩니다.

마지막으로, 이 자습서에서는 프라미스를 반환하는 웹 API의 일반적인 사용 사례를 인용할 것입니다: Fetch API.

프라미스와 함께 Fetch API 사용하기

가장 유용하고 자주 사용되는 Web API 중 하나는 프라미스를 반환하는 Fetch API입니다. 이는 네트워크를 통해 비동기 리소스 요청을 만들 수 있게 합니다. fetch는 두 부분으로 구성되어 있어서 then을 연결해야 합니다. 다음 예제는 사용자 데이터를 가져오기 위해 GitHub API에 요청을 하는 방법을 보여줍니다:

// GitHub API에서 사용자 가져오기
fetch('https://api.github.com/users/octocat')
  .then((response) => {
    return response.json()
  })
  .then((data) => {
    console.log(data)
  })
  .catch((error) => {
    console.error(error)
  })

fetch 요청은 https://api.github.com/users/octocat URL로 보내지며, 응답을 비동기적으로 기다립니다. 첫 번째 then은 응답을 익명 함수에 전달하여 응답을 JSON 데이터로 형식화한 다음, JSON을 두 번째 then에 전달하여 데이터를 콘솔에 기록합니다. catch 문은 발생할 수 있는 오류를 콘솔에 기록합니다.

이 코드를 실행하면 다음과 같은 결과가 생성됩니다:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

이것은 https://api.github.com/users/octocat에서 요청한 데이터를 JSON 형식으로 렌더링한 것입니다.

이 튜토리얼의 이 부분은 프라미스가 비동기 코드 처리에 대한 많은 개선을 포함한다는 것을 보여줍니다. 그러나 비동기 작업을 처리하기 위해 then을 사용하는 것이 콜백 피라미드보다 더 이해하기 쉽지만, 일부 개발자는 여전히 동기식 형식으로 비동기 코드를 작성하는 것을 선호합니다. 이러한 요구를 해결하기 위해 ECMAScript 2016 (ES7)에서는 프라미스 작업을 보다 쉽게 만들기 위해 async 함수와 await 키워드를 도입했습니다.

비동기 함수 및 async/await

async 함수를 사용하면 동기적으로 보이는 방식으로 비동기 코드를 처리할 수 있습니다. async 함수는 여전히 약속(promises)을 사용하지만 더 전통적인 JavaScript 구문을 갖추고 있습니다. 이 섹션에서는 이 구문의 예제를 시도해 볼 것입니다.

// 비동기 함수 생성
async function getUser() {
  return {}
}

비록 이 함수가 아직 비동기 처리를 다루고 있지는 않지만, 이는 전통적인 함수와는 다르게 동작합니다. 함수를 실행하면, 반환 값 대신 [[PromiseStatus]][[PromiseValue]]를 갖는 프로미스가 반환됩니다.

console.log(getUser())

다음을 실행하면 다음이 나타납니다:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: Object

이는 프로미스를 처리할 때와 마찬가지로 then을 사용하여 async 함수를 처리할 수 있다는 것을 의미합니다. 다음 코드를 사용하여 이를 확인해 보세요:

getUser().then((response) => console.log(response))

getUser 호출은 반환 값을 콘솔에 로깅하는 익명 함수에 값을 전달합니다.

프로그램을 실행하면 다음과 같은 결과를 얻게 됩니다:

Output
{}

async 함수는 내부에서 호출된 프로미스를 await 연산자를 사용하여 처리할 수 있습니다. awaitasync 함수 내에서 사용될 수 있으며, 지정된 코드를 실행하기 전에 프로미스가 settle될 때까지 기다립니다.

이 지식을 활용하여 마지막 섹션에서의 Fetch 요청을 async/await를 사용하여 다음과 같이 다시 작성할 수 있습니다:

// async/await를 사용하여 fetch 처리
async function getUser() {
  const response = await fetch('https://api.github.com/users/octocat')
  const data = await response.json()

  console.log(data)
}

// 비동기 함수 실행
getUser()

여기서의 await 연산자는 요청이 데이터로 채워진 후에 data가 로그되지 않도록 보장합니다.

이제 최종 datagetUser 함수 내에서 처리될 수 있으며, then을 사용할 필요가 없습니다. 이것이 data를 로그하는 결과입니다:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

참고: 많은 환경에서 async를 사용하여 await를 사용해야합니다. 그러나 일부 새로운 버전의 브라우저와 Node는 최상위 await를 사용할 수 있게하여 await를 감싸는 async 함수를 만들지 않아도됩니다.

마지막으로, 비동기 함수 내에서 완료된 프로미스를 처리하기 때문에 함수 내에서 오류를 처리할 수도 있습니다. thencatch 메서드를 사용하는 대신 예외를 처리하기 위해 try/catch 패턴을 사용할 것입니다.

다음 강조 표시된 코드를 추가하십시오:

// 성공과 오류 처리하기 - async/await를 사용하여
async function getUser() {
  try {
    // try에서 성공 처리
    const response = await fetch('https://api.github.com/users/octocat')
    const data = await response.json()

    console.log(data)
  } catch (error) {
    // catch에서 오류 처리
    console.error(error)
  }
}

프로그램은 오류를 받으면 이를 콘솔에 기록하고 catch 블록으로 이동합니다.

현대의 비동기 JavaScript 코드는 대부분 async/await 구문으로 처리되지만, 특히 async/await로 처리할 수 없는 추가 기능을 가진 프로미스에 대한 작동 지식을 가지는 것이 중요합니다. 이는 프로미스가 Promise.all()과 같은 기능을 수행할 수 있기 때문입니다.

참고: async/await는 코드에 더 많은 유연성을 추가하기 위해 제너레이터와 프로미스를 결합하여 재현할 수 있습니다. 더 자세한 내용은 저희의 JavaScript에서 제너레이터 이해하기 튜토리얼을 확인하세요.

결론

웹 API는 종종 데이터를 비동기적으로 제공하므로, 비동기 작업의 결과를 처리하는 방법을 배우는 것은 JavaScript 개발자로서 필수적인 부분입니다. 이 글에서는 호스트 환경이 이벤트 루프를 사용하여 코드의 실행 순서를 처리하는 방법을 스택과 큐를 사용하여 배웠습니다. 또한 콜백, 프로미스 및 async/await 구문을 사용하여 비동기 이벤트의 성공 또는 실패를 처리하는 세 가지 방법의 예제를 시도해 보았습니다. 마지막으로 비동기 작업을 처리하기 위해 Fetch 웹 API를 사용했습니다.

브라우저가 병렬 이벤트를 처리하는 방법에 대한 자세한 정보는 Mozilla 개발자 네트워크의 동시성 모델 및 이벤트 루프를 읽어보세요. JavaScript에 대해 더 알고 싶다면, 저희 JavaScript 코딩하는 방법 시리즈로 돌아가세요.

Source:
https://www.digitalocean.com/community/tutorials/understanding-the-event-loop-callbacks-promises-and-async-await-in-javascript