임시저장 구현해보기

da.circle·2023년 4월 4일
2

생각보다 오래걸린 임시저장 구현 로직을 정리해보려한다.
사용 라이브러리 및 언어 : React / TypeScript

레이아웃

  • 사용자로부터 제목, 내용, 좋아요 종류, 카테고리, 업로드 한 파일을 받는다.

임시저장 API

POST

  • 맨 처음 임시저장 API요청은 POST로 요청한다.
    (없던 게시물을 등록하는 과정이기 때문)
  • state로 첫 API 요청인지 아닌지를 구분한다.
    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();
});

PATCH

  • 두번째 임시저장 API요청부터는 PATCH로 요청한다.
    (처음에 POST 요청을 통해 DB에 저장한 임시저장 글을 PATCH로 수정)
  • 처음 POST요청이 성공하면 isFirstSavefalse로 저장해서 isFirstSave === false로 조건을 주면 된다.
  • POST요청의 응답으로 받은 feedId를 body에 추가한다.
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();
 });

임시저장 로직

  1. 화면에서 title, content, categoryId를 받아서 state에 저장한다.
  • 제목과 내용을 글자 제한때문에 내용 길이도 저장해준다.
  • 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);
};
  1. 임시저장 토스트 문구를 상황에 따라 나타내는 함수를 만든다.
  • title : 제목 입력받는 input value
  • content : 내용 입력받는 textarea value
  • isSaveSuccess : 임시저장 성공 여부
  • categoryId : 선택한 카테고리 Id
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]);
  1. 각 API를 함수로 만든다.
    saveFeedPost() , saveFeedPatch()

  2. 상황에 따른 API를 요청하는 함수를 만든다.

  • props로 title(input), content(textarea), category(li)ref.current를 받는다.
  • 제목 또는 내용이 빈 값이거나 카테고리Id가 0이라면(선택X) 메세지만 출력
  • 제목, 내용, 카테고리가 모두 값이 있고, isFirstSavetrue라면 POST요청
  • 제목, 내용, 카테고리가 모두 값이 있고, isFirstSavefalse라면 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;
  }
};
  1. 1분마다 saveFeed함수를 실행하는 코드를 짠다.
  • setInterval을 사용해서 1분마다 saveFeed함수를 실행하도록 한다.
  • useEffect의 의존성 배열에는 isFirstSave만 넣는다.
    → useEffect에 넣어서 첫 렌더링 후에 setInterval을 실행하도록 한다.

    의존성 배열에 isFirstSave만 넣는 이유
    자동 임시저장은 값들의 변화와는 상관없이 무조건 주기적으로 실행해야 한다고 생각한다.
    의존성 배열에 title, content 등을 넣어버리면 제목, 내용이 변할 때마다 setInterval이 초기화되므로 임시저장이 실행되지 않는다.
    isFirstSave는 처음 임시저장이 실행된 후에 값이 한 번만 바뀌고, 그 이후로는 값이 바뀌지 않는다.
    또한 useEffect가 단순히 빈 배열이면 isFirstSave 값의 변화를 인지하지 못하므로 넣어준다.

useEffect(() => {
  const showMessage = setInterval(() => {
    saveFeed(
      inputValueRef.current,
      textareaValueRef.current,
      selectRef.current
    );
  }, 60000);

  return () => clearInterval(showMessage);
}, [isFirstSave]);
  • setInterval을 사용하는 경우에는 clearInterval로 초기화를 해주어야 한다.

saveFeed에 props로 current를 넘겨주는 이유

처음 시도한 코드는 아래와 같다.
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 기본도 다시 공부해야 할 것 같다.

profile
프론트엔드 개발자를 꿈꾸는 사람( •̀ ω •́ )✧

0개의 댓글

관련 채용 정보