[Personal Project] 로그인/회원가입 기능이 추가된 가계부 (2)

liinyeye·2024년 6월 13일
0

Project

목록 보기
17/44
post-thumbnail

프로필 업데이트

프로필 업데이트 api

export const updateProfile = async (formData) => {
  console.log(formData);
  const accessToken = localStorage.getItem('accessToken');
  if (accessToken) {
    try {
      const response = await authApi.patch('/profile', formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
          Authorization: `Bearer ${accessToken}`
        }
      });
      return response.data;
    } catch (error) {
      toast.warn('프로필 업데이트에 실패했습니다.');
    }
  }
};

왜 formData로 데이터를 전달해야하는가?

patch 매서드의 인자 중 headers를 보면 'Content-Type': 'multipart/form-data' 즉, formData 객체 형태로 전달해야한다는 것을 확인해볼 수 있다. 일반적인 JSON객체는 이미지 파일 데이터를 포함할 수 없기 때문에 데이터를 formData형태로 변형해 서버에 전달해줘야한다.

formData 객체란?

formData 객체는 웹 브라우저에서 폼 데이터를 쉽게 관리하고 전송할 수 있도록 도와주는 객체이다.

아래의 경우는 form 태그를 사용하지 않았기 때문에 append를 사용하여 key/value 쌍을 추가해준다.

  const formData = new FormData();
  formData.append('nickname', nickname);
  formData.append('avatar', avatar);
  const handleUpdateProfile = async () => {
    // 폼 데이터 생성해주기
    const formData = new FormData();
    formData.append('nickname', nickname);
    formData.append('avatar', avatar);
    
    const response = await updateProfile(formData);
    console.log('response : 프로필 업데이트 성공', response);
    if (response.success) {
      dispatch(
        setUser({
          ...user,
          nickname: response.nickname,
          avatar: response.avatar
        })
      );
    }
    handleClose();
  };

로그인 후 프로필 이미지 변경 전에 디폴트 이미지 넣어주기

처음 시도한 방법

로그인 시 user 객체 데이터를 보면 avatar : null 인 것을 확인할 수 있다.

그래서 원래는 로그인 시 avatar 데이터를 받아서 user상태를 저장할 때, dispatch(setUser({ userId, nickname, avatar })); avatar:defaultImg를 초기값으로 잡아서 넣어주고 싶었다.

 const defaultAvatar = defaultImg; // 기본 이미지 경로 설정
 const userAvatar = avatar || defaultAvatar; // avatar가 없으면 기본 이미지 사용
 dispatch(setUser({ userId, nickname, avatar: userAvatar }));
  // 로그인
  const onSubmitLogin = async () => {
    const { userId, nickname, avatar } = await login({
      id: formData.id,
      password: formData.password
    });
    toast.success('로그인 성공!');
    dispatch(setUser({ userId, nickname, avatar }));
    navigate('/home');
  };

하지만 작동이 잘 되지 않았고, 어떤 부분에서 문제가 있는지 정확히 파악이 어려워 내일 튜터님께 질문을 해보려 한다.

해결방법

img 경로에 user.avatar가 없을 경우에 defaultImg를 넣어주고, 프로필 업데이트 시 해당 이미지가 보일 수 있도록 코드를 변경했다.

<img src={user.avatar ? user.avatar : defaultImg} />

지출 CRUD

문제점

useRef는 동기적으로 작동하고 useQuery로 데이터를 가져오는 것은 비동기적으로 작동하기 때문에 처음에 컴포넌트가 마운트 됐을 때 selectedExpense 값이 undefined가 되어 화면에 데이터를 그려주지 못하는 문제가 생겼다.

해결 방법

데이터를 성공적으로 가져오고 난 뒤, 즉 isSuccess가 true인 경우에만 find 매서드를 사용해 prevData를 가져오고, 아직 false인 경우 빈 객체를 반환해주는 로직으로 변경했다. 마찬가지로 input이 defaultValue값을 제대로 가져와 화면에 그려줄 수 있도록 isSuccess가 true인 경우에만 해당 ui를 보여줄 수 있도록 수정했다.

이전에 코드를 작성할 때는 useRef를 사용해보기 위해 일부로 useState를 사용하지 않았는데, 이 경우에는 동기적으로 작성하는 로직과 비동기적으로 작성하는 로직이 충돌되지 않도록 오히려 useState를 사용하는 것이 더 적절하다는 생각이 들었다.

다시 공부한 부분

find() 메서드
find 메서드는 배열에서 조건에 맞는 첫 번째 요소를 반환합니다. 조건에 맞는 요소가 없으면 undefined를 반환합니다.

const array1 = [5, 12, 8, 130, 44];

const found = array1.find((element) => element > 10);

console.log(found);
// Expected output: 12

아래 로직에서 처음에는 find가 배열 매서드이기 때문에 삼항연산자의 false일 때의 값에도 배열이 들어가야 하지 않나? 생각했다. 하지만 다시 매서드에 대한 설명을 찾아보니 조건에 맞는 첫 번째 요소를 반환하는 매서드이기 때문에 배열이 아닌 객체가 들어가야했다.

const prevData = isSuccess ? selectedExpense.find((item) => item.id === id) : {};

지출 수정 & 삭제

queryKey: ['expense', id] 이 부분에서 쿼리 키에 id가 포함되는 이유는 각 쿼리는 고유한 쿼리를 구별하고 관리하기 위해서이다. 즉 각 id에 대해 고유한 쿼리 키가 생성된다. 따라서 지출 수정 및 삭제할 때는 해당 id의 데이터만 가져오기 때문에 id를 넣어준다.

  // 데이터 가져오기
  const {
    data: selectedExpense,
    isLoading,
    error,
    isSuccess,
    refetch
  } = useQuery({
    // 고유한 queryKey로 각 id별 데이터를 구분
    queryKey: ['expense', id],
    queryFn: getExpense
  });

TanStack query 사용한 수정, 삭제 함수

데이터가 수정 혹은 삭제되어 상태가 변할 경우 리랜딩해서 UI를 바꿔줘야하는데, 이 때 queryClient.invalidateQueries(['expense']);로 쿼리를 무효화해서 다시 데이터를 가져오는 로직이 필요하다. 이 로직이 없다면 화면을 새로고침해야 수정된 상태가 UI에 반영된다.

  const mutationEdit = useMutation({
    mutationFn: putExpense,
    onSuccess: () => {
      navigate('/home');
      queryClient.invalidateQueries(['expense']);
    }
  });

  const mutationDelete = useMutation({
    mutationFn: deleteExpense,
    onSuccess: () => {
      queryClient.invalidateQueries(['expense']);
    }
  });

수정, 삭제 api

export const putExpense = async (updatedExpense) => {
  const { id, ...rest } = updatedExpense;
  try {
    const response = await expenseApi.put(`/expenses/${id}`, rest);
    return response.data;
  } catch (error) {
    toast.warn('지출 데이터 수정에 실패했습니다.');
  }
};

export const deleteExpense = async (id) => {
  console.log(id);
  try {
    const response = await expenseApi.delete(`/expenses/${id}`);
    return response.data;
  } catch (error) {
    toast.warn('지출 데이터 삭제에 실패했습니다.');
  }
};
profile
웹 프론트엔드 UXUI

0개의 댓글