React-query와 useState 상태관리 간의 비동기 타이밍 이슈 (문제 해결)

Devinix·2023년 9월 13일
0

[문제 해결]

목록 보기
2/29
post-thumbnail

개요

리액트를 활용해 admin 페이지를 개발하던 도중이었다. react-quill을 통해 이미지 업로드 기능을 추가하려고 할 때, 파라미터로 전달하기 위한 ikey 값을 리액트 쿼리를 이용해 비동기적으로 서버로부터 받아와야 했다. 이 ikey는 이미지 저장 위치의 고유 식별자로써 활용되었다. 나는 ikey 값을 상태로 관리하고, 그 상태를 imageHandler 함수에 전달하여 이미지를 업로드하려고 시도했다. 그런데 실행 결과 ikey 값이 기대했던 값이 아닌 초기 값(빈 값)으로 나타났다.

코드를 살펴보자.

// 데이터를 관리하기 위한 state
const [values, setValues] = useState({
  email: "",
  ikey: "",
});


// 선택된 이미지 파일을 관리하기 위한 state
const [selectedFile, setSelectedFile] = useState<object | null>(null);


// ReactQuill 에디터에 접근하기 위한 ref
const quillRef = useRef<ReactQuill | null>(null);


// 이메일을 sessionStorage에서 가져와서 value값에 전달
useEffect(() => {
  const emailFromStorage = sessionStorage.getItem("email");
  if (emailFromStorage) {
    setValues((prevValues) => ({
      ...prevValues,
      email: emailFromStorage,
    }));
  }
}, []);


// email 값을 기반으로 아이템 키를 가져오는 react-query mutation
const getItemKeyMutation = useMutation({
  mutationFn: () => {
    return getItemKeyApi(values.email);
  },
  onSuccess: (response) => {
    const ikeyData: IItemKeyResponse = response.data;
    setValues((prevValues) => ({
      ...prevValues,
      ikey: ikeyData.ikey,
    }));
    console.log("ikey data :", data);
  },
});


// email 값이 변경될 때마다 getItemKeyMutation을 실행
useEffect(() => {
  if (values.email) {
    getItemKeyMutation.mutate();
  }
}, [values.email]);


// 에디터에서 이미지 버튼 클릭 시 이미지 업로드를 위한 핸들러
const imageHandler = (): void => {

  // 이미지를 저장할 input type=file DOM 생성
  const input = document.createElement("input");
  input.setAttribute("type", "file");
  input.setAttribute("accept", "image/*");
  input.click();

  // input에서 파일 선택 변경 이벤트 리스너 추가
  input.addEventListener("change", async () => {
    console.log("onChange");

    if (input.files && input.files.length > 0) {
      try {
        setSelectedFile(input.files[0]);

        const formData = new FormData();
        
        console.log("전송 될 ikey: ", values.ikey);
        
        formData.append("ikey", values.ikey);
        formData.append("email", values.email);
        formData.append("file", input.files[0]);

        const config = {
          mod: "cors",
          headers: {
            "Content-Type": "multipart/form-data",
          },
        };

        // 이미지 파일을 백엔드 서버에 업로드
        const result = await axios.post("/itempic.upload", formData, config);

        console.log("백엔드로부터 내려받는 데이터", result.data.pkey);
        const IMG_URL = "./itempic.get/" + result.data.pkey;

        // Quill 에디터에서 이미지를 삽입
        const editor: any = quillRef?.current?.getEditor();
        if (editor) {
          const range = editor.getSelection();
          editor.insertEmbed(range.index, "image", IMG_URL);
        }
      } catch (error) {
        console.log(error);
      }
    }
  });
};


코드를 간단하게 요약해보겠다.

  1. 사용자의 이메일 주소를 기반으로 ikey를 api 요청하여 가져온다. (react-query 사용)

  2. 사용자가 ReactQuill 에디터의 이미지 버튼을 클릭하면, 이미지 파일을 선택하고 서버에 업로드한다. (이 때, 파라미터중 하나로 ikey가 사용된다.)

  3. 업로드된 이미지의 url을 받아와서, ReactQuill 에디터에 이미지를 삽입한다.

문제 상황

나는 위 과정에서 react-query를 이용하여 받아온 ikey를 values에 업데이트 하였고, 업데이트 된 values의 ikey를 파라미터들 중 하나로 사용해 서버에 요청을 보내려고 했으나, 무슨 이유인지 아무리 시도해도 ikey가 빈 값으로 전달되는 문제가 발생했다


// imageHandler 함수 내부 입니다. (서버에 요청을 보내는 부분)

setSelectedFile(input.files[0]);

const formData = new FormData();

// !! 빈 값으로 출력됨
console.log("전송 될 ikey: ", values.ikey);

// 서버에 전송 될 파라미터의 내용
formData.append("ikey", values.ikey);
formData.append("email", values.email);
formData.append("file", input.files[0]);

const config = {
  mod: "cors",
  headers: {
    "Content-Type": "multipart/form-data",
  },
};

// 이미지 파일을 백엔드 서버에 업로드
const result = await axios.post("/itempic.upload", formData, config);

콘솔도 확인해봤지만, 계속해서 빈 값만 출력되었다.

원인

이 문제는 useState 상태 업데이트의 비동기적 특성과 react-query의 비동기 작업 사이의 타이밍 차이로 인해 발생했다. 상태 업데이트는 비동기적으로 이루어지기 때문에, setValues 함수를 호출하더라도 상태는 즉시 업데이트되지 않는다. 또한, react-query를 통해 데이터를 가져오는 작업 역시 비동기적으로 처리되기 때문에, 이 두 작업 간의 타이밍 차이로 인해 ikey 값이 적절하게 업데이트되기 전에 함수로 전달된 것이었다.

해결 과정

이 문제를 해결하기 위해 react-query의 상태를 직접 참조하는 방식으로 코드를 수정했다. useState의 set함수로 상태를 업데이트 하여 파라미터로 전달하는 대신, react-query의 반환 값인 data를 직접 사용하여 ikey 값을 파라미터로 전달했다. 이 과정을 통해 react-query에서 가져온 최신의 ikey 값을 항상 참조할 수 있게 되었다.


수정된 코드를 보자.

// !! ikey값을 직접 참조하기 위해 react-query의 반환데이터를 ikeyData라는 이름으로 저장
const ikeyData = getItemKeyMutation.data?.data.ikey;


// 에디터에서 이미지 버튼 클릭 시 이미지 업로드를 위한 핸들러
const imageHandler = (): void => {

  // 이미지를 저장할 input type=file DOM 생성
  const input = document.createElement("input");
  input.setAttribute("type", "file");
  input.setAttribute("accept", "image/*");
  input.click();

  // input에서 파일 선택 변경 이벤트 리스너 추가
  input.addEventListener("change", async () => {
    console.log("onChange");

    if (input.files && input.files.length > 0) {
      try {
        setSelectedFile(input.files[0]);

        const formData = new FormData();
        
        
        // !! react-query 반환 데이터인 ikeyData를 그대로 전달
        console.log("전송 될 ikey: ", ikeyData);
        
        formData.append("ikey", ikeyData);
        formData.append("email", values.email);
        formData.append("file", input.files[0]);

        const config = {
          mod: "cors",
          headers: {
            "Content-Type": "multipart/form-data",
          },
        };

        // 이미지 파일을 백엔드 서버에 업로드
        const result = await axios.post("/itempic.upload", formData, config);

        console.log("백엔드로부터 내려받는 데이터", result.data.pkey);
        const IMG_URL = "./itempic.get/" + result.data.pkey;

        // Quill 에디터에서 이미지를 삽입
        const editor: any = quillRef?.current?.getEditor();
        if (editor) {
          const range = editor.getSelection();
          editor.insertEmbed(range.index, "image", IMG_URL);
        }
      } catch (error) {
        console.log(error);
      }
    }
  });
};

결론

문제의 핵심은 ikey 값을 가져오고 사용하는 방식에 있었다. 초기 코드에서는 비동기적으로 ikey 값을 가져와 상태에 저장하는 과정이 있었기 때문에, 이 값이 올바르게 설정되기 전에 다른 코드가 실행될 수 있는 타이밍 이슈가 발생했다. 반면, 수정된 코드에서는 react-query의 상태를 직접 참조하여 이러한 타이밍 이슈를 회피했다. 비동기 연산과 상태 관리는 React에서 핵심적인 주제 중 하나이다. 그러나 이 둘을 결합할 때 주의가 필요하다는 것을 이 문제를 통해 알게 되었다.

profile
프론트엔드 개발

0개의 댓글