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

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

Conversation

Rose-my
Copy link

@Rose-my Rose-my commented Jun 26, 2024

배포링크

5주차 미션: React-Vote

🌱 필수 구현

  • UI/UX에 대한 감각을 최대한 발휘해 디자인을 적용해 봅니다.
  • HTTPS를 통해 서버와 통신합니다.
  • API Fetch는 어떤 방식을 사용하든 무방합니다 (Fetch API, axios 등)
  • Promise.then() 보단 async/await를 사용해 보세요. 더 최신 스펙이랍니다.

🪴 Team

👨‍💻 팀원 정보 및 역할

김민영 안혜연
WEB Frontend WEB Frontend

👨‍💻 팀원 역할 분담

▶️ View & API

View API
🧑🏻‍🎨 김민영 회원가입, 로그인 POST
에러핸들링
🐰 안혜연 결과 GET
투표 (파트, 데모데이) GET, POST

📄 서버 통신

🔻 러비팀은 Next가 아닌 React+typescript로 구현하기로해서 이번 투표 과제는 CSR로 구현했습니다.

1. axios

  • 왜 fetch가 아닌 axios를 사용했는가?
    사실 자바스크립트에는 fetch api가 존재하지만, 더 많은 기능을 지원하기에 개발자들은 서버 통신에 보다 편리한 axios를 선호한다고..!
Axios Fetch
설치 필요 O 설치 필요 X
CSRF 보호 기능이 있다 별도 기능이 없다
자동으로 JSON 데이터 형식으로 변환해준다 json() 메서드를 따로 사용해서 JSON 형태로 바꿔 주어야 한다
요청 취소 및 타임아웃을 걸 수 있다. -
HTTP 요청을 가로챌 수 있다. -
상태코드가 에러 코드를 내뱉으면 reject 한다. 에러 응답을 받더라도 resolve 하고 네트워크 장애가 발생한 경우에만 거부한다. (에러 뱉어주지 X)

2. async/await

  • 왜 promise가 아닌 async, await를 사용했는가?
    ⚠️ Promise & then 의 문제점

    • 가독성이 떨어지고, 디버깅이 불편하다 (길어지는 체이닝)
    • 예외처리 (try…catch 와 .then / .catch의 혼용이 헷갈림)

    👉 그래서 등장한 async/await

async function 함수명( ){
  await 비동기함수();
}

3. customAxios

  • axios를 사용할 때마다 baseURL을 작성하는 등 반복되는 코드를 작성하지 않기 위해서 커스터마이징한 인스턴스를 모듈화하여 사용
import axios from "axios";

export const customAxios = axios.create({
  baseURL: `${import.meta.env.VITE_APP_BASE_URL}`,
  headers: {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*",
  },
});

4. 인터셉터

  • 로그인을하면 헤더에 토큰을 넣어주는 작업
    👉 authorization header
    • 투표는 access 토큰에 따라 사용자를 식별해야하기 때문에 토큰을 확인하는 절차가 필수였습니다. 따라서 요청을 인터셉트해서 access token의 정보를 헤더에 넣어주도록 했습니다.
    • 토큰은 Cookie에 넣어서 관리했고 access token이 있으면 헤더의 Authorization에 Bearer ${token} 형태로 토큰 값을 넣어서 요청합니다.
    • useLayoutEffect 훅을 이용해서 렌더링 전에 훅 실행
import { getCookie } from "@api/cookie";
import { customAxios } from "@api/customAxios";
import { useLayoutEffect } from "react";

export default function useSetInterceptors() {
  useLayoutEffect(() => {
    const requestInterceptor = customAxios.interceptors.request.use(
      (request) => {
        const accessToken = getCookie("accessToken");
        if (accessToken) {
          request.headers.Authorization = accessToken;
        }
        return request;
      },
      (error) => {
        console.error("Request error:", error);
        return Promise.reject(error);
      },
    );

    return () => {
      customAxios.interceptors.request.eject(requestInterceptor);
    };
  }, []);
}

5. react-query와 custom-hook

🔻 GET - useQuery 사용

첫 번째 파라미터로 unique key를 포함한 배열이 들어간다. 이후 동일한 쿼리를 불러올 때 유용하게 사용된다.
첫 번째 파라미터에 들어가는 배열의 첫 요소는 unique key로 사용되고, 두 번째 요소부터는 query 함수 내부의 파라미터로 값들이 전달된다.
두 번째 파라미터로 실제 호출하고자 하는 비동기 함수가 들어간다. 이때 함수는 Promise를 반환하는 형태여야 한다.
최종 반환 값은 API의 성공, 실패 여부, 반환값을 포함한 객체이다.

import { useQuery } from "react-query";
import { getTopicsById } from "@api/getTopicsById";
import { ResponseTypes } from "@api/getTopicsById";

export function useGetTopicsById(topicID: number) {
  const result = useQuery<ResponseTypes, Error>(
    ["getTopicsById", topicID],
    () => getTopicsById(topicID),
    {
      onError: (error) => {
        console.log("해당 Topic이 존재하지 않습니다.", error);
      },
    },
  );

  return result;
}

🔻 POST - useMutation 사용

반환값은 useQuery와 동일하지만, 처음 사용 시에 post 비동기 함수를 넣어준다. 이때 useMutation의 첫 번째 파라미터에 비동기 함수가 들어가고, 두 번째 인자로 상황 별 분기 설정이 들어간다는 점이 차이이다.

실제 사용 시에는 mutation.mutate 메서드를 사용하고, 첫 번째 인자로 API 호출 시에 전달해주어야하는 데이터를 넣어주면 된다
const { mutate: postVoteMutate } = usePostVote();

import { useMutation } from "react-query";
import { postSignUp } from "@api/postSignUp";
import { useNavigate } from "react-router-dom";

export function usePostSignUp() {
  const navigate = useNavigate();

  return useMutation(postSignUp, {
    onSuccess: () => {
      console.log("회원가입 성공");
      navigate("/signin");
    },
    onError: (error) => {
      console.log("회원가입 실패", error);
    },
  });
}

👉 왜 React Query를 사용했는가?

  1. React Query는 React Application에서 서버 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리입니다.
  2. 복잡하고 장황한 코드가 필요한 다른 데이터 불러오기 방식과 달리 React Component 내부에서 간단하고 직관적으로 API를 사용할 수 있습니다.
  3. 더 나아가 React Query에서 제공하는 캐싱, Window Focus Refetching 등 다양한 기능을 활용하여 API 요청과 관련된 번잡한 작업 없이 “핵심 로직”에 집중할 수 있습니다.
  4. React-Query는 캐싱을 통해 동일한 데이터에 대한 반복적인 비동기 데이터 호출을 방지하고, 이는 불필요한 API 콜을 줄여 서버에 대한 부하를 줄이는 좋은 결과를 가져옵니다.
  5. 컴포넌트 내부에서 Server 데이터를 가져오고 있는데, 이때 onSuccess와 onError 함수를 통해 fetch 성공과 실패에 대한 분기를 아주 간단하게 구현할 수 있어서 이는 Server 데이터를 불러오는 과정에서 구현해야할 추가적인 설정들을 진행할 필요가 없습니다.
  6. 📍 즉, Client 데이터는 상태 관리 라이브러리가 관리하고, Server 데이터는 React-Query가 관리하는 구조라고 생각하면 되고 이를 통해 우리는 Client 데이터와 Server 데이터를 온전하게 분리할 수 있습니다.
장점
재사용성 API 호출과 처리를 분리하여 여러 컴포넌트에서 재사용 가능
에러 핸들링 useMutation의 onError 콜백을 사용하여 에러를 처리
성공 처리 onSuccess 콜백을 사용해 성공 시 필요한 후속 작업(예: 페이지 이동)을 쉽게 관리할 수 있음
상태 관리 React Query를 사용하여 서버 상태를 관리하면, 상태를 직접 관리할 필요 없이 자동으로 업데이트

6. 우리가 리액트쿼리를 적용한 방법

📦api
┣ 📜cookie.ts
┣ 📜customAxios.ts
┣ 📜getVotingOptionsById.ts
┗ 📜postSignIn.ts

  • api 폴더에 있는 함수는 API 호출을 수행합니다.
  • customAxios를 사용해 API 요청을 보내고, 서버로부터의 응답을 반환합니다.

📦hooks
┣ 📜useGetVotingOptionsById.ts
┗ 📜usePostSignIn.ts

  • hooks 폴더에 있는 함수는 요청을 처리하는 역할
  • 예를 들어, 로그인에 성공하면 쿠키에 토큰을 저장하고, 실패 시 에러 메시지를 표시하는 등 커스텀 할 수 있습니다.
  • 컴포넌트에서 hooks들을 호출하여 다양하게 사용할 수 있습니다

🎀 참고

🔗 리액트쿼리 공식문서.

Rose-my and others added 30 commits May 23, 2024 15:54
@Rose-my Rose-my changed the title 러비팀 [7주차] Team 러비 김민영 & 안혜연 미션 제출합니다. Jun 26, 2024
@Rose-my Rose-my changed the title [7주차] Team 러비 김민영 & 안혜연 미션 제출합니다. [7주차] 러비 미션 제출합니다. Jun 26, 2024
Copy link

@songess songess 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와 àxios`를 사용해 api 로직을 깔끔하게 작성해주셔서 보기 편했습니다.
리팩토링을 하게된다면 UX적인 고민을 조금 더 해보면 좋을 거 같아요!

"@pages/*": ["pages/*"],
"@styles/*": ["styles/*"],
"@utils/*": ["utils/*"],
"@type/*": ["type/*"]
Copy link

Choose a reason for hiding this comment

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

다른 것들은 alias가 잘 적용되고 있는데 type은 제대로 적용되고 있지 않은 거 같아요. 폴더 명에 맞게 "@_types/*": ["types/*"] 같은 방식으로 수정하면 될 거 같아요!

"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
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.

@songess 왜케 깜찍하게 말하세요

Comment on lines +24 to +25
{ index: true, element: <SignIn /> },
{ path: "/signin", element: <SignIn /> },
Copy link

Choose a reason for hiding this comment

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

동일한 컴포넌트를 보여주는데 같은 URL로 해도 될 거 같아요!

export const Router = createBrowserRouter([
{
path: "/",
element: <Interceptors />,
Copy link

Choose a reason for hiding this comment

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

로직을 완벽하게 이해한게 아니라 조심스럽지만, 현재 <Interceptors />가 모든 컴포넌트의 요청을 인터셉트하고 있어서 토큰이 필요없는 로그인/회원가입 요청 시에도 토큰이 담기고 있는 거 같아요. axios의 인스턴스를 만들때 두 개의 인스턴스를 만들어서 하나는 인터셉터를 통해 토큰을 담아 보내는 인스턴스, 다른 하나는 토큰을 담지 않고 보내는 인스턴스를 사용해 해결할 수 있을 거 같아요.

<QueryClientProvider client={queryClient}>
<Suspense fallback="..loading">
<ThemeProvider theme={theme}>
<RouterProvider router={Router} />
Copy link

Choose a reason for hiding this comment

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

router를 사용해 훨씬 깔끔한 코드가 된 거 같아요.

import ResultsOnlyLayout from "@components/layout/ResultsOnlyLayout";
import Demo from "@components/result/Demo";

export default function ResultsOnlyDemo() {
Copy link

Choose a reason for hiding this comment

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

뒤로가기버튼이 있었으면 좋을 거 같아요.

setSelected(selected === itemKey ? null : itemKey);
};

const handleSubmit = () => {
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 +91 to +94
<VoteBtn
text="결과보기"
onClick={() => navigate(`/result/${type}`)}
/>
Copy link

Choose a reason for hiding this comment

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

결과보기 버튼의 색상이 disabled가 되어 비활성화된 버튼처럼 보여서 좀 더 짙은 색 혹은 다른 색을 사용해도 좋을 거 같아요.

$disableClick={true}>
<TextWrapper>
<LeftTextWrapper>
<RankBox>{index + 1}</RankBox>
Copy link

Choose a reason for hiding this comment

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

공동순위에 대한 로직도 있었으면 좋았을 거 같아요.

import { AxiosResponse } from "axios";
import { customAxios } from "./customAxios";

export interface VotingOptionDtoTypes {
Copy link

Choose a reason for hiding this comment

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

src/api에 선언되어있는 type들은 types안으로 몰아주면 좋을 거 같아요!

Copy link

@Programming-Seungwan Programming-Seungwan left a comment

Choose a reason for hiding this comment

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

tanstack query와 vite를 이용한 프로젝트였다는 점에서 인상 깊었습니다!
이번 과제도 고생 많으셨습니다

Comment on lines +20 to +21
"styled-components": "^6.1.11",
"styled-reset": "^4.5.2"

Choose a reason for hiding this comment

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

스타일링을 위해서 styled-components 라이브러리를 사용하셨네요! 제가 지난 번에 전체 톡방에 올린 것처럼, reactJS 와 같이 CSR을 지원하는 라이브러리에서는 충분하지만 nextJS와 같이 서버 사이드 렌더링을 지원하는 경우 사용자가 네트워크가 느린 환경에서 애플리케이션을 이용할 경우 스타일링이 적용되지 않은 레이아웃만을 볼 가능성이 있어요!
앞으로도 다양한 기술 스택 선정을 해보시면 좋을 것 같습니다.

"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.2.5",
"typescript": "^5.2.2",
"vite": "^5.2.0",

Choose a reason for hiding this comment

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

vite 번들러를 이용해서 리액트 프로젝트를 진행하셨네요. 기존의 cra은 내부적으로 webpack을 이용하여 번들링을 진행하는 반면에 vite는 esbuild라는 GO 언어를 통해 작성된 번들러를 개발환경에서, 배포환경에서는 rollup 을 이용해 HMR까지 지원하고 있다고 해요!
속도가 빠른 vite를 선택하신 점 좋아보여요! 다만 vite가 global 변수를 이용하려면 추가 설정을 해주어야 하고 webAPI의 사용이 일부 제한 된다는 점을 조심해서 사용하시면 좋을 것 같습니다.

function App() {
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback="..loading">

Choose a reason for hiding this comment

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

음... Suspense 컴포넌트를 이용해서 데이터를 로딩 중일 때 대체 UI를 보여주려면 fallback 속성에는 컴포넌트가 들어와야 하는 것 아닌가요?

Copy link

Choose a reason for hiding this comment

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

그냥 "..loading" 텍스트가 뜨게끔 해두었습니다..

Comment on lines +5 to +19
export function setCookie(
name: string,
value: string | undefined,
options: object,
) {
return cookies.set(name, value, { ...options });
}

export function getCookie(name: string) {
return cookies.get(name);
}

export function removeCookie(name: string) {
return cookies.remove(name);
}

Choose a reason for hiding this comment

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

cookie에 여러 값들을 저장하고, 조회하고, 삭제하는 기능의 함수들을 만들어주셨네요! 다만 이는 백엔드 단의 로직에서 set-cookie 헤더를 통해 요청을 response로 받는 시점에 바로 넣어줄 수도 있어요. 뿐만 아니라 httpOnly 같은 속성을 이용하면 Javascript로 cookie에 접근하는 것도 막아줄 수 있고요!
document.cookie 처럼 JS를 통해 쿠키에 바로 접근할 수 있게 만든다면 보안적으로 이슈가 될 수도 있다고 생각합니다.

baseURL: `${import.meta.env.VITE_APP_BASE_URL}`,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",

Choose a reason for hiding this comment

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

해당 헤더는 preflight 요청 시에 본 요청에 대한 cors 이슈를 해결해주기 위해서 백엔드 단에서 진행하는 헤더 설정으로 알고 있어요! 프론트엔드의 출처(프로토콜 + 도메인 주소 + 포트번호)를 여기에 명시해주면, 해당 출처로부터의 요청은 브라우저가 막지 않게 되는 것이지요!

따라서 러비 팀에서 axios 인스턴스를 만들어 쓸때 필요한 설정이 아니라 백엔드에서 진행해야 하는 것으로 생각하는데, 이렇게 코드를 작성하신 이유가 따로 있나 궁금합니다.

name: string;
}

export interface ResponseTypes {

Choose a reason for hiding this comment

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

getFinalResults.ts 파일에서도 동일한 이름의 인터페이스가 있는데, 좀더 semantic하게 작명하시면 좋을 것 같습니다.

Copy link

Choose a reason for hiding this comment

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

저는 한 파일에 하나의 api 함수만 존재한다면 여기서 type 정의 후 사용해도 괜찮을 거 같아요~ 다만 어떤 api들에 대한 type은 분리되어 있어서 파트너 간 정한 규칙으로 일관성을 유지했다면 더 좋을 거 같네요. 그리고 승완님 말씀처럼 타입을 export 해서 다른 파일에서도 사용한다면 일반적인 네이밍보다는 특수한 네이밍이 나을 거 같아요. GetTopicByIdRes 이런 식으로요~


export async function postEmail(props: postEmailTypes) {
const { email } = props;
const response = await customAxios.post("/api/users/verify", {

Choose a reason for hiding this comment

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

이메일 인증 방식까지 거치신 부분 너무 좋습니다 👍

Choose a reason for hiding this comment

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

맞아요 최고입니다....👍

import { useLayoutEffect } from "react";

export default function useSetInterceptors() {
useLayoutEffect(() => {

Choose a reason for hiding this comment

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

useLayoutEffect 훅을 이용하여 렌더링하여 계산된 DOM 결과가 화면에 paint 되기 전에 실행하고 싶은 동작을 잘 구현하신 것 같습니다. 훅의 유연한 사용 좋습니다 👍

(request) => {
const accessToken = getCookie("accessToken");
if (accessToken) {
request.headers.Authorization = accessToken;

Choose a reason for hiding this comment

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

쿠키에 저장한 accessToken을 요청의 헤더에 싣어 전송하고 있는데, 이는 어찌보면 불필요한 동작이 아닐까 싶거든요. 쿠키의 속성에 domain과 path 를 명시해 놓는다면, 해당 주소로 전송되는 모든 요청에는 자동으로 쿠키가 들어가는 것으로 알고 있습니다.

Copy link

@youdame youdame Jun 28, 2024

Choose a reason for hiding this comment

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

@Programming-Seungwan 승완님이 말씀하신 건 토큰을 set-cookie 헤더에 담아서 클라이언트에게 보내주는 경우를 의미하는 것 같아요. 이 경우에는 로그인 이후에 클라이언트에서 토큰에 대한 아무런 코드를 쓰지 않아도 리퀘스트를 보내기만 하면 인증서가 Cookie 헤더에 함께 담겨서 전송되죠.

그런데 러비 팀에서는 로그인 시 서버에서 액세스토큰을 바디에 담아 응답값으로 넘겨주고 클라이언트에서는 그 토큰을 쿠키라는 저장 공간에 담아서 사용하고 있는 거 같아요. 따라서 서버가 set-cookie 헤더에 인증서를 추가해서 넘겨주는 게 아니기 때문에 서버에 요청을 보낼 때마다 자동으로 인증서가 전달되는 게 아닌 거죠.

그래서 임의적으로 클라이언트가 별도의 코드를 작성해서 쿠키에 담아둔 토큰을 가져다가 넣어주는 작업이 필요해요. 마치 그냥 로컬스토리지에 토큰을 저장하는 것처럼 러비팀에서는 쿠키에 토큰을 저장했다고 생각하시면 될 거 같아요!

Comment on lines +2 to +3
import { getFinalResult } from "@api/getFinalResult";
import { ResponseTypes } from "@api/getFinalResult";

Choose a reason for hiding this comment

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

한 코드로 넣어주시면 좋을 것 같아요

Copy link

@youdame youdame left a comment

Choose a reason for hiding this comment

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

고생하셨습니다~ 남겨드린 리뷰 참고해주세요!

"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
Copy link

Choose a reason for hiding this comment

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

@songess 왜케 깜찍하게 말하세요

name: string;
}

export interface ResponseTypes {
Copy link

Choose a reason for hiding this comment

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

저는 한 파일에 하나의 api 함수만 존재한다면 여기서 type 정의 후 사용해도 괜찮을 거 같아요~ 다만 어떤 api들에 대한 type은 분리되어 있어서 파트너 간 정한 규칙으로 일관성을 유지했다면 더 좋을 거 같네요. 그리고 승완님 말씀처럼 타입을 export 해서 다른 파일에서도 사용한다면 일반적인 네이밍보다는 특수한 네이밍이 나을 거 같아요. GetTopicByIdRes 이런 식으로요~

(request) => {
const accessToken = getCookie("accessToken");
if (accessToken) {
request.headers.Authorization = accessToken;
Copy link

@youdame youdame Jun 28, 2024

Choose a reason for hiding this comment

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

@Programming-Seungwan 승완님이 말씀하신 건 토큰을 set-cookie 헤더에 담아서 클라이언트에게 보내주는 경우를 의미하는 것 같아요. 이 경우에는 로그인 이후에 클라이언트에서 토큰에 대한 아무런 코드를 쓰지 않아도 리퀘스트를 보내기만 하면 인증서가 Cookie 헤더에 함께 담겨서 전송되죠.

그런데 러비 팀에서는 로그인 시 서버에서 액세스토큰을 바디에 담아 응답값으로 넘겨주고 클라이언트에서는 그 토큰을 쿠키라는 저장 공간에 담아서 사용하고 있는 거 같아요. 따라서 서버가 set-cookie 헤더에 인증서를 추가해서 넘겨주는 게 아니기 때문에 서버에 요청을 보낼 때마다 자동으로 인증서가 전달되는 게 아닌 거죠.

그래서 임의적으로 클라이언트가 별도의 코드를 작성해서 쿠키에 담아둔 토큰을 가져다가 넣어주는 작업이 필요해요. 마치 그냥 로컬스토리지에 토큰을 저장하는 것처럼 러비팀에서는 쿠키에 토큰을 저장했다고 생각하시면 될 거 같아요!

Copy link

Choose a reason for hiding this comment

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

혹시 이 부분을 따로 커스텀 훅으로 정의해서 사용하신 이유가 있나요? useLayoutEffect를 사용하신 이유도 궁금합니다. 왜냐면 제가 알기로는 이런 인스턴스를 정의하고 그 아래에 바로 customAxios.interceptors.request.use라는 코드를 작성하면 언제나 해당 인스턴스가 실행될 때마다 인터셉터가 동작하는 걸로 알고있거든요. 쿠키를 사용하면 뭔가 다른 걸까요..?

Comment on lines +111 to +122
export default function SignUp() {
const [name, setName] = useState("");
const [ID, setId] = useState("");
const [PW, setPw] = useState("");
const [pwCheck, setPwCheck] = useState("");
const [email, setEmail] = useState("");
const [emailCheck, setEmailCheck] = useState("");
const [selectedTeam, setSelectedTeam] = useState("");
const [selectedPart, setSelectedPart] = useState("");
const [isEmailClicked, setIsEmailClicked] = useState(false);
const [verificationCode, setVerificationCode] = useState("");

Copy link

Choose a reason for hiding this comment

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

모든 input 컴포넌트를 제어 컴포넌트로 동작하게 만들면 하나의 인풋값이 바뀔 때마다 페이지 전체가 리렌더링 되는 현상이 발생하게 돼요! 그래서 이걸 방지하기 위해 react-hook-form 같은 라이브러리를 사용하기도 합니다. 참고하시면 좋을 거 같아요 ~ 리액트 훅폼

Comment on lines +170 to +180
<InputContainer name="PASSWORD" state={PW} setState={setPw} />
<InputContainer custom={true}>
<label>CONFIRM PASSWORD</label>
<div>
<input
type="text"
id="pwCheck"
placeholder="CONFIRM PASSWORD"
value={pwCheck}
onChange={(e) => setPwCheck(e.target.value)}
/>
Copy link

Choose a reason for hiding this comment

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

스크린샷 2024-06-28 오후 4 07 56 password에 해당하는 인풋에 type=password가 설정되어있지 않아서 이렇게 비밀번호 내용이 가려지지 않고 다 보이고 있어요..!

Comment on lines +195 to +216
이메일 인증
</VerifyBtn>
</div>
</InputContainer>
{isEmailClicked && (
<InputContainer custom={true}>
<label>VERIFY EMAIL</label>
<div>
<input
type="text"
id="emailCheck"
placeholder="이메일 인증코드"
value={emailCheck}
onChange={(e) => setEmailCheck(e.target.value)}
/>
<EmailVerifyText $emailVerify={emailVerify}>
인증코드 확인
</EmailVerifyText>
</div>
</InputContainer>
)}
</FilledInput>
Copy link

Choose a reason for hiding this comment

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

버튼은 주로 클릭하는 용도로 사용하는데, PW 확인이나 인증코드 확인이라고 적힌 버튼은 클릭하는 용도가 아니라 유효성 검증에 대한 것들이라 다른 식으로 표현했다면 더 좋았을 거 같아요~

Comment on lines +58 to +69
<VoteWrapper onClick={() => navigate("/results-only/front")}>
FE 파트장 투표결과
<br />
바로가기
</VoteWrapper>
<VoteWrapper onClick={() => navigate("/results-only/back")}>
BE 파트장 투표결과
<br />
바로가기
</VoteWrapper>
<VoteWrapper onClick={() => navigate("/results-only/demo")}>
데모데이 투표결과
Copy link

Choose a reason for hiding this comment

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

버튼 태그에서 다른 로직 없이 이동만 하고 있기 때문에 useNavigate로 페이지를 이동하는 것보다 Link 태그를 사용하는 게 더 좋을 거 같아요! 그리고 Wrapper라는 네이밍만으로는 이게 버튼 태그인지 알기 어려워보입니다 ㅠㅠ

Copy link

Choose a reason for hiding this comment

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

스크린샷 2024-06-28 오후 4 22 35

데모데이 투표 결과를 보면 제목이 BE 파트장 투표 결과로 떠용..

Copy link

Choose a reason for hiding this comment

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

getTopicById랑 getFinalResult랑 getVotingOptionById가 이름만 봐서는 어떤 역할을 하는 함수인지 잘 모르겠어요. 파트장 투표 결과를 가져오는 건지 아니면 데모데이 투표 결과를 가져오는 건지 잘 모르겠네요. 유지 보수 시에 다른 사람들도 쉽게 이해할 수 있는 네이밍이면 좋을 거 같습니다!

Copy link

@CSE-pebble CSE-pebble left a comment

Choose a reason for hiding this comment

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

마지막 과제 수고하셨습니다!! PR 자세하게 작성해주셔서 코드 읽는데 편했습니다 ㅎㅎ

Comment on lines +9 to +11
onError: (error) => {
console.log("이메일 인증 실패", error);
},

Choose a reason for hiding this comment

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

이메일 인증 실패 시 콘솔에만 에러 메세지가 뜨고 있어서, alert 창 등을 통해 사용자에게 실패 여부를 알려주면 UX 개선에 도움될 것 같아요!


export async function postEmail(props: postEmailTypes) {
const { email } = props;
const response = await customAxios.post("/api/users/verify", {

Choose a reason for hiding this comment

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

맞아요 최고입니다....👍

Comment on lines +50 to +60
const EmailVerifyText = styled.p<{ $emailVerify: boolean }>`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 3.625rem;
background-color: ${({ $emailVerify, theme }) =>
$emailVerify ? theme.colors.active : theme.colors.confirm};
color: ${({ theme }) => theme.colors.black};
${({ theme }) => theme.fonts.SignBtnText};
`;

Choose a reason for hiding this comment

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

cursor:pointer를 넣어주면 더 좋을 것 같아요!

<Section>
<FilledInput>
<InputContainer name="ID" state={ID} setState={setId} />
<InputContainer name="PASSWORD" state={PW} setState={setPw} />

Choose a reason for hiding this comment

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

맞아요! 회원가입 때도 마찬가지로 안 보이도록 처리해주시면 좋을 것 같아요

<>
<VoteHeader />
<>{children}</>
<VoteBtn text="돌아가기" onClick={() => navigate("/vote/main")} />

Choose a reason for hiding this comment

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

돌아가기 버튼을 눌렀을 때 메인페이지로 가는 것보다는 이전 페이지로 돌아가는게 더 좋을 것 같아요! 투표하기 전에 결과를 본 후에 다시 투표를 하는 상황 등을 고려해서요!

<VoteHeader />
<CenterWrapper>
<HeaderText>파트장 투표</HeaderText>
<SubTitleText>* 본인 파트 + 한 번 투표할 수 있습니다</SubTitleText>

Choose a reason for hiding this comment

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

사소하고 개인적인 의견이지만 + 기호보다는 "본인 파트만 한 번 투표할 수 있습니다." 등의 텍스트로 바꾸면 좋을 것 같아요!

Comment on lines +76 to +78
<HeaderText>
{type === "front" ? "FE 파트장 투표" : "BE 파트장 투표"}
</HeaderText>

Choose a reason for hiding this comment

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

HeaderText가 VotePart 페이지에서만 쓰인다면 좋은 로직 같지만 다른 페이지에서도 계속 반복되고 있는 것 같아요! 아예 Header 공통 컴포넌트를 따로 만드는게 더 좋을 것 같습니다!

Choose a reason for hiding this comment

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

theme 적극 활용하신 점 너무 좋습니다!!

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