웹사이트 검색

javascript - 배열을 동기적으로 채우거나 수정하고 수정된 배열을 프로미스 안에서 반환


저는 ReactJS와 Redux에 꽤 새로워서 이전에는 이것을 다뤄본 적이 없습니다. 제 프로젝트에서 API 호출로 데이터를 가져오고 있습니다. 객체에 새로운 속성을 추가하여 데이터를 수정하고 싶습니다. 하지만 코드가 동기적으로 실행되지 않기 때문에 수정된 배열 대신 수정되지 않은 배열이 반환되는 것 같습니다.

export function loadThings() {
  return dispatch => {
    return dispatch({
     type: 'LOAD_THINGS',
     payload: {
       request: {
         url: API_GET_THINGS_ENDPOINT,
         method: 'GET'
       }
     }
    }).then(response => {
      let things = response.payload.data;
      // Retrieve all items for the loaded "things"
      if(things) {
        things.forEach((thing, thingIndex) => {
            things[thingIndex].items = []
            if (thing.hasOwnProperty('channels') && thing.channels) {
              thing.channels.forEach(channel => {
                if (channel.hasOwnProperty('linkedItems') && channel.linkedItems) {
                  channel.linkedItems.forEach(itemName => {
                    dispatch(loadItems(itemName)).then(
                      item => things[thingIndex].items.push(item) // push the items inside the "things"
                    )
                  })
                }
              })
            }
        })
      }
      things.forEach(data => console.log(data.items.length, data.items)) // data.items.length returns 0, data.items returns a populated array
      return things // return the modified array
    }).catch(error => {
      //todo: handle error
      return false
    })
  }
}

보시다시피, 저는 response라는 데이터를 반환하는 API 호출을 수행합니다. 배열은 모든 "things"으로 채워집니다. 만약 things가 존재하면, 저는 "items"라는 추가 정보를 불러오고 싶습니다. things 배열의 정보를 기반으로, 저는 또 다른 API 호출을 수행할 것입니다(이는 loadItems 함수를 디스패치하여 이루어집니다) 이 호출은 또 다른 promise를 반환합니다. 그 API 호출 결과의 데이터를 기반으로, 저는 things 객체의 items 속성(배열)에 값을 추가할 것입니다.

댓글에서 볼 수 있듯이, 제가 things 배열을 반복하고 방금 생성한 items 속성을 기록하면, 사실상 길이가 0으로 반환되는데, 이는 things 배열이 수정되기 전에 반환되고 있음을 의미합니다.

저는 두 가지를 알고 싶습니다:

  • 내 코드가 비동기로 실행되는 원인이 무엇인가요? dispatch(loadItems(itemName)) 함수가 프로미스를 반환하기 때문인가요?
  • 내 코드를 동기적으로 실행할 수 있는 방법은 무엇인가요?

참고: 이 함수 loadThings() 또한 프라미스를 반환합니다 (redux에 익숙하지 않은 경우).

내가 직접 코드를 고치기 위해 시도한 것을 알고 싶을 수도 있습니다

코드가 비동기로 실행되는 이유를 이해하지 못해서, 저는 희망 없는 시도를 하고 있었습니다. 예를 들어, 코드를 또 다른 Promise.all로 감싸고 그 프로미스 안에서 수정된 배열을 반환하는 것입니다. 그 프로미스의 then 메서드를 사용해 things 배열을 수정했지만, 결과는 똑같았습니다. 아마도 return things가 그 promise 외부에서 실행되기 때문일 것입니다.

무슨 일이 일어나고 있는지 정말 알고 싶어요

편집 요청에 따라 질문에 loadItems() 코드를 추가했습니다:

export function loadItems(itemName) {
  return dispatch => {
    const url = itemName ? API_GET_ITEMS_ENDPOINT + `/${itemName}` : API_GET_ITEMS_ENDPOINT;
    return dispatch({
      type: 'LOAD_ITEMS',
      payload: {
        request: {
          url: url,
          method: 'GET'
        }
      }
    }).then(response => {
      return response.payload.data
    })
  }
}

답변 #1

제 접근 방식은 things를 매핑하여 모든 항목에 대한 프라미스를 배열로 만들고 이를 Promise.all로 감싸는 것이며, 이는 Promise.all의 배열을 제공하게 됩니다.

그런 다음 things와 이 프로미스 배열을 또 다른 Promise.all에 반환하고, 다음 then 블록에서는 간단한 for 루프로 배열을 각 thing에 할당할 수 있습니다:

export function loadThings() {
  return dispatch => {
    return dispatch({
      type: 'LOAD_THINGS',
      payload: {
        request: {
          url: API_GET_THINGS_ENDPOINT,
          method: 'GET'
        }
      }
    }).then(response => {
      let things = response.payload.data;
      // Retrieve all items for the loaded "things"
      const items = things.map((thing) => {
        const thingItems = []
        if (thing.hasOwnProperty('channels') && thing.channels) {
          thing.channels.forEach(channel => {
            if (channel.hasOwnProperty('linkedItems') && channel.linkedItems) {
              channel.linkedItems.forEach(itemName => {
                thingItems.push(dispatch(loadItems(itemName)));
              });
            }
          });
        }

        return Promise.all(thingItems);
      });

      return Promise.all([things, Promise.all(items)])
    })
    .then(([things, thingItems]) => {
      things.forEach((thing, index) => {
        thing.items = thingItems[index];
      })

      return things;
    })
    .catch(error => {
      //todo: handle error
      return false
    })
  }
}

편집: dispatch(loadItmes(itemName)) 호출을 직접 thingItems 안에 넣어야 합니다.

답변 #2

다음과 같이 리팩토링할 수 있을 것 같아요:

export function loadThings() {
  return dispatch => {
    return dispatch({
     type: 'LOAD_THINGS',
     payload: {
       request: {
         url: API_GET_THINGS_ENDPOINT,
         method: 'GET'
       }
     }
    }).then(response => {
      let things = response.payload.data;
      // Retrieve all items for the loaded "things"
      if( things ) {
        return Promise.all( things.reduce( (promises, thing) => {
          if (thing.channels) {
            thing.items = [];
            promises.push( ...thing.channels.map( channel => 
              channel.linkedItems && 
              channel.linkedItems.map( item => 
                loadItems(item).then( result => thing.items.push( result ) ) 
              ) ).flat().filter( i => !!i ) );
          }
          return promises;
        }, []) );
      }
      return things;
    }).catch(error => {
      //todo: handle error
      return false
    })
  }
}

만약 things이 있다면, 이는 채널과 해당 채널의 linkedItems를 확인하고, 결과를 thing.items 배열로 다시 푸시할 약속(promise)을 생성할 것입니다.

Promise.all를 반환함으로써, loadThings의 후속 작업은 Promise.all이 해결될 때에만 완료됩니다. 만약 항목이 없다면, 단순히 항목이 반환됩니다(이는 falsy 값이 되기 때문에, 그 표현이 얼마나 유효할 수 있는지 궁금합니다).

사실 리팩토링을 직접 테스트해보진 않아서 상황에 맞게 조정해야 할 괄호가 있을 수도 있지만, 대략적인 아이디어는 줄 수 있을 것 같아요?