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주차 기본/심화/생각 과제 ] #9

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

[ 4주차 기본/심화/생각 과제 ] #9

wants to merge 33 commits into from

Conversation

se0jinYoon
Copy link
Contributor

✨ 구현 기능 명세

🌱 기본 조건

  • .env 파일 사용하기

🧩 기본 과제

[ 로그인 페이지 ]

  1. 로그인
    • 아이디와 비밀번호 입력후 로그인 버튼을 눌렀을시 성공하면 /mypage/:userId 로 넘어갑니다. (여기서 userId는 로그인 성공시 반환 받은 사용자의 id)
  2. 회원가입 이동
    • 회원가입을 누르면 /signup으로 이동합니다.

[ 회원가입 페이지 ]

  1. 중복체크 버튼

    • ID 중복체크를 하지 않은 경우 검정색입니다.
    • ID 중복체크 결과 중복인 경우 빨간색입니다.
    • ID 중복체크 결과 중복이 아닌 경우 초록색입니다.
  2. 회원가입 버튼

    • 다음의 경우에 비활성화 됩니다.
    • ID, 비밀번호, 닉네임 중 비어있는 input이 있는 경우
    • 중복체크를 하지 않은 경우
    • 중복체크의 결과가 중복인 경우
    • 회원가입 성공시 /login 으로 이동합니다.

[ 마이 페이지 ]

  1. 마이 페이지
    • /mypage/:userId 의 userId를 이용해 회원 정보를 조회합니다.
    • 로그아웃 버튼을 누르면 /login으로 이동합니다.

🌠 심화 과제

[ 로그인 페이지 ]

  1. 토스트
    • createPortal을 이용합니다.
    • 로그인 실패시 response의 message를 동적으로 받아 토스트를 띄웁니다.

[ 회원가입 페이지 ]

  1. 비밀번호 확인

    • 회원가입 버튼 활성화를 위해서는 비밀번호와 비밀번호 확인 일치 조건까지 만족해야 합니다.
  2. 중복체크

    • 중복체크 후 ID 값을 변경하면 중복체크가 되지 않은 상태(색은 검정색)로 돌아갑니다.

생각과제

  • API 통신에 대하여
  • 로딩 / 에러 처리를 하는 방법에는 어떤 것들이 있을까?
  • 패칭 라이브러리란 무엇이고 어떤 것들이 있을까?
  • 패칭 라이브러리를 쓰는 이유는 무엇일까?

💎 PR Point

파일구조

📂src
┣ 📂assets
┃ ┣ 📂constants
┃    ┗ 📜constants.js
┣ 📂components
┃ ┣ 📂Layout
┃ ┃ ┣ 📜BtnWrapper.jsx
┃ ┃ ┣ 📜ContentWrapper.jsx
┃ ┃ ┗ 📜InputWrapper.jsx
┃ ┣ 📂Pages
┃ ┃ ┣ 📜Login.jsx
┃ ┃ ┣ 📜MyPage.jsx
┃ ┃ ┗ 📜Signup.jsx
┃ ┣ 📂UI
┃ ┃ ┣ 📜ErrorToast.jsx
┃ ┃ ┗ 📜Input.jsx
┃ ┗ 📜Router.jsx
┣ 📂styles
┃ ┣ 📜GlobalStyle.js
┃ ┗ 📜theme.js
┣ 📜App.jsx
┣ 📜api.js
┗ 📜main.jsx
 📜.env
📜index.html

📁 UI > Input.jsx

로그인과 회원가입 페이지에서 반복되어 사용되는 input을 공통 컴포넌트로 분리하여 사용

    <InputDiv>
      <InputLabel>{props.label}</InputLabel>
      <UserInput
        type="text"
        name={props.label}
        placeholder={props.placeholder}
        content={props.content}
        onChange={props.onChange}
      ></UserInput>
      {props.content === '중복체크' && (
        <IsExistIdBtn onClick={props.onClick} $isDuplicate={props.isDuplicate} $isBtnClicked={props.btnClicked}>
          {props.content}
        </IsExistIdBtn>
      )}
    </InputDiv>

props.label : input 의 라벨값을 전달 받습니다.
name={props.label} : 전달된 라벨값을 name 속성에 저장하여 dispatch의 action값으로 사용합니다.
placeholder={props.placeholder} : input의 placeholder값을 전달받습니다.
content={props.content} : content에 중복체크가 들어있다면 중복체크버튼 생성 및 input의 width값을 변경시킵니다.
onChange={props.onChange} : input에 입력이 감지되면 state에 저장하는 함수를 전달받습니다.
onClick={props.onClick} : 중복체크버튼이 클릭되면 API 요청 보내는 함수를 전달받습니다.

📁 Pages > Signup.jsx

label과 placeholder를 상수로 작성 후 파일을 분리하고 import하여 사용하였습니다.

// assets > constants > constants.js
const SIGNUP_LABEL = ['ID', '비밀번호', '비밀번호 확인', '닉네임'];

const SIGNUP_PLACEHOLDER = [
  '아이디를 입력해주세요',
  '비밀번호를 입력해주세요',
  '비밀번호를 다시 한 번 입력해주세요',
  '닉네임을 입력해주세요',
];
  • 입력값 저장 useReducer
    중복체크 후 ID 값을 변경하면 중복체크가 되지 않은 상태(색은 검정색)로 돌아갑니다.를 위해 ID일 경우의 조건 추가
  // input의 필드별 입력값 저장
  const onChangeHandler = (e) => {
    if (isClickedExistBtn && e.target.name === 'ID') {
      setIsClickedExistBtn((prev) => !prev);
      dispatch({ type: e.target.name, value: e.target.value });
    } else {
      dispatch({ type: e.target.name, value: e.target.value });
    }
  };
  • 중복확인
    reducer의 state에 저장된 ID값을 이용하여 API 요청
    응답의 isExist값을 isExist state에 저장, isClickedExistBtn 업데이트

    // 아이디 중복여부 bool
    const [isExist, setIsExist] = useState(false);
    // 중복 버튼 색 변경, 리셋용
    const [isClickedExistBtn, setIsClickedExistBtn] = useState(false);
    // 중복 확인 버튼
    const onClickDuplicateBtn = async (e) => {...}
  • 회원가입 버튼 활성화
    각 조건에 부합하는지 확인 후 상수에 저장
    모든 값이 valid하다면 signupValid state값을 true로 설정
    useEffect를 사용하여 불필요한 리렌더링 방지

    // 회원가입 버튼 활성화
    const [signupValid, setSignupValid] = useState(false);
    
      useEffect(() => {
        const idValid = !isExist && isClickedExistBtn && inputVal.username.length !== 0;
        const passwordValid = inputVal.password.length !== 0 && inputVal.password === inputVal.passwordCheck;
        const nicknameValid = inputVal.nickname.length !== 0;
    
        setSignupValid((prev) => {
          if (idValid && passwordValid && nicknameValid) {
            return true;
          } else {
            return false;
          }
        });
      }, [...]);
  • 회원가입 API 요청
    reducer의 state값에 저장된 값들을 필드에 맞게 API 요청
    회원가입 성공시 로그인페이지로 navigate

      const onSignupSubmit = async (e) => { 
        ... {
             username: `${inputVal.username}`,
              nickname: `${inputVal.nickname}`,
              password: `${inputVal.password}`,
           }
           ...
           navigate('/login');
     } ;

📁 Pages > Login.jsx

  • 입력값 저장 useReducer
    회원가입 폼과 작동방식 동일합니다.
    const onChangeHandler = (e) => {
        dispatch({ type: e.target.name, value: e.target.value });
      };
  • 로그인 요청, 에러 토스트
    reducer의 state에 저장된 값을 이용하여 API 요청
    받아온 응답의 id값을 이용하여 myPage로 이동
    에러 발생시 에러 메세지를 toastState의 메시지 업데이트, flag변경하여 화면에 띄우기
      // 토스트 모달 
      const [toastState, setToastState] = useState({
        message: '',
        flag: false,
      });
      // 로그인 요청
      const onLoginSubmit = async (e) => {
             ...
            {
              username: `${inputVal.id}`,
              password: `${inputVal.password}`,
            },
            ...
          const userInfo = response.data;
          navigate(`/mypage/${userInfo.id}`);
        } catch (error) {
          setToastState({ message: error.response.data.message, flag: true });
        }
      };

📁Pages > MyPage.jsx

원래는 context API를 활용해서 로그인페이지의 reducer state값을 활용해 로그인 버튼이 눌릴 때 context에 업데이트하는 방식을 사용하여 구현하였습니다.
❄️ 관련 커밋 !
하지만 과제 구현사항의 /mypage/:userId 의 userId를 이용해 회원 정보를 조회합니다.를 만족시키기 위해 context API를 없애고 다시 구현하였습니당 ..

  useEffect(() => {
    const getUserData = async () => {
      try {
        const response = await API.get(`api/v1/members/${userId}`);
        setUserInfo({ id: response.data.username, nickname: response.data.nickname });
      } catch (error) {
        console.log(error.message);
      }
    };
    getUserData();
  }, [userId]);

api.js

.env파일에 BASE_URL을 작성하고 사용하였습니다.
.env파일은 gitignore에 포함시켜 커밋되지 않도록 하였습니다.

import axios from "axios";

const API = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_URL,
});

export default API;

🥺 소요 시간, 어려웠던 점

  • 2일
  • .env파일에 BASE_URL을 저장하고 사용해본 적이 없어서 열심히 구글링하면서 찾았습니다 ...... 처음에는 process.env.REACT_APP_BASE_URL로 했는데 계속 안되어서 왜이런가 했더니 vite에서는 CRA와 다르게 접근을 해야하더군요 ... 이 부분에서 살짝 울고싶었어요 . . . ^_^
  • MyPage의 프로필 이미지를 처음에는 assets > img > profileImg.jpeg에 저장해두고 경로를 불러와서 사용하였는데 ... 서버에 요청을 보내며 유저 정보를 받아오기 시작하니 이미지 경로를 인식하지 못해서 자꾸 이미지가 안뜨더라구요 ..? 구글링해보니 public > img > profileImg.jpeg에 저장해두면 된다고 해서 그렇게 했더니 인식이 되더라구요 ! 이 부분에서도 엄청 헤맸답니다 ... 왜 이런거죠 ? 아직도 정확한 이유를 모르겠네요 .........
  • 최대한 중복된 로직이 없도록 구현하고 싶어서 공통 컴포넌트로 어떤 걸 빼서 사용할 수 있을지 고민했습니다 ! 지난번과 다르게 시작 전에 어떤 상태값을 전달받아야 하는지, 도메인에 종속되지 않고 렌더링 되는 UI는 무엇이 있는지 등을 미리 생각하고 진행하였더니 비교적 수월하게 컴포넌트 분리를할 수 있었던 것 같아요 !
  • 서버에 요청 보내는 부분이 side effect여서 따로 분리해서 사용해야하는지 고민했습니당 .. custom hook을 사용해볼까하다가 상태값들이 많이 얽혀있고, 로그인과 회원가입에서 사용하는 input의 필드값들이 동일하지 않기 때문에 각 컴포넌트별 useReducer로 관리하는게 맞을 것 같아서 이렇게 구현하였습니다 !

🌈 구현 결과물

노션 링크 첨부합니다아

Copy link

@lydiacho lydiacho 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 +4 to +22
const Input = (props) => {
return (
<InputDiv>
<InputLabel>{props.label}</InputLabel>
<UserInput
type="text"
name={props.label}
placeholder={props.placeholder}
content={props.content}
onChange={props.onChange}
></UserInput>
{props.content === '중복체크' && (
<IsExistIdBtn onClick={props.onClick} $isDuplicate={props.isDuplicate} $isBtnClicked={props.btnClicked}>
{props.content}
</IsExistIdBtn>
)}
</InputDiv>
);
};

Choose a reason for hiding this comment

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

사용자의 입력을 받는 부분 공통 컴포넌트 분리 너무 잘해주셔쎈요!! 최고 ㅎㅎ
여기서 한단계만 더 깔끔한 코드를 만들어보자면, 현재 props. 가 반복되고 있는데, 이 반복되는 객체 접근은 구조분해할당 한번만으로 싹 생략시킬 수 있다는 점!

아래와 같이 바꿔줄 수 있겠죠? ㅎㅎ

전달받은 값에 따라 조건부 렌더링 해준 것도 너무너무 좋아요!! 👍

Suggested change
const Input = (props) => {
return (
<InputDiv>
<InputLabel>{props.label}</InputLabel>
<UserInput
type="text"
name={props.label}
placeholder={props.placeholder}
content={props.content}
onChange={props.onChange}
></UserInput>
{props.content === '중복체크' && (
<IsExistIdBtn onClick={props.onClick} $isDuplicate={props.isDuplicate} $isBtnClicked={props.btnClicked}>
{props.content}
</IsExistIdBtn>
)}
</InputDiv>
);
};
const Input = ({label, placeholder, content, onChange, onClick, isDuplicate, btnClicked}) => {
return (
<InputDiv>
<InputLabel>{label}</InputLabel>
<UserInput
type="text"
name={label}
placeholder={placeholder}
content={content}
onChange={onChange}
></UserInput>
{content === '중복체크' && (
<IsExistIdBtn onClick={onClick} $isDuplicate={isDuplicate} $isBtnClicked={btnClicked}>
{content}
</IsExistIdBtn>
)}
</InputDiv>
);
};

Comment on lines +21 to +44
const reducerFn = (state, action) => {
switch (action.type) {
case 'ID':
return {
...state,
username: action.value,
};
case '비밀번호':
return {
...state,
password: action.value,
};
case '비밀번호 확인':
return {
...state,
passwordCheck: action.value,
};
case '닉네임':
return {
...state,
nickname: action.value,
};
}
};

Choose a reason for hiding this comment

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

우와.. 저는 이걸 하나하나 state 값으로 관리하느라 state 개수가 늘어나서 지저분하다고 생각했고, 그래서 그 부분이 이번 과제에서 가장 아쉬웠던 부분이라고 할 수 있는데, 네개의 입력값들을 하나의 객체 데이터로 관리하면서 useReduer로 바꿔주니까 제가 원하는 딱 깔끔한 코드의 형태가 만들어진 것 같아요.

const onChangeHandler = (e) => {
if (isClickedExistBtn && e.target.name === 'ID') {
setIsClickedExistBtn((prev) => !prev);
dispatch({ type: e.target.name, value: e.target.value });

Choose a reason for hiding this comment

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

여기서 name 속성을 사용해주었는데, input에서 name 속성은 보통 서버에게 입력값을 보낼 때 붙어지는 이름 값을 지정해줄 때 사용해요! 저희는 여기서 form 태그를 활용해 서버로 input 값을 바로 보내주는 로직이 아니고, 단순히 input 태그에 이름을 붙여주고자 하는 것이기 때문에 일반적인 id 등을 사용하는 것이 조금 더 적합할 것 같습니다!!

Comment on lines +89 to +93
const response = await API.get(`/api/v1/members/check`, {
params: {
username: `${inputVal.username}`,
},
});

Choose a reason for hiding this comment

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

여기서 이렇게 Params 속성을 추가로 보내주는 방법도 있지만, 간단하게 url 뒤에 queryString 형태로 붙여서 하나의 완성된 요청주소를 작성할 수도 있답니다!

Suggested change
const response = await API.get(`/api/v1/members/check`, {
params: {
username: `${inputVal.username}`,
},
});
const response = await API.get(`/api/v1/members/check?username=${inputVal.username}`);

Comment on lines +99 to +102
setIsClickedExistBtn(true);
console.log('이미 사용 중인 아이디입니다.');
} else {
setIsClickedExistBtn(true);

Choose a reason for hiding this comment

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

여기는 isExist가 true일 때와 false일 때 모두 동일한 행위를 해주고 있는 것 같은데 혹시 실수인걸까요?? 혹은 별도로 이렇게 분리시켜서 중복 작성해주신 이유가 있을까요?!?

Comment on lines +120 to +123
{
headers: {
'Content-Type': 'application/json',
},

Choose a reason for hiding this comment

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

이 header 값은 default이기 때문에 보통 생략해주어도 문제 없습니다!! 물론 명시해주면 명시해주는대로 그만의 장점이 있지만요 ☺️

Comment on lines +133 to +154
<ContentWrapper header={'회원가입'} onSubmit={onSignupSubmit}>
<InputWrapper>
{SIGNUP_LABEL.map((label, idx) => (
<Input
key={idx}
label={label}
content={idx === 0 ? '중복체크' : ''}
placeholder={SIGNUP_PLACEHOLDER[idx]}
onChange={onChangeHandler}
onClick={onClickDuplicateBtn}
isDuplicate={isExist}
btnClicked={isClickedExistBtn}
/>
))}
</InputWrapper>

<BtnWrapper>
<SignupBtn type="submit" disabled={!signupValid ? true : false} $valid={signupValid}>
회원가입
</SignupBtn>
</BtnWrapper>
</ContentWrapper>

Choose a reason for hiding this comment

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

다시한번 !!! 감격적인 모듈화의 현장... 제가 너무나도 좋아하는 형태의 컴포넌트 구조예요... 😭 우리 잔디 최고시다...


font-size: 17px;
font-weight: bold;
cursor: ${({ $valid }) => ($valid ? 'pointer' : 'default')};

Choose a reason for hiding this comment

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

이런 부분 섬세하게 챙겨주는 것 너무 좋아요!
저도 버튼 비활성화일 때 pointer로 뜨는 것 절대 못참는 타입입니다 ㅋㅋ

Copy link

@rachel5640 rachel5640 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 +3 to +16
const SIGNUP_LABEL = ['ID', '비밀번호', '비밀번호 확인', '닉네임'];

const SIGNUP_PLACEHOLDER = [
'아이디를 입력해주세요',
'비밀번호를 입력해주세요',
'비밀번호를 다시 한 번 입력해주세요',
'닉네임을 입력해주세요',
];

const LOGIN_LABEL = ['ID', 'PASSWORD'];

const LOGIN_PLACEHOLDER = ['아이디를 입력해주세요', '비밀번호를 입력해주세요'];

export {SIGNUP_LABEL, SIGNUP_PLACEHOLDER, LOGIN_LABEL, LOGIN_PLACEHOLDER};

Choose a reason for hiding this comment

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

항상 서진언니 코드 보면 상수파일 따로 분리해주고 아래를 모듈화 해주어서 진짜 구조 보기가 진짜 편하고 효율적이라고 느껴용..!

Comment on lines +1 to +13
import React, { useState, useReducer, useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { createPortal } from 'react-dom';
import styled from 'styled-components';

import { LOGIN_LABEL, LOGIN_PLACEHOLDER } from '../../assets/constants/constants';
import API from '../../api';

import ContentWrapper from '../Layout/ContentWrapper';
import InputWrapper from '../Layout/InputWrapper';
import BtnWrapper from '../Layout/BtnWrapper';
import Input from '../UI/Input';
import ErrorToast from '../UI/ErrorToast';

Choose a reason for hiding this comment

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

이렇게 import 하는 요소들 개행해주니까 너무 깔끔하고 좋다!!🥹

Comment on lines +21 to +41
const reducerFn = (state, action) => {
switch (action.type) {
case 'ID':
return {
...state,
username: action.value,
};
case '비밀번호':
return {
...state,
password: action.value,
};
case '비밀번호 확인':
return {
...state,
passwordCheck: action.value,
};
case '닉네임':
return {
...state,
nickname: action.value,

Choose a reason for hiding this comment

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

액션 타입에 따라 상태를 정의하는 리듀서 함수를 쓰면 따로 업데이트를 해줘야하는 일 없이 액션에 따라 변화를 반영할 수 있군요...
배워갑니다👍👍👍

Comment on lines +63 to +68
setSignupValid((prev) => {
if (idValid && passwordValid && nicknameValid) {
return true;
} else {
return false;
}

Choose a reason for hiding this comment

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

처음에는 왜 prev를 파라미터에 넣어줬지 했는데 이렇게 이전 상태에 대한 값을 넘겨주는 방식으로 구현하니 비동기적으로 업데이트 되는 문제도 해결하면서 효율적으로 관리할 수 있겠네요...언니는 진짜....천재구나....

Comment on lines +63 to +68
setSignupValid((prev) => {
if (idValid && passwordValid && nicknameValid) {
return true;
} else {
return false;
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
setSignupValid((prev) => {
if (idValid && passwordValid && nicknameValid) {
return true;
} else {
return false;
}
setSignupValid(idValid && passwordValid && nicknameValid)

이렇게만 해줘도 되겠는뎅?!

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