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

[김영주] Sprint11 #316

Conversation

purplenib
Copy link
Collaborator

@purplenib purplenib commented Aug 23, 2024

요구사항

  • Github에 PR(Pull Request)을 만들어서 미션을 제출합니다.
  • 피그마 디자인에 맞게 페이지를 만들어 주세요.
  • 기존의 스프린트 미션 8에 이어서 React, Typescript를 사용합니다.

기본

  • 유효한 정보를 입력하고 스웨거 명세된 “/auth/signUp”으로 POST 요청해서 성공 응답을 받으면 회원가입이 완료됩니다.
  • 회원가입이 완료되면 “/login”로 이동합니다.
  • 회원가입 페이지에 접근시 로컬 스토리지에 accessToken이 있는 경우 ‘/’ 페이지로 이동합니다.
  • 회원가입을 성공한 정보를 입력하고 스웨거 명세된 “/auth/signIn”으로 POST 요청을 하면 로그인이 완료됩니다.
  • 로그인이 완료되면 로컬 스토리지에 accessToken을 저장하고 “/” 로 이동합니다.
  • 로그인/회원가입 페이지에 접근시 로컬 스토리지에 accessToken이 있는 경우 ‘/’ 페이지로 이동합니다.
  • 로컬 스토리지에 accessToken이 있는 경우 상단바 ‘로그인’ 버튼이 판다 이미지로 바뀝니다.

심화

  • 로그인, 회원가입 기능에 react-hook-form을 활용해봅니다.

주요 변경사항

Feat

  • 로그인 기능
  • 회원가입 기능
  • 로그아웃 기능
     - 클릭 시 로컬 스토리지에 보관된 토큰 삭제

Refactor

  • 폼 컴포넌트 분리
  • 전체 폴더 구조 재정리

Chore

  • react-hook-form 설치 및 적용

스크린샷

로그아웃 기능

image
image


멘토에게

  • 아직 지난 코드 리뷰 사항 반영 못했습니다! 다음 미션에 반영하여 PR 올려보겠습니다 🙏
  • 전반적인 폴더 구조를 재정리했습니다.

@purplenib purplenib added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Aug 23, 2024
@purplenib purplenib requested a review from Taero-Kim August 23, 2024 19:46
</div>
<ul className="flex flex-row gap-3">
<li>
<Link to="https://www.facebook.com/">
Copy link
Collaborator

Choose a reason for hiding this comment

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

p4;
sns 별 고정 링크와 같은 경우에는 상수로 만들어서 관리하는 것도 좋을 것 같아요!

const SNS_LINK = Object.freeze({
  FACEBOOK: "https://www.facebook.com/",
  TWITTER: "https://x.com/",
  YOUTUBE: "https://www.youtube.com/",
  INSTAGRAM: "https://www.instagram.com/"
})

window.addEventListener("resize", handleResize);

Copy link
Collaborator

Choose a reason for hiding this comment

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

p4;
리사이즈 관련한 로직은 커스텀훅으로 따로 분리해도 활용도가 높을 것 같습니다!

ex) useResize 에 로직 분리

const { isMobile } = useResize();

return (
<section className="flex flex-col content-center gap-[40px] ">
<ul className="w-full flex flex-col content-start flex-grow gap-6">
{commentsData.list.length === 0 ? (
Copy link
Collaborator

Choose a reason for hiding this comment

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

p4;
!comments.list.length 이 조건으로도 검사할 수 있을 것 같아요!
혹은 리턴부 위에서 해당 조건을 한 번더 변수화해서 사용해도 괜찮을 것 같아요.

const hasNoComments = !comments.list.length;

return (
   <div>
     {hasNoComents ? ... : ...}
   </div>
)

/>
<p className="text-gray-400">
아직 {category}
{hasFinalConsonant(category) ? "이" : "가"} 없어요.
Copy link
Collaborator

Choose a reason for hiding this comment

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

디테일한 ux 처리 좋습니다👍

const LikeCount = ({ count, divClassName }: LikeCountProps) => {
return (
<div
className={`${divClassName} flex items-center text-xs font-medium gap-1 text-gray-500`}
Copy link
Collaborator

Choose a reason for hiding this comment

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

p4;
divClassName으로 props를 전달 받는 것보다는 wrapperClassName 등의 이름이 유지보수하기 더 좋을 것 같아요!

// GET, 공통 apiCall 함수
export async function getFromApi<T>(url: string): Promise<T> {
try {
const response = await fetch(url);
Copy link
Collaborator

Choose a reason for hiding this comment

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

p4;
fetch 시에 엔드포인트만 작성할 수 있도록 여기 BASE_URL을 설정해놓는 것도 좋을 것 같아요!

  try {
    const response = await fetch(`${BASE_URL}${path}`);

return { data, error, loading, postData };
}

export default useApiPost;
Copy link
Collaborator

Choose a reason for hiding this comment

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

POST 요청에 대한 로딩, 에러 및 데이터와 콜백 등 사용하시기 좋게 추상화를 잘 해놓으셨네요!

if (token) {
fetchUserInfo(token);
}
}, []);
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3;
로컬스토리지의 토큰을 통해 인증을 잘 구현하셨어요!

다만, 인증과 관련된 상태의 경우 일반적으로 전체 페이지에 걸쳐 전역적으로 사용되는 경우가 많아서,
요 user 관련한 로직을 UserProfile 내에 한정하여 처리하는 대신

Context를 하나 만들어, 모든 컴포넌트에서 로그인 상태나 유저 정보가 필요한 경우 useContext로 접근할 수 있도록
처리하면 어떨까요?

export const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
      const token = localStorage.getItem("accessToken");
      
     if (!token) return;

     fetchUserInfo(token);
}, []);

  const handleLogout = () => {
    localStorage.removeItem("accessToken");
    setUser(null);
  };
  ...
  
  return (
    <AuthContext.Provider value={{ user, loading, handleLogin, handleLogout }}>
      {children}
    </AuthContext.Provider>
  );
};

...

<AuthProvider>
   <Routes>
        ...
    </Routes>
</AuthProvider>

...

const UserProfile = () => {
   const { user } = useContext(AuthContext);

  ...
}

const [isLoggedOutVisible, setIsLoggedOutVisible] = useState<boolean>(false);

useEffect(() => {
const token = localStorage.getItem("accessToken");
Copy link
Collaborator

Choose a reason for hiding this comment

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

p4;
추후에 여유가 되신다면, accessToken의 만료기한을 검증하고 refreshToken 등으로 이를 갱신하는 로직을 추가해봐도 좋을 것 같아요!

jwt 토큰에는 일반적으로 만료기한이 설정되어 있습니다.
서버에서 전달 받은 jwt를 디코딩하면 만료 기한(Exp)을 확인하실 수 있습니다.

따라서, 로컬스토리지에 저장되어 있는 accessToken의 값을 디코딩하여,
만료 시간을 파악하고 토큰이 만료된 경우, 새로운 accessToken을 발급받는 등의 처리도 하실 수 있습니다

예를들어 Jwt의 유효 시간을 확인하는 방식은 아래와 같습니다.

// jwt 디코딩
const decodeToken = (token) => {
  const payload = token.split('.')[1];
  const decodedPayload = atob(payload);
  return JSON.parse(decodedPayload);
}

// 토큰의 유효 시간 확인 함수
const isTokenExpired = (token) => {
  const decoded = decodeToken(token);
  const currentTime = Math.floor(Date.now() / 1000);

  return decoded.exp < currentTime; // exp가 현재 시간보다 이전이면 토큰 만료
}

className="h-8 w-8 cursor-pointer"
onClick={handleProfileClick}
/>
{isLoggedOutVisible && (
Copy link
Collaborator

Choose a reason for hiding this comment

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

p4;
요건 따로 loggedOutVisible 상태를 관리하기보다는,
기존 user 정보의 상태를 이용하여 null인 경우에 '로그아웃' 텍스트가 보여질 수 있도록 처리하는 건 어떨까요?

{!user && ( <button> 로그아웃 </button>)}

@Taero-Kim
Copy link
Collaborator

영주님 고생 많으셨습니다!
폴더 구조는 확실히 깔끔하고 분리가 잘되어 있는 것 같아요!

이번에 코드를 보니 특히 api 호출하는 부분에서는 로직들을 재사용하기 위해 추상화를 시도하신 흔적이 잘 느껴졌습니다.
코드에 남긴 리뷰대로, 유저 인증에 관련한 상태를 전역에서 관리할 수 있도록 해보고
토큰의 만료기한도 고려하여, 갱신하는 로직을 추가해보는 것도 좋을 것 같아요.

별개로 천천히 여유가 되신다면 next도 한 번 적용하셔서, 기존 순수 리액트 프로젝트로 할때와 어떤 장단점이 있는지 경험해보는 것도 좋을 것 같아요!

@Taero-Kim Taero-Kim merged commit a1b3932 into codeit-bootcamp-frontend:React-김영주 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