오늘 배운 것

react query의 동작 방식에 대해서는 여전히 잘 모르지만, 일단은 코드를 작성해야 하는 상황이다. 다행히 다른 팀원이 작성해 둔 코드가 있어서 그걸 참고해 보려고 한다. 오늘 개발해야 할 부분은 투두나 서브투두를 클릭할 때 나오는 모달에서 투두나 서브투두를 삭제하는 로직을 react query로 작성하는 것이다. 기존 코드는 fetch()를 사용해서 API를 호출하고 있었는데, 이는 똑같이 동작하긴 하지만 중복된 코드가 너무 많아서 코드가 복잡해진다는 단점이 있었다. 그래서 똑같이 동작하는 코드를 다르게 짜면 되겠다. 

 

우선 투두 모달(TodoModal.jsx) 컴포넌트에서 사전에 작성해 둔, 필요한 리액트 커스텀 훅(useTodoDeleteMutation)을 불러온다. 이때 useTodoDeleteMutation이라는 커스텀 훅은 tanstack query의 useMutation()을 사용해서 만든 커스텀 훅으로, mutate라는 함수를 기본적으로 리턴한다. 이 {mutate: deleteTodo} 부분은 해당 함수를 mutate라는 이름으로 사용하면 여러 커스텀 훅을 사용할 때 헷갈릴 수 있으니, 함수에 deleteTodo라는 alias(다른 이름)를 준다는 의미이다. 

 

그리고 deleteTodoIsSuccess는 마찬가지로 해당 커스텀 훅에서는 훅이 성공적으로 실행되었는지의 여부를 isSuccess라는 변수에 담아 리턴하는데, 한 컴포넌트 내에서 여러 훅을 사용하는 경우 이름이 중복될 수 있으니 해당 변수에 deleteTodoIsSuccess 라는 alias를 준다는 의미이다. 

const { mutate: deleteTodo, isSuccess: deleteTodoIsSuccess } = useTodoDeleteMutation();

 

그 다음에 해당 컴포넌트(TodoModal) 내에서 useEffect 기본 훅을 사용해서, 훅이 성공적으로 실행되면 실행되게 하고 싶은 로직을 안에 정의해 준다. 

useEffect(() => {
    if (deleteTodoIsSuccess) {
      queryClient.invalidateQueries(TODO_QUERY_KEY);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [deleteTodoIsSuccess]);

 

그러면 이제 컴포넌트 내에서 해당 커스텀 훅을 사용할 수 있고, 훅이 성공적으로 실행되었을 경우 추가로 다른 로직이 실행되도록 할 수도 있다. 이제는 아까 deleteTodo라는 alias로 받아온 함수를 직접 사용해주면 된다. 

const handleDelete = async item_id => {
    if (isTodo) {
      deleteTodo({ accessToken: accessToken, todoId: item_id });
    } else {
      deleteSubTodo({ accessToken: accessToken, subTodoId: item_id });
    }
    setVisible(false);
  };

 

해당 함수에 파라미터를 넘겨줄 때는, 함수의 커스텀 훅(useTodoDeleteMutation)의 mutationFn이라는 속성의 값으로 정의된 함수(아래에 나올 deleteTodoFetcher)에서 명시되어 있는 파라미터를 그대로 넘겨줘야 한다. 

 

이제 커스텀 훅인 useTodoDeleteMutation의 코드를 보자. 

const deleteTodoFetcher = async ({ accessToken, todoId }) => {
  const data = await Api.deleteTodo({ accessToken, todoId });
  return data;
};

export const useTodoDeleteMutation = () => {
  return useMutation({
    mutationFn: deleteTodoFetcher,
    onError: error => {
      console.error('Error deleting todo:', error);
    },
  });
};

 

이 커스텀 훅에서는 useMutation()이라는 tanstack query의 내장 함수를 이용해서 위의 컴포넌트에 mutate 함수와 기타 props(isSuccess 등)를 리턴해 준다. 이때 useMutation()을 사용하면서 기본적으로 mutationFn이라는 속성을 정의해 주어야 하는데, 이는 비동기 태스크를 실행하고 Promise를 리턴할 함수, 즉 useMutation으로 실행하고 싶은 비동기 함수의 이름이다. 

공식문서에서 나온 useMutation에서의 mutationFn의 소개

 

여기서는 deleteTodoFetcher이라는 비동기 함수를 선언하였고, 안에는 Api라는 레포에서 다른 팀원이 따로 만들어 둔 클래스를 통해 API 서버의 deleteTodo API를 호출하는 로직이 담겨져 있다. 여기에서 파라미터로 accessToken과 todoId를 받기 때문에, 앞서 컴포넌트에서 accessToken과 todoId를 파라미터로 넣어 준 것이다. 

 

마지막으로 Api의 deleteTodo 로직을 보면 다음과 같다. 

deleteTodo: ({ accessToken, todoId }) => {
    return handleRequest(() =>
      axios.request({
        url: API_PATH.todos,
        method: 'DELETE',
        headers: metadata(accessToken),
        data: { todo_id: todoId },
      }),
    );
  },

 

handleRequest 역시 따로 개발하면서 정의해 둔 함수로, 함수를 호출할 때 에러 처리를 하도록 try/catch로 한번 감싸 준 함수라고 생각하면 된다. 

 

즉 앞으로 API 서버에서 새로운 API를 호출하고 싶다면, Api 클래스에서 새 함수를 정의하고, 이를 커스텀 훅으로 만든 다음, 이걸 사용하려는 컴포넌트에서 훅을 호출하는 로직을 작성해 주면 되겠다. 여러 파일을 거쳐서 처음에 로직을 파악하는 것은 조금 복잡할 수 있지만, 이전의 한눈에 보기 불편하고 중복이 많은 스파게티 코드보다는 훨씬 나은 방향인 것 같다. 

 

그리고 PR을 올린 다음, 휴가를 즐기고 몇 시간 뒤 댓글로 달린 P1, P2, P3 리뷰들 중 내가 생각하기에 이번 PR에서 꼭 개선되어야 하는 부분들을 추가로 반영했다. 대표적인 것이 selectedDate라는 상태 변수의 timezone 문제였다.

 

이전에 비슷한 문제가 있어서 GPT에게 해결 방법을 물어봤었는데, GPT는 복잡한 설정을 할 바에는 직접 timezone을 바꾸는 함수를 utils에 만들어서 사용할 것을 권해줬다. 그도 그럴 것이 UTC 표준 시간에서 9시간만 더하면 딱 한국 시간대이기 때문이다. 그래서 이런 함수를 utils에 만들어 두고, 그때 당시 담당했었던 캘린더로 투두 날짜를 변경해줄 때 잘 사용했었다. 

 

기존에 WeeklyCalendar에서 주간 캘린더에 사용될 날짜를 나타내는 코드는 다음과 같았다. 

const getWeekDates = date => {
    const start = date.clone().startOf('ISOWeek');
    const r = Array.from({ length: 7 }, (_, i) =>
      start.clone().add(i, 'days'),
    );
    return r;
  };

 

그런데 생각해보니 WeeklyCalendar이라는 날짜 컴포넌트에서도 selectedDate 변수를 사용하고 있었는데, 이 컴포넌트에서는 해당 함수를 사용하고 있지 않았다. 로그를 찍어보니 시간이 자정 시간보다 9시간 느린 오후 15시를 가리키고 있었다. 그래서 냅다 utils에 정의한 'convertGmtToKst' 라는 함수를 적용해서 바꿔주려고 했다. 그런데 오류가 났다. 

const getWeekDates = date => {
    const start = date.clone().startOf('ISOWeek');
    const r = Array.from({ length: 7 }, (_, i) =>
      convertGmtToKst(start.clone().add(i, 'days')),
    );
    return r;
  };

 

 

배열 r의 원소 타입과 기존에 사용되던 코드의 원소 타입이 같지 않아서 생기는 문제인 것 같았다. typeof 연산자로는 해당 변수가 object라는 정보밖에는 알 수가 없어서, 어떻게 다른지 알기 위해서는 구체적인 클래스 이름을 알아야 했다. 

console.log(r[0].constructor.name);

 

그랬더니 이전 코드에서는 'Moment'가, 이후로 바뀐 코드에서는 'Date'가 나왔다. utils에 정의한 convertGmtToKst 함수는 Date 객체를 리턴하고 있었는데, 기존의 WeeklyCalendar에서는 moment()를 사용하고 있기 때문에 moment에서 사용 가능한 메소드를 Date 객체는 사용할 수 없었던 문제였다. 

 

최종적으로 코드를 이렇게 바꿔서 Date 객체를 다시 moment로 바꿔주니 잘 동작하였다!

const getWeekDates = date => {
    const start = date.clone().startOf('ISOWeek');
    const r = Array.from({ length: 7 }, (_, i) =>
      moment(convertGmtToKst(new Date(start.clone().add(i, 'days')))),
    );
    return r;
  };

 

그리고 위와 비슷한 방법들로 이번에는 투두 모달창 안에서 동작하는 기능이 아닌, 모달창 밖에 있는 투데이 뷰에서 동작하는 기능들에 대해서 유사한 작업(버그 수정, 불필요한 코드 dependency 제거, 기존 로직 react query 로직으로 변경 등)을 해서 마저 다른 이슈를 완료하였다. 이제 내일은 이 유사한 로직을 기반으로 인박스 뷰를 작업할 예정이다. 

 

'개발 일기장 > SWM Onestep' 카테고리의 다른 글

20240804 TIL  (0) 2024.08.04
20240803 TIL: moment, date, timezone  (0) 2024.08.03
20240801 TIL  (0) 2024.08.01
20240731 TIL  (0) 2024.07.31
20240730 TIL: RN에서 UI Kitten으로 모달에서 캘린더 띄우기  (0) 2024.07.30

+ Recent posts