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

[7주차] SNIFF 미션 제출합니다. #3

Open
wants to merge 75 commits into
base: main
Choose a base branch
from

Conversation

oooppq
Copy link

@oooppq oooppq commented Dec 24, 2023

[key Features]

  • jwt + cookie를 통해 유저 인증 구현
  • 회원가입시 입력사항에 대한 유효성 검증
  • 모바일, 데스크탑 모두 대응

[기능 설명]

  • 로그인한 사람만 투표를 할 수 있습니다.
  • 로그인한 사람만 투표 결과를 볼 수 있습니다.
  • 투표는 한 번만 가능하고 수정할 수 없습니다.

배포

SNIFF-VOTE

Features

유저 인증

jwt를 통한 인증을 구현했습니다. 일반적으로 jwt를 localStorage에 저장하는 경우가 많은데, localStorage는 server-side에서 접근할 수 없기에 next로 구현할 때에는 cookie를 사용하기로 결정했습니다. cookie는 server 환경에서도 접근할 수 있기 때문입니다. 서버 컴포넌트에서 접근할 때에는 next 내장 메소드를 통해 직접 cookie에 접근하고, 클라이언트 컴포넌트에서 접근할 때에는 앱 내부에서 만든 api를 통해 접근할 수 있도록 구현했습니다. 즉, server-side와 client-side에서 모두 jwt를 포함한 유저 정보에 접근할 수 있도록 한 것입니다.

유저 인증정보를 받아오기 위해 메소드를 따로 만들었습니다. server-side에서 사용하기 위해 getSession()이라는 메소드를 만들었고, 내부 api요청으로 인해 promise가 포함되어 있어 async/await을 사용했습니다. client-side에서는 useSession()이라는 메소드를 사용할 수 있는데, 유저 정보를 state로 관리하고 useEffect를 통해 내부 api 호출 결과를 바탕으로 유저 인증 정보가 설정되도록 구현했습니다.

아쉬운 점은 cookie 형태로 저장했을 때 보안 이슈가 있긴 한데, 이 때문에 암호화해서 cookie에 저장해볼까 생각도 했지만 좀 오바하는 것 같아서 그냥 이쁘게 저장하도록 했습니다.

회원가입

회원가입할 때 입력된 값에 따라 유효성 여부를 판단하고, 이에 따른 메시지를 display 하도록 구현했습니다. 상당히 귀찮고 번거로운 작업이었는데, 대충하기 찝찝해서 그냥 해버렸어요.. 유효성 검증할 때에는 정규표현식을 사용했는데, js에서는 정규표현식에 부합하는지 확인해볼 수 있는 기능을 제공하므로 상당히 편리합니다.

아이디와 이메일같은 경우는 유효성 검증 외에도, 서버에 중복 여부에 대한 요청을 진행해야 했습니다. 근데, 입력값이 바뀔 때마다 서버에 요청을 보내면 요청이 엄청나게 많이 발생할 것이기 때문에, 뭔가 조치가 필요했습니다. 따라서, 저번 과제때 사용한 debounce 기능을 적용하기로 했습니다.

0.5초 이내에 발생한 입력 중 가장 마지막 입력만 반영하도록 구현했습니다. 아이디와 이메일에만 적용하기 뭐해서 전체적으로 다 적용했고, 덕분에 onChange event로 인한 리렌더링도 어느정도 줄일 수 있었습니다.

<입력값을 제대로 입력하지 않았을 때>
스크린샷 2023-12-24 오후 9 07 18

<입력값을 제대로 입력했을 때>
스크린샷 2023-12-24 오후 9 07 51

모바일, 데스크탑 대응

대단한 것은 아니고, 투표할 때 컴퓨터로만 하면 불편하니까 모바일도 볼 수 있도록 해놨습니다. 디자인이 따로 있던건 아니라서 그냥 저희 맘대로 해버렸어요ㅎㅎ..

스크린샷 2023-12-24 오후 8 52 28

느낀점

next.js는 공부하면 공부할수록 어려워지는 프레임워크인 것 같습니다. 상당히 많은 기능들이 있고, 프로젝트마다 다양하게 설정할 수 있는게 큰 장점인 것 같지만 개발할 때의 난이도도 올라가는 느낌이에요.. 무튼 작은 규모지만 서버까지 만들어서 결과물을 내니까 보람있었어요!
그리고, 백엔드 친구들과 같이 하니까 api 규격 다루고, 테스트해보고, 뭐 이것저것 해야하는게 많았어서 되게 유익했습니다. 모두모두 열심히 잘해준 것 같아요! 앞으로 진행할 프로젝트 엄청 수월하게 잘 진행될 것 같습니다~~

oooppq and others added 30 commits November 20, 2023 15:27
기존에는 svgr을 사용하여 svg 파일을 컴포넌트 형태로 사용했는데, 세오스 로고 하나 때문에
의존성 하나 추가하는 것도 비효율적이고, 반응형으로 크기를 변경시키기에 불편해서 png + next/Image로
이미지를 다루기로 변경했다.
[feat] navbar 및 홈 페이지 레이아웃 적용
password input 처리와 input name을 설정하기 위해 속성을 변경했다.
서버에 로그인 요청을 한 후, response 값을 쿠키에 저장하도록 구현했다.
서버 사이드에서는 localStorage에 접근할 수 없기 때문에 쿠키를 사용하도록 결정했다.
서버에서 로그아웃 api를 제공하면 추가해서 수정할 예정이다.
middleware를 통해 검증하기 때문에 홈화면에서 라우티하는 과정이 간소화됐다.
flowerseok and others added 24 commits December 24, 2023 00:27
기본 제공 메소드인 fetch로 과제에서 필요한 모든 기능을 구현할 수 있으므로 axios를 삭제했다.
굳이 client side에서 렌더링될 필요 없는 페이지이기 때문에 서버 컴포넌트로 변경했다.
투표/결과 페이지는 로그인하지 않으면 접근할 수 없도록 막았고,
로그인된 상태에서 회원가입 페이지 접근을 막았으며,
투표한 사람들이 다시 투표 페이지로 진입할 수없도록 막았다.
[chore] 코드 정리
[style] 투표/결과 페이지 디자인 수정
Copy link

@rmdnps10 rmdnps10 left a comment

Choose a reason for hiding this comment

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

next.js 의 기능을 풍부하게 활용하신 것 같아요, 과제 하시느라 고생 많으셨습니다:)
서버사이드에서 로컬스토리지에 접근이 불가능하여 그 대용으로 쿠키를 이용해야 되겠다는 것도 배워갑니다!

Comment on lines +1 to +16
export const useDebounce = <T extends (...args: any[]) => any>(
fn: T,
delay: number
) => {
let timeout: ReturnType<typeof setTimeout>;

return (...args: Parameters<T>): ReturnType<T> => {
let result: any;
if (timeout) clearTimeout(timeout); // 기존의 timeout 삭제
timeout = setTimeout(() => {
// 새로운 timeout 할당
result = fn(...args);
}, delay);
return result;
};
};

Choose a reason for hiding this comment

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

함수가 특정 지연 후에만 호출되도록 보장하는 useDebounce훅이 인상이 깊었고, 제네릭 표현식을 유연하게 사용하시는 것 같습니다:)

}: AuthorizedInfoProps) => {
return (
<>
<span className="shrink-0 mr-2 bg-ceos-1 text-white rounded-[20px] text-xs md:text-xl px-5 py-2.5 box-border justify-center items-center inline-flex">{`${userInfo.teamName} ${userInfo.part} ${userInfo.name}`}</span>

Choose a reason for hiding this comment

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

Next + Tailwindcss 활용을 잘하신 거 같아요 !

Comment on lines +32 to +43
return new Response('Authorized', {
status: 200,
});
} else if (res.status === 404) {
return new Response('Wrong ID', {
status: 404,
});
} else if (res.status === 401) {
return new Response('Wrong Password', {
status: 401,
});
} else {

Choose a reason for hiding this comment

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

Next.js로 서버 역할까지 구현하신 점이 멋있어요~~

Comment on lines +1 to +9
import { useState } from 'react';

export const useJoinHandler = () => {
const [wrongNameFlag, setWrongNameFlag] = useState<boolean | undefined>(
undefined
);
const [wrongIdFlag, setWrongIdFlag] = useState<boolean | undefined>(
undefined
);

Choose a reason for hiding this comment

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

회원가입할 때 발생할 수 있는 모든 상황들을 고려하셔군요 ㄷㄷ 👍 커스텀 훅으로 잘 빼놓으신 것 같아요! 수고 많으셨습니다...

이유는 모르겠으나, getSession을 통해 쿠키의 userInfo를 삭제할 때 에러가 발생했다.
Cookies can only be modified in a Server Action or Route Handler.
위와 같은 에러가 발생했는데, 일반적인 메소드에서 쿠키를 변경하려 할 때 발생하는 에러인 것 같다.
나는 서버 컴포넌트에서 해당 메소드를 사용하면 문제가 되지 않을 줄 알았는데, 로그인처럼 서버 액션을
따로 만들어서 쿠키를 삭제해주어야 하는 모양이다. 일단 귀찮아서 getSession 내의 쿠키 삭제 로직을 지웠고,
middleware를 통해 라우팅시 유효기간이 지난 유저정보를 삭제할 수 있도록 수정했다.
Copy link

@kyuhho kyuhho left a comment

Choose a reason for hiding this comment

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

깔끔하게 구현된 Next 코드를 보면서 정말 마음이 편안해졌습니다...이번 과제도 고생하셨습니다 🔥🔥

Comment on lines +11 to +21
useEffect(() => {
(async () => {
try {
const res = await fetch('/api/session', { cache: 'no-cache' });
if (res.ok) {
const stored = await res.json();
setUserInfo(stored);
} else throw new Error();
} catch {}
})();
}, [pathName]);
Copy link

Choose a reason for hiding this comment

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

커스텀 훅 내부에서 api를 콜하는 방식으로 구현해서 코드가 더 깔끔한거 같습니다 👍👍

Copy link

Choose a reason for hiding this comment

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

회원가입 시에 발생하는 오류를 처리하다보니 코드가 길어졌었는데, 깔끔한 예외처리를 보니 마음이 편안해집니다...

Copy link

Choose a reason for hiding this comment

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

이번 과제에서는 못해서 아쉬웠지만..🥲 다른 프로젝트에서 유효성 검사를 하려고 하면 코드가 너무 길어져서 저도 고민이었는데, 이렇게도 할 수 있다는 점 같이 배워갑니다🔥

Comment on lines +40 to +44
<AuthInput
placeHolder="이름 [1~5자]"
inputName="username"
handleChangeInput={useDebounce(handleChangeName, 500)}
/>
Copy link

Choose a reason for hiding this comment

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

회원가입 구현 시에 onBlur 이벤트 시에 vaild 체크를 했는데, useDebounce 로 input 값이 바뀔 때마다 체크를 하는게 정말 좋아요!! 새로 배워갑니다 !!

Copy link

@silverain02 silverain02 left a comment

Choose a reason for hiding this comment

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

이번주도 고생하셨슴다!!

Comment on lines +17 to +29
cookies().set(
'user_info',
JSON.stringify({
...authInfo,
expTime: new Date().getTime() + EXP_LIMIT,
}),
{
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: EXP_LIMIT,
path: '/',
}

Choose a reason for hiding this comment

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

저희는 localStorage에 토큰 저장해서 처리했는데 이렇게 쿠키 쓸 수도 있군요! 보안 상 더 안정적일 것 같네요 ~!

Copy link

Choose a reason for hiding this comment

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

쿠키로 관리하는 법 배워갑니다!


const handleChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
const str = e.target.value;
if (/^([a-zA-Z0-9가-힣]){1,5}$/.test(str)) {

Choose a reason for hiding this comment

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

정규식 처리까지 해주셨네요 굿입니다~!

Comment on lines +48 to +55
} else if (response.status === 409) {
// console.log('투표 실패:', response.data.message);
} else if (response.status === 403) {
// console.log('투표 실패:', response.data.message);
}
} catch (error) {
// console.error('투표 실패:', error);
}

Choose a reason for hiding this comment

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

투표 실패 이유도 alert로 사용자에게 알려주면 좋을 것 같아요!


const getDemoResults = async () => {
const res = await fetch(
`${process.env.NEXT_PUBLIC_SERVER_URL}/demoday/results`,

Choose a reason for hiding this comment

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

서버 url env파일로 뺀 부분 좋네요! url 수정 시 처리하기 편할 것 같아용

Copy link

@mod-siw mod-siw left a comment

Choose a reason for hiding this comment

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

이번 과제도 수고하셨습니다! 코드 리뷰하면서 Next 다루는 법 뿐만 아니라 효율적으로 코드 작성하는 법까지 다방면으로 많이 배워갑니다👍

Comment on lines +17 to +29
cookies().set(
'user_info',
JSON.stringify({
...authInfo,
expTime: new Date().getTime() + EXP_LIMIT,
}),
{
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: EXP_LIMIT,
path: '/',
}
Copy link

Choose a reason for hiding this comment

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

쿠키로 관리하는 법 배워갑니다!

const session = useSession();
const token = session?.accessToken;
const filteredVoteItems = VOTE_ITEMS.filter(
(item) => item.team !== session?.teamName
Copy link

Choose a reason for hiding this comment

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

아예 투표시에 로그인한 팀을 투표 후보에서 제외시키신 거 보고 되게 깔끔해서 놀랐어요! 간단히 투표 후보들 나열 후 본인 팀 투표시 알림 뜨는 정도로 생각했는데 이렇게 하면 별다른 알림 없이 바로 투표할 수 있어 좋을 것 같습니다👍👍

파트장 투표
</>
</VoteBanner>
{!userInfo?.candidateVoted ? (
Copy link

Choose a reason for hiding this comment

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

투표한 상태면 투표하기 버튼이 없어지는 거 보고 센스 있고 깔끔하다고 생각했습니다👍👍

Comment on lines +9 to +22
const HoveringButton = ({
children,
handleClickButton,
buttonStyle,
}: HoveringButtonProps) => {
return (
<button
className={`${buttonStyle} font-semibold text-white rounded-[20px] transition ease-in-out delay-100 bg-ceos-1 hover:-translate-y-1 hover:scale-110 hover:bg-ceos-2 duration-300`}
onClick={handleClickButton}
>
{children}
</button>
);
};
Copy link

Choose a reason for hiding this comment

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

이렇게 호버시 스타일을 따로 빼니 훨씬 깔끔하네요! tailwind css의 단점이 가독성이라고 생각했는데, 이렇게도 단점을 보완할 수 있구나 배워갑니다!

Copy link

Choose a reason for hiding this comment

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

이번 과제에서는 못해서 아쉬웠지만..🥲 다른 프로젝트에서 유효성 검사를 하려고 하면 코드가 너무 길어져서 저도 고민이었는데, 이렇게도 할 수 있다는 점 같이 배워갑니다🔥

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants