일이 지났습니다. 시의성에 유의하세요
최근 Promise 관련 내용을 보다가 이 글을 발견했는데, 꽤 괜찮아서 기록해둡니다.
Promises 자체는 매우 간단합니다. 단지 핵심을 찾을 수만 있다면 말이죠. 아래는 Promise에 대해 정말로 Promise을 이해했는지 확인할 수 있는 혼동하기 쉬운 몇 가지 포인트입니다. 그중 몇 가지는 정말 저를 미치게 만들었죠.
중첩된 Promises
여러 Promise들이 서로 중첩되어 있는 경우:
1 | |
이렇게 작성한 이유는 두 Promise의 결과를 모두 처리해야 하기 때문입니다. 따라서 체이닝을 할 수 없는데, then() 메서드는 오직 이전 then()에서 반환된 결과만 받아들이기 때문입니다. (즉, 이 두 Promise는 동시에 처리되어야 하는 순차적 관계가 없지만, then은 순차적 관계가 있습니다. 만약 전자가 throw error라면 바로 catch 처리 단계로 넘어갑니다)
사실 이렇게 작성한 진짜 이유는 all() 메서드를 모르기 때문입니다:
이런 지저분한 코드를 해결하는 방법:
1 | |
훨씬 깔끔해졌습니다. q.all()는 promise 객체를 반환하고, 이 결과들을 배열로 결합하여 resolve 메서드에 전달합니다. 이후 then 메서드에서 호출할 때, spread() 메서드는 이 배열을 분할하여 DoSomethingOnThem 함수에 전달합니다.
(참고: Promise.all()은 promise들을 요소로 가지는 배열을 인자로 받습니다. 요소들 간에는 순서가 없이 동시에 실행되며, 최종적으로 then 메서드에 전달되는 값은 각 promise 메서드의 return 값들로 구성된 배열입니다. 여기서 작성자는 node의 q 모듈을 예시로 사용했습니다)
끊어진 체이닝
다음과 같은 코드가 있다고 가정해보세요:
1 | |
이 코드의 문제점은 somethingComplicated() 함수의 error에서 발생한 오류가 잡히지 않는다는 것입니다. Promises은 체이닝이 가능해야 함을 의미합니다(그렇지 않으면 then이라고 부를 이유가 없죠, 그냥 done를 사용하면 됩니다). 각각 호출된 then() 메서드는 새로운 promise를 반환하며, 이 새로운 promise는 다음 then() 메서드에서 계속 사용됩니다. 일반적으로 마지막 호출은 catch() 메서드여야 하며, 체인 어디에서든 발생한 error은 여기서 잡혀 처리됩니다.
위 코드에서는 첫 번째 promise를 반환할 때 체인이 끊어집니다. then으로 처리된 새로운 promise을 마지막 then 호출에 반환하지 않기 때문이죠(즉, then은 기존 promise를 변경하지 않으며, 단지 처리한 후 새로운 promise를 반환합니다).
이 문제를 해결하는 방법:
1 | |
기억하세요. 항상 마지막 then()의 결과를 반환해야 합니다(체이닝을 사용할 수 있도록).
혼란스러운 컬렉션
요소들로 구성된 배열이 있고, 이 배열의 각 요소에 대해 비동기 작업을 수행하고 싶습니다. 그래서 재귀 호출을 포함한 어떤 작업이 필요하다는 것을 알게 됩니다.
1 | |
음… 이 코드는 직관적이지 않습니다. 문제의 핵심은 체인의 길이를 알 수 없을 때 체이닝이 고통스러워진다는 점입니다. (JavaScript ES5+ 네이티브 배열 메서드) map()과 reduce()를 모른다면 말이죠.
해결책:
q.all 매개변수는 promise들로 구성된 배열이며, 결과를 배열로 모아 resolve 메서드에 전달합니다. 배열 요소 각각에 이 비동기 호출 메서드를 적용하기 위해 간단히 배열의 map 메서드를 사용할 수 있습니다. 다음과 같이요:
1 | |
처음의 재귀 호출 같은 비해결책과 달리, 이 코드는 배열의 각 요소를 동기적으로 비동기 호출 함수에 전달합니다. 시간적으로 훨씬 효율적이죠.
만약 promises을 순서대로 반환해야 한다면, reduce를 사용할 수 있습니다:
1 | |
완전히 깔끔하지는 않지만, 처음보다는 확실히 나아졌습니다.
유령 Promise
확정된 메서드(즉, Promise 실행 시작 시점에 이 메서드가 주어지며, 실행 결과에 따라 결정되는 것이 아닌 경우)가 있는데, 때로는 비동기 호출이 필요하고 때로는 필요하지 않을 수 있습니다. 따라서 두 경우를 모두 처리하기 위해 Promise를 생성하는데, 이는 비동기와 동기 경우 모두에서 코드 일관성을 유지하기 위함입니다(추상화와 분리를 위해). 실제로는 한 경우만 발생할 수 있더라도 말이죠.
1 | |
위 코드는 안티 패턴 중 가장 나쁜 경우는 아니지만, 더 명확하게 작성할 수 있습니다. Q()로 value 또는 promise을 감싸는 것이죠. Q() 메서드는 값과 promise 모두를 인자로 받습니다:
1 | |
참고: 처음에 이 경우에 Q.when() 사용을 권장했지만, Kris Kowal 님의 코멘트 덕분에 오류에서 벗어났습니다. Q.when()을 사용하지 마세요. Q()만으로 충분하며, 후자가 더 명확합니다.
소제목의 의미는
then에서fulfilled과rejected를 동시에 설정하여,rejected함수가then함수의 매개변수로 전달되는fulfilled의 오류를 처리할 수 있도록 하는 것을 기대했지만, 이는 불가능합니다.fulfilled의error는 다음then()으로만 전달될 뿐 현재rejected함수에 의해 처리될 수 없기 때문에, 이 소제목은 '과도한 갈망’으로 명명되었습니다—오류를 처리하고 싶어 하지만, 오류는 결코 이를 처리할 수 있도록 전달되지 않습니다—역자 주
then() 메서드는 두 개의 매개변수를 받습니다: fulfilled 상태를 조작하는 함수와 rejected 상태를 조작하는 함수입니다. 다음과 같은 코드를 작성해 본 적이 있을 것입니다:
1 | |
이렇게 작성할 때의 문제점은 fulfilled 상태에서 발생하는 error이 오류 처리 함수에 전달되지 않는다는 것입니다.
이 문제를 해결하는 방법은 오류 처리 함수를 별도의 then 메서드에 두는 것입니다:
1 | |
또는 catch()을 사용하는 방법도 있습니다:
1 | |
이렇게 하면 체인 호출 중 발생하는 모든 error이 처리될 수 있습니다.
잊혀진 Promise
어떤 메서드를 호출하면 promise가 반환되지만, 이 promise을 잊고 또 다른 promise을 생성하는 경우가 있습니다:
1 | |
이 코드는 정말로 promsie의 간결한 특성을 완전히 버린 것입니다—쓸모없는 코드가 너무 많습니다.
해결책은 단순히 promise을 반환하는 것입니다:
1 | |
저는 인생의 중요한 선택의 기로에 섰을 때, 누군가 최선의 방법을 알려주어 소중한 시간을 헛되이 보내지 않기를 바라곤 합니다. 그런 마음으로 저는 자주 블로그를 쓰며, 광활한 인터넷의 이 작은 구석에 제게는 단 한 번뿐인 인생 경험을 기록하여 도움이 필요한 분들에게 도움이 되기를 바랍니다.