1주차기능구현까지는 마무리했지만 지저분하고 중복된 코드가 많아서 리팩토링을 하게 되었다.
기존코드
const { data: todoListData, refetch } = useQuery<
unknown,
unknown,
TodoListItem[]
>({
queryKey: ["todoList"],
queryFn: async () => {
const response = await axios.get("http://localhost:8080/todos", {
headers: {
Authorization: token,
},
});
return response?.data?.data;
},
});
기존에 axios로 요청을 할때마다 http://localhos:8080
이라는 url은 같은데 써주어야 하는 게 가독성도 좋지 않고 비효율적이라 느껴졌다. 또 headers에 token을 넣어주었는데 요청마다 계속 반복되어서 찝찝한 기분이 들었다.
axios로 instance를 만들어서 위의 문제들을 해결할 수 있었다. 아래처럼 interceptor
를 사용해서 then이나 catch로 처리되기 전에 요청이나 응답을 가로챌 수 있다.
instance를 생성하면서 baseUrl을 지정해줄 수 있었고 요청을 하기전에 headers에 token을 넣어줄 수 있었다. 추가로 얻은 수확은 응답도 가로챌 수 있어서 response.data.data로 destructuring을 한 결과를 내보내줄 수 있게 되었다. 이전에는 위의 코드처럼 매번 지저분하게 return 해줬어야 했는데 말이다.
import axios from "axios";
import { TOKEN } from "@/constants/common";
const token = localStorage.getItem(TOKEN);
export const instance = axios.create({
baseURL: "http://localhost:8080",
timeout: 1000,
});
// 요청 가로채기
instance.interceptors.request.use(
function (config) {
// headrs가 undefined일 수 있다는 type error를 해결하기 위해 넣은 코드
config.headers = config.headers ?? {};
if (!!token) {
config.headers["Authorization"] = token;
}
config.headers["Content-Type"] = "application/json; charset=utf-8";
return config;
},
function (error) {
console.log(error);
return Promise.reject(error);
}
);
// 응답 가로채기
instance.interceptors.response.use(
function (response) {
return response.data.data;
},
function (error) {
return Promise.reject(error);
}
);
이제 아래처럼 훨씬 간결한 코드를 작성할 수 있게 되었다.
const { data: todoListData, refetch } = useQuery<
unknown,
unknown,
TodoListItem[]
>({
queryKey: ["todoList"],
queryFn: async () => {
return instance.get("todos");
},
});
기존에는 페이지에서 직접 useMutation이나 useQuery를 작성해주다보니 interface까지 뒤엉켜 코드가 아주 길어지는 현상이 있었다. hooks 폴더 아래에 mutation과 query를 리턴해주는 hook을 작성하여 사용해주게 되었다.
// useSignUp.ts
export const useSignUp = () => {
const navigate = useNavigate();
return useMutation<SignUpResponse, unknown, SignUpVariable, unknown>({
mutationFn: (variables) => {
return instance.post("users/create", variables);
},
onSuccess: (data) => {
localStorage.setItem(TOKEN, data?.token);
alert("회원가입에 성공했습니다.");
navigate("/");
},
});
};
// 사용하는 곳
const { mutate } = useSignUp();
로그인이나 회원가입 등의 요청이 성공했을 때 적절히 안내해주는 컴포넌트를 추가해야겠다고 생각했다. 하지만 Dialog, Toast, Snackbar 등 다양한 컴포넌트 중 어떤 것을 사용해야 할 지 몰라 찾아보게 되었다.
Android Developers 공식 채널의 영상에서 세가지의 차이를 알 수 있었다.
Dialog
Dialog는 중요한 정보를 전달하거나 유저의 결정이 필요한 상황에 사용한다. 창 안에 유저가 클릭할 수 있는 두가지 정도의 액션을 넣어서 제공할 수 있다. 하지만 유저들이 멈춰서 dialog를 처리해야하므로 방해받는다 느낄 수 있다. 그러므로 신중하게 사용해야 한다.
Toast
Toast는 동작에 대한 간단한 피드백을 보여주는 팝업이다. 사용자를 방해할 일은 없지만 삭제 요청을 빠르게 취소하고 싶거나 액션이 필요한 상황에는 적합하지 않다.
Snackbar
Snackbar는 위의 두가지를 적절히 섞어 놓은 느낌 같다. 피드백을 보여주면서도 유저에게 액션을 제공할 수 있다. 영상에서는 애매한 상황에서 snackbar를 사용하는 게 적절하다고 말한다.
고민한 끝에 Snackbar를 사용하기로 결정했다. 투두 아이템을 취소하는 기능도 있기 때문에 snackbar가 필요할 것이라 판단했고 그때 사용자가 취소도 할 수 있도록 undo를 넣어야겠다고 생각했다.
로그인이나 회원가입 이후에는 화면이 이동하는데 이때 snackbar가 사라져버려서 어떻게 할지 고민하다 전역상태를 사용하기로 했다. 가장 상위 컴포넌트인 App.tsx에 snackbar 컴포넌트를 넣어놓고 필요할 때 수정할 수 있도록 했다.
// atom
export const snackbarProps = atom<SnackbarProps>({
key: "snackbarProps",
default: {},
});