웹 프론트엔드쪽 API를 짜다가, await로 객체를 받아오며 동시에 destructuring하여 변수를 생성하고 싶었던 때가 있었다 (Destructuring Assignment = 해체 할당. (MDN에선 구조 분해 할당이라고 한다.)
당시 코드는 다음과 같은 구조로, 객체를 던지는 비동기 함수와 그 리턴값을 받아서 할당하려는 변수이다.
> const f = async () => ({ a: 1 })
undefined
> const g = async () => ({ b: 2 })
undefined
>
> (async () => { let [a, b] = await Promise.all([ f().a, g().b ]); console.log(`a=${a}, b=${b}`); })()
Promise {
<pending>,
domain:
Domain {
domain: null,
_events:
[Object: null prototype] {
removeListener: [Function: updateExceptionCapture],
newListener: [Function: updateExceptionCapture],
error: [Function: debugDomainError] },
_eventsCount: 3,
_maxListeners: undefined,
members: [] } }
> a=undefined, b=undefined
> (async () => { let [ { a }, { b } ] = await Promise.all([ f(), g() ]); console.log(`a=${a}, b=${b}`); })()
Promise {
<pending>,
domain:
Domain {
domain: null,
_events:
[Object: null prototype] {
removeListener: [Function: updateExceptionCapture],
newListener: [Function: updateExceptionCapture],
error: [Function: debugDomainError] },
_eventsCount: 3,
_maxListeners: undefined,
members: [] } }
> a=1, b=2
(Node.js 터미널 텍스트를 냅다 캡처했더니 가독성이 안 좋지만, 매의 눈으로 한번 봐주길 바란다!)
Node.js 인터프리터에서 당시 상황을 재현해보았다. 함수 f와 g는 비동기로 객체를 던지고, destructuring assignment을 두번 시도했다. 위에선 내 의도대로 동작하지 않고 밑에서는 성공적으로 console.log 한것을 볼 수 있다.
위에서 실패한 이유는 Promise 객체를 일반 객체로 착각하고서 다루었기 때문이다.
f()
는 Promise 객체를 반환한다. await
를 붙여주기 전까지는 Promise 객체다. 1번째 시도에서는 Promise 객체에 대고 .a
를 시도했으니 undefined를 받
는게 당연하다. Promise 객체에는 프로퍼티 a가 없기때문에.
a=undefined, b=undefined
두번째에서는 받아온 Promise 객체에 await
를 붙여준 다음에 { a }
로 destructuring을 했다. await로 일반 객체가 되었으므로 객체에 프로퍼티로 접근
할 수 있다.
a=1, b=2
Promise와 await에 익숙치 않다면 Promise.all과 같이 써놓아 보기에 헷갈릴 수 있다. 다음과 같이 좀더 단순한 코드를 보자. 함수 f는 앞에서와 같이 await
로 받을 객체를 던진다.
const f = async () => ({ data: 3 })
data를 변수에 할당하고 싶으면 await를 다음과 같이 붙여주자.
(async () => {
console.log((await f()).data) // prints '3'
const { data } = await f()
console.log({data}) // prints '{ data: 3 }'
})()
다시 한번 보자. 만약 await f().data
로 접근한다면 undefined을 받는다. Promise 객체에 data로 접근하기 때문이다. await를 적절하게 붙여주기만 해
도 버그가 없어진다.
2022-11-25 추가: 위의 예제가 잘 와닿지 않아서 다시 node.js로 작성해봤다.
> const f = async () => ({data:3})
undefined
> (async () => { console.log('A: ', (await f()).data, 'B: ', await f().data); })()
Promise { <pending> }
> A: 3 B: undefined
애초에 API가 { data: 'value' }
식으로 값을 주기 때문에 해체 할당을 해야한다. API 스펙이 문제라고 볼 수 있다. API를 변경해서 바로 값을 주도록
변경하면 문제가 해결될 것이다. 함수 콜에 await를 안붙이지만 않는다면…?
const fetchData = async () => {
const response = await axios.get('/api/url/fetch/data/from/server');
return response.data;
}
const handler = async () => {
const data = await fetchData() // API data
}
핸들러는 data를 받았으니 바로 사용하면 된다.
내 경우엔 이미 만들어진 API를 바로 변경하기엔 좋지 못한 상황이었다. 처음부터 덜 헷갈리게 API를 만들었으면 좋았을 것이다. 바꾸기 어렵다면 있는대
로 적절하게 사용해보자.
2022-11-25 추가: 내가 쓴 글인데 지금 읽어보니 되게 어렵다. 핵심은 Promise 래핑과 await 언래핑을 잘 파악하면 된다. async 함수는 Promise로 래핑된 객체를 반환하고, await 키워드는 언래핑을 해주는 것이다. 특히 Promise와 async/await가 무슨 관계인지 잘 안다면 이해가 쉬워진다.