🔗 https://auth-accounting.vercel.app/
[ feedback ]
- App.js에서 accessToken이 localStorage에 있을때 Home으로 가면 로그인화면으로 이동하지 않도록 해보면 좋을것 같아요.
-> redux나 contextAPI 를 이용해서 유저정보를 전역으로 관리하도록 해요
-> App에서 CreateBrowserRouter Element에 유저정보 유무에따라 Navigate 컴포넌트를 넣어줄지 Outlet컴포넌트를 넣어줄지 체크해서 넣어줘요.
- getUserInfo도 tanstack-query를 적용해보면 좋을것같아요.
-> tanstack-query를 적용하면 redux에 값을 안 넣어줘도 된답니다.
- Input onChange로 이용하여 Input State를 변경할 경우 글자 입력마다 리렌더링이 발생합니다. ref를 사용해서 렌더링을 줄여보는것도 좋을것 같아요.
- tanstack-query를사용할때 queryKey를 사용하는데 마다 문자열로 넣어주기 보단 queryKey를 한 군데에서 변수로 관리해주는게 유지보수 하기 더 편해요.
UI이쁘게잘만들었네요:)
이번에는 튜터분께서 아주 상세하게 피드백을 남겨주셨다. 다 도움이 되는 것들이라 이번에는 피드백에 맞춰 코드를 한 번 수정해보기로 했다.
수정 방법
- 로그인 상태 전역관리
- 로그인 유무에 따라 Navigate 컴포넌트 사용하는 방법으로 라우터 변경
이전에는 필요에 따라 개별 컴포넌트에서 로컬스토리지에 저장되어있는 accessToken을 가져오거나 user의 상태를 가져와서 사용해줬는데, 로그인 상태를 전역으로 더 간편하게 확인할 수 있도록 리덕스 authSlice를 새로 만들어줬다. 상태만 boolean값으로 관리해주는 것이기 때문에 생각보다 로직이 간단했다.
// 토큰 유무 조건으로 로그인 상태 확인
const initialState = {
isLogin: localStorage.getItem('accessToken')
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
loginHandler: (state, action) => {
state.isLogin = true;
},
logoutHandler: (state, action) => {
state.isLogin = false;
}
}
});
export const { loginHandler, logoutHandler } = authSlice.actions;
export default authSlice.reducer;
가장 첫 화면에 로그인/회원가입 페이지를 보여주기 위해 어쩔 수 없이 루트 경로를 로그인 페이지로 설정했었다. 하지만 로그인 페이지가 루트 경로이면 안 된다는 피드백을 들었고 이에 따라 메인 페이지를 루트 경로로 재설정, accessToken 유무에 따라 로그인 되지 않은 유저는 로그인 페이지로 리디렉션하는 로직으로 바꾸기로 했다.
const router = createBrowserRouter([
{
path: '/', // 로그인 페이지 루트면 안됨.
element: <Login />
},
{
// 로그인 정보가 있으면 실행되는
path: '/',
element: <MainLayout />, // // 여기서 토큰 유무 조건 확인해서 navigate -> 새로운 컴포넌트
children: [
{ path: 'home', element: <Home /> },
{ path: 'detail/:id', element: <Detail /> }
]
}
]);
PublicRoute, PrivateRoute 함수를 별도로 선언해주고 로그인 상태에 따라 삼항 연산자로 경로를 설정해준다. 이때 처음에 인자로 받은 element에 중괄호를 빠뜨려서 오류가 생겼는데 다음에는 잘 보고 생각해서 코드를 작성해야겠다...!
추가로 MainLayout 컴포넌트의 자식 컴포넌트에도 PrivateRoute 컴포넌트를 씌워줘야하는지 고민했는데, 어차피 자식 컴포넌트로 이동할 때 부모 컴포넌트의 경로를 거쳐서 자식 컴포넌트 경로로 이동하는 것이기 때문에 구지 반복해서 써줄 필요는 없었다.
또, MainLayout과 Home 컴포넌트의 path를 동일하게 가져가서 홈 화면을 바로 보여줄 수 있도록 하려고 했는데, 이때 루트가 동일하다면 path="/"같이 같은 경로를 적어줘도 되지만 자식 컴포넌트에 index라고 적어도 마찬가지로 적용된다.
작성하면서 다시 보니 createBrowserRouter를 사용하면서도 함수만 조건부로 잘 넣어주면 위의 방식을 사용하는게 더 깔끔해보인다. 이 때 PublicRoute와 PrivateRoute 함수를 따로 정의하고 export해서 사용하는 방법도 가독성 면에서 좋을 것 같다.
<방식 1>
const PublicRoute = ({ element }) => {
const isLogin = useSelector((state) => state.auth.isLogin);
return isLogin ? <Navigate to="/" /> : element;
};
const PrivateRoute = ({ element }) => {
const isLogin = useSelector((state) => state.auth.isLogin);
return isLogin ? element : <Navigate to="/login" />;
};
const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<PublicRoute element={<Login />} />} />
<Route path="/" element={<PrivateRoute element={<MainLayout />} />}>
{/* 어차피 자식 컴포넌트로 이동할 때 부모 컴포넌트에서 경로를 거치기 때문에 자식 컴포넌트에는 PrivateRoute를 해줄 필요가 없음. */}
<Route index element={<Home />} />
<Route path="detail/:id" element={<Detail />} />
</Route>
</Routes>
</BrowserRouter>
);
};
export default Router;
<방식 2>
const PublicRoute = ({ element }) => {
const isLogin = useSelector((state) => state.auth.isLogin);
return isLogin ? <Navigate to="/" /> : element;
};
const PrivateRoute = ({ element }) => {
const isLogin = useSelector((state) => state.auth.isLogin);
return isLogin ? element : <Navigate to="/login" />;
};
const AppRouter = () => {
const router = createBrowserRouter([
{
path: '/login',
element: <PublicRoute element={<Login />} />
},
{
path: '/',
element: <PrivateRoute element={<MainLayout />} />,
children: [
{ path: '/', element: <Home /> },
{ path: 'detail/:id', element: <Detail /> }
]
}
]);
return <RouterProvider router={router} />;
};
export default AppRouter;
찾아보니 라이브러리를 따로 설치해서 사용하는 좀 더 복잡한 방법이 있었고, 이에 대해 튜터님께 어디서 어떤 방식으로 관리를 해야할지 질문했다. 생각보다 방법은 간단했는데, 한 파일 안에서 유지보수가 쉽게 관리해주면 되는 것이기 때문에 나의 경우에는 api url를 모아둔 api.js 파일에서 쿼리키도 같이 관리해주기로 했다.
import axios from 'axios';
export const authApi = axios.create({
baseURL: 'https://moneyfulpublicpolicy.co.kr'
});
export const expenseApi = axios.create({
baseURL: 'https://innovative-petalite-ton.glitch.me'
});
export const queryKeys = {
expenses: 'expenses',
users: 'users'
};
import { queryKeys } from '../../api/api';
const profileUpdate = useMutation({
mutationFn: updateProfile,
onSuccess: () => {
queryClient.invalidateQueries([queryKeys.users]);
}
});
이번 프로젝트에서 tanstack-query를 처음 써봤는데 생각보다 사용 방법도 간편하고 코드를 좀 더 효율적으로 짤 수 있었다. useState로 user 상태 관리를 할 때는 user의 상태가 업데이트될 때마다 dispatch를 사용해서 액션을 넘겨줬는데, tanstack-query를 사용하니 쿼리키와 해당 함수만 잘 안결해주면 따로 상태를 업데이트 해줄 필요 없이 간편하게 사용할 수 있었다.
<이전 코드>
const handleUpdateProfile = async () => {
const formData = new FormData();
formData.append('nickname', nickname);
formData.append('avatar', imgRef.current.files[0]);
const response = await updateProfile(formData);
console.log('response : 프로필 업데이트 성공', response);
if (response.success) {
dispatch(
setUser({
...user,
nickname: response.nickname,
avatar: response.avatar
})
);
toast.success('프로필 업데이트가 완료되었습니다.');
}
};
<수정한 코드>
const queryClient = useQueryClient();
const profileUpdate = useMutation({
mutationFn: updateProfile,
onSuccess: () => {
queryClient.invalidateQueries([queryKeys.users]);
}
});
const handleUpdateProfile = async () => {
const formData = new FormData();
formData.append('nickname', nickname);
formData.append('avatar', imgRef.current.files[0]);
profileUpdate.mutate(formData);
toast.success('프로필 업데이트가 완료되었습니다.');
handleClose();
};