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주차] TIG 미션 제출합니다. #6

Open
wants to merge 64 commits into
base: master
Choose a base branch
from

Conversation

Programming-Seungwan
Copy link

@Programming-Seungwan Programming-Seungwan commented Jun 26, 2024

TEAM TIG vote 과제 frontend by 은수, 승완

배포링크

배포 링크

기술 스택

  • nextJS
  • tailwindCSS
  • typescript
  • jwt

디자인

피그마 링크

페이지 별 기능 ⚒️

  • 반응형으로 구현
  • parallel routes & intercepting route를 사용한 모달 구현
  1. 회원 가입 페이지
    • 아이디 & 이메일의 중복 불가
  2. 로그인 페이지
    • 로그인 성공 시, BE로부터 jwt 반환 & 추후 로그인 사용자 요청은 jwt를 매번 전송(쿠키 같은 방식 이용)
    • 로그아웃 기능
  3. 투표 페이지
    • 파트장 투표 : 본인 파트의 파트장만 투표 가능
    • 공통
      • 로그인 한 사용자만 투표 가능 / 로그인 안한 사용자도 조회 가능 but 투표는 불가능
      • 본인 혹은 본인이 속한 팀에 투표 불가
      • 투표는 한 번만 가능
  4. 투표 결과 페이지
    • 로그인 안한 사용자도 조회 가능
    • 투표 수 동일 시 공동순위

주요 포인트 📌

인증

로그인 기능이 필요한 웹 페이지에는 인증을 어떻게 할 것인가?의 논쟁이 있다고 생각합니다. 제가 생각하는 인증의 구현 방식은 크게 2가지입니다

  1. 세션 기반의 인증 방식 : 백엔드 서버는 로그인 정보를 기반으로 세션을 생성하고, 이를 브라우저에게 반환합니다. 백엔드 서버는 이를 데이터베이스에 넣어두거나, 자체 메모리에 올려두는 방식으로 추후 사용자의 인증 요청을 진행합니다. 이는 상태성을 지니는 것이므로, 무상태성을 지녀야 수평 확장을 할 수 있는 백엔드 시스템을 구축하기에 불리한 측면이 있습니다.
  2. jwt 토큰 기반의 인증 방식 : 서버는 비밀키만을 보유하고, 프론트엔드(브라우저)에서 jwt를 보관하고 있다가 전송하면 이를 해석하여 인증을 시도합니다. 이는 백엔드 시스템을 stateless 하게 설계할 수 있다는 장점이 있습니다.

JWT를 어디에 저장할 것인가?

  1. localStorage : 백엔드 시스템이 따로 있지 않은 경우에 간편하게 저장할 수 있다. 하지만 xss 공격에 취약할 수 있다(Javascript로 접근 가능하기 때문) -> localStorage.getItem('jwtToken') 과 같은 방식
  2. cookie : httpOnly와 secure 속성을 이용하면 Javascript로 접근 불가능하고, https 전송에서만 사용될 수 있기에 보안성이 높음. 하지만 4kb라는 용량 제한이 있음. 자동으로 백엔드 주소로 전송되어 따로 Authorization 헤더에 싣어보내줄 필요가 없음

하지만 cookie는 csrf(Cross Site Request Forgery) 공격에 취약함. 이는 위조된 웹 사이트를 해커가 만들어서 본인이 원하는 정상적인 도메인으로 cookie를 자동 전송되게 하는 방법임 -> samesite 속성이나 anti csrf token으로 방어 가능

JWT를 저장하는 추천되는 방법

accessToken은 메모리에 들고 있고, refreshToken은 csrf 공격을 막아준 cookie에 저장하기.

  1. Outh 2.0 프로토콜에서 백엔드가 refresh token은 httpOnly, secure, samesite 속성을 설정해서 바로 브라우저에 꽂아주기
  2. accessToken은 백엔드가 프론트엔드에 보내는 response body에 포함해서 보내주기
  3. 사용자는 accessToken을 변수에 들고 있고, 없다면 백엔드에 refreshToken을 이용해서 다시 받아온다. 이때 cors로 보안을 강화하여 해커가 refreshToken이 있더라도 백엔드로의 accessToken을 얻기위한 접근은 block된다.
    참고 링크

CORS

CORS 정책은 브라우저 자체 단에서 출처(프로토콜 + 도메인 주소 + 포트번호)가 다를 경우 요청을 차단하는 개념입니다.

  1. preflight 요청 : option 메서드로 전송되며, 백엔드 서버에서 Access-Control-Allow-Origin 헤더에 프론트엔드의 주소가 포함되어 있어야 합니다.
  2. jwt와 같은 토큰 정보를 헤더에 싣어보낼 때, Access-Control-Expose-Headers 헤더에 해당 정보를 보내는 헤더명을 명시해주면 프론트엔드에서 Javascript로 접근할 수 있습니다.

tailwindCSS와 nextJS

nextJS는 서버 사이드 렌더링을 지원하는 reactJS 기반의 프레임워크입니다. 기존의 styled-components 같은 라이브러리를 이용하여 스타일링을 진행하면, 내부적으로 reactJS의 상태 개념을 이용하여 만들어낸 hash 값을 클래스명으로 이용하기 때문에 로직이 클라이언트 단에서 이루어집니다. 만약 Javascript 실행 속도가 느린 환경이라면 사용자에게는 스타일링이 적용되지 않은, 단순 html 즉 layout만 보이는 순간이 존재할 수도 있습니다. 따라서 전처리기, 후처리기의 원리를 차용한 tailwindCSS를 이용하면 좀 더 편하지 않을까 싶습니다.

nextJS의 장점

  1. cors나 https mixed content 오류를 해결하는 방법론으로 nextJS 서버를 이용할 수 있다. 이를 프록시 서버로서 활용하는 것이다. 하지만 reactJS는 s3로 배포하는 데에 반해 nextJS는 EC2를 통하기 때문에 더 복잡하다.
  2. loading.tsx 컴포넌트를 통해 자체적으로 Suspense 컴포넌트를 적용해줄 수 있다.

느낀점 🧐

  1. 은수
    그동안 백엔드 없이 구현했기 때문에 겪을 수 없던 CORS, 토큰관련 문제들을 경험해볼 수 있어서 좋았습니다. 모달을 만들 때 리액트에서 처럼 createPortal이 아닌 parallel & intercepting routes를 사용해 만들어 모달이 라우팅되는 것처럼 구현했는데 익숙치 않아서 그런지 더 번거로운 느낌이 들어 빨리 익숙해지고 싶다는 생각이 들었습니다. 쿠키를 사용하는 것, 중복된 코드들에 대한 리팩토링을 하지 못한 아쉬움이 남습니다. 본격적으로 프로젝트를 시작하게 되면 처음부터 재사용성을 고려한 코드를 작성해야겠다는 다짐을 했습니다.

  2. 승완
    지난 과제를 진행할 때에는 어떤 컴포넌트는 서버 컴포넌트로 만들 것인지를 많이 고민하면서 했는데, 이번에는 백엔드와의 cors 이슈를 해결하느라 이에 대해 다소 소홀했던 것 같습니다. 또한 사실 jwt를 쿠키에 저장해두고, 매번 자동으로 브라우저의 동작 원리에 따라 jwt를 쿠키에 담아 백엔드 단으로 전송하는 로직이 좀 더 바람직한데, 이를 하지 못한 것 같아 다소 아쉽습니다. accessToken과 refreshToken을 각각 클라이언트에서 어떻게 저장할지 고려해보면 더 좋을 것 같습니다.

Programming-Seungwan and others added 30 commits June 17, 2024 17:24
1. 과제 구현 기능 및 세부 사항은 리드미에 작성
2. 기존의 과제 리드미는 Assignment.md 파일로 이동
3. 대부분 지난 번 netflix 과제와 설정은 동일함
640px를 breakpoint로 잡아 그전까진 100%, 이후엔 640px 고정
Header 컴포넌트 구현 및 기본 테마, width 설정 merged by seungwan
CEOS 클릭시 랜딩페이지로 이동
- 높이 작을때 발생하는 overflow 방지
Feat/vote result merged by seungwan
로그인 페이지 및 회원 가입 페이지 구현
1. 클라이언트 코드 단에서 곧바로 진행하면 CORS 문제가 발생
1. 투표 상태와 연결 완료. 처음 렌더링 시에 사용자의 정보를 백엔드 api로 받아와서 상태로 연결해주면 됨
2. signupform에서 팀별 순서 재설정 -> 백엔드 로직이 원인
1. 로그인 되면, authorization헤더에 있는 jwt와, body에 있는 username, part 를 읽어들인다.
2. 읽어들인 정보를 각각 localStorage에 있는 jwtToken, username, part라는 key 값에 넣어둔다. 로그인 되면 홈 화면으로 보냄
3. 헤더 컴포넌트는 useEffect() 훅을 통해 dom에 mount 될 때 로컬 스토리지에 접근하여 part와 username을 보여준다. 로그아웃 & 로그인 버튼도 조건부 상태로 렌더링
songess and others added 27 commits June 26, 2024 16:22
Feat/eunsu merged by seungwan
- Parallel, Intercepting routes 사용
Modal feat made by eunsu, merged by seungwan
1. next.config.mjs 파일에 rewrites로 source와 destination 설정
2. 기존의 api 콜을 source에 해당하는 것으로 수정
1. 로그인된 사용자에게도 로그인 안된 사용자는 투표 못한다고 뜨는 문제
2. router.push에 {scroll: false} 속성 부여
3. 아예 replace() 메서드를 쓰는 게 나을 수도 있음
Copy link

@Shunamo Shunamo left a comment

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.

경고창을 모달을 만들어주셨네요! 예쁘고 섬세해요 👍
Modal 컴포넌트를 하나만 만들어서 message를 prop으로 받아서 모달 재사용하는 방법도 좋을 것 같아요!!


try {
const tmpResponse = await fetch(
'http://43.202.139.24:8080/api/user/signup',
Copy link

Choose a reason for hiding this comment

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

여기 api 주소 위에 정의해놓으신거로 바꿔주면 좋을 것 같아요!

Suggested change
'http://43.202.139.24:8080/api/user/signup',
'/api/user/signup ',


try {
const tmpResponse = await fetch(
'http://43.202.139.24:8080/api/user/login',
Copy link

Choose a reason for hiding this comment

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

여기도요!!

} catch (error) {
console.log(error);
return new Response('Login Failed!', {
status: 400,
Copy link

Choose a reason for hiding this comment

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

클라이언트에서 직접 상태를 지정할수도 있지만 서버에서 받은 상태 코드를 그대로 사용하는 것도 좋은 방법일 것 같아요!

loginmodal: React.ReactNode;
}) {
return (
<div className="w-full sm:w-[640px] bg-backgroundColor h-dvh flex flex-col overflow-y-scroll">
Copy link

Choose a reason for hiding this comment

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

반응형ㅎㅎ

import { useRouter } from 'next/navigation';
import { useState, useEffect } from 'react';

const TeamName = ['AZITO', 'BEATBUDDY', 'TIG', 'BULDOG', 'COUPLELOG'];
Copy link

Choose a reason for hiding this comment

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

저는 이번에 데이터는 따로 data.ts에서 모아서 관리했는데 더 편하고 깔끔했어요! 따로 data들을 모아놓는 파일을 만드는 것도 좋은 것 같아요

body: JSON.stringify(sendingDataObject),
});
if (response.ok) {
alert('팀 투표가 완료되었습니다.');
Copy link

Choose a reason for hiding this comment

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

alert를 쓰니까 확실히 사용성이 좋아지네요... 사실 저는 윈도우 알람 모달을 별로 안좋아해서 하나도 안썼는데 있는게 훨씬 나은 것 같아요👍


// 백엔드로부터 해당 유저의 상태를 쿠키나 로컬 스토리지에 있는 jwt를 이용하여 받아오고, 이를 상태와 연결하는 side effect
useEffect(() => {
const localStorageToken = localStorage.getItem('jwtToken');
Copy link

Choose a reason for hiding this comment

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

혹시 useEffect 내부에 정의하신 이유가 따로 있을까요..!?localStorage.getItem('jwtToken')가 반복되고 있는데, 함수 상단에 const jwtToken = localStorage.getItem('jwtToken'); 이렇게 변수에 정의해두고 사용하면 더 좋을 것 같아요!

const tmpData: teamProp[] = data.sort((a, b) => {
if (a.voteCount === b.voteCount) {
// voteCount가 같을 경우, teamName으로 알파벳 순 정렬
return a.teamName.localeCompare(b.teamName);
Copy link

Choose a reason for hiding this comment

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

오 ㅎㅎ 동점자까지 고려하신거 대박이에요 LGTM 👍 👍 (이러케쓰는건가 ㅋㅋ)

localStorage.removeItem('username');
setPart(null);
setUsername(null);
router.push('/');
Copy link

Choose a reason for hiding this comment

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

이 onClick 핸들러를 따로 함수로 분리해서 사용하면 더 깔끔하게 바꿀 수 있을 것 같아요!

const handleLogout = () => {
  localStorage.removeItem('jwtToken');
  localStorage.removeItem('part');
  localStorage.removeItem('username');
  setPart(null);
  setUsername(null);
  router.push('/');
};
``` 이런식으로요!

Copy link

@Rose-my Rose-my left a comment

Choose a reason for hiding this comment

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

마지막 과제까지...! 너무 수고하셨어요 > < 그동안 많이 배웠습니다ㅏ TIG 플젝도 화이팅하세요 !!

Comment on lines +40 to +45
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('jwtToken')}`,
},
body: JSON.stringify(sendingDataObject),
Copy link

Choose a reason for hiding this comment

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

API 요청시마다 매번 헤더 적는것 보다는 공통으로 빼면 더 좋을 것 같아요!

credentials: 'include',
});

const data = await response.json();
Copy link

Choose a reason for hiding this comment

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

getTeamData 함수에서 API 요청 후 바로 response.json()을 호출하기 전에 response.ok를 확인하면 서버 오류를 더 잘 처리할 수 있을 것 같아요 !!

Copy link

Choose a reason for hiding this comment

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

Suggested change
const data = await response.json();
if (!response.ok) {
throw new Error('데이터를 가져오는데 실패했습니다.');
}
const data = await response.json();

Comment on lines +33 to +40
onClick={() => {
localStorage.removeItem('jwtToken');
localStorage.removeItem('part');
localStorage.removeItem('username');
setPart(null);
setUsername(null);
router.push('/');
}}
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.

Suggested change
onClick={() => {
localStorage.removeItem('jwtToken');
localStorage.removeItem('part');
localStorage.removeItem('username');
setPart(null);
setUsername(null);
router.push('/');
}}
const handleLogout = () => {
localStorage.removeItem('jwtToken');
localStorage.removeItem('part');
localStorage.removeItem('username');
setPart('');
setUsername('');
router.push('/');
};
const handleLogin = () => {
router.push('/login');
};
onClick={handleLogout}
onClick={handleLogin}

Copy link

@noeyeyh noeyeyh left a comment

Choose a reason for hiding this comment

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

TIG팀 안녕하세요! 벌써 스터디 마지막 코드리뷰네욥 ㅜㅜ
항상 깔끔한 코드 보고 많이 배워갑니다! 친절한 모달창까지..
프로젝트도 화이팅해요!!

}
// voteCount 내림차순 정렬
return b.voteCount - a.voteCount;
});
Copy link

Choose a reason for hiding this comment

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

VoteCount가 같은 경우도 꼼꼼하게 처리해주셨네요. 저도 공동 순위 고려해서 코드 수정해야겠어욥,.

} catch (error) {
console.log(error);
return new Response('Sign up Failed!', {
status: 400,
Copy link

Choose a reason for hiding this comment

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

만약 서버에서 상세하게 실패 경우에 따라 메세지를 작성해뒀다면 에러 객체에서 메세지도 출력해주면 좋을 것 같아요!
ㅎㅎ 그래서 회원 가입 실패 이유도 alert 창으로 띄워서 알려주면 좋을 것 같아용

return (
<div className="w-full h-full flex flex-col justify-center items-center gap-[40px]">
<div className="font-semibold text-[28px]">투표는 한 번씩만 가능합니다.</div>
<button
Copy link

Choose a reason for hiding this comment

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

오 모달창 친절해용

export interface teamProp {
teamName: string;
voteCount: number;
}
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.

5 participants