useMutation을 이용해 병렬 처리하기(w. Promise.all)

gydotb·2025년 3월 2일
6

troubleshooting

목록 보기
3/3
post-thumbnail

useQueryuseQueries에 대해 쓰기도 전에 어쩌다보니 useMutation 작성 뻘짓 일기부터 쓰게 되었다😓 

하지만 이번에 안 쓰면 정말 다 까먹을 것 같았는걸…

기존 설계

Fetch 조건

Form 제출을 위한 API 호출 단계로, 내용에 대한 부분과 입력 완료에 대한 부분이 나뉘어져 있다. (총 2가지 API를 호출해야 한다.)

  1. 내용에 대한 경우, 수정 제출(PUT)과 새로운 입력 제출(POST)가 모두 있을 수 있다.

    예를 들어서, useFieldArray로 추가된 항목은 POST로 제출되어야 하고, 기존에 있던 항목의 경우 변동 사항이 있을 때 PUT으로 제출되어야 한다.

  2. 입력 완료의 경우 첫 제출(POST)일 수도 있고, 이전 제출 사항을 수정한 경우(PATCH)일 수도 있다.

개선 과정

1단계 : 순차적 API 호출

처음에는 단순히 순차적으로 API를 호출하는 방식으로 코드를 작성했다.

코드

const onSubmit = () => {
    if (getValues("postContent").length > 0) {
      postContent(getValues("postContent"));
    }

    /* 수정 요청 시, 변경 안된 field는 반영하지 않음. */
    if (getFieldState("putContent").isDirty) {
      let tempIsDone: ContentUpdateT[] = getValues("putContent").map(
        (v: ContentResponseT) => {
		        // 변경된 filed에 대한 처리
        }
      );
      let tempChange = getValues("changed");
      if (tempChange) {
        let tempChangeRq: ContentUpdateT[] = tempChange.map((v: ContentResponseT) => {
          // schema
        });
        putContent([...tempIsDone, ...tempChangeRq]);
      } else {
        putContent(tempIsDone);
      }
    }
 
    /* 제출 관련 API 호출 */
    if (isDoneBefore) {
      //수정된 경우
      toPatchIsDone();
    } else {
      toPostIsDone();
    }
    // 성공 시 로직
  };

개선 필요 사항

  1. 위의 코드에서 내용의 POSTPUT에 대한 필터링이 필요해지며 코드가 너무 길어졌으며 성공과 실패 관련 부분을 붙이기 어려웠다. (중복 코드 생성)
  2. 내용 업로드와 제출이 순서대로 수행되길 원했으나 현재는 그러지 못하고 있는 상황이었다. 즉, 내용 업로드가 되었을 때만 제출할 수 있도록 하고 싶었다.

따라서 위의 조건을 만족시키기 위하여 Promise.all을 활용해보기로 했다.

2단계 : fetch와 Promise.all을 활용한 병렬 fetching

fetch를 활용해 API 제출을 하는 방식임은 위와 동일하나, POST와 PUT에 대한 동시 호출 및 에러 핸들링을 가능해야 했기 때문에 Promise.all을 이용했다. 이 Promise.all에 대한 then-catch를 이용해 내용과 관련된 부분이 모두 완료되면 Form 입력 완료에 대한 API 호출을 하도록 설정했다.

Promise.all의 특징

이 메서드는 여러 프로미스의 결과를 집계할 때 유용하게 사용할 수 있습니다. 일반적으로 다음 코드를 계속 실행하기 전에 서로 연관된 비동기 작업 여러 개가 모두 이행되어야 하는 경우에 사용됩니다. 입력 값으로 들어온 프로미스 중 하나라도 거부 당하면 Promise.all()은 즉시 거부합니다.

MDN - Promise.all

코드

const onSubmit = () => {
    // flags
    let toPostContent = undefined;
    let toPutContent = undefined;
    let toPostIsDone = undefined;
    let toPatchIsDone = undefined;

		/* 로직 처리 */
    
    // API 요청 - 작업 목록
    Promise.all([
      toPostContent ? postContent(toPostContent) : null,
      toPutContent ? putContent(toPutContent) : null,
    ])
      .then(() => {
        // 성공 시, isDone 수정
        if (toPostIsDone) {
          postIsDone(toPostIsDone)
            .then(() => {
	             // 성공 시 로직
            })
            .catch((e) => {
              console.log(e);
            });
        } else if (toPatchIsDone) {
          patchIsDone(toPatchIsDone)
            .then(() => {
              // 성공 시 로직
            })
            .catch((e) => {
              console.log(e);
            });
        } else {
          // 업로드 실패
          throw new Error("work log data is not defined");
        }
      })
      .catch((e) => {
        console.log(e);
      });
  };

해당 코드의 문제점

useQuery를 이용하여 fetch를 수행함으로서 서버 데이터 컨트롤과 View 데이터 컨트롤을 분할하고 있었으나, useMutation을 사용하지 않아 그 경계가 모호해졌다.

3단계 : mutateAsync의 도입

코드 리뷰에서 mutateAsync 에 대해 언급해주셔서 관련 자료를 찾아보았다.

mutationAsync와 일반 mutate는 어떻게 다를까?

mutate는 아무 것도 반환하지 않는 반면, mutateAsync는 변형의 결과를 포함하는 프로미스를 반환합니다. 그래서 변형 응답에 접근해야 할 때 mutateAsync를 사용하고 싶을 수 있지만, 저는 거의 항상 mutate를 사용해야 한다고 주장하고 싶습니다.

콜백을 통해 data나 error에 여전히 접근할 수 있으며, 오류 처리에 대해 걱정할 필요가 없습니다. mutateAsync는 프로미스 제어권을 개발자에게 넘기기 때문에, 수동으로 오류를 잡아야 하며, 그렇지 않으면 처리되지 않은 프로미스 거부를 받을 수 있습니다.

mutateAsync가 더 우수한 상황은 정말로 프로미스가 필요한 경우입니다. 이는 여러 변형을 동시에 발동시키고 모두 완료되기까지 기다리고 싶거나, 콜백으로 인한 콜백 지옥에 빠질 수 있는 종속적인 변형이 있는 경우에 필요할 수 있습니다.

(번역) Mastering Mutations in React Query

즉, mutateAsync를 사용한 이유는 Promise 형태로 이용하기 위해서였고 try-catch문을 통해 이를 구성할 수 있었다.

mutate 코드

/* 
  mutations
  - todoMutation : todo에 대한 POST OR PUT 수행, Promise.all로 병렬 동작
  - logMutation : worklog에 대한 POST OR PATCH 수행
   */
  const { mutateAsync: contentMutate } = useMutation({
    mutationFn: async (data: {
      toPostContent: ContentRequestT[] | undefined;
      toPutContent: ContentUpdateT[] | undefined;
    }) => {
      await Promise.all([
        data.toPostContent ? postContent(data.toPostContent) : null,
        data.toPutContent ? putContent(data.toPutContent) : null,
      ]);
    },
    onError: () => {
      // error 처리
    },
  });

  const { mutateAsync: isDoneMutate } = useMutation({
    mutationFn: async (data: {
      toPostIsDone: RequestIsDoneT | undefined;
      toPatchIsDone: PatchIsDoneT | undefined;
    }) => {
      if (data.toPostIsDone) {
        await postIsDone(data.toPostIsDone);
      } else if (data.toPatchIsDone) {
        await patchIsDone(data.toPatchIsDone);
      } else {
        throw new Error("work log data is not defined");
      }
    },
    onSuccess: () => {
      // 성공 시 로직
    },
    onError: () => {
	    // error 처리
      });
    },
  });
   

onSubmit code

/* 최종 제출(mutation 포함) */
  const onSubmit = async () => {
     // flags
    let toPostContent = undefined;
    let toPutContent = undefined;
    let toPostIsDone = undefined;
    let toPatchIsDone = undefined;

    /* 로직 처리 */
   
    // mutation을 이용한 API 비동기 호출
    // mutation 중 하나라도 오류 발생 시, catch
    try {
      await contentMutate({
        toPostContent: toPostContent,
        toPutContent: toPutContent,
      });
      await isDoneMutate({
        toPostIsDone: toPostIsDone,
        toPatchIsDone: toPatchIsDone,
      });
    } catch (error) {
      console.log(error);
    }
  };

결론 및 추후 개선 방안

Promise와 동기 비동기는 항상 어려운 기분이다. 뭐든 끝은 없다지만 정말 얘는 가장 끝이 없는 기분이랄까.

그래서 그런지 아직 이 코드만으로 완전히 완성된 단계는 아니라고 생각된다. mutation에 대한 이해가 아직 부족해 제대로 코드 분리를 하지 못한 것도 있고, 보다 나은 제출 방법이 있을 거라 생각되어 이외의 방법도 찾아보고자 한다.

특히 mutateAsync를 제대로 이해하지 못해... 이 부분에 대해서 좀 더 테스트 해보고 답을 얻고자 한다!

혹시 좋은 아이디어가 있다면 댓글로 공유해주세요…🥺

profile
프론트엔드 개발이 좋다 왜냐하면 프론트엔드 개발이 좋기 때문이다.

0개의 댓글