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주차 기본/심화/공유 과제] 회원가입 & 로그인 #5

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

Conversation

maylh
Copy link
Contributor

@maylh maylh commented Nov 12, 2024

✨ 구현 기능 명세

💡 기본 과제

  • React + TypeScript
  • Axios 라이브러리 사용
  • ThemeProvider, GlobalStyle 사용
  1. 로그인
  • 로그인 타이틀
  • 아이디(이름) 입력 Input
  • 비밀번호 입력 Input
  • 로그인 버튼 (hover시 배경색 바꾸기 (transition 적용))
  • 회원가입 버튼 (회원가입 페이지로 이동)
  1. 회원가입
  • 이름 - 비밀번호 - 취미 입력이 한 페이지에서 일어남 (컴포넌트만 갈아끼우기)
  • 상단에 회원가입 타이틀
  • 하단에는 로그인 페이지로 가는 링크
  1. 회원가입(이름)
  • 이름 입력 Input
  • 다음 버튼 (비밀번호 입력 폼 나옴)
  • Input 비어있을 때 버튼 비활성화
  1. 회원가입(비밀번호)
  • 비밀번호 입력 Input
  • 비밀번호 확인 Input
  • 둘 중 하나라도 비어있으면 버튼 비활성화
  • 두 비밀번호가 다르면 버튼 비활성화
  • 다음 버튼 (취미 입력 폼 나옴)
  1. 회원가입(취미)
  • 취미 입력 Input
  • 회원가입 버튼
  • Input 비어있을 때 버튼 비활성화
  • 회원가입 실패 시 에러메시지 alert 출력
  • 회원가입 성공 시 회원번호 alert 출력하고, login 페이지로 이동
  1. 마이페이지
  • 헤더에 취미, 내 정보 메뉴 탭
  • 헤더에 로그아웃 버튼
  • 로그아웃 버튼 클릭 시 token 저장 정보 삭제하고 로그인 페이지로 이동 (token 저장 위치는 자율)
  • 헤더 취미, 내 정보 취미 페이지, 내 정보 페이지 출력 (1개의 페이지로 구현해도 되고, url 달라도 됨)
  1. 마이페이지(취미)
  • 나의 취미 출력
  • 사용자 번호 입력 Input
  • 검색 버튼
  • 검색 오류시 alert
  • 검색된 취미 출력
  1. 마이페이지(내 정보)
  • 비밀번호만 입력하면 비밀번호만 변경
  • 취미만 입력하면 취미만 변경
  • 둘 다 입력하면 둘다 변경
  • 둘 다 비어있으면 alert

🔥 심화 과제

  • any 사용하지 않기
  1. 회원가입 (이름)
  • 8글자 넘어가도 버튼 비활성화 처리
  • 8글자 넘어가는 것에 대해 에러메시지 출력
  1. 회원가입 (비밀번호)
  • 비밀번호 보이기 버튼 추가
  • 8글자 넘어가도 버튼 비활성화 처리
  • 8글자 넘어가는 것에 대해 에러메시지 출력
  • 비밀번호 불일치 에러 메시지 출력
  • (선택) 에러메시지 한개만 출력해도 됨 (우선순위는 알아서)
  1. 회원가입 (취미)
  • 8글자 넘어가도 버튼 비활성화 처리
  • 8글자 넘어가는 것에 대해 에러메시지 출력

공유과제

제목: 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}); 와 같이 설정해주는 방식으로 해결을 하긴 하였으나 ... 다소 찝찝해서 혹시 더 나은 방법을 알고 계시다면 피드백 주시면 감사하겠습니다 😶

이슈가 발생했던 코드는 아래와 같습니다.

<SearchHobbySection>
          <h2>다른 사람들의 취미</h2>
          <Input
            type="text"
            value={userId}
            name="userId"
            placeholder="사용자 번호를 입력하세요"
            onChange={(e) => setUserId(e.target.value)}
          />
          <Button type="button" onClick={handleSearch}>
            검색
          </Button>
          {otherUserHobby !== null && (
              <p>{${userId}번 사용자의 취미: ${otherUserHobby}}</p>
          )}
 </SearchHobbySection>
const handleSearch = async () => {
    try {
      const fetchedHobby = await getOtherHobby(Number(userId));
      if (fetchedHobby !== null) {
        setOtherUserHobby(fetchedHobby);
      } else {
        setOtherUserHobby(null);
      }
    } catch (error: any) {
      if (error.response?.status === 404 && error.response?.data?.code === '01') {
        alert('존재하지 않는 사용자입니다');
      } else {
        alert('오류가 발생했습니다. 다시 시도해주세요.');
      }
    }
  };

에러는 어디서 어떻게 핸들링해야 할까 ?

제 코드를 보시면 아시겠지만 ,,

try {
    const response = await axios.put(
      `${import.meta.env.VITE_APP_BASE_URL}/user`, 
      data, 
      {
        headers: {
          token: token,
        },
      }
    );

    return response.data;
  } catch (err: any) {
    throw err;
  }

catch 문에서 에러 발생 시 throw로 던져주고 사용하는 컴포넌트 내에서 status 및 code 일치여부 확인을 통해 각 케이스에 맞게 에러 메시지를 띄워주고 있습니다. 이 방식이 적절한 것인지, 혹은 더 나은 방식이 있는지 잘 모르겠어요


🥲 소요 시간

  • 2 days

🖼️ 구현 결과물

B8E565A7-2718-47E1-9EEB-073D8E580C1C.mov

비밀번호 보이기 버튼 동작

CBEA87DF-0DAB-40B3-9547-AA8E4F71B033.mov

@maylh maylh self-assigned this Nov 12, 2024
Copy link

@Minn-Choi Minn-Choi left a 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">

Choose a reason for hiding this comment

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

ko !!

Comment on lines +1 to +17
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;
}
};

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;

Choose a reason for hiding this comment

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

logError(err);

이런 식으로 에러를 던지기 전에 로그를 기록해도 좋을 것 같아요오 !

Comment on lines +20 to +31
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('');
}
};

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

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'을 지정할 필요는 없을 것 같습니답 !!

Copy link
Member

Choose a reason for hiding this comment

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

button의 type 기본값은 submit 아닌가요...?

Copy link
Member

@gudusol gudusol left a comment

Choose a reason for hiding this comment

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

회원가입 부분에 funnel 구조까지 사용 하셨네요! 과제 하느라 고생 많으셨습니다!

👍👍👍

Comment on lines +42 to +47
<>
<ThemeProvider theme={Theme}>
<Global styles={globalStyle} />
<RouterProvider router={router} />
</ThemeProvider>
</>
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
<>
<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>
Copy link
Member

Choose a reason for hiding this comment

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

button태그는 기본 속성이 type="submit"이라서 굳이 type을 다시 설정해주지 않아도 괜찮습니다!

Comment on lines +4 to +9
interface ButtonProps {
type: 'button' | 'submit'
onClick?: () => void;
disabled?: boolean;
children: React.ReactNode;
}
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
interface ButtonProps {
type: 'button' | 'submit'
onClick?: () => void;
disabled?: boolean;
children: React.ReactNode;
}
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
}

요렇게 버튼 엘리먼트의 기본속성을 상속받으면 button이 가지고 있는 속성들을 명시하지 않고 사용할 수 있습니다!

Comment on lines +11 to +17
const Button = ({ type, onClick, disabled, children }: ButtonProps) => {
return (
<StyledButton type={type} onClick={onClick} disabled={disabled}>
{children}
</StyledButton>
);
};
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
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;
Copy link
Member

Choose a reason for hiding this comment

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

현재 프로젝트에서는 ThemeProvider를 사용하고 있으니 색상을 사용할 때는 가능하면 theme에서 가져다가 사용하는 것이, 유지보수나 가독성 측면에서 더 좋을 것 같습니다!

Comment on lines +30 to +32
const response = await putMyInfo({ hobby: newHobby, password: newPassword });
alert('사용자 정보가 업데이트 되었습니다.');
navigate('/login');
Copy link
Member

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}>
Copy link
Member

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('');
Copy link
Member

Choose a reason for hiding this comment

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

현재 pwd와 hobby같은 상태를 각 퍼널 컴포넌트마다 별도로 선언하여 관리하고 있습니다. 하지만 이 값이 이미 SignUpContext의 state에서 관리되고 있으므로, 중복 상태를 선언하지 않고, 컨텍스트에서 제공하는 상태를 가져와 사용하는 것이 더 좋아 보입니다.

이렇게 하면

  1. 여러 곳에서 같은 데이터를 관리하지 않아도 되므로 데이터 동기화 문제 방지
  2. 상태나 데이터 추적이 용이해져서 유지보수성 향상
  3. 불필요한 state를 지우며 코드가 간결해짐

등의 장점이 있습니다!

@@ -0,0 +1,44 @@
import React, { createContext, useState, useContext, ReactNode } from 'react';
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
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`, {
Copy link
Member

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을 지정해주지 않아도 되서 조금 더 간결하게 코드를 작성할 수 있을 것 같습니다!

@gudusol
Copy link
Member

gudusol commented Dec 5, 2024

앗 제가 PR의 질문을 이제 봐서 userId이슈를 이제 봤는데요,
searchedUserId등으로 state를 하나 더 만들고, 검색을 할때마다 이 값을 set 해주며, 검색된 유저의 id를 따로 저장해두면 되지않을까요?!

@gudusol
Copy link
Member

gudusol commented Dec 5, 2024

글구 하나 꿀팁으로
image

요렇게 마크다운에 백틱 3개 사용해서 코드 작성하실 때

image

요렇게 언어 지정해주면 코드가 더 예쁘게? 가독성 좋게 나옵니다!

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