[2019-02-08][JavaScript] 자바스크립트의 awaited Promise 객체에 프로퍼티로 올바르게 접근하기

telnet turtle·2022년 11월 25일
0

개요

웹 프론트엔드쪽 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가 무슨 관계인지 잘 안다면 이해가 쉬워진다.

profile
프론트엔드 엔지니어

0개의 댓글