-
Notifications
You must be signed in to change notification settings - Fork 35
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
The head ref may contain hidden characters: "Next-\uC815\uC6D0\uC2DD-Sprint11"
[정원식] Sprint 11 #320
Conversation
const [isClient, setIsClient] = useState(false); | ||
|
||
useEffect(() => { | ||
setIsClient(true); | ||
}, [accessToken]); |
There was a problem hiding this comment.
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';
There was a problem hiding this comment.
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;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
질문주신 react-query, zustand 관련해서 답변 드렸습니다!
마지막 과제 제출까지 수고하셨습니다 🙏
const [isClient, setIsClient] = useState(false); | ||
|
||
useEffect(() => { | ||
setIsClient(true); | ||
}, [accessToken]); |
There was a problem hiding this comment.
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;
}
{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> | ||
)} |
There was a problem hiding this comment.
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>
);
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
전반적으로 잘 작성하셨습니다.
물론 key를 사용하지 않기 때문에, useMutation
의 장점을 전부 살리진 않았지만, 이해도가 충분히 느껴지는 코드였어요.
이런 코드를 작성했을 때 아래 사항도 고민해보면 좋아요.
-
useMutation
을 사용하지 않고 작성하면 어떻게 작성해볼 수 있을까?
-> fetch를 하면 단순하게 코드가 더 간단하고, 직관적이겠죠? 라이브러리 의존성이 없어서 업데이트에 맞출 필요도 없구요. 제공하는 많은 기능이 없으니 필요한 로직만 정확하게 구현할 수 있습니다. 불필요한 학습 곡선도 필요없구요. -
useMutation
을 사용해서 어떤점을 편하게 해주는걸까?
-> 상태관리 (loading, error, success) , 재시도 로직, 취소 기능, 낙관적 업데이트(UI 업데이트를 먼저 후 서버 응답 반영) 같은게 있어요.
type NullablePick<T, K extends keyof T> = { | ||
[P in K]: T[P] | null; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
굳! 👍 지난번 optionalPick 부터 NullablePick 까지 아주 좋습니다.
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; | ||
}); | ||
}, |
There was a problem hiding this comment.
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
필요하지 않을 수 있어요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
subscribeWithSelector
도 사실 내부 원리를 찾아보면, 상태가 변경될 때마다 선택자 함수를 실행하여 이전 값과 비교하는 로직이 있는데 이를 위해
- 각 구독마다 추가적인 비교 로직이 실행이 되고
- 이전 선택 값을 저장하기 위해 추가 메모리를 사용해요.
결국 각 구독마다 추가적인 비교 연산이 생기므로 누적되면 전반적은 웹 성능에 부정적인 영향을 줄 수 있겠죠? 무조건적인 사용이 항상 이롭지는 않거든요. 이런 관점들을 보면서 사용해보면 더 좋겠습니다. 🙏
There was a problem hiding this comment.
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)
즉 여러 컴포넌트에서 동일한 최적화 혜택을 받을 수 있죠. 대신 구독이 많을 경우, 비교로직도 그만큼 많아지겠죠? 모든 것은 트레이드 오프가 있기 때문에 모든 방법을 알고 그때 마다 최선의 방법을 고르시면 되겠습니다.
요구사항
기본
회원가입
"/auth/signUp"
으로 POST 요청해서 성공 응답을 받으면 회원가입이 완료됩니다."/login"
로 이동합니다.accessToken
이 있는 경우"/"
페이지로 이동합니다.로그인
"/auth/signIn"
으로 POST 요청을 하면 로그인이 완료됩니다.accessToken
을 저장하고"/"
로 이동합니다.accessToken
이 있는 경우"/"
페이지로 이동합니다.메인
accessToken
이 있는 경우 상단바의 '로그인' 버튼이 판다 이미지로 바뀝니다.심화
react-hook-form
사용주요 변경사항
react-hook-form
사용하여 SingUpForm, SignInForm 컴포넌트 구현스크린샷
멘토에게