분류 javascript

자바 스크립트에서의 콜백과 약속

컨텐츠 정보

  • 조회 364 (작성일 )

본문

JavaScript를 처음 사용하고 약속의 작동 방식을 이해하기가 어려우면 이 기사가 보다 명확하게 이해하는 데 도움이 되기를 바랍니다.


https://dev.to/jsmanifest/callbacks-vs-promises-in-javascript-2d5k 


그렇게 말하면서,이 기사는 약속을 이해하는 데 조금 확신이 없는 사람들을 위한 것입니다.


이 게시물은 async / await를 사용하여 약속을 실행하지는 않지만 기능 측면에서는 동일하지만 async / await는 대부분의 상황에서 더 구문 적 설탕입니다.


"무엇" 


약속은 실제로 JavaScript의 네이티브가 되기 전에 한참 동안 나왔습니다. 예를 들어, 약속이 기본이 되기 전에 이 패턴을 구현 한 두 라이브러리는 Qwhen입니다.


그렇다면 약속은 무엇입니까? 비동기 작업의 최종 완료 또는 실패를 나타내는 JavaScript 객체에서 약속합니다. 콜백 방식이나 약속을 사용하여 비동기 작업을 수행하면 결과를 얻을 수 있습니다. 그러나 둘 사이에는 약간의 차이가 있습니다.


콜백과 약속의 주요 차이점 


둘 사이의 주요 차이점은 콜백 접근 방식을 사용할 때 일반적으로 콜백을 함수에 전달하여 완료되면 호출되어 함수의 결과를 얻는 것입니다. 약속에서는 반환 된 약속 객체에 콜백을 첨부한다는 것입니다.


Callbacks:


function getMoneyBack(money, callback) {
  if (typeof money !== 'number') {
    callback(null, new Error('money is not a number'))
  } else {
    callback(money)
  }
}

const money = getMoneyBack(1200)
console.log(money)


Promises:


function getMoneyBack(money) {
  return new Promise((resolve, reject) => {
    if (typeof money !== 'number') {
      reject(new Error('money is not a number'))
    } else {
      resolve(money)
    }
  })
}

getMoneyBack(1200).then((money) => {
  console.log(money)
})


The Promise Object 


JavaScript에서 약속을 구성하는 핵심이기 때문에 promise 객체를 언급 한 것이 좋습니다.


질문은 왜 JavaScript에서 약속이 필요한가 하는 것입니다.


글쎄,이 질문에 더 잘 대답하기 위해 우리는 왜 콜백 접근법을 사용하는 것이 대다수의 자바 스크립트 개발자에게 "충분하지 않은"지 물어야 했습니다.


Callback Hell 


콜백 접근 방식을 사용하는 한 가지 일반적인 문제는 한 번에 여러 개의 비동기 작업을 수행해야 할 때 콜백 지옥으로 쉽게 끝날 수 있으며, 이는 관리하기 어렵고 힘들게 만드는 악몽이 될 수 있다는 것입니다 읽을 코드-모든 개발자의 최악의 악몽입니다.


다음은 그 예입니다.


function getFrogsWithVitalSigns(params, callback) {
  let frogIds, frogsListWithVitalSignsData
  api.fetchFrogs(params, (frogs, error) => {
    if (error) {
      console.error(error)
      return
    } else {
      frogIds = frogs.map(({ id }) => id)
      // The list of frogs did not include their health information, so lets fetch that now
      api.fetchFrogsVitalSigns(
        frogIds,
        (frogsListWithEncryptedVitalSigns, err) => {
          if (err) {
            // do something with error logic
          } else {
            // The list of frogs health info is encrypted. Our friend texted us the secret key to use in this step. This is used to decrypt the list of frogs encrypted health information
            api.decryptFrogsListVitalSigns(
              frogsListWithEncryptedVitalSigns,
              'pepsi',
              (data, errorr) => {
                if (errorrr) {
                  throw new Error('An error occurred in the final api call')
                } else {
                  if (Array.isArray(data)) {
                    frogsListWithVitalSignsData = data
                  } else {
                    frogsListWithVitalSignsData = data.map(
                      ({ vital_signs }) => vital_signs,
                    )
                    console.log(frogsListWithVitalSignsData)
                  }
                }
              },
            )
          }
        },
      )
    }
  })
}

const frogsWithVitalSigns = getFrogsWithVitalSigns({
  offset: 50,
})
  .then((result) => {
    console.log(result)
  })
  .catch((error) => {
    console.error(error)
  })


코드 스니펫에서 어색한 모양이 형성되어 있음을 시각적으로 볼 수 있습니다. 3 개의 비동기 API 호출에서 콜백 지옥이 일반적인 상단에서 하단 방향의 반대 방향으로 가라 앉기 시작했습니다.


promise를 사용하면 .then 메소드를 연결하여 첫 번째 핸들러의 루트에 코드를 유지할 수 있으므로 더 이상 문제가 되지 않습니다. 


function getFrogsWithVitalSigns(params, callback) {
  let frogIds, frogsListWithVitalSignsData
  api
    .fetchFrogs(params)
    .then((frogs) => {
      frogIds = frogs.map(({ id }) => id)
      // The list of frogs did not include their health information, so lets fetch that now
      return api.fetchFrogsVitalSigns(frogIds)
    })
    .then((frogsListWithEncryptedVitalSigns) => {
      // The list of frogs health info is encrypted. Our friend texted us the secret key to use in this step. This is used to decrypt the list of frogs encrypted health information
      return api.decryptFrogsListVitalSigns(
        frogsListWithEncryptedVitalSigns,
        'pepsi',
      )
    })
    .then((data) => {
      if (Array.isArray(data)) {
        frogsListWithVitalSignsData = data
      } else {
        frogsListWithVitalSignsData = data.map(
          ({ vital_signs }) => vital_signs,
        )
        console.log(frogsListWithVitalSignsData)
      }
    })
    .catch((error) => {
      console.error(error)
    })
  })
}

const frogsWithVitalSigns = getFrogsWithVitalSigns({
  offset: 50,
})
  .then((result) => {
    console.log(result)
  })
  .catch((error) => {
    console.error(error)
  })

콜백 코드 스니펫에서 몇 단계 더 깊이 중첩 된 경우 상황이 추악하고 관리하기 어려워집니다.


콜백 지옥에서 발생하는 문제 


이 "콜백 지옥 (callback hell)"을 나타내는 이전 코드 스니펫을 살펴보면 약속에서 언어에 대한 좋은 추가가 되었다고 말하는 충분한 증거로 작용하는 위험한 문제 목록을 얻을 수 있습니다.


1. 읽기가 점점 어려워졌습니다

  1. 코드가 두 방향으로 움직이기 시작했습니다 (위에서 아래로, 왼쪽에서 오른쪽으로)


2. 관리하기가 점점 어려워졌습니다

  1. 코드가 더 깊게 중첩되면서 무슨 일이 있었는지 명확하지 않았습니다.

  2. 우리는 항상 외부 범위에서 이미 선언 된 것과 동일한 이름을 가진 변수를 실수로 선언하지 않도록 해야 합니다 (이를 섀도잉이라고 함).

  3. 우리는 세 곳의 다른 위치에서 세 가지 다른 오류를 고려해야 했습니다.

    1. 오류를 가리지 않도록 각 오류의 이름을 바꿔야 했습니다. 이 작업 과정에서 추가 요청을 한 경우 위 범위의 오류와 충돌하지 않는 추가 변수 이름을 찾아야 합니다.


예제를 면밀히 살펴보면 이러한 문제의 대부분이 .the와 약속을 연결할 수 있게 되어 다음에 설명 할 것입니다.


Promise Chaining 


약속 체인은 비동기 작업 체인을 실행해야 할 때 절대적으로 유용합니다. 체인으로 연결된 각 작업은 체인의 다음에 의해 제어되는 이전 작업이 완료되는 즉시 시작할 수 있습니다.


이러한 블록은 내부적으로 설정되어 콜백 함수가 약속을 반환하도록 허용하며, 이후 약속을 각 체인에 적용합니다.


.catch 블록에서 오는 거부 된 약속 외에도 .the에서 반환 된 모든 것은 해결 된 약속이 됩니다.


짧고 빠른 예는 다음과 같습니다.


const add = (num1, num2) => new Promise((resolve) => resolve(num1 + num2))

add(2, 4)
  .then((result) => {
    console.log(result) // result: 6
    return result + 10
  })
  .then((result) => {
    console.log(result) // result: 16
    return result
  })
  .then((result) => {
    console.log(result) // result: 16
  })


Promise Methods 


JavaScript의 Promise 생성자는 약속에서 하나 이상의 결과를 검색하는 데 사용할 수 있는 몇 가지 정적 메소드를 정의합니다.


Promise.all 


비동기 작업 일괄 처리를 누적하고 결국 각 값을 배열로 받으려면 이 목표를 충족시키는 약속 방법 중 하나는 Promise.all입니다.


Promise.all은 모든 작업이 성공적으로 완료되었을 때 작업 결과를 수집합니다. 이것은 Promise.allSettled와 유사합니다. 단, 이러한 작업 중 하나 이상이 실패하면 결국 약속 체인의 .catch 블록으로 끝나는 약속이 오류와 함께 거부됩니다.


약속 거부는 작업 시작부터 완료 시점까지 언제라도 발생할 수 있습니다. 모든 결과가 완료되기 전에 거부가 발생하면 완료되지 않은 결과는 중단되고 종료되지 않습니다. 다시 말해, 그 "모두"중 하나이거나 전혀 처리되지 않은 것입니다.


다음은 Promise.all 메소드가 약속 한 getFrogs 및 getLizards를 사용하고 결과를 로컬 스토리지에 저장하기 전에 .then 핸들러 내부의 배열로 검색하는 간단한 코드 예제입니다.


const getFrogs = new Promise((resolve) => {
  resolve([
    { id: 'mlo29naz', name: 'larry', born: '2016-02-22' },
    { id: 'lp2qmsmw', name: 'sally', born: '2018-09-13' },
  ])
})

const getLizards = new Promise((resolve) => {
  resolve([
    { id: 'aom39d', name: 'john', born: '2017-08-11' },
    { id: '20fja93', name: 'chris', born: '2017-01-30' },
  ])
})

function addToStorage(item) {
  if (item) {
    let prevItems = localStorage.getItem('items')
    if (typeof prevItems === 'string') {
      prevItems = JSON.parse(prevItems)
    } else {
      prevItems = []
    }
    const newItems = [...prevItems, item]
    localStorage.setItem('items', JSON.stringify(newItems))
  }
}

let allItems = []

Promise.all([getFrogs, getLizards])
  .then(([frogs, lizards]) => {
    localStorage.clear()

    frogs.forEach((frog) => {
      allItems.push(frog)
    })
    lizards.forEach((lizard) => {
      allItems.push(lizard)
    })
    allItems.forEach((item) => {
      addToStorage(item)
    })
  })
  .catch((error) => {
    console.error(error)
  })

console.log(localStorage.getItem('items'))
/*
  result:
    [{"id":"mlo29naz","name":"larry","born":"2016-02-22"},{"id":"lp2qmsmw","name":"sally","born":"2018-09-13"},{"id":"aom39d","name":"john","born":"2017-08-11"},{"id":"20fja93","name":"chris","born":"2017-01-30"}]
*/

Promise.race 


이 메소드는 반복 가능한 약속의 약속 중 하나가 약속의 가치 또는 이유와 함께 해결되거나 거부 될 때마다 이행하거나 거부하는 약속을 리턴합니다.


다음은 promise1과 promise2와 Promise.race 메서드 사이의 간단한 예입니다.


const promise1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve('some result')
  }, 200)
})

const promise2 = new Promise((resolve, reject) => {
  reject(new Error('some promise2 error'))
})

Promise.race([promise1, promise2])
  .then((result) => {
    console.log(result)
  })
  .catch((error) => {
    console.error(error)
  })


결과는 다음과 같습니다.


promise.race error 


반환 된 값은 다른 약속이 200 밀리 초 지연되어 약속 거부로 끝났습니다.


Promise.allSettled 


Promise.allSettled 메소드는 약속 중 하나가 실패 할 때 즉시 오류를 거부하지 않고 Promise.allSettled가 약속을 반환한다는 점을 제외하고는 비슷한 목표를 공유한다는 점에서 Promise.all과 다소 유사합니다. 각 항목이 약속 작업의 결과를 나타내는 배열로 결과를 누적하거나 해결하거나 거부합니다. 이것이 의미하는 것은 항상 배열 데이터 유형으로 끝나는 것입니다.


다음은 이에 대한 예입니다.


const add = (num1, num2) => new Promise((resolve) => resolve(num1 + num2))
const multiply = (num1, num2) => new Promise((resolve) => resolve(num1 * num2))
const fail = (num1) =>
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error('You, my friend, were too late')), 200),
  )
const fail2 = (num1) =>
  new Promise((resolve, reject) =>
    setTimeout(
      () => reject(new Error('Being late is never a good habit')),
      100,
    ),
  )
const promises = [add(2, 4), multiply(5, 5), fail('hi'), fail2('hello')]

Promise.allSettled(promises)
  .then((result) => {
    console.log(result)
  })
  .catch((error) => {
    console.error(error)
  })

promise.allsettled 


Promise.any 


Promise.any는 현재 TC39 프로세스의 3 단계에 있는 Promise 생성자를 추가하는 제안입니다.


Promise.any가 제안하는 것은 반복 가능한 약속을 수락하고 주어진 약속이 모두 거부 된 경우 거부 이유를 보유한 AggregateError로 거부되거나 거부 된 첫 번째 약속에서 성취 된 약속을 반환하려고 시도하는 것입니다.


이는 15 개의 약속을 소비 한 작업이 있고 그 중 14 개의 약속이 해결되는 동안 실패한 경우 Promise.any의 결과는 해결 된 약속의 가치가 됨을 의미합니다.


const multiply = (num1, num2) => new Promise((resolve) => resolve(num1 * num2))
const fail = (num1) =>
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error('You, my friend, were too late')), 200),
  )

const promises = [
  fail(2),
  fail(),
  fail(),
  multiply(2, 2),
  fail(2),
  fail(2),
  fail(2, 2),
  fail(29892),
  fail(2),
  fail(2, 2),
  fail('hello'),
  fail(2),
  fail(2),
  fail(1),
  fail(),
]

Promise.any(promises)
  .then((result) => {
    console.log(result) // result: 4
  })
  .catch((error) => {
    console.error(error)
  })


자세한 내용은 여기를 참조하십시오.


Success/Error Handling Gotcha 


다음과 같은 변형을 사용하여 성공 또는 실패한 약속 작업을 처리 할 수 ​​있습니다.


변형 1 :


add(5, 5).then(
  function success(result) {
    return result
  },
  function error(error) {
    console.error(error)
  },
)


변형 2 :

add(5, 5)
  .then(function success(result) {
    return result
  })
  .catch(function(error) {
    console.error(error)
  })


그러나 이 두 예는 정확히 동일하지 않습니다. 변형 2에서 resolve 처리기에서 오류를 발생 시키려고 시도하면 .catch 블록 내에서 catch 된 오류를 검색 할 수 있습니다.


add(5, 5)
  .then(function success(result) {
    throw new Error("You aren't getting passed me")
  })
  .catch(function(error) {
    // The error ends up here
  })

그러나 변형 1에서 resolve 처리기 내에서 오류를 발생 시키려고 시도하면 오류를 잡을 수 없습니다.


add(5, 5).then(
  function success(result) {
    throw new Error("You aren't getting passed me")
  },
  function error(error) {
    // Oh no... you mean i'll never receive the error? :(
  },
)


결론 


이것으로 이 포스트의 끝이 끝납니다! 나는 이것이 귀중한 것으로 나타났으며 앞으로 더 많은 것을 기대하기를 바랍니다!