Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[정원식] Sprint 11 #320

Conversation

wonsik3686
Copy link
Collaborator

@wonsik3686 wonsik3686 commented Aug 23, 2024

요구사항

기본

회원가입

  • 유효한 정보를 입력하고 스웨거 명세된 "/auth/signUp"으로 POST 요청해서 성공 응답을 받으면 회원가입이 완료됩니다.
  • 회원가입이 완료되면 "/login"로 이동합니다.
  • 회원가입 페이지에 접근 시 로컬 스토리지에 accessToken이 있는 경우 "/" 페이지로 이동합니다.

로그인

  • 회원가입을 성공한 정보를 입력하고 스웨거 명세된 "/auth/signIn"으로 POST 요청을 하면 로그인이 완료됩니다.
  • 로그인이 완료되면 로컬 스토리지에 accessToken을 저장하고 "/"로 이동합니다.
  • 로그인/회원가입 페이지에 접근 시 로컬 스토리지에 accessToken이 있는 경우 "/" 페이지로 이동합니다.

메인

  • 로컬 스토리지에 accessToken이 있는 경우 상단바의 '로그인' 버튼이 판다 이미지로 바뀝니다.

심화

  • react-hook-form 사용

주요 변경사항

  • react-hook-form 사용하여 SingUpForm, SignInForm 컴포넌트 구현
  • zustand 사용하여 auth store 구현
  • TanStack Query 사용하여 useSignIn, useLogin 구현

스크린샷

멘토에게

  • react-hook-form, zustand, tanstack query 라이브러리를 적절하게 사용했는지 피드백 부탁드립니다 😀
  • useSignIn 같은 훅에서 tanstack query 사용하면서 store, axios 사용했는데 구조적으로 혹시 더 나은 방법이 있다면 피드백 부탁드립니다
  • zustand 의 subscribe 도 많이 사용하는지 궁금합니다

@wonsik3686 wonsik3686 added 미완성🫠 죄송합니다.. 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. labels Aug 23, 2024
@wonsik3686 wonsik3686 removed the 미완성🫠 죄송합니다.. label Aug 25, 2024
Comment on lines +18 to +22
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, [accessToken]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클라이언트 상태일 때 추가적인 랜더링을 더 해야되는 상황이 아니라면 아래처럼 사용하는건 어떨까요? 랜더링 사이클에 영향을 주지 않으며, 추가적인 상태 업데이트가 없습니다.

const isClient = typeof window !== 'undefined';

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그리고 만약, 리랜더링이 필요한 경우라면 아래처럼 공통 훅을 만들어서 빼서 사용하는 것도 좋겠습니다.

function useClientSideRendering() {
  const [isClient, setIsClient] = useState(false);
  useEffect(() => {
    setIsClient(true);
  }, []);
  return isClient;
}

Copy link
Collaborator

@arthurkimdev arthurkimdev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문주신 react-query, zustand 관련해서 답변 드렸습니다!
마지막 과제 제출까지 수고하셨습니다 🙏

Comment on lines +18 to +22
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, [accessToken]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그리고 만약, 리랜더링이 필요한 경우라면 아래처럼 공통 훅을 만들어서 빼서 사용하는 것도 좋겠습니다.

function useClientSideRendering() {
  const [isClient, setIsClient] = useState(false);
  useEffect(() => {
    setIsClient(true);
  }, []);
  return isClient;
}

Comment on lines +54 to +88
{isClient && accessToken && (
<div className={styles['navbar__right']}>
<Link
href="/addboard"
id="login-link-button"
className="button navbar__login-link-button"
>
<Image
className={styles['navbar__profile']}
src={imgProfile}
alt="프로필"
/>
</Link>
<UIButton
className={styles['navbar__profile']}
type="box"
handleClick={logout}
>
로그아웃
</UIButton>
</div>
)}
{isClient && !accessToken && (
<div id="login-link-button" className="navbar__login-link-button">
<UIButton
className={styles['navbar__profile']}
type="box"
handleClick={() => {
push('login');
}}
>
로그인
</UIButton>
</div>
)}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인증 상태에 따라 UI 랜더링을 다르게 하는 방법 중 아래 방법도 있습니다.

if (accessToken) {
    return (
      <div className={styles['navbar__right']}>
        <Link
          href="/addboard"
          aria-label="프로필 페이지로 이동"
          className={styles['navbar__profile-link']}
        >
          <Image
            className={styles['navbar__profile-image']}
            src={imgProfile}
            alt="프로필"
          />
        </Link>
        <UIButton
          className={styles['navbar__logout-button']}
          type="box"
          handleClick={logout}
          aria-label="로그아웃"
        >
          로그아웃
        </UIButton>
      </div>
    );
  }

  return (
    <div className={styles['navbar__login-button-wrapper']}>
      <UIButton
        className={styles['navbar__login-button']}
        type="box"
        handleClick={() => push('login')}
        aria-label="로그인 페이지로 이동"
      >
        로그인
      </UIButton>
    </div>
  );

Comment on lines +1 to +33
import { signInUser } from '@lib/api/AuthApi';
import { useAuthStore } from '@store/useAuthStore';
import { useMutation } from '@tanstack/react-query';
import { SignInUserRequest } from '@type/AuthTypes';
import { useRouter } from 'next/router';

const useLogin = () => {
const route = useRouter();
const { setUser, setAccessToken, setRefreshToken, clearAuth } =
useAuthStore();
const { mutate: login, ...returns } = useMutation({
mutationFn: async ({ ...params }: SignInUserRequest) => {
return await signInUser(params);
},
onSuccess: (res, req, context) => {
console.log('useLogin mutation success: ', res, req, context);
if (res) {
setUser(res.data.user);
setAccessToken(res.data.accessToken);
setRefreshToken(res.data.refreshToken);
}
},
});

const logout = () => {
clearAuth();
route.reload();
};

return { login, logout, ...returns };
};

export default useLogin;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전반적으로 잘 작성하셨습니다.
물론 key를 사용하지 않기 때문에, useMutation 의 장점을 전부 살리진 않았지만, 이해도가 충분히 느껴지는 코드였어요.
이런 코드를 작성했을 때 아래 사항도 고민해보면 좋아요.

  1. useMutation 을 사용하지 않고 작성하면 어떻게 작성해볼 수 있을까?
    -> fetch를 하면 단순하게 코드가 더 간단하고, 직관적이겠죠? 라이브러리 의존성이 없어서 업데이트에 맞출 필요도 없구요. 제공하는 많은 기능이 없으니 필요한 로직만 정확하게 구현할 수 있습니다. 불필요한 학습 곡선도 필요없구요.

  2. useMutation 을 사용해서 어떤점을 편하게 해주는걸까?
    -> 상태관리 (loading, error, success) , 재시도 로직, 취소 기능, 낙관적 업데이트(UI 업데이트를 먼저 후 서버 응답 반영) 같은게 있어요.

Comment on lines +1 to +3
type NullablePick<T, K extends keyof T> = {
[P in K]: T[P] | null;
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굳! 👍 지난번 optionalPick 부터 NullablePick 까지 아주 좋습니다.

Comment on lines +33 to +65
export const useAuthStore = create(
persist(
subscribeWithSelector(
immer(
combine(initialState, (set) => ({
setUser: (user: User | null) => {
set((state) => {
state.user = user;
});
},
setAccessToken: (accessToken: BasicType['accessToken']) => {
set((state) => {
state.accessToken = accessToken;
});
},
setRefreshToken: (refreshToken: BasicType['refreshToken']) => {
set((state) => {
state.refreshToken = refreshToken;
});
},
clearAuth: () => {
set((state) => {
state.user = null;
state.accessToken = null;
state.refreshToken = null;
});
},
clearTokens: () => {
set((state) => {
state.accessToken = null;
state.refreshToken = null;
});
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전반적으로 별도 피드백이 필요 없을 정도로 잘 구현하셨습니다. 물론 현업에서는 3가지 미들웨어를 전부 사용할 때도 있고 아닌 경우도 있어요. 각 미들웨어의 장점들을 바탕으로 오버엔지니어링이 되지 않은 선에서 각 store에 맞춰 추가하면 되겠습니다. 예를들어, 상태가 1~2개 단순한 경우 성능 최적화가 불필요하기 때문에 subscribeWithSelector 필요하지 않을 수 있어요.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subscribeWithSelector 도 사실 내부 원리를 찾아보면, 상태가 변경될 때마다 선택자 함수를 실행하여 이전 값과 비교하는 로직이 있는데 이를 위해

  1. 각 구독마다 추가적인 비교 로직이 실행이 되고
  2. 이전 선택 값을 저장하기 위해 추가 메모리를 사용해요.
    결국 각 구독마다 추가적인 비교 연산이 생기므로 누적되면 전반적은 웹 성능에 부정적인 영향을 줄 수 있겠죠? 무조건적인 사용이 항상 이롭지는 않거든요. 이런 관점들을 보면서 사용해보면 더 좋겠습니다. 🙏

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오히려 단순하게 shallow 옵션 값을 추가하여 아래처럼 특정 컴포넌트에서 선택적으로 적용하는 방법도 고민해볼 수 있어요.
shallow 는 zustand에서 제공하는 함수에요.
아래 방식의 장점은 객체의 얕은 비교를 수행하여 불필요한 리렌더링을 방지하고, 사용하는 곳에서 최적화를 고민하고 결정하는거죠.

const state = useStore(state => ({ user: state.user }), shallow)

아래 처럼 subscribeWithSelector 방법은 스토어 레벨에서 최적화가 이루어지는거예요.

const useStore = create(subscribeWithSelector((set) => ({ ... })))
const state = useStore(state => state.user)

즉 여러 컴포넌트에서 동일한 최적화 혜택을 받을 수 있죠. 대신 구독이 많을 경우, 비교로직도 그만큼 많아지겠죠? 모든 것은 트레이드 오프가 있기 때문에 모든 방법을 알고 그때 마다 최선의 방법을 고르시면 되겠습니다.

@arthurkimdev arthurkimdev merged commit 6fdfa2a into codeit-bootcamp-frontend:Next-정원식 Aug 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants