-
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주차 기본/심화/공유 과제] 회원가입 & 로그인 #5
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.
api에 관련된 파일들을 다 나눠놓은게 매우 인상깊은 코드였습니다 !!
깔꼼하고 보기 좋아서 많이 배워갑니다 😭😍
그리고 저는 throw가 낯설었는데 한번 공부해봐도 좋을 것 같아서 알아보려합니답 !!
너무너무너무 수고하셨어요
@@ -0,0 +1,13 @@ | |||
<!doctype html> | |||
<html lang="en"> |
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.
ko !!
import axios from 'axios'; | ||
|
||
export const getMyHobby = async () => { | ||
const token = localStorage.getItem('authToken'); | ||
|
||
try { | ||
const response = await axios.get(`${import.meta.env.VITE_APP_BASE_URL}/user/my-hobby`, { | ||
headers: { | ||
token: token, | ||
}, | ||
}); | ||
|
||
return response.data?.result?.hobby || null; | ||
} catch (err: any) { | ||
throw err; | ||
} | ||
}; |
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.
오 ! api별로 파일을 만들면 더 코드가 깔끔하겠군뇽 !! 😮
|
||
return null; | ||
} catch (err: any) { | ||
throw err; |
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.
logError(err);
이런 식으로 에러를 던지기 전에 로그를 기록해도 좋을 것 같아요오 !
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
const value = e.target.value; | ||
setName(value); | ||
|
||
if (value.length > 8) { | ||
setIsValid(false); | ||
setErrorMessage('이름은 8자 이하로 입력해주세요.'); | ||
} else { | ||
setIsValid(true); | ||
setErrorMessage(''); | ||
} | ||
}; |
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 handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setName(value);
if (value.length > 8) {
setErrorMessage('이름은 8자 이하로 입력해주세요.');
} else {
setErrorMessage('');
}
};
요렇게 하면 더 간결해질 거 같아요 !
isValid={isValid} | ||
errorMessage={errorMessage} | ||
/> | ||
<Button type='button' onClick={handleNext} disabled={!name || !isValid}> |
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.
Button의 type='button'은 기본값이 button이라서 type='button'을 지정할 필요는 없을 것 같습니답 !!
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.
button의 type 기본값은 submit 아닌가요...?
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.
회원가입 부분에 funnel 구조까지 사용 하셨네요! 과제 하느라 고생 많으셨습니다!
👍👍👍
<> | ||
<ThemeProvider theme={Theme}> | ||
<Global styles={globalStyle} /> | ||
<RouterProvider router={router} /> | ||
</ThemeProvider> | ||
</> |
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.
<> | |
<ThemeProvider theme={Theme}> | |
<Global styles={globalStyle} /> | |
<RouterProvider router={router} /> | |
</ThemeProvider> | |
</> | |
<ThemeProvider theme={Theme}> | |
<Global styles={globalStyle} /> | |
<RouterProvider router={router} /> | |
</ThemeProvider> |
크게 상관없지만 요건 없어도 괜찮을 것 같네요!
errorMessage="비밀번호를 입력해 주세요." | ||
onChange={handleChange} | ||
/> | ||
<Button type="submit">로그인</Button> |
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.
button태그는 기본 속성이 type="submit"이라서 굳이 type을 다시 설정해주지 않아도 괜찮습니다!
interface ButtonProps { | ||
type: 'button' | 'submit' | ||
onClick?: () => void; | ||
disabled?: boolean; | ||
children: React.ReactNode; | ||
} |
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.
interface ButtonProps { | |
type: 'button' | 'submit' | |
onClick?: () => void; | |
disabled?: boolean; | |
children: React.ReactNode; | |
} | |
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { | |
children: React.ReactNode; | |
} |
요렇게 버튼 엘리먼트의 기본속성을 상속받으면 button이 가지고 있는 속성들을 명시하지 않고 사용할 수 있습니다!
const Button = ({ type, onClick, disabled, children }: ButtonProps) => { | ||
return ( | ||
<StyledButton type={type} onClick={onClick} disabled={disabled}> | ||
{children} | ||
</StyledButton> | ||
); | ||
}; |
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 Button = ({ type, onClick, disabled, children }: ButtonProps) => { | |
return ( | |
<StyledButton type={type} onClick={onClick} disabled={disabled}> | |
{children} | |
</StyledButton> | |
); | |
}; | |
const Button = ({ children, ...props }: ButtonProps) => { | |
return ( | |
<StyledButton {...props}> | |
{children} | |
</StyledButton> | |
); | |
}; |
또한 요렇게 props를 통해서 속성을 받아오면, Button 컴포넌트를 더 유연하게 만들고, 코드를 간결하게 작성할 수 있으니 이런 방식도 고려해보면 좋을 것 같습니다!
border: none; | ||
border-radius: 4px; | ||
background-color: ${Theme.colors.primary}; | ||
color: white; |
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.
현재 프로젝트에서는 ThemeProvider를 사용하고 있으니 색상을 사용할 때는 가능하면 theme에서 가져다가 사용하는 것이, 유지보수나 가독성 측면에서 더 좋을 것 같습니다!
const response = await putMyInfo({ hobby: newHobby, password: newPassword }); | ||
alert('사용자 정보가 업데이트 되었습니다.'); | ||
navigate('/login'); |
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.
현재 코드에서는 response를 받아오지만, 요청이 성공했는지 확인 없이 바로 성공 로직을 실행하고 있습니다. 요청이 성공했는지 여부를 확인한 후, 성공 시에만 로직을 실행하도록 수정하면 코드의 안정성과 더욱 향상될 것 같습니다.
isValid={isValid} | ||
errorMessage={errorMessage} | ||
/> | ||
<Button type='button' onClick={handleNext} disabled={!name || !isValid}> |
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.
button의 type 기본값은 submit 아닌가요...?
|
||
const StepPwd = ({ onNext }: { onNext: () => void }) => { | ||
const { handlePwd } = useSignUpContext(); | ||
const [pwd, setPwd] = useState(''); |
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.
현재 pwd와 hobby같은 상태를 각 퍼널 컴포넌트마다 별도로 선언하여 관리하고 있습니다. 하지만 이 값이 이미 SignUpContext의 state에서 관리되고 있으므로, 중복 상태를 선언하지 않고, 컨텍스트에서 제공하는 상태를 가져와 사용하는 것이 더 좋아 보입니다.
이렇게 하면
- 여러 곳에서 같은 데이터를 관리하지 않아도 되므로 데이터 동기화 문제 방지
- 상태나 데이터 추적이 용이해져서 유지보수성 향상
- 불필요한 state를 지우며 코드가 간결해짐
등의 장점이 있습니다!
@@ -0,0 +1,44 @@ | |||
import React, { createContext, useState, useContext, ReactNode } from 'react'; |
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, { createContext, useState, useContext, ReactNode } from 'react'; | |
import { createContext, useState, useContext, ReactNode } from 'react'; |
사용하지 않는 import는 지워주기!
const token = localStorage.getItem('authToken'); | ||
|
||
try { | ||
const response = await axios.get(`${import.meta.env.VITE_APP_BASE_URL}/user/my-hobby`, { |
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.
axios instance를 만들어서 사용하면 API 마다 base url을 지정해주지 않아도 되서 조금 더 간결하게 코드를 작성할 수 있을 것 같습니다!
앗 제가 PR의 질문을 이제 봐서 userId이슈를 이제 봤는데요, |
✨ 구현 기능 명세
💡 기본 과제
취미
,내 정보
메뉴 탭로그아웃
버튼취미
,내 정보
취미 페이지, 내 정보 페이지 출력 (1개의 페이지로 구현해도 되고, url 달라도 됨)🔥 심화 과제
공유과제
제목: Error Boundary로 에러 핸들링하기
링크 첨부 : https://wave-web.tistory.com/127
❗️ 내가 새로 알게 된 점
Outlet
React Router의 Outlet에 대해 새롭게 알게 되었습니다.
라우터의 중첩된 구조에서 사용하는 컴포넌트로, 라우터가 특정 경로에 일치하는 컴포넌트를 렌더링 할 때 해당 컴포넌트의 하위 컴포넌트를 표시하는 역할을 합니다 (children을 사용하는 것과 같은 효과) 주로 중첩된 라우트에서 사용되며 부모 라우트 컴포넌트 내에서 자식 라우트 컴포넌트를 렌더링 할 때 활용한다고 합니다.
createContext
Context는 리액트 컴포넌트간에 어떠한 값을 공유할수 있게 해주는 기능을 말하는데, 말로만 듣다가 처음으로 사용해보았습니다.
회원가입 기능을 구현할 때 SignUpContext context를 사용하여 회원가입에 필요한 정보를 관리하였습니다.
createContext로 SignUpContext를 만들어 SignUpState 관련 상태와 함수들을 정의하고 SignUpProvider를 통해 하위 컴포넌트들에 전달한 뒤 각 컴포넌트 내부에서 useSignUpContext를 사용해 핸들링 함수를 가져오고 유저로부터 입력받은 값을 상태에 저장하는 흐름입니다 !
❓ 구현 과정에서의 어려웠던/고민했던 부분
퍼널 구조
3단계로 나누어진 회원가입 절차를 퍼널 구조를 통해 구현하였습니다. 처음에는
switch
문을 사용하는 방식도 고려해보았으나 (로직이 단순해서), 퍼널을 통한 많은 페이지 관리하기 라는 아티클을 읽고, 퍼널 방식을 적용해보기로 했습니다. useFunnel hook을 작성하고, 각 스텝에 해당하는 컴포넌트들을 부모 컴포넌트에서 렌더링 한 후 Funnel로 감싸는 방식입니다.취미 검색 기능 구현 시 발생한 입력 값 동기화 문제
마이 페이지의 취미 탭에서 다른 사용자의 취미를 검색하는 기능을 구현하던 중에 검색 버튼의 클릭 여부와 상관 없이 input 창에 입력한 값이
<p>{${userId}번 사용자의 취미: ${otherUserHobby}}</p>
의userId
에 업데이트 되는 이슈가 있었습니다.1번 유저의 취미를 알고 싶어서 input 창에 1을 입력한 후에 검색하면 예를 들어
1번 사용자의 취미 : 코딩
이라고 뜰 텐데, 여기서 다시 13번 유저의 취미를 알고 싶어서 input 창에 13을 입력하면13번 사용자의 취미 : 코딩
이라고 뜨는거죠 ... onChange 이벤트가 발생할 때마다 실시간으로 입력 값이 userId에 반영되는게 원인인 것 같은데 해결 방법을 모르겠어서 (+ useEffect 쓰면 해결할 수 있을 것 같은데 안쓰고 싶어서)searchResult
라는 state를 두고setSearchResult(
${userId}번 사용자의 취미: ${fetchedHobby});
와 같이 설정해주는 방식으로 해결을 하긴 하였으나 ... 다소 찝찝해서 혹시 더 나은 방법을 알고 계시다면 피드백 주시면 감사하겠습니다 😶이슈가 발생했던 코드는 아래와 같습니다.
에러는 어디서 어떻게 핸들링해야 할까 ?
제 코드를 보시면 아시겠지만 ,,
catch 문에서 에러 발생 시 throw로 던져주고 사용하는 컴포넌트 내에서 status 및 code 일치여부 확인을 통해 각 케이스에 맞게 에러 메시지를 띄워주고 있습니다. 이 방식이 적절한 것인지, 혹은 더 나은 방식이 있는지 잘 모르겠어요
🥲 소요 시간
2 days
🖼️ 구현 결과물
B8E565A7-2718-47E1-9EEB-073D8E580C1C.mov
비밀번호 보이기 버튼 동작
CBEA87DF-0DAB-40B3-9547-AA8E4F71B033.mov