본문 바로가기

개발/Node.JS

Node.js에서 마스터 비동기 대기

비동기 함수 (async / await)를 사용하여 콜백 또는 약속 기반 Node.js 애플리케이션을 단순화하는 방법을 학습합니다.

비동기 언어 구문은 C #의 async / await, Kotlin의 coroutines 및 Go의 goroutines와 같이 잠시 다른 언어에서 사용되었습니다. Node.js 8이 출시되면서 오랫동안 기다려온 비동기 기능도 Node.js에 포함되었습니다.



Node에서 비동기 함수 란 무엇입니까?

비동기 함수 선언은 AsyncFunction객체를 반환 합니다. 이들은 Generator실행이 중단 될 수 있다는 점에서 -s 와 유사합니다 유일한 차이점은 객체 Promise대신 항상 a 를 반환한다는 것입니다 { value: any, done: Boolean }실제로, 그들은 매우 유사하여 co 패키지를 사용하여 유사한 기능을 얻을 수 있습니다.

비동기 함수에서 Promise거부를 유발할 수 있습니다.

약속을 가지고 구현 된 논리가 있다면

function handler (req, res) {
  return request('https://user-handler-service')
    .catch((err) => {
      logger.error('Http error', err)
      error.logged = true
      throw err
    })
    .then((response) => Mongo.findOne({ user: response.body.user }))
    .catch((err) => {
      !error.logged && logger.error('Mongo error', err)
      error.logged = true
      throw err
    })
    .then((document) => executeLogic(req, res, document))
    .catch((err) => {
      !error.logged && console.error(err)
      res.status(500).send()
    })
}

다음을 사용하여 동기식 코드처럼 보이게 할 수 있습니다 async/await.

async function handler (req, res) {
  let response
  try {
    response = await request('https://user-handler-service')  
  } catch (err) {
    logger.error('Http error', err)
    return res.status(500).send()
  }

  let document
  try {
    document = await Mongo.findOne({ user: response.body.user })
  } catch (err) {
    logger.error('Mongo error', err)
    return res.status(500).send()
  }

  executeLogic(document, req, res)
}

이전 버전의 V8에서는 처리되지 않은 약속 거부가 자동 삭제되었습니다. 이제는 Node에서 경고 메시지를받습니다. 따라서 리스너를 만들지 않아도됩니다. 그러나이 경우 오류를 처리하지 않을 때 앱을 알 수없는 상태로 만드는 경우 앱을 중단하는 것이 좋습니다.

process.on('unhandledRejection', (err) => { 
  console.error(err)
  process.exit(1)
})

비동기 함수가있는 패턴

약속이나 콜백으로 해결할 때 복잡한 패턴이나 외부 라이브러리를 사용해야하므로 비동기 작업을 동기식으로 처리하는 기능이 매우 편리합니다.

비동기 적으로 얻은 데이터를 루프하거나 if-else조건문을 사용해야하는 경우 입니다.

지수 적 백 오프로 다시 시도하십시오.

재시도 논리를 구현하는 것은 Promises를 사용하여 매우 서투른 작업이었습니다.

function requestWithRetry (url, retryCount) {
  if (retryCount) {
    return new Promise((resolve, reject) => {
      const timeout = Math.pow(2, retryCount)
 
      setTimeout(() => {
        console.log('Waiting', timeout, 'ms')
        _requestWithRetry(url, retryCount)
          .then(resolve)
          .catch(reject)
      }, timeout)
    })
  } else {
    return _requestWithRetry(url, 0)
  }
}

function _requestWithRetry (url, retryCount) {
  return request(url, retryCount)
    .catch((err) => {
      if (err.statusCode && err.statusCode >= 500) {
        console.log('Retrying', err.message, retryCount)
        return requestWithRetry(url, ++retryCount)
      }
      throw err
    })
}

requestWithRetry('http://localhost:3000')
  .then((res) => {
    console.log(res)
  })
  .catch(err => {
    console.error(err)
  })

그것은 그것을보기 위해 나에게 두통을 주었다. 우리는 그것을 다시 작성 async/await하고 훨씬 더 간단하게 만들 수 있습니다 .

function wait (timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, timeout)
  })
}

async function requestWithRetry (url) {
  const MAX_RETRIES = 10
  for (let i = 0; i <= MAX_RETRIES; i++) {
    try {
      return await request(url)
    } catch (err) {
      const timeout = Math.pow(2, i)
      console.log('Waiting', timeout, 'ms')
      await wait(timeout)
      console.log('Retrying', err.message, i)
    }
  }
}


중간 값

앞의 예제처럼 끔찍하지는 않지만 3 개의 비동기 함수가 서로 의존하는 경우 다음과 같은 몇 가지 추악한 솔루션 중에서 선택해야합니다.

functionA약속은 다음 리턴 functionB그 값을 필요로하고 functionC해결 된 양의 값이 필요 functionA년대와 functionB의 약속을.

해결책 1 : .then크리스마스 트리
function executeAsyncTask () {
  return functionA()
    .then((valueA) => {
      return functionB(valueA)
        .then((valueB) => {          
          return functionC(valueA, valueB)
        })
    })
}

이 솔루션을 통해 우리가 얻을 valueA제 3의 주변 폐쇄에서 then와 valueB값으로 이전 약속에 해결합니다. 우리는 크리스마스 트리를 평평하게 할 수 없기 때문에 우리는 그 결말을 놓치고 valueA사용할 수 없게 될 것입니다 functionC.


엔터프라이즈 급 Node.js 개발에 대한 도움이 필요하십니까? 
RisingStack의 전문가를 고용하십시오!

해결책 2 : 상위 범위로 이동
function executeAsyncTask () {
  let valueA
  return functionA()
    .then((v) => {
      valueA = v
      return functionB(valueA)
    })
    .then((valueB) => {
      return functionC(valueA, valueB)
    })
}

크리스마스 트리에서는 valueA사용할 수 있도록 더 높은 범위를 사용했습니다. 이 경우는 비슷하게 작동하지만 이제는 -s valueA범위 밖에서 변수를 만들었 .then으므로 해결 된 첫 번째 Promise의 값을 할당 할 수 있습니다.

이것은 확실히 작동하고, .then사슬을 평평하게하고 의미 상으로 정확합니다. 그러나 변수 이름 valueA이 함수의 다른 곳에서 사용되는 경우 새로운 버그를위한 방법을 열어줍니다 또한 같은 값에 대해 두 개의 이름 - valueA및 v을 사용해야 합니다.

해결책 3 : 불필요한 배열
function executeAsyncTask () {
  return functionA()
    .then(valueA => {
      return Promise.all([valueA, functionB(valueA)])
    })
    .then(([valueA, valueB]) => {
      return functionC(valueA, valueB)
    })
}

valueAPromise와 함께 배열에 전달되어 functionB나무를 평평하게 할 수 있는 다른 이유는 없습니다 그들은 완전히 다른 유형 일 수 있기 때문에 배열에 전혀 속하지 않을 확률이 높습니다.

해결 방법 4 : 도우미 함수 작성
const converge = (...promises) => (...args) => {
  let [head, ...tail] = promises
  if (tail.length) {
    return head(...args)
      .then((value) => converge(...tail)(...args.concat([value])))
  } else {
    return head(...args)
  }
}

functionA(2)
  .then((valueA) => converge(functionB, functionC)(valueA))

물론 컨텍스트 저글링을 숨기려는 도우미 함수를 작성할 수는 있지만 읽는 것이 매우 어렵고 기능적 마술에 익숙하지 않은 자들에게는 이해하기 쉽지 않을 수 있습니다.

async/await우리의 문제 를 사용함으로써 마술처럼 사라졌습니다 :
async function executeAsyncTask () {
  const valueA = await functionA()
  const valueB = await functionB(valueA)
  return function3(valueA, valueB)
}

async / await를 사용한 여러 병렬 요청

이것은 이전과 유사합니다. 한 번에 여러 비동기 작업을 실행하고 다른 위치에서 해당 값을 사용하려는 경우 다음과 async/await같이 쉽게 수행 할 수 있습니다 .

async function executeParallelAsyncTasks () {
  const [ valueA, valueB, valueC ] = await Promise.all([ functionA(), functionB(), functionC() ])
  doSomethingWith(valueA)
  doSomethingElseWith(valueB)
  doAnotherThingWith(valueC)
}

앞의 예에서 보았 듯이이 값을 전달하려면이 값을 상위 범위로 이동하거나 비 의미 배열을 만들어야합니다.

배열 반복 메소드

당신은 사용할 수 있습니다 mapfilter그리고 reduce그들은 꽤 unintuitively 행동하지만, 비동기 기능. 다음 스크립트가 콘솔에 인쇄 할 내용을 추측 해보십시오.

  1. 지도
function asyncThing (value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), 100)
  })
}

async function main () {
  return [1,2,3,4].map(async (value) => {
    const v = await asyncThing(value)
    return v * 2
  })
}

main()
  .then(v => console.log(v))
  .catch(err => console.error(err))
  1. 필터
function asyncThing (value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), 100)
  })
}

async function main () {
  return [1,2,3,4].filter(async (value) => {
    const v = await asyncThing(value)
    return v % 2 === 0
  })
}

main()
  .then(v => console.log(v))
  .catch(err => console.error(err))

function asyncThing (value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), 100)
  })
}

async function main () {
  return [1,2,3,4].reduce(async (acc, value) => {
    return await acc + await asyncThing(value)
  }, Promise.resolve(0))
}

main()
  .then(v => console.log(v))
  .catch(err => console.error(err))

솔루션 :

  1. [ Promise { <pending> }, Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]
  2. [ 1, 2, 3, 4 ]
  3. 10

iteratee의 리턴 값을 로그하면 map우리가 기대하는 배열을 보게 될 것이다 : [ 2, 4, 6, 8 ]유일한 문제는 각 값이 Promise에 의해 래핑된다는 것 AsyncFunction입니다.

따라서 값을 가져 오려면 반환 된 배열을 다음에 전달하여 래핑을 해제해야합니다 Promise.all.

main()
  .then(v => Promise.all(v))
  .then(v => console.log(v))
  .catch(err => console.error(err))

원래, 먼저 모든 약속이 해결 될 때까지 기다렸다가 값을 맵핑합니다.

function main () {
  return Promise.all([1,2,3,4].map((value) => asyncThing(value)))
}

main()
  .then(values => values.map((value) => value * 2))
  .then(v => console.log(v))
  .catch(err => console.error(err))

이것은 조금 더 단순한 것 같습니다.

async/await당신이 당신의 iteratee에 일부 긴 실행 동기 논리와 다른 장기 실행 비동기 작업이있는 경우 버전은 여전히 유용 할 수 있습니다.

이렇게하면 첫 번째 값을 얻 자마자 계산을 시작할 수 있습니다. 계산을 실행하기 위해 모든 약속이 해결 될 때까지 기다릴 필요가 없습니다. 결과가 여전히 약속에 싸여 있지만 순차적으로 그렇게했다면 훨씬 빨리 해결됩니다.

어때 filter뭔가 잘못된 것이 있습니다 ...

글쎄, 당신은 짐작 [ false, true, false, true ]했겠지요 : 반환 값은 사실이지만 약속에 싸여 원래 배열의 모든 값을 되 찾을 수 있습니다. 불행하게도이 문제를 해결하기 위해 할 수있는 일은 모든 값을 확인한 다음 필터링하는 것입니다.

축소 는 매우 간단합니다. Promise.resolve리턴 된 누적 await기가 랩되고 랩되어야하므로 초기 값을 감쌀 필요가 있음을 명심 하십시오 .

.. 그것은 명령형 코드 스타일에 사용되도록 상당히 명확하게 의도 되었기 때문에.

.then체인을보다 "순수하게" 보이게 만들려면 Ramda pipeP와 composeP기능을 사용할 수 있습니다 .


엔터프라이즈 급 Node.js 개발에 대한 도움이 필요하십니까? 
RisingStack의 전문가를 고용하십시오!

콜백 기반 Node.js 응용 프로그램 다시 작성

비동기 함수 Promise는 기본적으로 를 반환 하므로 모든 콜백 기반 함수를 다시 작성하여 약속을 사용한 다음 await그 해답 을 사용할 수 있습니다 util.promisifyNode.js 의 함수를 사용하여 콜백 기반 함수를 Promise 기반 함수를 반환하도록 설정할 수 있습니다.

Promise 기반 응용 프로그램 다시 작성

간단한 .then체인을 아주 간단하게 업그레이드 할 수 있으므로 async/await즉시 사용할 수 있습니다.

function asyncTask () {
  return functionA()
    .then((valueA) => functionB(valueA))
    .then((valueB) => functionC(valueB))
    .then((valueC) => functionD(valueC))
    .catch((err) => logger.error(err))
}
 

~로 변할 것이다

async function asyncTask () {
  try {
    const valueA = await functionA()
    const valueB = await functionB(valueA)
    const valueC = await functionC(valueB)
    return await functionD(valueC)
  } catch (err) {
    logger.error(err)
  }
}

async / await를 사용하여 Node.js 앱 다시 작성

  • if-else조건문과 for/while루프 의 오래된 개념을 좋아했다면 ,
  • try-catch블록이 오류를 처리하는 방법 이라고 생각하면 ,

당신은 좋은 시간을 사용하여 서비스를 다시 작성해야합니다 async/await.

지금까지 보았 듯이 여러 패턴을 훨씬 쉽게 코드하고 읽을 수 있기 때문에 Promise.then()체인 보다 몇 가지 패턴에 더 적합 합니다. 그러나 지난 몇 년 동안 함수 프로그래밍 열풍에 빠져 있다면이 언어 기능을 전달할 수 있습니다.