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

liinyeye·2024년 6월 14일
0

Project

목록 보기
18/44
post-thumbnail

완성된 웹사이트

🔗 https://auth-accounting.vercel.app/

Login / Sign UP

Home

Profile Update Modal

Detail

피드백 반영 및 트러블 슈팅

피드백 반영

[ feedback ]

  1. App.js에서 accessToken이 localStorage에 있을때 Home으로 가면 로그인화면으로 이동하지 않도록 해보면 좋을것 같아요.
    -> redux나 contextAPI 를 이용해서 유저정보를 전역으로 관리하도록 해요
    -> App에서 CreateBrowserRouter Element에 유저정보 유무에따라 Navigate 컴포넌트를 넣어줄지 Outlet컴포넌트를 넣어줄지 체크해서 넣어줘요.
  1. getUserInfo도 tanstack-query를 적용해보면 좋을것같아요.
    -> tanstack-query를 적용하면 redux에 값을 안 넣어줘도 된답니다.
  1. Input onChange로 이용하여 Input State를 변경할 경우 글자 입력마다 리렌더링이 발생합니다. ref를 사용해서 렌더링을 줄여보는것도 좋을것 같아요.
  1. tanstack-query를사용할때 queryKey를 사용하는데 마다 문자열로 넣어주기 보단 queryKey를 한 군데에서 변수로 관리해주는게 유지보수 하기 더 편해요.

UI이쁘게잘만들었네요:)

이번에는 튜터분께서 아주 상세하게 피드백을 남겨주셨다. 다 도움이 되는 것들이라 이번에는 피드백에 맞춰 코드를 한 번 수정해보기로 했다.

App.js에서 accessToken이 localStorage에 있을때 Home으로 가면 로그인화면으로 이동하지 않도록 하기

수정 방법

  • 로그인 상태 전역관리
  • 로그인 유무에 따라 Navigate 컴포넌트 사용하는 방법으로 라우터 변경

(1) 로그인 상태 redux로 전역관리

이전에는 필요에 따라 개별 컴포넌트에서 로컬스토리지에 저장되어있는 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;

(2) 라우터 수정

이전 코드

가장 첫 화면에 로그인/회원가입 페이지를 보여주기 위해 어쩔 수 없이 루트 경로를 로그인 페이지로 설정했었다. 하지만 로그인 페이지가 루트 경로이면 안 된다는 피드백을 들었고 이에 따라 메인 페이지를 루트 경로로 재설정, accessToken 유무에 따라 로그인 되지 않은 유저는 로그인 페이지로 리디렉션하는 로직으로 바꾸기로 했다.

일반적인 라우팅 설정 예시

  • 루트 경로("/"): 메인 페이지나 대시보드
  • 로그인 페이지("/login"): 로그인 페이지
  • 회원가입 페이지("/register"): 회원가입 페이지
  • 대시보드("/dashboard"): 인증된 사용자용 대시보드
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;

queryKey를 한군데에서 변수로 관리해주기

찾아보니 라이브러리를 따로 설치해서 사용하는 좀 더 복잡한 방법이 있었고, 이에 대해 튜터님께 어디서 어떤 방식으로 관리를 해야할지 질문했다. 생각보다 방법은 간단했는데, 한 파일 안에서 유지보수가 쉽게 관리해주면 되는 것이기 때문에 나의 경우에는 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 사용하여 User 상태 관리하기

이번 프로젝트에서 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();
  };
profile
웹 프론트엔드 UXUI

0개의 댓글