위코드 팀 프로젝트 1차 - 로그인 컴포넌트

37940 단어 WeCodeWeCode

개요


1차 프로젝트 회고록
(위 그림은 원본사이트, 아래는 클론코딩 사이트)
원본 사이트인 이솝과 최대한 동일하게 구현하려는 목표를 세웠습니다.
일반적으로 볼 수 있는 이분화된 로그인 / 회원가입 기능과 다르게 이솝에서는 첫 화면에서 이메일 주소를 입력받습니다.
해당 이메일 주소가 서버에 이미 존재하는 가입된 계정일시에는 로그인 화면을 띄어줍니다.
반대로 이메일 주소가 없으면 회원가입 화면을 띄어줍니다.
그리고 비밀번호를 찾으려는 사용자에게는 이메일을 입력하는 화면을 띄어주고 그 이메일에 맞춰서 비밀번호 재설정 메일을 보내줍니다.
비밀번호 찾는 기능은 현재의 부족한 실력과 1차 프로젝트의 라이브러리 사용 제한으로 인해서 이메일을 발송하는 척만 하는 더미기능으로만 구현하기로 했습니다.


구조를 어떻게 짤것인가?

로그인과 회원가입 페이지가 통합된 상황이라 컴포넌트 내부의 모델을 짜는데 상당히 많은 고민을 했습니다.
많은 state가 필요한 상황이였고 이를 어디에 배치하느냐도 정말로 중요했기에 실제 코드를 작성하다가 문제가 발생하면 해결하기 위해서 state를 추가하거나 삭제하고 다른 컴포넌트에 배치하기를 여러번 겪었습니다.

처음으로 만든 구조는 Login Mode에 맞는 각각의 컴포넌트를 만들고 이를 단순히 Login.js에서 제어하는 구조였습니다.
하지만 작성하다보니 너무나 많은 중복코드가 발생하게 되었고 React의 장점을 살리지 못하는것 같아서 결국 앞서 하루 이틀정도 소모된 해당구조를 포기하고 새로 작성하기로 결정했습니다.
되돌아보니 이런 구조로 짜더라도 내부에서 공통 컴포넌트를 적극적으로 사용하기만 한다면 오히려 이후에 완성한 컴포넌트에 비해 장점을 가질 수도 있단 생각이 들었습니다.


새로 작성한 구조에서는 Login.js가 가장 최상단에 위치하며 바로 아래에 LoginForm.js가 위치하며 로그인 모드에 맞춰서 다른 props를 LoginForm.js에 구현하는 방식으로 만들었습니다.
로그인 모드에 맞춰서 LoginForm안에 있는 컴포넌트에서 다른 하부 컴포넌트들의 내용물을 바꾸는 식으로 만들었습니다.

유데미 강의에서 본 훌륭한 리액트 제작자의 소양으로 꼽혔던 내용에 대해 뼈저리게 느낄 수 있는 경험이였습니다.
컴포넌트에 대한 결정, 어떤것을 State화 시킬것이며 어디에 둘것인지 잘 결정하는것이 작업물이 점점 완성되고 다른 컴포넌트와 결합되면서 정말 큰 차이를 발생하게 된다는걸 몸소 배울 수 있었습니다.
여러 컴포넌트의 결합에 있어서 state를 생각할때 최대한 컴포넌트 그 자체에서 해결 할 수 있는것을 해결하고 다른 컴포넌트와 주고받는 것을 최소화 시키는것이 재사용성 측면에서 좋은 구조라는 생각이 들었는데 이에 대해서는 좀 더 경험해보고 공부해봐야 알 것 같습니다.


Login.js

const Login = ({ onCloseLoginModal, onSetLoginedUserInfo }) => {
  const [loginMode, setLoginMode] = useState('main');

  const [userInfo, setUserInfo] = useState({
    email: '',
    firstName: '',
    lastName: '',
    password: '',
    rePassword: '',
  });

  const [inputValidity, setInputValidity] = useState({
    email: false,
    firstName: false,
    lastName: false,
    password: false,
    rePassword: false,
    emailContainAt: false,
    samePassword: false,
  });

  ... 중략

	const insertLoginForm = dataName => {
    return (
      <LoginForm
        formData={dataName}
        onChangeLoginMode={changeLoginModeHandler}
        onCloseModal={closeModalHandler}
        loginMode={loginMode}
        userInfo={userInfo}
        setUserInfo={setUserInfo}
        onClearUserInfo={clearUserStateHandler}
        inputValidity={inputValidity}
        onSetInputValidity={setInputValidity}
        onSetLoginedUserInfo={onSetLoginedUserInfo}
      />
    );
  };

  return (
    <div className="login">
      <LoginCard>
        {loginMode === 'main' && insertLoginForm(FIRSTWINDOW_DATA)}
        {loginMode === 'signIn' && insertLoginForm(SIGNIN_DATA)}
        {loginMode === 'signUp' && insertLoginForm(SIGNUP_DATA)}
        {loginMode === 'resetPw' && insertLoginForm(RESETPW_DATA)}
        {loginMode === 'receivedPw' && insertLoginForm([])}
      </LoginCard>
    </div>
  );

가장 최상단에 위치한 컴포넌트입니다.
state는 loginmode, inputValidity, userInfo를 배치했습니다.
유효성 검사와 백앤드 서버에 보낼 유저정보를 저장하기 위한 역할을 하고 loginMode는 loginForm에 들어갈 상수데이터와 loginForm 내부의 출력컴포넌트를 결정하는 역할을 했습니다.

LoginedForm.js

 return (
    <form className="loginForm" onSubmit={sumbmitHandler}>
      <section className="loginTopBtnArea">
        <LoginCloseMiniBtn
          onCloseModal={onCloseModal}
          onClearUserInfo={onClearUserInfo}
        />
        {loginMode === 'main' || (
          <LoginBackMiniBtn
            onChangeLoginMode={onChangeLoginMode}
            onClearUserInfo={onClearUserInfo}
          />
        )}
      </section>
      <section className="loginMessageArea">
        <LoginMessage
          loginMode={loginMode}
          inputValidity={inputValidity}
          loginError={loginError}
        />
      </section>

      <section className="loginInputArea">
        {formData.map(({ infoType, inputType, string }) => (
          <LoginInput
            key={infoType}
            loginMode={loginMode}
            infoType={infoType}
            inputType={inputType}
            inputText={string}
            onResetLoginErrorMsg={resetLoginErrorMsgHandler}
            onSetUserInfo={setUserInfo}
            userInfo={userInfo}
            onSetInputValidity={onSetInputValidity}
            inputValidity={inputValidity}
          />
        ))}
      </section>
      <section className="loginMainBtnArea">
        {loginMode === 'receivedPw' || (
          <LoginSubmitButton
            onChangeLoginMode={onChangeLoginMode}
            loginMode={loginMode}
            inputValidity={inputValidity}
          />
        )}
        {loginMode === 'receivedPw' && (
          <LoginCloseMainBtn onCloseModal={onCloseModal} />
        )}
      </section>
      <section className="loginBottomBtnArea">
        {loginMode === 'signIn' && (
          <button type="button" onClick={goToResetPw}>
            패스워드 재설정하기
          </button>
        )}
        {loginMode === 'signUp' && (
          <button type="button" onClick={goToSignIn}>
            <p>위솝 계정을 가지고 계십니까?</p>
          </button>
        )}
      </section>
    </form>
  );
};

로그인 컴포넌트에서는 state를 에러메시지 출력을 위한 loginError만 가지고 있습니다.
login.js에 있는 state를 통해서 출력되는 컴포넌트의 상태를 결정하게 됩니다.
loginMode가 signUp이면 예시그림처럼 그에 맞는 상수데이터를 사용하며 map으로 LoginInput 컴포넌트를 조작하여 5개의 LoginInput이 뜨게 하는 식입니다.

   const mainEmailInfoSubmit = () => {
    fetch(API.loginMainAddress, {
      method: 'POST',
      body: JSON.stringify({
        email: userInfo.email,
      }),
    })
      .then(res => res.json())
      .then(res => {
        const { message } = res;
        const resCondition = {
          true: function () {
            setLoginError('');
            goToSignIn();
          },
          VALIDATION_ERROR: function () {
            setLoginError('wrongEmail');
          },
          false: function () {
            goToSignUp();
            onClearUserInfo();
            setLoginError('');
          },
        };

        resCondition[message]();
      });
  };

그리고 백엔드와의 통신코드도 이 컴포넌트에 위치하게 됩니다.
통신결과에 맞춰서 에러메시지를 출력하거나 성공시에는 토큰을 로컬스토리지에 저장하고 loginedUser의 state(이 state는 로그인 컴포넌트의 부모컴포넌트인 Nav 컴포넌트에 위치하고 있습니다.)를 바꾸는 식입니다.
예시 코드는 로그인창 처음에 이메일만 입력하는 상황에서 백엔드에서 기존가입회원인지에 대한 여부를 받고 그에 맞춰서 loginMode를 변화시켜서 전체적인 렌더링을 변화시키는 방법입니다.

결과적으로 이런식으로 내용구성이 변화하게 됩니다.

유효성검사

const LoginMessage = ({ loginMode, loginError }) => {
  return (
    <div className="loginMessage">
      <p className="titleMessage">{TITLE_MSG[loginMode]}</p>
      <p className="contentMessage">{CONTENT_MSG[loginMode]}</p>
      <p className="loginErrorMessage">{ERROR_MSG[loginError]}</p>
    </div>
  );
};

잘못된 형식의 이메일을 입력했거나 비밀번호 확인창에 먼저 입력한 비밀번호와 다른 비밀번호를 입력했을시 경고 메시지를 사용자에게 출력하고 백앤드로 해당 정보를 보내는것을 막아야 합니다.
Login.js에 위치한 inputValidity 컴포넌트의 값을 바꿔가면서 에러메시지를 출력하고 내용을 변화시켰고 모두 true가 떠있을때 제출이 가능하게 설정하였습니다.
물론 백엔드에서도 정보를 받고나서 유효하지 않은 상태면 에러메시지를 출력하지만 프론트에서도 먼저 문제가 발생하지 않게 필터링 하는것도 중요하다는 생각이 들었습니다.

Blocker : 렌더링 타이밍 문제

유효성 검사를 할때 input창에 입력된 데이터를 event.target.value로 읽고 이를 state화 시킨 이후에 에러가 있는지 확인하고 메시지 출력을 바꾸는 식으로 했습니다.
그런데 이런식으로 하니 입력내용과 다르게 한박자 늦게 에러메시지가 출력되는 문제가 있었습니다.
이 문제의 원인을 찾아보니 useState가 성능향상을 위해서 비동기적으로 작동하기 때문이였습니다.
이를 해결하기 위해 useEffect도 사용해보고 여러가지 방법을 사용해보았는데 LoginInput.js 내부에 메시지에 대한 state를 만들어서 해결했습니다.

로그인 완료후

로그인이 완료되면 Login 컴포넌트의 부모컴포넌트인 Nav바에 유저에 대한 정보가 전달되며 로컬 스토리지에 토큰이 저장됩니다.
이 정보들을 기반으로 Nav바에서 유저이름을 출력하고 카트에 아이템 담기 기능등을 토글 형태로 제어 할 수 있게 되었습니다.
새로 회원가입을 했으면 백엔드 서버에 입력한 정보들이 전달되었습니다.

좋은 웹페이지 즐겨찾기