생각보다 오래걸린 임시저장 구현 로직을 정리해보려한다.
사용 라이브러리 및 언어 : React / TypeScript
const [isFirstSave, setIsFirstSave] = useState(true);
axios
.post<SaveResultType>(
url,
{
title: titleValue,
content: contentValue,
estimation: selectedLike,
category: selectCategory,
fileLinks: fileLink,
},
{ headers: { Accept: `application/json`, Authorization: token } }
)
.then(response => {
setIsFirstSave(false);
setIsSaveSuccess(true);
saveAlertMessage();
setFeedId(response.data.result.id);
return;
})
.catch(error => {
setIsSaveSuccess(true);
saveAlertMessage();
});
isFirstSave
를 false
로 저장해서 isFirstSave === false
로 조건을 주면 된다.axios
.patch<SaveResultType>(
url,
{
feedId: feedId,
title: titleValue,
content: contentValue,
estimation: selectedLike,
category: selectCategory,
fileLinks: fileLink,
},
{ headers: { Accept: `application/json`, Authorization: token } }
)
.then(response => {
setIsFirstSave(false);
setIsSaveSuccess(true);
saveAlertMessage();
return;
})
.catch(error => {
setIsFirstSave(false);
setIsSaveSuccess(false);
saveAlertMessage();
});
getTitle
, getContent
: onchange
handleSelectChange
: onclick
const [title, setTitle] = useState('');
const [titleLength, setTitleLength] = useState(0);
const [content, setContent] = useState('');
const [contentLength, setContentLength] = useState(0);
const getTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
setTitleLength(e.target.value.length);
setTitle(e.target.value);
};
const getContent = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setContentLength(e.target.value.length);
setContent(e.target.value);
};
const handleSelectChange = (categoryId: number) => {
setCategoryId(categoryId);
};
useEffect(() => {
if (title.trim() === '' || content.trim() === '') {
setSaveMessage('제목과 내용을 입력해주세요.');
return;
}
if (categoryId === 0) {
setSaveMessage('카테고리를 선택해주세요.');
return;
}
if (isSaveSuccess) {
setSaveMessage('임시저장되었습니다.');
return;
}
if (isSaveSuccess === false) {
setSaveMessage('임시저장에 실패했습니다.');
return;
}
}, [title, content, isSaveSuccess, categoryId]);
각 API를 함수로 만든다.
saveFeedPost()
, saveFeedPatch()
상황에 따른 API를 요청하는 함수를 만든다.
title(input)
, content(textarea)
, category(li)
의 ref.current
를 받는다.isFirstSave
가 true
라면 POST요청isFirstSave
가 false
라면 PATCH요청const inputValueRef = useRef<HTMLInputElement>(null);
const textareaValueRef = useRef<HTMLTextAreaElement>(null);
const selectRef = useRef<HTMLLIElement>(null);
const saveFeed = (
inputValueRef: HTMLInputElement | null,
textareaValueRef: HTMLTextAreaElement | null,
selectValueRef: HTMLLIElement | null
) => {
const titleValue = inputValueRef?.value.trim();
const contentValue = textareaValueRef?.value.trim();
const selectCategory = selectValueRef?.value;
if (!titleValue || !contentValue || selectCategory === 0) {
saveAlertMessage();
return;
}
if (titleValue && contentValue && isFirstSave && selectCategory !== 0) {
saveFeedPost(titleValue, contentValue, selectCategory);
return;
}
if (
titleValue &&
contentValue &&
isFirstSave === false &&
selectCategory !== 0
) {
saveFeedPatch(titleValue, contentValue, selectCategory);
return;
}
};
의존성 배열에 isFirstSave만 넣는 이유
자동 임시저장은 값들의 변화와는 상관없이 무조건 주기적으로 실행해야 한다고 생각한다.
의존성 배열에 title, content 등을 넣어버리면 제목, 내용이 변할 때마다 setInterval이 초기화되므로 임시저장이 실행되지 않는다.
isFirstSave는 처음 임시저장이 실행된 후에 값이 한 번만 바뀌고, 그 이후로는 값이 바뀌지 않는다.
또한 useEffect가 단순히 빈 배열이면 isFirstSave 값의 변화를 인지하지 못하므로 넣어준다.
useEffect(() => {
const showMessage = setInterval(() => {
saveFeed(
inputValueRef.current,
textareaValueRef.current,
selectRef.current
);
}, 60000);
return () => clearInterval(showMessage);
}, [isFirstSave]);
처음 시도한 코드는 아래와 같다.
saveFeedPost와 saveFeedPatch에는 state로 관리하는 title, content, categoryId를 사용했다.
useEffect(() => {
const showMessage = setInterval(() => {
saveFeed();
}, 60000);
return () => clearInterval(showMessage);
}, []);
문제 발생1 )
막상 실행을 해보니 useEffect 안에서 title, content, categoryId, isFirestSave 값을 초기값으로만 인식하고, 값이 변하더라도 계속 초기값만 사용해서 saveFeed를 호출했다.
하지만 의존성 배열에 저 값들을 넣으면, 사용자가 한 글자 입력하면 setInterval이 처음부터 1분을 카운트다운하기 때문에 사용자가 제목과 내용을 입력하는 동안에는 임시저장이 되지 않았다.
해결1 )
useRef를 통해 가져온 ref를 saveFeed 함수에 props로 넘겼다.(4,5번 코드 참고)
useEffect의 의존성 배열이 빈 배열이어도 title, content 등의 입력값을 실시간으로 잘 받아와서 임시저장이 정상적으로 실행되었다!
참고) React에서 setInterval 사용하기 - iborymagic.tistory
문제 발생2 ) 값이 변하는 걸 실시간으로 임시저장하는 문제는 해결했지만, 처음 POST 요청 후에는 PATCH로 요청해야 하는데, POST요청만 1분마다 실행되고 있었다.
확인을 해보니, isFirstSave가 true에서 false로 바뀐걸 useEffect에서 알지 못하는 상태였다. 이걸 ref로 가져올 수도 없고..
해결2 )
그냥 의존성 배열에 넣었다. 어차피 게시물 페이지 접속 후에 단 한번만 값이 바뀌는 state고, 값이 바뀌는 타이밍도 처음 요청이 성공한 직후이므로 사용자 입장에서는 1분마다 계속 실행하는 것처럼 보일 것이라고 생각했기 때문이다.
로직 자체는 복잡하지 않은데.. useEffect 의존성 배열에 아무것도 넣지 않고 input과 content의 최신 값을 인식하도록? 해야하는 것이 어려웠다.
(설명한 코드 이외에도 코드가 너무 길고 많아서 리팩토링을 1순위로 해야겠다😅)
리액트 공부가 많이 부족하다고 느꼈다. 기능 구현도 좋지만 React 기본도 다시 공부해야 할 것 같다.