리액트를 활용해 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);
}
}
});
};
코드를 간단하게 요약해보겠다.
사용자의 이메일 주소를 기반으로 ikey를 api 요청하여 가져온다. (react-query 사용)
사용자가 ReactQuill 에디터의 이미지 버튼을 클릭하면, 이미지 파일을 선택하고 서버에 업로드한다. (이 때, 파라미터중 하나로 ikey가 사용된다.)
업로드된 이미지의 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에서 핵심적인 주제 중 하나이다. 그러나 이 둘을 결합할 때 주의가 필요하다는 것을 이 문제를 통해 알게 되었다.