-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
매주 폭풍성장하는게 코드에 너무 잘 보이는 것 같아서 잔디 조장은 너무 행복합니다... 지난 과제에서 구현에 아쉬웠던 부분을 이번 과제에서는 처음부터 신경쓰면서 오래 고민하고 설계한 티가 팍팍!! 나는 것 같아요 앞으로도 이렇게 우상향 곡선 그리면서 성장해봅시다 수고 많았어요 :)
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> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사용자의 입력을 받는 부분 공통 컴포넌트 분리 너무 잘해주셔쎈요!! 최고 ㅎㅎ
여기서 한단계만 더 깔끔한 코드를 만들어보자면, 현재 props.
가 반복되고 있는데, 이 반복되는 객체 접근은 구조분해할당 한번만으로 싹 생략시킬 수 있다는 점!
아래와 같이 바꿔줄 수 있겠죠? ㅎㅎ
전달받은 값에 따라 조건부 렌더링 해준 것도 너무너무 좋아요!! 👍
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> | |
); | |
}; |
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, | ||
}; | ||
} | ||
}; |
There was a problem hiding this comment.
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 }); |
There was a problem hiding this comment.
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 등을 사용하는 것이 조금 더 적합할 것 같습니다!!
const response = await API.get(`/api/v1/members/check`, { | ||
params: { | ||
username: `${inputVal.username}`, | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기서 이렇게 Params 속성을 추가로 보내주는 방법도 있지만, 간단하게 url 뒤에 queryString 형태로 붙여서 하나의 완성된 요청주소를 작성할 수도 있답니다!
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}`); |
setIsClickedExistBtn(true); | ||
console.log('이미 사용 중인 아이디입니다.'); | ||
} else { | ||
setIsClickedExistBtn(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기는 isExist가 true일 때와 false일 때 모두 동일한 행위를 해주고 있는 것 같은데 혹시 실수인걸까요?? 혹은 별도로 이렇게 분리시켜서 중복 작성해주신 이유가 있을까요?!?
{ | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 header 값은 default이기 때문에 보통 생략해주어도 문제 없습니다!! 물론 명시해주면 명시해주는대로 그만의 장점이 있지만요
<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> |
There was a problem hiding this comment.
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')}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이런 부분 섬세하게 챙겨주는 것 너무 좋아요!
저도 버튼 비활성화일 때 pointer로 뜨는 것 절대 못참는 타입입니다 ㅋㅋ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
항상 값을 하나하나 넣어주는 방식으로 코드를 짜왔는데 반복되는 부분들을 모두 모듈화해준 걸 보면서 여기서도 진짜 유지보수하기도, 읽기에도 편하구나 라는 걸 언니의 코드를 읽으면서 실감하는 것 같아요!! 이번주 과제도 너무 고생많았어요!!!🥰🥰
const SIGNUP_LABEL = ['ID', '비밀번호', '비밀번호 확인', '닉네임']; | ||
|
||
const SIGNUP_PLACEHOLDER = [ | ||
'아이디를 입력해주세요', | ||
'비밀번호를 입력해주세요', | ||
'비밀번호를 다시 한 번 입력해주세요', | ||
'닉네임을 입력해주세요', | ||
]; | ||
|
||
const LOGIN_LABEL = ['ID', 'PASSWORD']; | ||
|
||
const LOGIN_PLACEHOLDER = ['아이디를 입력해주세요', '비밀번호를 입력해주세요']; | ||
|
||
export {SIGNUP_LABEL, SIGNUP_PLACEHOLDER, LOGIN_LABEL, LOGIN_PLACEHOLDER}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
항상 서진언니 코드 보면 상수파일 따로 분리해주고 아래를 모듈화 해주어서 진짜 구조 보기가 진짜 편하고 효율적이라고 느껴용..!
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'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이렇게 import 하는 요소들 개행해주니까 너무 깔끔하고 좋다!!🥹
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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
액션 타입에 따라 상태를 정의하는 리듀서 함수를 쓰면 따로 업데이트를 해줘야하는 일 없이 액션에 따라 변화를 반영할 수 있군요...
배워갑니다👍👍👍
setSignupValid((prev) => { | ||
if (idValid && passwordValid && nicknameValid) { | ||
return true; | ||
} else { | ||
return false; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
처음에는 왜 prev를 파라미터에 넣어줬지 했는데 이렇게 이전 상태에 대한 값을 넘겨주는 방식으로 구현하니 비동기적으로 업데이트 되는 문제도 해결하면서 효율적으로 관리할 수 있겠네요...언니는 진짜....천재구나....
setSignupValid((prev) => { | ||
if (idValid && passwordValid && nicknameValid) { | ||
return true; | ||
} else { | ||
return false; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setSignupValid((prev) => { | |
if (idValid && passwordValid && nicknameValid) { | |
return true; | |
} else { | |
return false; | |
} | |
setSignupValid(idValid && passwordValid && nicknameValid) |
이렇게만 해줘도 되겠는뎅?!
✨ 구현 기능 명세
🌱 기본 조건
.env
파일 사용하기🧩 기본 과제
[ 로그인 페이지 ]
/mypage/:userId
로 넘어갑니다. (여기서userId
는 로그인 성공시 반환 받은 사용자의 id)/signup
으로 이동합니다.[ 회원가입 페이지 ]
중복체크 버튼
회원가입 버튼
/login
으로 이동합니다.[ 마이 페이지 ]
/mypage/:userId
의 userId를 이용해 회원 정보를 조회합니다.🌠 심화 과제
[ 로그인 페이지 ]
createPortal
을 이용합니다.[ 회원가입 페이지 ]
비밀번호 확인
중복체크
생각과제
💎 PR Point
파일구조
📁 UI > Input.jsx
로그인과 회원가입 페이지에서 반복되어 사용되는
input
을 공통 컴포넌트로 분리하여 사용📁 Pages > Signup.jsx
label과 placeholder를 상수로 작성 후 파일을 분리하고 import하여 사용하였습니다.
useReducer
중복체크 후 ID 값을 변경하면 중복체크가 되지 않은 상태(색은 검정색)로 돌아갑니다.
를 위해 ID일 경우의 조건 추가중복확인
reducer의 state에 저장된 ID값을 이용하여 API 요청
응답의
isExist
값을isExist
state에 저장,isClickedExistBtn
업데이트회원가입 버튼 활성화
각 조건에 부합하는지 확인 후 상수에 저장
모든 값이 valid하다면
signupValid
state값을 true로 설정useEffect
를 사용하여 불필요한 리렌더링 방지회원가입 API 요청
reducer의 state값에 저장된 값들을 필드에 맞게 API 요청
회원가입 성공시 로그인페이지로
navigate
📁 Pages > Login.jsx
useReducer
회원가입 폼과 작동방식 동일합니다.
reducer의 state에 저장된 값을 이용하여 API 요청
받아온 응답의 id값을 이용하여
myPage
로 이동에러 발생시 에러 메세지를
toastState
의 메시지 업데이트,flag
변경하여 화면에 띄우기📁Pages > MyPage.jsx
원래는
context API
를 활용해서로그인
페이지의 reducer state값을 활용해 로그인 버튼이 눌릴 때 context에 업데이트하는 방식을 사용하여 구현하였습니다.❄️ 관련 커밋 !
하지만 과제 구현사항의
/mypage/:userId 의 userId를 이용해 회원 정보를 조회합니다.
를 만족시키기 위해 context API를 없애고 다시 구현하였습니당 ..api.js
.env
파일에 BASE_URL을 작성하고 사용하였습니다..env
파일은gitignore
에 포함시켜 커밋되지 않도록 하였습니다.🥺 소요 시간, 어려웠던 점
2일
.env
파일에BASE_URL
을 저장하고 사용해본 적이 없어서 열심히 구글링하면서 찾았습니다 ...... 처음에는process.env.REACT_APP_BASE_URL
로 했는데 계속 안되어서 왜이런가 했더니vite
에서는CRA
와 다르게 접근을 해야하더군요 ... 이 부분에서 살짝 울고싶었어요 . . . ^_^MyPage
의 프로필 이미지를 처음에는assets
>img
>profileImg.jpeg
에 저장해두고 경로를 불러와서 사용하였는데 ... 서버에 요청을 보내며 유저 정보를 받아오기 시작하니 이미지 경로를 인식하지 못해서 자꾸 이미지가 안뜨더라구요 ..? 구글링해보니public
>img
>profileImg.jpeg
에 저장해두면 된다고 해서 그렇게 했더니 인식이 되더라구요 ! 이 부분에서도 엄청 헤맸답니다 ... 왜 이런거죠 ? 아직도 정확한 이유를 모르겠네요 .........side effect
여서 따로 분리해서 사용해야하는지 고민했습니당 ..custom hook
을 사용해볼까하다가 상태값들이 많이 얽혀있고, 로그인과 회원가입에서 사용하는 input의 필드값들이 동일하지 않기 때문에 각 컴포넌트별useReducer
로 관리하는게 맞을 것 같아서 이렇게 구현하였습니다 !🌈 구현 결과물
노션 링크 첨부합니다아