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('프로필 업데이트에 실패했습니다.');
}
}
};
patch 매서드의 인자 중 headers를 보면 'Content-Type': 'multipart/form-data'
즉, formData 객체 형태로 전달해야한다는 것을 확인해볼 수 있다. 일반적인 JSON객체는 이미지 파일 데이터를 포함할 수 없기 때문에 데이터를 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} />
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
});
데이터가 수정 혹은 삭제되어 상태가 변할 경우 리랜딩해서 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']);
}
});
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('지출 데이터 삭제에 실패했습니다.');
}
};