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주차 기본/심화/생각 과제 ] 🍁 로그인/회원가입 #4

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

Conversation

hae2ni
Copy link
Contributor

@hae2ni hae2ni commented Nov 17, 2023

✨ 구현 기능 명세

🧩 기본 과제

[ 로그인 페이지 ]

  1. 로그인

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

    • 회원가입을 누르면 /signup으로 이동합니다.

[ 회원가입 페이지 ]

  • ID, 비밀번호, 닉네임 input 3개와 중복체크 버튼이 필요합니다.
  1. 중복체크 버튼

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

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

[ 마이 페이지 ]

  1. /mypage/:userIduserId를 이용해 회원 정보를 조회합니다.
    • 프로필 사진은 자유롭게 넣어주시면 됩니다!
    • 로그아웃 버튼을 누르면 /login으로 이동합니다.

🌠 심화 과제

[ 로그인 페이지 ]

  1. 로그인
    1. 토스트
      • createPortal을 이용합니다.

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

[ 회원가입 페이지 ]

  1. 비밀번호 확인

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

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

  • 🖤 API 통신에 대하여

  • 로딩 / 에러 처리를 하는 방법에는 어떤 것들이 있을까?

  • 패칭 라이브러리란 무엇이고 어떤 것들이 있을까?

  • 패칭 라이브러리를 쓰는 이유는 무엇일까?


💎 PR Point

1️⃣ router 설정

import { BrowserRouter, Routes, Route } from "react-router-dom";
import OnBoarding from "../pages/OnBoarding";
import Login from "../pages/Login";
import Signup from "../pages/Signup";
import Mypage from "../pages/Mypage";

const Router = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<OnBoarding />} />
        <Route path="/login" element={<Login />} />
        <Route path="/Signup" element={<Signup />} />
        <Route path="/mypage/:userId" element={<Mypage />} />
      </Routes>
    </BrowserRouter>
  );
};

export default Router;

2️⃣ 로그인 구현 + toastModal

 const handleLogin = async () => {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_APP_BASE_URL}/api/v1/members/sign-in`,
        {
          username: idValue,
          password: pwValue,
        }
      );
      const { data } = response;
      navigate(`/mypage/${data.id}`);
    } catch (ex) {
      if (ex.response.status === 400) {
        console.log("비밀번호로 인한 로그인실패!");
        setShowModal(true);
        setModalMessage(ex.response.data.message);
      } else {
        console.log("axios 에러");
      }
    }
  };

  useEffect(() => {
    const timer = setTimeout(() => {
      setShowModal(false);
    }, 3000);
    return () => {
      clearTimeout(timer);
    };
  }, [showModal]);

-> 로그인 post 구현입니다!
-> 모달은 400에러가 사용자가 없을 때더라구요! 그때 message를 받아와서, Modal의 children 안에 넣어두었습니다.
토스트 모달이기에, 3초정도 지속하고 사라지도록 useEffect로 구현하였습니다.

3️⃣ 회원가입
❤️ 로그인에 비해 state 수가 워낙 많아서 useReducer를 사용해 주었어요!

const reducer = (state, action) => {
  switch (action.type) {
    case "SET_ID":
      return { ...state, idValue: action.payload };
    case "SET_PW":
      return { ...state, pwValue: action.payload };
    case "SET_PW_CHECK":
      return { ...state, pwCheckValue: action.payload };
    case "SET_NAME":
      return { ...state, nameValue: action.payload };
    case "SET_EXIST":
      return { ...state, isExist: action.payload };
    case "SET_PW_VALID":
      return { ...state, isPwValid: action.payload };
    case "SET_DISABLED":
      return { ...state, disabled: action.payload };
    default:
      return state;
  }
};

⭐ 각각의 친구들(?)을 제어하는 함수를 만들었습니다! ⭐

🧡 회원가입 post

  const handleSignup = async () => {
    try {
      await axios.post(`${import.meta.env.VITE_APP_BASE_URL}/api/v1/members`, {
        username: state.idValue,
        nickname: state.nameValue,
        password: state.pwValue,
      });

      navigate("/login");
    } catch (err) {
      console.log(err);
    }
  };

💛 비밀번호 확인

  const pwConfirm = useCallback(() => {
    if (
      !state.pwValue ||
      !state.pwCheckValue ||
      state.pwValue != state.pwCheckValue
    ) {
      dispatch({ type: "SET_PW_VALID", payload: false });
    } else if (
      state.pwValue &&
      state.pwCheckValue &&
      state.pwValue == state.pwCheckValue
    ) {
      dispatch({ type: "SET_PW_VALID", payload: true });
    }
  }, [state.pwValue, state.pwValue, state.pwCheckValue]);

  useEffect(() => {
    pwConfirm();
  }, [pwConfirm]);
~~~~

💚 회원가입 버튼의 비활성 또는 활성화

~~~~
useEffect(() => {
    const trimmedId = state.idValue.trim();
    const trimmedPw = state.pwValue.trim();
    const trimmedName = state.nameValue.trim();
    if (
      !trimmedId ||
      !trimmedPw ||
      !trimmedName ||
      state.isExist === 0 ||
      state.isExist ||
      !state.isPwValid
    ) {
      dispatch({ type: "SET_DISABLED", payload: true });
    } else {
      dispatch({ type: "SET_DISABLED", payload: false });
    }
  }, [
    state.idValue,
    state.pwValue,
    state.nameValue,
    state.isExist,
    state.isPwValid,
  ]);

💙 중복버튼 체크 api 통신

  const getMemberInfo = async () => {
    try {
      const response = await axios.get(
        `${import.meta.env.VITE_APP_BASE_URL}/api/v1/members/check`,
        {
          params: {
            username: value,
          },
        }
      );
      const isExistData = response.data.isExist;
      if (isExistData) {
        setIsExist(true);
      } else if (!isExistData) {
        setIsExist(false);
      }
    } catch (err) {
      console.log(err);
    }
  };

  useEffect(() => {
    setIsExist(0);
  }, [value]);

💜 중복 버튼 체크 스타일 변화

  color: ${({ theme }) => theme.colors.white};
  ${({ theme }) => theme.fonts.body02};
  background-color: ${({ isExist }) =>
    isExist === 0 ? "black" : isExist ? "red" : "green"};

=> 회원가입 비활성화도 비슷하게 구현되었습니다!

4️⃣ 마이페이지

❤️ 마이페이지 api 통신


  const getMypage = async () => {
    try {
      const response = await axios.get(
        `${import.meta.env.VITE_APP_BASE_URL}/api/v1/members/${userId}`,
        {
          userId: userId,
        }
      );
      const { data } = response;
      setUserData(data);
    } catch (err) {
      console.log(err);
    }
  };

  useEffect(() => {
    getMypage();
  }, []);

🧡 로그아웃 버튼 클릭시 로그인으로 이동 ( 로그인에서 회원가입 이동도 같은 맥락으로 구현)

 <LogoutButton to="/login">Logout</LogoutButton>;

5️⃣ 폴더 구조 및 설계

|-- 📁 .github
|-- 📁 node_modules
|-- 📁 public
|-- 📁 src
	|-- 📁 constants // 상수(placeholder)
	|-- 📁 components
			|-- 📁 common
	|-- 📁 pages
	|-- 📁 style
			|-- globalStyle.js
			|-- theme.js
			|-- 📁 common
			|-- 📁 layout
	|-- 📁 utils //데이터 통신 함수 (그,,, 리팩토링 때 할 예정^^!)
	|-- App.jsx
	|-- main.jsx
	|-- Router.jsx
|-- .eslintrc.cjs
|-- .gitignore
|-- .prettierrc
|-- index.html
|-- package.json
|-- README.md
|-- yarn.lock

🥺 소요 시간, 어려웠던 점

  • 7h-8h
  • 처음 레이아웃 잡고, 디자인 잡는데서 시간이 오래 걸렸습니다ㅠㅠ..ㅎ

🌈 구현 결과물

1️⃣ 로그인하러가기 + 회원가입으로 이동
https://github.com/DO-SOPT-WEB/HyeinKwon/assets/100409061/1bd29b85-2a87-4a88-9949-eb84a69ba8a3

2️⃣ 로그인 성공시 마이페이지로 이동 + 마이페이지 회원정보 조회

_2023_11_17_10_04_59_316.mp4

3️⃣ 로그아웃 버튼 누르면 로그인으로 이동 + 로그인 실패하면 toast모달 띄우기(3초뒤 사라짐)

_2023_11_17_10_06_43_4.mp4

4️⃣ 로그인 화면에서 회원가입으로 이동 + 아이디 중복 확인(체크하지 않은 경우 검정, 중복인 경우 빨강, 중복 아닌 경우 초록, 중복체크 후 id값 변경하면 다시 색 돌아가기)
https://github.com/DO-SOPT-WEB/HyeinKwon/assets/100409061/8a4d3e11-1c42-49b7-aaf9-39ae93f47682

5️⃣ 중복체크시 중복 체크 일 때 회원가입 버튼 비활성화
https://github.com/DO-SOPT-WEB/HyeinKwon/assets/100409061/833c0d34-fa35-4706-9ca9-b7ea98f61d0a

6️⃣ 중복체크 풀고 모두 다 입력완료 시 회원가입 버튼 활성화 + 회원가입 되면 로그인 페이지로 이동
https://github.com/DO-SOPT-WEB/HyeinKwon/assets/100409061/b9543217-00b2-4f11-a377-272a07685e3c

7️⃣ 회원가입 실시한 아이디 + 비밀번호로 로그인 성공, 성공 후 마이페이지 이동 => 회원가입 성공

_2023_11_17_10_10_59_580.mp4

8️⃣ 회원가입 버튼 비활성/활성화 조건 모두 구현

_2023_11_17_10_11_58_955.mp4

Copy link

@doyn511 doyn511 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 +36 to +37
<UserInform inform="ID" text={userData.nickname} />
<UserInform inform="닉네임" text={userData.username} />
Copy link

Choose a reason for hiding this comment

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

타임 타임 이거 받아오는 값 바뀐거 아닌가요?!?!?!? ID에 username이고 닉네임에 nickname 아닌가?!

import { Link } from "react-router-dom";

export default function LogoutBtn() {
return <LogoutButton to="/login">Logout</LogoutButton>;
Copy link

Choose a reason for hiding this comment

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

useNavigate만 썼는데 안쓰고도 이게 가능하구나!

Comment on lines +20 to +25
const isExistData = response.data.isExist;
if (isExistData) {
setIsExist(true);
} else if (!isExistData) {
setIsExist(false);
}
Copy link

Choose a reason for hiding this comment

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

나는 그냥 바로 response.data.isExist를 조건문에 넣어줬는데 요렇게 따로 빼주는게 훨신 깔끔한거 같다!
그리고 조건문 쓸 때 나도 if, else if 로 해주긴 했는데 왜 else로 했을 땐 제대로 동작을 하지 않을까....? if-else는 안되고 if-else if는 또 되더라고.. 아직도 이유를 모르겠어 ..

Comment on lines +31 to +33
useEffect(() => {
setIsExist(0);
}, [value]);
Copy link

Choose a reason for hiding this comment

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

요기로 중복체크 버튼 클릭했다가도 input 값이 수정되면 다시 검정색으로 되돌아가게 하는 코드 맞지?!

Copy link

Choose a reason for hiding this comment

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

Flag가 무슨 역할을 하는지 궁금해!

console.log("성공");
return response;
} catch (error) {
console.error("로그인 실패", error.message);
Copy link

Choose a reason for hiding this comment

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

error.message는 API로 주어졌던 메시지가 출력되는건가?!
여기서 console.error 처음 알아가..!

[정리] console.log와 console.error는 모두 console에 출력하는 거지만, console.log는 stdout, console.error는 stderr에 해당한다..!

Choose a reason for hiding this comment

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

엥 나도 좋은 정보 감사합니다!

Comment on lines +21 to +40
const reducer = (state, action) => {
switch (action.type) {
case "SET_ID":
return { ...state, idValue: action.payload };
case "SET_PW":
return { ...state, pwValue: action.payload };
case "SET_PW_CHECK":
return { ...state, pwCheckValue: action.payload };
case "SET_NAME":
return { ...state, nameValue: action.payload };
case "SET_EXIST":
return { ...state, isExist: action.payload };
case "SET_PW_VALID":
return { ...state, isPwValid: action.payload };
case "SET_DISABLED":
return { ...state, disabled: action.payload };
default:
return state;
}
};
Copy link

Choose a reason for hiding this comment

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

이거 좀 좋은거 같다.. 난 아직 useReducer 쓰는게 익숙하지 않아서 좀 더 공부를 해보도록 할게.. payload도 여기서 처음 봤어! 리팩토링 할 때 언니 코드 참고해야지 히히

payload : 보내고자 하는 데이터를 의미 (혹시 이게 아니라면 고쳐서 알려주면 고마울거 가타..🫶)

Comment on lines +61 to +75
const pwConfirm = useCallback(() => {
if (
!state.pwValue ||
!state.pwCheckValue ||
state.pwValue != state.pwCheckValue
) {
dispatch({ type: "SET_PW_VALID", payload: false });
} else if (
state.pwValue &&
state.pwCheckValue &&
state.pwValue == state.pwCheckValue
) {
dispatch({ type: "SET_PW_VALID", payload: true });
}
}, [state.pwValue, state.pwValue, state.pwCheckValue]);
Copy link

Choose a reason for hiding this comment

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

여기 useCallback을 쓴 이유가 deps안에 있는 값들이 변경됐을 때 함수를 다시 실행하도록 하려 한건가..?
근데 왜 deps 안에 state.pwValue가 두개야?! 저 두개가 서로 다른건가?

Comment on lines +77 to +79
useEffect(() => {
pwConfirm();
}, [pwConfirm]);
Copy link

Choose a reason for hiding this comment

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

위에서 pwConfirm()을 만들고 useEffect에 넣어주는거랑, 바로 pwConfirm에 useEffect를 적용하는거랑 다른 점이 있나?
저 위의 함수에 useEffect가 아니라 useCallback을 적용한 이유가 따로 있는지 알고싶어!

Comment on lines +83 to +85
const trimmedId = state.idValue.trim();
const trimmedPw = state.pwValue.trim();
const trimmedName = state.nameValue.trim();
Copy link

Choose a reason for hiding this comment

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

헉...... 공백이 있을수도 있구나.... 별 생각 없이 그냥 입력된 값을 그대로 넘겨줬는데 앞뒤로 공백을 제거해서 넘겨주는게 좀 더 좋을거 같다..! 나도 적용해볼게 ㅎㅎ

String.tirm() : 문자열 앞 뒤의 공백을 제거해줌

Copy link
Member

@binllionaire binllionaire 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 +14 to +22
try {
const response = await axios.get(
`${import.meta.env.VITE_APP_BASE_URL}/api/v1/members/${userId}`,
{
userId: userId,
}
);
const { data } = response;
setUserData(data);
Copy link
Member

Choose a reason for hiding this comment

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

요기 data로 바로 선언했으면 바로 값넣을 수 있었을텐데 가독성을 위해서인가용?

Copy link
Member

Choose a reason for hiding this comment

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

signup이랑 login 컴포넌트가 비슷해서 재사용 하는 방벙으로 했어도 좋을것 같아!

onChange={(e) =>
dispatch({ type: "SET_ID", payload: e.target.value })
}
title="ID"
Copy link
Member

Choose a reason for hiding this comment

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

접근성 때문에 넣은거 맞지?! input에는 title 속성으로 주는구낭 배워가요 !

Comment on lines +1 to +6
export const placeholder = {
ID_HOLDER: "아이디를 입력해주세요",
PW_HOLDER: "비밀번호를 입력해주세요",
PW_COMFIRM_HOLDER: "비밀번호를 다시 한 번 입력해주세요",
NICKNAME_HOLDER: "닉네임을 입력해주세요",
};
Copy link
Member

Choose a reason for hiding this comment

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

상수화 깔끔하다!! 좋아요 🖤

<Header>Sign Up</Header>
<div>
<IdInputBox
isExist={state.isExist}
Copy link
Member

Choose a reason for hiding this comment

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

styled componenet에 prop넘겨줄 때 warning 안떴오?!
나는 앞에 $를 붙이라는 경고가 뜨더라구

Comment on lines +38 to +44
} catch (ex) {
if (ex.response.status === 400) {
console.log("비밀번호로 인한 로그인실패!");
setShowModal(true);
setModalMessage(ex.response.data.message);
} else {
console.log("axios 에러");
Copy link
Member

Choose a reason for hiding this comment

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

이거 response.data.message 에 에러가 담겨서 나와!
등록되지 않은 유저 라는 메세지도 있어서 이걸로 받아줘야 할 것 같아용!!

<ThemeProvider theme={theme}>
<GlobalStyle />
<Router />
<Flag />

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 +7

export default function LoginBtn({ onClick }) {
return <LoginButton onClick={onClick}>로그인</LoginButton>;
}

Choose a reason for hiding this comment

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

회원가입이랑 로그인버튼을 따로 만들어줬구나!

Comment on lines +6 to +14
export default function Modal({ children }) {
return (
<ModalPortal>
<ModalLayout>
<ToastModal message={children} />
</ModalLayout>
</ModalPortal>
);
}

Choose a reason for hiding this comment

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

childern을 prop으로 받아주는건가?

Comment on lines +42 to +48
export const St = {
centerFlex,
centerFlexColumn,
Input,
Button,
InputFlagWrapper,
};

Choose a reason for hiding this comment

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

오옹.. 신기하다 St 네이밍을 하면서 파일을 분리시켜줬네!

Comment on lines +51 to +62
const InputFlagWrapperSignup = styled(St.InputFlagWrapper)`
width: 40rem;
`;

const InputInSignup = styled(St.Input)`
width: 25rem;
`;

const DoubleInputWrapper = styled.div`
${St.centerFlex};
gap: 1rem;
`;

Choose a reason for hiding this comment

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

혹시 잘 몰라서 St.InputFlagWrapperSingup과 DoubleInputWrapper의 차이를 물어봐도 될까?

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