Promise.all로 데이터를 순서대로 fetch 하기

ChaeChae·2022년 11월 1일
0

회사에서 재활 운동 관련 서비스를 개발하고 있는데 여러 개의 운동 세트와 세트별 운동 목록을 보여주는 페이지가 있다. 세트 정보와 그 안에 포함된 운동의 정보를 한 번에 보내주는 API가 있으면 참 좋겠지만, 지난 글에 언급했던 것처럼 외주업체에서 만든 API를 사용하고 있어 원하는 API가 없었다. 세트 목록을 받아오는 API와 세트 id로 해당 세트 내 운동 목록을 받아오는 API가 분리되어 있었다. 그래서 두 API를 조합하여 다음과 같이 세트 순서대로 운동 목록이 들어있는 중첩된 배열을 만들고자 했다.

[
  [운동1-1, 운동1-2, 운동1-3], // 세트 1
  [운동2-1, 운동2-2, 운동2-3, 운동2-4, 운동2-5], // 세트 2
  // ...
];

배열 순서대로 fetch 하기

위와 같은 형태의 데이터를 만들기 위해 가장 먼저 떠올린 방법은 아래와 같다. 세트 목록을 먼저 받아오고, 세트 목록을 순회하며 해당 세트 내 운동 목록을 받아오는 것이다. 하지만 원하는 결과가 나오지 않았다. fetch가 비동기 함수이기 때문에 데이터가 세트 순서대로 담기지 않았다.

const [setList, setSetList] = useState([]);
const [exerciseList, setExerciseList] = useState([]);

// 세트 목록 받아오기
fetch(url)
  .then((json) => {
    // 세트 순서대로 순회
    for (let i = 0; i < json.length; i++) {
      // 세트 목록 저장
      setSetList((prev) => [...prev, json[i]]);

      // 현재 세트 내 운동 목록 받아오기
      fetch(url)
        .then((json) => {
          // 세트 순서대로 운동 목록 저장
          setExerciseList((prev) => [...prev, json]);
        });
    }
  });

레거시 코드

내가 입사하기 전에 외주업체에서 기능 개발에 참고하라고 보내준 코드가 있다. 혹시나 이 부분에 대한 해결책이 있을까 해서 찾아봤는데 아래와 같이 구현을 해놓았다. 배열을 큐처럼 사용하여 운동 세트 id를 뒤에 push하고, 맨 앞의 데이터를 꺼내 fetch 요청에 담아 보내고 있다. 무슨 이유인지 재귀함수까지 사용했는데, 이렇게까지 복잡하게 생각할 필요는 없을 것 같았다. 로직을 파악하기 어렵고 가독성이 떨어져 다른 방법을 찾아보기로 했다.

// 세트 정보 쿼리 결과 수신 대기 중 여부(true면 대기중)
const [waitQuerySet, setWaitQuerySet] = useState(false);
// 세트와 세트 내 운동 목록의 순서를 맞추기 위한 큐(reqest body에 들어갈 데이터)
const reqBodyQueue = [];
// body는 큐에 계속 쌓되 fetch 재귀 호출은 한 번만 실행
function queueBodyData(bodyData) {
  reqBodyQueue.push(bodyData);
  if (reqBodyQueue.length === 1) {
    fetchFromQueue();
  }
}

if (waitQuerySet === false && setList.length === 0) {
  setWaitQuerySet(true);

  fetch(`${BASE_URL}/set`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
    credentials: "include",
  })
    .then((res) => res.json())
    .then((json) => {
      for (let i = 0; i < json.length; i++) {
        setSetList((prev) => [...prev, json[i]]);
        queueBodyData({ setId: json[i].id });
      }
    });
}

function fetchFromQueue() {
  if (reqBodyQueue.length > 0) {
    fetch(`${BASE_URL}/exercise`, {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(reqBodyQueue[0]),
      credentials: "include",
    })
      .then((res) => res.json())
      .then((json) => {
        setExerciseList((prev) => [...prev, json]);
        reqBodyQueue.shift();
        fetchFromQueue();
      });
  } else setWaitQuerySet(false);
}

useEffect와 Promise.all 사용하기

해답은 의외로 간단하게 Promise에 있었다. fetch는 비동기 함수라 Promise 객체를 리턴하는데, Promise 클래스에는 여러 개의 promise가 담긴 배열을 받아 병렬적으로 처리하는 유용한 메서드들이 있다(MDN - Promise concurrency 참고). 그중 Promise.all()은 지금처럼 데이터를 모두 받아온 후 렌더링해야 하는 상황에서 유용하게 쓸 수 있다. 또한 세트별 운동 목록은 세트 목록에 대해 의존성을 가지므로 각각 useEffect를 사용하고, 운동 목록을 받아오는 useEffect의 dependency array에 setList를 추가해주었다.

  1. 첫 번째 useEffect에서 세트 목록을 받아온다.
  2. setList가 업데이트되면 두 번째 useEffect가 실행되어 운동 목록을 받아온다.
    2-1. setList를 map으로 돌면서 각 세트의 id로 운동 목록을 받아와 promises 배열을 생성한다.
    2-2. Promise.all에 promises 배열을 인자로 넣으면 배열의 순서 그대로 데이터를 얻을 수 있다.
useEffect(() => {
  fetch(`${BASE_URL}/set`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
    credentials: "include",
  })
    .then((res) => res.json())
    .then((json) => {
      setSetList(json);
    })
    .catch((err) => console.log(err));
}, []);

useEffect(() => {
  const promises = setList.map((set) =>
    fetch(`${BASE_URL}/exercise`, {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ setId: set.id }),
      credentials: "include",
    })
      .then((res) => res.json())
      .catch((err) => console.log(err))
  );

  Promise.all(promises).then((json) => {
    setExerciseList(json);
  });
}, [setList]);

함수를 추출하여 리팩터링하기

위의 코드도 잘 작동하지만, fetch 관련 로직이 반복되어 리팩터링을 하고 싶었다. 반복되는 부분을 추출하여 별도의 파일에 getData라는 함수를 만들었다. 동료 개발자에게도 이 함수를 사용할 수 있도록 알려주었다.

const getData = ({ apiEndpoint, body }) =>
  fetch(`${BASE_URL}${apiEndpoint}`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
    credentials: "include",
  })
    .then((res) => res.json())
    .catch((err) => console.log(err));

getData 함수를 사용하니 불필요한 반복이 줄어들고 API 요청에 필요한 파라미터도 쉽게 확인할 수 있었다. 레거시 코드에서 49줄로 장황하게 구현했던 로직을 16줄로 정리하니 훨씬 가독성 좋고 깔끔한 코드가 되었다.

useEffect(() => {
  getData({
    apiEndpoint: "/set",
  }).then(setSetList);
}, []);

useEffect(() => {
  const promises = setList.map((set) =>
    getData({
      apiEndpoint: "/exercise",
      body: { setId: set.id },
    })
  );

  Promise.all(promises).then(setExerciseList);
}, [setList]);
profile
정리 장인 && FE 개발자

0개의 댓글