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

[4주차 기본/심화/공유 과제] 회원가입 & 로그인 구현 #5

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

Conversation

KIMGEONHWI
Copy link
Member

@KIMGEONHWI KIMGEONHWI commented Nov 11, 2024

✨ 구현 기능 명세

💡 기본 과제

  • React + TypeScript
  • Axios 라이브러리 사용
  • ThemeProvider, GlobalStyle 사용
  1. 로그인
  • 로그인 타이틀
  • 아이디(이름) 입력 Input
  • 비밀번호 입력 Input
  • 로그인 버튼 (hover시 배경색 바꾸기 (transition 적용))
  • 회원가입 버튼 (회원가입 페이지로 이동)
  1. 회원가입
  • 이름 - 비밀번호 - 취미 입력이 한 페이지에서 일어남 (컴포넌트만 갈아끼우기)
  • 상단에 회원가입 타이틀
  • 하단에는 로그인 페이지로 가는 링크
  1. 회원가입(이름)
  • 이름 입력 Input
  • 다음 버튼 (비밀번호 입력 폼 나옴)
  • Input 비어있을 때 버튼 비활성화
  1. 회원가입(비밀번호)
  • 비밀번호 입력 Input
  • 비밀번호 확인 Input
  • 둘 중 하나라도 비어있으면 버튼 비활성화
  • 두 비밀번호가 다르면 버튼 비활성화
  • 다음 버튼 (취미 입력 폼 나옴)
  1. 회원가입(취미)
  • 취미 입력 Input
  • 회원가입 버튼
  • Input 비어있을 때 버튼 비활성화
  • 회원가입 실패 시 에러메시지 alert 출력
  • 회원가입 성공 시 회원번호 alert 출력하고, login 페이지로 이동
  1. 마이페이지
  • 헤더에 취미, 내 정보 메뉴 탭
  • 헤더에 로그아웃 버튼
  • 로그아웃 버튼 클릭 시 token 저장 정보 삭제하고 로그인 페이지로 이동 (token 저장 위치는 자율)
  • 헤더 취미, 내 정보 취미 페이지, 내 정보 페이지 출력 (1개의 페이지로 구현해도 되고, url 달라도 됨)
  1. 마이페이지(취미)
  • 나의 취미 출력
  • 사용자 번호 입력 Input
  • 검색 버튼
  • 검색 오류시 alert
  • 검색된 취미 출력
  1. 마이페이지(내 정보)
  • 비밀번호만 입력하면 비밀번호만 변경
  • 취미만 입력하면 취미만 변경
  • 둘 다 입력하면 둘다 변경
  • 둘 다 비어있으면 alert

🔥 심화 과제

  • any 사용하지 않기
  1. 회원가입 (이름)
  • 8글자 넘어가도 버튼 비활성화 처리
  • 8글자 넘어가는 것에 대해 에러메시지 출력
  1. 회원가입 (비밀번호)
  • 비밀번호 보이기 버튼 추가
  • 8글자 넘어가도 버튼 비활성화 처리
  • 8글자 넘어가는 것에 대해 에러메시지 출력
  • 비밀번호 불일치 에러 메시지 출력
  • (선택) 에러메시지 한개만 출력해도 됨 (우선순위는 알아서)
  1. 회원가입 (취미)
  • 8글자 넘어가도 버튼 비활성화 처리
  • 8글자 넘어가는 것에 대해 에러메시지 출력

공유과제

제목: Typescript 왜 사용할까? 제대로 알고 사용하자

링크 첨부 : https://wave-web.tistory.com/121


❗️ 내가 새로 알게 된 점

React.Dispatch<React.SetStateAction>
상위 컴포넌트에서 useState 상태 변경 함수를 넘길 때 () => void 로 넘겼었는데 정의 할 수 있는 타입이 있었다.
DispatchsetStateAction을 통해 setState 함수의 타입을 정의해주면 상태 업데이트 필요 시 요구하는 인자와 반환 값을 정확하게 명시할 수 있다.
setState가 인자로 받고, 업데이트하게 될 값의 타입이 string임을 쉽게 파악할 수 있다. 상태를 문자열로 설정하거나, 이전 문자열을 기반으로 새로운 문자열을 설정할 수 있도록 한다.
해당 프로퍼티의 의미를 알기 쉽게 함으로써 가독성을 좋게 하고, 추후 유지보수에도 용이하다.
boolean인 경우에는 React.Dispatch<React.SetStateAction<boolean>>을 사용한다.


❓ 구현 과정에서의 어려웠던/고민했던 부분

퍼널 로직으로 회원가입 페이지 관리하기

이번 과제 중에서 가장 많은 시간을 투자하고, 고민을 많이했던 부분입니다.
회원가입 페이지의 흐름이 이름 => 비밀번호 => 취미 순서로 입력되어야 하고, 한 페이지에서 관리하다보니 누가봐도 더러운 코드에 가독성도 많이 떨어지는 것이 느껴졌습니다.

  • 여러 페이지에 걸쳐있는 상태를 관리하기가 어렵다.
  • 상태는 여러 페이지에서 수집되지만, 상태가 수집되는 곳과 사용되는 곳은 다르기 때문에 상태를 관리하기 위해서는 전역 상태를 사용해야 한다. 그러나, 전역 상태를 사용하게 되면 상태가 사용되는 흐름을 따라가면서 코드를 읽어야 하기 때문에 유지 보수성이 떨어지게 된다.

https://www.youtube.com/watch?v=NwLWX2RNVcw
전에 우연히 퍼널 설계 관련한 영상을 본것이 기억이나, 해당 영상을 참고하여 퍼널 설계를 바탕으로 회원가입 페이지를 구현하였습니다.

useFunnel.ts

import { useState } from "react";

type UseFunnelReturnType<T> = {
  currentStep: T;
  next: () => void;
  prev: () => void;
  reset: () => void;
  isLastStep: boolean;
  isFirstStep: boolean;
};

function useFunnel<T>(steps: T[]): UseFunnelReturnType<T> {
  const [currentIndex, setCurrentIndex] = useState(0);

  const next = () => {
    setCurrentIndex((prevIndex) =>
      prevIndex < steps.length - 1 ? prevIndex + 1 : prevIndex
    );
  };

  const prev = () => {
    setCurrentIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : prevIndex));
  };

  const reset = () => {
    setCurrentIndex(0);
  };

  return {
    currentStep: steps[currentIndex],
    next,
    prev,
    reset,
    isLastStep: currentIndex === steps.length - 1,
    isFirstStep: currentIndex === 0,
  };
}

export default useFunnel;

이 useFunnel 훅은 여러 단계로 이루어진 UI 흐름(회원가입 단계를 순차적으로 진행하는 폼)을 관리할 수 있도록 돕는 커스텀 훅입니다. 현재 단계, 다음 단계로의 이동, 이전 단계로의 이동 등을 쉽게 처리할 수 있습니다.

const funnel = useFunnel(["name", "password", "hobby"]);

<Form>
        {funnel.currentStep === "name" && (
          <NameStep name={name} setName={setName} onNext={funnel.next} />
        )}
        {funnel.currentStep === "password" && (
          <PasswordStep
            password={password}
            confirmPassword={confirmPassword}
            setPassword={setPassword}
            setConfirmPassword={setConfirmPassword}
            onNext={funnel.next}
          />
        )}
        {funnel.currentStep === "hobby" && (
          <HobbyStep hobby={hobby} setHobby={setHobby} onNext={onSubmit} />
        )}
</Form>

step값에 따라 각 컴포넌트 UI를 조건부 렌더링 할 수 있습니다.
각 컴포넌트에서 다음 버튼을 누를 때 step 상태를 원하는 컴포넌트로 업데이트 된다.
이 방식을 통해 step이 추가되어도 유연하게 대응할 수 있고, 전역 상태를 사용하지 않아도 되어 어떤 상태가 어떤 UI에서 수집되는지도 한눈에 볼 수 있게 된다. => 확장 가능성, 가독성 up


🥲 소요 시간

  • 12h

🖼️ 구현 결과물

회원가입

2024-11-12.3.51.46.mov

로그인, 취미 조회

2024-11-12.3.52.55.mov

마이페이지 - 내정보 수정

2024-11-12.3.55.03.mov

@KIMGEONHWI KIMGEONHWI self-assigned this Nov 11, 2024
@KIMGEONHWI KIMGEONHWI changed the title [4주차 기본/심화/공유 과제] 회원가입 & 로그인 (취미) 구현 [4주차 기본/심화/공유 과제] 회원가입 & 로그인 구현 Nov 11, 2024
Copy link

@minjeoong minjeoong left a comment

Choose a reason for hiding this comment

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

코드가 읽기 편하고 전체적으로 깔끔하게 잘 짜신 것 같아요 !
4주차도 너무 고생 많으셨습니다 :) 👍🏻👏🏻👏🏻

Comment on lines +13 to +20
try {
const response = await axios.post(
`${import.meta.env.VITE_BASE_URL}/login`,
{
username,
password,
}
);

Choose a reason for hiding this comment

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

p3: axios 환경 세팅으로 BASE_URL 을 지정할 수 있어요!
사용하는 배포된 api url 가 2개 이상이라면 이렇게 지정하면 안되겠지만,
지금처럼 한개의 api만 사용한다면 이렇게 지정해두는 방법도 있습니다!
/login 등 api 경로를 다른 파일로 나눠둔 후 axios.get(PATH_API.MY_HOBBY) 이런식으로 부르는 방법도 참고해주시면 좋을 것 같아요 !! 🍀

const api = axios.create({
  baseURL: PATH_API.API_DOMAIN,
  headers: {
    'Content-Type': 'application/json',
  },
  // withCredentials:true, // 쿠키 cors 통신 설정
});

background: ${({ theme }) => theme.colors.gray2};
color: ${({ theme }) => theme.colors.white1};
border: none;
border-radius: 5px;
Copy link

@minjeoong minjeoong Nov 20, 2024

Choose a reason for hiding this comment

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

p4: 보통 border-radius 는 px 로 처리하시는지 궁금합니다 !

Copy link
Member Author

Choose a reason for hiding this comment

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

p4: 보통 border-radius 는 px 로 처리하시는지 궁금합니다 !

넵 저는 border 관련 스타일링은 px로 처리합니다! 해당 내용은 민정님 코리에 달았으니 참고 부탁드려요.

Comment on lines +35 to +41
`${import.meta.env.VITE_BASE_URL}/user/my-hobby`,
{
headers: {
token: token,
},
}
);
Copy link

@minjeoong minjeoong Nov 20, 2024

Choose a reason for hiding this comment

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

p2: 헤더 세팅도 마찬가지로 axios 환경 세팅에서 가능합니다
매번 세팅하는 것보다 유지보수성 차원에서 좋을 것 같은데, 시간없어서 이렇게 하신 거라면 백번 이해합니다...........🥺🥺

api.interceptors.request.use(
  (config) => {
    // 요청 전 처리
    const token = localStorage.getItem('accessToken');
    if (token) {
      config.headers.token = `${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error),
);

Comment on lines +44 to +46
{funnel.currentStep === "name" && (
<NameStep name={name} setName={setName} onNext={funnel.next} />
)}
Copy link

@minjeoong minjeoong Nov 20, 2024

Choose a reason for hiding this comment

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

p5: 오 funnel 은 처음 보는데, .next 하면 다음 스텝으로 넘어가는 시스템인가보네요!
저도 step 부분에서 코드가 더러워질 것 같아서 고민을 좀 했었는데,
이런 방식으로 짜면 좀 더 깔끔해질 것 같네요 !! 배워갑니다..👏🏻

Comment on lines +12 to +27
function useFunnel<T>(steps: T[]): UseFunnelReturnType<T> {
const [currentIndex, setCurrentIndex] = useState(0);

const next = () => {
setCurrentIndex((prevIndex) =>
prevIndex < steps.length - 1 ? prevIndex + 1 : prevIndex
);
};

const prev = () => {
setCurrentIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : prevIndex));
};

const reset = () => {
setCurrentIndex(0);
};

Choose a reason for hiding this comment

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

p5: 코드 엄청 깔끔하고 좋네요.. 👍🏻

Choose a reason for hiding this comment

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

p5: PR에 남겨주신 퍼널이군요!! 많이 배워갑니다 ㅎㅎ

Copy link

@heesunee heesunee left a comment

Choose a reason for hiding this comment

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

역시 리드다운 코드군요 .. 👍 딱히 집히는 부분 없이 술술.. 술술 읽혔습니다 ...
사실 스타일링 하다보면 컴포넌트 이름을 대충 짓게 될 때도 있는데 네이밍이나 funnel, 또 pr에 남겨주신 dispatch 부분들도 코드를 보면서 이런식으로 사용하는 구나 - 하고 많이 배워가는 것 같아요. 민정님 의견처럼 api 관련 부분만 리팩토링하면 더 좋을 것 같습니다! 사실 웹 1팀 중에 마지막으로 코드 리뷰를 진행했는데 다른 분들 코드에 남겨주신 코멘트랑, 건휘님 코드 보고 합세 코드 빨리 고쳐야겠다고 생각중입니다.. 하하 ㅎㅎㅎ 4주차 과제 너무 고생많으셨습니다!!

Comment on lines +22 to +33
<NavItem
active={activeTab === "hobby"}
onClick={() => setActiveTab("hobby")}
>
취미
</NavItem>
<NavItem
active={activeTab === "info"}
onClick={() => setActiveTab("info")}
>
내 정보
</NavItem>

Choose a reason for hiding this comment

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

p5: 저는 취미랑 정보페이지를 따로 만들어서 이동하게 했는데, 이렇게도 구현할 수 있었네요. 배워갑니다!

const NavItem = styled.span<{ active: boolean }>`
font-size: 1.2rem;
cursor: pointer;
color: ${({ active }) => (active ? "white" : "#ddd")};

Choose a reason for hiding this comment

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

p4: 저는 active 상태에 따라 다른 색을 보여주고 싶을때, inactive, active로 이름을 붙여서 사용하는 편인데 참고해주셔도 좋을것 같아요! 여기 하나는 #ddd로 색상이 정의되어있어서 제안드립니다 ㅎㅎ

Comment on lines +12 to +27
function useFunnel<T>(steps: T[]): UseFunnelReturnType<T> {
const [currentIndex, setCurrentIndex] = useState(0);

const next = () => {
setCurrentIndex((prevIndex) =>
prevIndex < steps.length - 1 ? prevIndex + 1 : prevIndex
);
};

const prev = () => {
setCurrentIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : prevIndex));
};

const reset = () => {
setCurrentIndex(0);
};

Choose a reason for hiding this comment

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

p5: PR에 남겨주신 퍼널이군요!! 많이 배워갑니다 ㅎㅎ

type="text"
placeholder="아이디"
value={username}
onChange={(e) => setUsername(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.

p5:코리 달다보니까 여기에 'e'가 'any' 타입이라고 하는데요! 사실 제가 해결방법을 몰라서... 찾아보니 들어오는 값에 따라서 e:string 하면 해결될 것 같다는 생각이 드네요!!

Comment on lines +13 to +21
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setName(value);

if (value.length > 8) {
setNameError("이름은 8글자 이하로 입력해주세요");
} else {
setNameError("");
}
Copy link

Choose a reason for hiding this comment

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

p5: 제가 에러 구현을 못해내서(사실 컴포넌트 내에 props로 받아서 출력하도록 만들어뒀거든요) 여기 많이 참고하겠습니다!!! 배워갑니다👍

Copy link

@ayla-12 ayla-12 left a comment

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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants