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주차 기본/공유 과제] 🔗 로그인, 회원가입 구현해보기 #6

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

Conversation

se0jinYoon
Copy link
Contributor

@se0jinYoon se0jinYoon commented May 10, 2024

✨ 구현 기능 명세

🧩 기본 과제

  1. 메인 페이지
    • 메인 이미지 or 비디오 넣기
  • 내정보페이지와 회원가입 페이지로 이동할 수 있는 버튼 구현
  1. 로그인 페이지

    • 아이디와 비밀번호를 입력할 수 있는 input구현
    • Login page 이미지 넣기
    • 로그인 버튼(기능)과 회원가입 페이지 이동 버튼 구현
    • 로그인 실패시 해당 에러메세지를 alert로 띄어주기
    • useParam 활용해서 id값 보유하고 있기.
  2. 회원가입 페이지

    • 아이디, 패스워드, 닉네임, 핸드폰 번호를 입력 받는 페이지 구현
    • 회원가입 버튼 클릭시 post api 통신을 진행하고 성공시 회원가입이 완료되었다는 메시지를 보여주는 alert 띄워준 후, 로그인 메인페이지로 이동
    • 아이디 중복, 비밀번호 형식 오류, 전화번호 형식 오류 등 모든 에러 alert로 메세지 보여주기
    • 비밀번호와 전화번호 형식은 input 아래에 보여주기
  3. 마이페이지

    • get 메소드를 사용해 사용자 정보를 가져오기
    • 서버에서 받아온 ID, 닉네임, 전화번호 데이터를 렌더링
    • 비밀번호 변경 토글을 사용해 비밀번호 변경 폼을 on/off할 수 있도록 구현
    • 기존 비밀번호 입력, 새로운 비밀번호 입력, 새로운 비밀번호 확인 input 구현
    • input이 비어있을 경우 api 작동되지 않도록 구현
    • 에러 발생시 api error객체 안 error message를 사용해 alert 띄우기
    • 홈 이동 버튼 구현

🔥 심화 과제

  1. 메인페이지

    • 비디오에 여러 기능을 적용
  2. 로그인 페이지

    • input이 비어있을 경우 api요청 보내지 않고 아래 error message를 띄워주기
  3. 회원가입 페이지
    input이 비어있는 상태로 api연결 시도했을시

    • 해당 input 테두리 색상 변경

    • input에 focus 맞추기

    • api요청 금지

    • 전화번호 양식 정규표현식으로 자동입력되도록 설정 (숫자만 입력해도 "-"가 붙도록)

    • 비밀번호 검증 유틸 함수 구현 (검증 통과되지 않을시 api요청 금지)

공유과제

  • prettier, eslint, styleLint에 대해
  • lighthouse에 대해

링크 첨부(팀 블로그 링크) :https://forweber.palms.blog/lighthouse


📌 내가 새로 알게 된 부분

상수데이터

각 페이지의 input 컴포넌트에서 필요한 값들을 미리 정의해 둔 후, map 을 사용하여 렌더링 하였습니다.

const SIGNUP_LABEL = [
  { label: 'ID', detailExist: false },
  {
    label: 'PW',
    detailExist: true,
    detail: '비밀번호 형식은 8자이상, 숫자, 특수문자, 영어 알파벳이 포함되어야 합니다.',
  },
  { label: '닉네임', detailExist: false },
  { label: '전화번호', detailExist: true, detail: '전화번호 형식은 010-****-**** 입니다.' },
];

const LOGIN_LABEL = [{ label: 'ID' }, { label: 'PW' }];

const MYPAGE_LABEL = [
  {
    label: 'ID',
    content: 'authenticationId',
  },
  {
    label: '닉네임',
    content: 'nickname',
  },
  {
    label: '전화번호',
    content: 'phone',
  },
];

const CHANGE_PW_LABEL = [
  { label: '기존 비밀번호', id: 'previousPassword' },
  { label: '새로운 비밀번호', id: 'newPassword' },
  { label: '비밀번호 확인', id: 'newPasswordVerification' },
];

export { SIGNUP_LABEL, LOGIN_LABEL, MYPAGE_LABEL, CHANGE_PW_LABEL };

공통 컴포넌트

1. 버튼

// 1️⃣ ButtonWrapper.jsx
const ButtonWrapper = (props) => {
  const { children } = props;
  return <BtnWrapper>{children}</BtnWrapper>;
};

// 2️⃣ Button.jsx
const Button = (props) => {
  const { content, onClick } = props;
  return (
    <ButtonWrapper {...props} onClick={onClick}>
      {content}
    </ButtonWrapper>
  );
};

export default Button;

const ButtonWrapper = styled.button`
  display: flex;
  justify-content: center;
  align-items: center;

  width = auto;
  height = auto;

  padding: 0.5rem 0.8rem;

  border: 1px solid ${({ theme }) => theme.colors.white};
  border-radius: 0.3rem;

  font-size: 1.2rem;
  cursor: pointer;

  // 3️⃣
  ${({
    theme,
    $marginTop = '',
    $buttonColor = 'skyblue',
    $fontColor = 'white',
    $hoverFontColor = 'white',
    $hoverColor = 'blue',
  }) => css`
    margin-top: ${$marginTop};

    background-color: ${theme.colors[$buttonColor]};
    color: ${theme.colors[$fontColor]};

    &:hover {
      border: 1px solid ${({ theme }) => theme.colors.blue};

      color: ${theme.colors[$hoverFontColor]};
      background-color: ${theme.colors[$hoverColor]};
    }
  `}
`;

1️⃣ 버튼을 감싸는 wrapper 컴포넌트에 children을 Prop으로 받아서 재사용
2️⃣ 버튼의 content와 style관련 코드를 prop으로 받아서 재사용
3️⃣ 기본 스타일을 미리 정의해둔 후, prop으로 받아온 값에 따라 버튼 스타일 변경

  1. input
// Input.jsx
const Input = (props) => {
  const { label, children, onChangeHandler } = props;

  return (
    <InputWrapper>
      <InputContainer>
        <InputLabel>{label}</InputLabel>
        <StyledInput id={label} onChange={onChangeHandler} />
      </InputContainer>
      {children}
    </InputWrapper>
  );
};

export default Input;

api 관련 함수 커스텀 훅으로 분리

컴포넌트 내부에 사이드이펙트를 담당하는 api 호출 함수를 포함하는 것은 좋지 못하다고 생각해서 로직 관련 함수는 따로 분리하여 사용하였습니다.

// 1️⃣ client.js
import axios from 'axios';

export const client = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_URL,
  headers: { 'Content-Type': 'application/json' },
});

// 2️⃣ getUserInfo.js
import { client } from './client';

export const getUserInfo = (userId) => {
  return client.get(`/member/info`, {
    headers: {
      memberId: userId,
    },
  });
};

// 3️⃣ useGetUserInfo.js
import { getUserInfo } from '../apis/getUserInfo';

const useGetUserInfo = (userId) => {
  const [userInfo, setUserInfo] = useState({
    authenticationId: '',
    nickname: '',
    phone: '',
  });

  useEffect(() => {
    const fetchUserInfo = async () => {
      try {
        const response = await getUserInfo(userId);
        setUserInfo(response.data.data);
      } catch (error) {
        console.log(error);
      }
    };

    fetchUserInfo();
  }, [userId]);

  return userInfo;
};

export default useGetUserInfo;

// 4️⃣ MyPage.js
const MyPage = () => { 
...
const { userId } = useParams();

const userInfo = useGetUserInfo(userId);
   return ...
}

1️⃣ axios 객체 생성
2️⃣ api 호출 함수 작성
3️⃣ api 함수를 컴포넌트에서 사용하는 로직에 추가하여 연결한 후 커스텀훅으로 정의
4️⃣ 필요한 컴포넌트에서 호출하여 사용

로그인/회원가입/비밀번호 변경 input에 입력받은 값 useReducer로 관리

// Login.jsx
// 1️⃣
const initialLoginState = {
  authenticationId: '',
  password: '',
};

// 2️⃣
const reducerFn = (state, action) => {
  switch (action.type) {
    case 'ID':
      return {
        ...state,
        authenticationId: action.value,
      };
    case 'PW':
      return {
        ...state,
        password: action.value,
      };
  }
};

const Login = () => {
  // 3️⃣
  const [inputVal, dispatch] = useReducer(reducerFn, initialLoginState);

  // 4️⃣
  const onChangeHandler = (e) => {
    dispatch({ type: e.target.id, value: e.target.value });
  };

  // 5️⃣
  const { submitLogin } = usePostLogin();

  const onClickLogin = () => {
    submitLogin(inputVal);
  };

return (...) 
}

1️⃣ useReducer에 전달할 초기 상태 정의
2️⃣ useReducer에 전달할 reducerFn 정의
3️⃣ useReducer 선언
4️⃣ input에 change 이벤트가 발생할 때마다 해당 input의 id 값에 따라 action.type을 정의하고, 입력된 value를 값으로 전달
5️⃣ 업데이트 된 reducer의 현재 상태값인 inputVal을 api의 인자로 전달
-> api의 인자로 바로 사용하기 위해서, api 명세서에 나와있는 필드명을 이용하여 reducer 상태객체의 key값을 정의해두었습니다.


💎 구현과정에서의 고민과정(어려웠던 부분) 공유!

딱히 없숨다!

🥺 소요 시간

  • 8h

🌈 구현 결과물

노션 링크 첨부합니다

Choose a reason for hiding this comment

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

이렇게 상수 데이터를 따로 모아두셨군요! 덕분에 각 컴포넌트에서 기능적으로 꼭 필요한 코드들만 작성되어 있어 훨씬 간결해진 것 같네요! 저도 다음부터는 상수데이터를 분리해 봐야겠어요!

const Login = () => {
const navigate = useNavigate();
const [inputVal, dispatch] = useReducer(reducerFn, initialLoginState);

Choose a reason for hiding this comment

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

useReducer로 ID와 password의 상태를 관리하신 부분이 정말 인상적입니다! +0+
저번 과제 이후 useReducer과 useState를 언제 어떻게 선택해야 할지에 대한 고민이 있던 찰나였습니다...
제가 이전에 찾아보았을 때는 배열, 객체와 같이 보다 복잡한 상태의 값을 관리할 경우 useReducer을 사용하는 게 좋고, 그렇지 않을 경우 코드 크기나 가독성을 고려해 useState를 사용하는 게 좋다고 결론을 내렸었는데,
useState와 useReducer 사용에 서진님의 기준이 있으신지도 여쭙고 싶습니다!

};
case 'inputVoidError':
return { ...state, inputVoidErrorMessage: action.value };
}

Choose a reason for hiding this comment

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

에러 처리까지 useReducer로 관리하니 로직이 정말 깔끔하고 간결해 보이네요!!

Copy link

Choose a reason for hiding this comment

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

p5) 저도 reducer 쓸 생각은 안했는데 state가 많으니까 reducer를 사용하는게 코드가 엄청 깔끔해지네요!

const Button = (props) => {
const { content, onClick } = props;
return (
<ButtonWrapper {...props} onClick={onClick}>

Choose a reason for hiding this comment

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

스프레드 연산자로 prop의 모든 값을 한번에 넘겨줄 수도 있군요! 서진님 덕분에 새롭게 배워가요!!!
이렇게 {...props}를 작성할 경우 {...props}를 작성하지 않은 코드와 어떤 차이점이 있는지 잘 감이 오지 않는데 해당 부분 여쭤봐도 될까요?!

Choose a reason for hiding this comment

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

Button.jsx에서의 ButtonWrapper과 컴포넌트명이 중복되는 것 같아욧!! 혼선을 방지하기 위해 ButtonWrapper.jsx의 ButtonWrapper과 Button.jsx의 ButtonWrapper의 컴포넌트명을 다르게 수정하면 좋을 것 같습니다!

@se0jinYoon se0jinYoon changed the title [4주차 기본/공유 과제] 🔗 로그인, 회원가입 구현해보기 (심화 커밍 쑨..) [4주차 기본/공유 과제] 🔗 로그인, 회원가입 구현해보기 May 12, 2024
Copy link

@rtttr1 rtttr1 left a comment

Choose a reason for hiding this comment

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

깔끔한 코드 잘 보고갑니다! 파일 분리, 상수 데이터등 분리를 많이 하셔서 코드를 짜니까 확실히 코드들이 간결하고 가독성도 좋네요!! 특히 reducer를 활용해서 여러 input state들을 관리하니 코드의 구조가 비슷해서 이해하기 훨씬 편했습니다~~~ 좋은 코드 잘 보고가고 이번 합세때도 좋은 코드 많이 알려주고 보여주시길 바랍니다ㅎ

export const client = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_URL,
headers: { 'Content-Type': 'application/json' },
});
Copy link

Choose a reason for hiding this comment

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

p5) axios.create로 axios 인스턴스를 만들고 불러와서 사용할수 있네요!! 배워갑니다..

$hoverFontColor = 'white',
$hoverColor = 'blue',
}) => css`
margin-top: ${$marginTop};
Copy link

Choose a reason for hiding this comment

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

p5) 혹시 여기서만 css 로 css 설정을 해주신 이유가 있을까요??

const { inputVoidErrorMessage, ...requestBody } = inputVal;
try {
const response = await patchChangePw(requestBody, userId);
alert(`✨ ${response.data.message} ✨`);
Copy link

Choose a reason for hiding this comment

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

p5) 휘황찬란한 텍스트 좋아요

} catch (error) {
if (error.response) {
const status = error.response.status;
if (status === 400 || status === 403) {
Copy link

Choose a reason for hiding this comment

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

p5) 500 에러는 제외하셨는데 혹시 이유를 알 수 있을까요??

<GlobalStyle />
<App />
</ThemeProvider>
</>
Copy link

Choose a reason for hiding this comment

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

p5) themeProvider로 한번 감싸줫는데 혹시 Fragment로 한번 더 감싼 이유가 있을까요??

return (
<LoginSignupWrapper title="LOGIN">
<LoginImg src="./src/assets/img/loginImg.jpeg" alt="로그인 이미지" />
{LOGIN_LABEL.map((label, idx) => (
Copy link

Choose a reason for hiding this comment

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

p5) 확실히 값들을 상수로 저장해서 map 메서드를 사용해서 요소들을 생성하는게 데이터 만들기는 귀찮아도 코드는 깔끔해지는것 같네요!

<MainWrapper>
<MainImg src="/src/assets/gifs/modalCongrats.gif" loop="infinite" />
<ButtonWrapper>
<Button content="MY PAGE" onClick={onClickMypage} />
Copy link

Choose a reason for hiding this comment

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

p5) 제가 생각했을땐 MY PAGE 같은 text정도는 prop으로 안사용하고 MY PAGE 으로 적어도 괜찮지 않나? 라고 생각했었는데 이렇게 text값들도 prop으로 넘겨서 사용할때의 장점이 있을까요??

};
case 'inputVoidError':
return { ...state, inputVoidErrorMessage: action.value };
}
Copy link

Choose a reason for hiding this comment

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

p5) 저도 reducer 쓸 생각은 안했는데 state가 많으니까 reducer를 사용하는게 코드가 엄청 깔끔해지네요!

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.

3 participants