댓글 검색 목록

[javascript] 콜백(Callback) 지옥에서 콜백 천국으로

페이지 정보

작성자 운영자 작성일 20-12-13 11:10 조회 683 댓글 0

많은 JavaScript 코드가 이렇게 생겼던 때를 기억하십니까?


router.put('/some-url', (req, res) => {
  fs.readFile(filePath(req), (err, file) => {
    if (err) res.status(500).send()
    else {
      parse(file, (err, parsed) => {
        if (err) res.status(500).send()
        else db.insert(parsed, err => {
          if (err) res.status(500).send()
          else res.status(200).send()
        })
      })
    }
  })
})


그 당시를 기억하지 못할 만큼 운이 좋은 사람들에게 이것은 명백한 이유로 콜백 지옥이라고 불렀습니다. 다행히도 우리는 계속 진행했으며 요즘에는 동등한 코드가 다음과 같이 보일 가능성이 높습니다.


router.put('/some-url', async (req, res) => {
  try {
    const file = await fs.readFile(filePath(req));
    const value = await parse(file);
    await db.insert(value);
    response.status(200).send();
  } catch {
    response.status(500).send();
  }
})



물론 async / await 및 Promises가 있으므로 당시 JS의 구문 기능이 부족한 콜백 지옥 시대를 비난하고 계속 진행하기 쉽습니다. 그러나 나는 되돌아보고, 핵심 문제를 분석하고, 어떻게 해결되었는지, 그리고 그 모든 것에서 무엇을 배울 수 있는지에 대한 가치가 있다고 생각합니다.


문제 


위의 지옥 같은 예의 전체 구조를 다시 살펴 보겠습니다.


doX(args, (err, res) => {
  if (err) { ... }
  else {
    doY(args, (err, res) => {
      if (err) { ... }
      ...
    })
  }
})


여기서 눈에 띄는 문제는 화면에 표시되는 내용의 대부분이 실제로 중요하지 않은 항목에만 사용된다는 것입니다.


doX(args /*, (err, res) => {
  if (err) { ... }
  else {*/
    doY(args /*, (err, res) => {
      if (err) { ... } */
      ...
    /*}*/)
  /*}*/
/*}*/)


비교를 위해 이것은 현대의 지옥이 아닌 버전과 동등한 구조입니다.


/* try { */
  /*await*/ doX(args)
  /*await*/ doY(args)
  ...
/*} catch { }*/


두 버전의 주석 처리 된 비트는 동일한 것을 나타냅니다. doX () 및 doY ()는 비동기 함수이며 일부 오류가 있을 수 있습니다. 하지만 지옥 같은 버전에서는 이러한 부가 노트를 위해 훨씬 더 많은 공간을 소비해야 하므로 코드를 훨씬 덜 읽을 수 있게 됩니다.


? 추가 구문 없이도 상용구를 자르고 코드를 더 읽기 쉬운 형식으로 재구성 할 수 있습니다. 역사적으로 말하면 Promise 라이브러리의 형태로 일어난 일입니다 (그러면 표준화 되고 약간의 구문 지원에 더 많은 사랑을 받았습니다).


doX(args)
.then(() => doY(args))
.then(() => ...)
.catch(() => { ... })


doX(args)
/*.then(() =>*/doY(args)/*)*/
/*.then(() =>*/.../*)*/
/*.catch(() => { ... })*/


이 코드와 지옥 코드의 중요한 차이점은 지옥 코드에서는 중요한 항목과 상용구 항목이 매우 얽혀있는 반면, 프라미스 라이브러리에서는 깔끔하게 분리되어있어 상용구의 양이 많은 경우에도 코드를 더 쉽게 읽을 수 있습니다. 거의 같다:


// without promises:
doX(args/*, (err, res) => { ... }*/)


// with promises:
doX(args)/*.then(() => { ... })*/


// with async/await:
/*await*/ doX(args)


Promises는 또한 비동기 프로그래밍 인체 공학에 도움이 되는 다른 중요한 기능을 제공합니다. 가장 중요한 것은 다음과 같습니다.


  • Promise는 연결될 때 자동으로 평면화 됩니다.
  • 약속이 공유됩니다.

그러나 이러한 특성은 유익하지만 앞서 언급 한 분리만큼 중요하지 않다고 생각합니다. 이를 설명하기 위해 분리 만 수행하고 다른 작업은 수행하지 않는 실험적 promise 라이브러리를 만들고 어떻게 작동하는지 살펴 보겠습니다.


실험 


그래서 처음에는 다음과 같은 함수로 시작했습니다.


doX(args, (err, res) => {...})


여기서 콜백은 기본 상용구 (그리고 지옥의 이름)이므로, 가장 쉬운 분리는 doX ()의 인수 목록에서 꺼내고 대신 지연된 함수에 넣는 것입니다.


doX(args)((err, res) => {...})


☝️ 이것은 기본적으로 doX가 구현되는 방법의 변경 사항입니다.


function doX(args, callback) {
  // do stuff
  // maybe do more
  callback(undefined, 42)
}


이에:


function doX(args) {
  // do stuff
  return callback => {
    // maybe do more
    callback(undefined, 42)
  }
}


즉, 우리는 다음과 같이 규칙을 변경했습니다.


마지막 인수로 콜백을 수락


에:


콜백을 인수로 받아들이는 함수를 반환


우리의 분리 규칙은 여전히 ​​같은 양의 상용구를 가지고 있기 때문에 그 자체로는 많은 도움이 되지 않은 것 같습니다. 그러나 그것은 우리가 상용구를 제거하는 데 도움이 되는 단순한 유틸리티의 문을 열었습니다. 이를 확인하기 위해 먼저 pipe() 유틸리티를 소개하겠습니다.


function pipe(...cbs) {
  let res = cbs[0];
  for (let i = 1; i < cbs.length; i++) res = cbs[i](res);
  return res;
}


간단히 말해서 다음과 같습니다.


pipe(a, b, c, d)


다음과 같습니다.


let x = a
x = b(x)
x = c(x)
x = d(x)


그리 멀지 않은 미래에 pipe()는 JavaScript 자체에 통합 될 수도 있습니다.


a |> b |> c |> d


어쨌든 pipe() 연산자를 사용하면 수동으로 콜백을 작성하지 않고도 (새로운 규칙) doX() (기억하는 것은 표준 콜백을 받는 함수 임)에 의해 반환 된 함수를 깔끔하게 변환 할 수 있습니다. 예를 들어 다음과 같이 then() 유틸리티를 만들 수 있습니다.


export function then(f) {
  return src => {
    src((err, res) => {
      if (!err) f(res)
    })

    return src
  }
}


이러한 유틸리티를 사용하면 비동기 코드가 다음과 같이 변환됩니다.


doX(args)((err, res) => { ... })


이에:


pipe(
  doX(args),
  then(() => { ... })
)


또는 더 나은 (파이프 라인 운영자 통합) :


doX(args) |> then(() => { ... })


표준 약속 라이브러리와 매우 비슷합니다.


doX(args).then(() => { ... })


간단한 catch() 유틸리티를 만들 수도 있습니다.


function catch(f) {
  return src => {
    src((err) => {
      if (err) f(err)
    })

    return src
  }
}


다음과 같은 비동기 코드를 제공합니다.


doX(args)
|> then(() => doY(args))
|> then(() => ...)
|> catch(() => { ... })


doX(args)
/*|> then(() =>*/ doY(args) /*)*/
/*|> then(() =>*/ ... /*)*/
/*|> catch(() => { ... })*/


거의 노력하지 않고 promise 라이브러리만큼 간결합니다. 더 좋은 점은,이 방법은 우리에게 확장 성을 제공합니다. 우리는 설정된 Promise 객체에 묶여 있지 않고 훨씬 더 광범위한 유틸리티 함수를 생성 / 사용할 수 있습니다.


function map(f) {
  return src => cb => src((err, res) => {
    if (err) cb(err, undefined)
    else cb(undefined, f(res))
  })
}


function delay(n) {
  return src => cb => src((err, res) => {
    if (err) cb(err, undefined)
    else setTimeout(() => cb(undefined, res), n)
  })
}


약간 거칠어지기 시작하십시오.


doX(args)
|> then(() => doY(args))
|> map(yRes => yRes * 2)
|> delay(200)
|> then(console.log)


실제 사례 


그래서 그것은 우리가 promise 라이브러리에서 제공하는 것과 동일한 편리함을 제공하는 유틸리티와 라이브러리를 만들 수 있게 해주는 단순한 규칙 변경으로 보입니다 (그리고 async / await 구문과 거의 유사합니다). 더 나은 관점을 얻기 위해 실제 사례를 살펴 보겠습니다. 이 목적을 위해 (대부분 호기심에서) 실험용 라이브러리를 구현하여 온라인 놀이터를 만들었습니다.


먼저, 가장 지옥 같은 버전에서 다음과 같이 보였던 원래 예제를 살펴 보겠습니다.


router.put('/some-url', (req, res) => {
  fs.readFile(filePath(req), (err, file) => {
    if (err) res.status(500).send()
    else {
      parse(file, (err, parsed) => {
        if (err) res.status(500).send()
        else db.insert(parsed, err => {
          if (err) res.status(500).send()
          else res.status(200).send()
        })
      })
    }
  })
})


최신 JavaScript 버전은 다음과 같습니다.


router.put('/some-url', async (req, res) => {
  try {
    const file = await fs.readFile(filePath(req));
    const value = await parse(file);
    await db.insert(value);
    response.status(200).send();
  } catch {
    response.status(500).send();
  }
})


새로운 콜백 규칙 코드는 다음과 같습니다.


router.put('/some-url', (req, res) => {
  fs.readFile(filePath(req))
  |> map(parse)
  |> flatten
  |> map(db.insert)
  |> flatten
  |> then(() => res.status(200).send())
  |> catch(() => res.status(500).send())
})


컨벤션은 우리를 async / await의 편리함과 매우 가깝게 만듭니다. 그래도 약간의 뉘앙스가 있습니다. flatten 유틸리티가 중간에 두 번 사용 되었습니까? Promise와 달리 콜백은 연결하는 동안 평평하지 않기 때문입니다. parse ()도 비동기라고 가정했습니다. 즉, promise-ish도 반환합니다. 그런 다음 map (parse)는 readFile ()의 결과를 새로운 promise-ish에 매핑합니다. 이 약속은 db.insert ()에 전달되기 전에 해결 된 값으로 평면화 되어야 합니다. async / await 코드에서 이것은 parse () 전에 await 키워드에 의해 수행되며 여기서는 flatten 유틸리티로 수행해야 합니다.


추신, flatten() 유틸리티는 본질적으로 매우 단순합니다.


function flatten(src) {
  return cb => src((err, res) => {
    if (err) cb(err, undefined)
    else res((err, res) => {
      if (err) cb(err, undefined)
      else cb(undefined, res)
    })
  })
}


또 다른 예를 살펴 보겠습니다. 여기에서는 PokéAPI에서 일부 Pokémon 정보를 가져와 해당 능력을 기록하려고 합니다.


fetch('https://pokeapi.co/api/v2/pokemon/ditto')
|> map(res => res.json())
|> flatten
|> then(res => console.log(res.abilities))


async(() => {
  let res = await fetch('https://pokeapi.co/api/v2/pokemon/ditto')
  res = await res.json()
  console.log(res.abilities)
})()


결론 


요약하자면, 콜백 지옥을 초래하는 주요 문제인 것 같습니다.


  • 많은 상용구 코드
  • 중요한 코드와 심하게 얽힌 상용구 코드

우리의 작은 실험에 따르면, 두 번째 문제를 가장 간단한 방법 (다른 변경 없이 상용구 코드와 중요한 코드를 분리하는 것)으로 해결하는 것이 매우 중요했습니다.이를 통해 상용구 코드를 작은 유틸리티 함수로 묶고 상용구 코드의 비율을 줄일 수 있었습니다. 중요한 코드는 언어 자체에 새로운 구문을 추가하는 것과 같이 손이 많이 가는 솔루션 만큼 편리합니다.


이 개념은 특히 중요합니다. 제거 할 수 없는 추악한 구현 세부 정보와 상용구가 있을 수 있지만 항상 함께 번들로 묶어 실제 중요한 코드와 분리 할 수 ​​있으며 가장 간단한 방법으로도 이 작업을 수행 할 수 있습니다. 지옥 같은 상황을 하늘의 상황으로


오늘날 우리가 직면하고 있는 다른 유사한 문제에도 동일한 방법론을 적용 할 수 있다는 점도 주목할 만합니다. (대부분) 비동기 함수의 문제를 해결했지만 비동기 스트림 (비동기 함수와 비슷하지만 하나가 아닌 무한한 출력이 많을 수 있음)과 같은 새로운 구조가 계속해서 도구 상자에 들어가 유사한 문제 해결을 요구합니다.


https://dev.to/loreanvictor/from-callback-hell-to-callback-heaven-4i0c



댓글목록 0

등록된 댓글이 없습니다.

웹학교 로고

온라인 코딩학교

코리아뉴스 2001 - , All right reserved.