관리 메뉴

거니의 velog

(3) 프론트엔드 프로젝트 : 시작 및 회원 인증 구현 3 본문

React_프론트엔드 프로젝트

(3) 프론트엔드 프로젝트 : 시작 및 회원 인증 구현 3

Unlimited00 2024. 2. 22. 17:07

(2) 리덕스로 폼 상태 관리하기

* 이번에는 리덕스로 회원가입과 로그인 폼의 상태를 관리하는 방법을 알아보자. 이전에 만들어 놓았던 auth 모듈을 다음과 같이 수정해 보자.

[modules/auth.js]

import { createAction, handleActions } from 'redux-actions';
import produce from 'immer';

const CHANGE_FIELD = 'auth/CHANGE_FIELD';
const INITIALIZE_FORM = 'auth/INITIALIZE_FORM';

export const changeField = createAction(
  CHANGE_FIELD,
  ({ form, key, value }) => ({
    form, // register , login
    key, // username, password, passwordConfirm
    value, // 실제 바꾸려는 값
  }),
);
export const initializeForm = createAction(INITIALIZE_FORM, (form) => form); // register / login

const initialState = {
  register: {
    username: '',
    password: '',
    passwordConfirm: '',
  },
  login: {
    username: '',
    password: '',
  },
};

const auth = handleActions(
  {
    [CHANGE_FIELD]: (state, { payload: { form, key, value } }) =>
      produce(state, (draft) => {
        draft[form][key] = value; // 예: state.register.username을 바꾼다
      }),
    [INITIALIZE_FORM]: (state, { payload: form }) => ({
      ...state,
      [form]: initialState[form],
    }),
  },
  initialState,
);

export default auth;

* 이제 컨테이너 컴포넌트를 만들어 보자. src 디렉터리에 containers 디렉터리를 만든 후 다양한 컨테이너 컴포넌트들을 종류별로 분류하여 만들어 보겠다.

* 앞으로 만든 컨테이너 컴포넌트에서는 useDispatch와 useSelector 함수를 사용하여 컴포넌트를 리덕스와 연동시킨다. 앞으로 이 프로젝트에서 작성할 모든 컨테이너 컴포넌트는 connect 함수 대신 Hooks를 사용하여 구현할 것이다.

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm } from '../../modules/auth';
import AuthForm from '../../components/auth/AuthForm';

const LoginForm = () => {
  const dispatch = useDispatch();
  const { form } = useSelector(({ auth }) => ({
    form: auth.login,
  }));

  // 인풋 변경 이벤트 핸들러
  const onChange = (e) => {
    const { value, name } = e.target;
    dispatch(
      changeField({
        form: 'login',
        key: name,
        value,
      }),
    );
  };

  // 폼 등록 이벤트 핸들러
  const onSubmit = (e) => {
    e.preventDefault();
    // 구현 예정
  };

  // 컴포넌트가 처음 렌더링될 때 form을 초기화함
  useEffect(() => {
    dispatch(initializeForm('login'));
  }, [dispatch]);

  return (
    <AuthForm
      type="login"
      form={form}
      onChange={onChange}
      onSubmit={onSubmit}
    />
  );
};

export default LoginForm;

* 위 컴포넌트에서는 onChange 함수와 onSubmit 함수를 구현하여 필요한 액션을 디스패치하도록 구현해 주었다. 또한, useEffect를 사용하여 맨 처음 렌더링 후 initializeForm 액션 생성 함수를 호출했다. 이 작업을 하지 않으면, 로그인 페이지에서 값을 입력한 뒤 다른 페이지로 이동했다가 다시 돌아왔을 때 값이 유지된 상태로 보이게 된다.

* 이 컨테이너 컴포넌트를 다 만든 뒤에는 LoginPage에서 기존 Authform을 LoginForm으로 대체시키자.

[pages/LoginForm.js]

import React from 'react';
import AuthTemplate from '../components/auth/AuthTemplate';
import LoginForm from '../containers/auth/LoginForm';

const LoginPage = () => {
  return (
    <AuthTemplate>
      <LoginForm />
    </AuthTemplate>
  );
};

export default LoginPage;

* 다음으로는 컨테이너에서 props로 넣어 주었던 onChange, onSubmit, form 값을 사용할 것이다.

[components/auth/AuthForm.js]

const AuthForm = ({ type, form, onChange, onSubmit }) => {
  const text = textMap[type]; // 타입(login, register)에 따라 출력되는 text의 값이 다르다.
  return (
    <AuthFormBlock>
      <h3>{text}</h3>
      <form onSubmit={onSubmit}>
        <StyledInput
          autoComplete="username"
          name="username"
          placeholder="아이디"
          onChange={onChange}
          value={form.username}
        />
        <StyledInput
          autoComplete="new-password"
          name="password"
          placeholder="비밀번호"
          type="password"
          onChange={onChange}
          value={form.password}
        />
        {type === 'register' && (
          <StyledInput
            autoComplete="new-password"
            name="passwordConfirm"
            placeholder="비밀번호 확인"
            type="password"
            onChange={onChange}
            value={form.passwordConfirm}
          />
        )}
        <ButtonWithMarginTop cyan fullWidth style={{ marginTop: '1rem' }}>
          {text}
        </ButtonWithMarginTop>
      </form>
      <Footer>
        {type === 'login' ? (
          <Link to="/register">회원가입</Link>
        ) : (
          <Link to="/login">로그인</Link>
        )}
      </Footer>
    </AuthFormBlock>
  );
};

export default AuthForm;

* 코드를 저장한 뒤 http://localhost:3000/login 페이지에 가 보세요. 그리고 인풋에 텍스트를 입력하고 나서 값이 리덕스 스토어에 잘 들어가는지 개발자 도구를 통해 확인해 보세요.

* 만약 회원가입 페이지를 보고 있다면 form 값이 없어서 에러가 발생할 것이다. 회원가입 페이지의 에러는 나중에 수정하겠다.

LoginForm 작동 확인

* 이제 RegisterForm 컴포넌트도 구현하겠다. LoginForm 컴포넌트를 복사한 뒤 내부에서 사용되는 키워드만 [Login > Register], [login -> register]로 고쳐 주면 된다.

[components/auth/RegisterForm.js]

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm } from '../../modules/auth';
import AuthForm from '../../components/auth/AuthForm';

const RegisterForm = () => {
  const dispatch = useDispatch();
  const { form } = useSelector(({ auth }) => ({
    form: auth.register,
  }));

  // 인풋 변경 이벤트 핸들러
  const onChange = (e) => {
    const { value, name } = e.target;
    dispatch(
      changeField({
        form: 'register',
        key: name,
        value,
      }),
    );
  };

  // 폼 등록 이벤트 핸들러
  const onSubmit = (e) => {
    e.preventDefault();
    // 구현 예정
  };

  // 컴포넌트가 처음 렌더링될 때 form을 초기화함
  useEffect(() => {
    dispatch(initializeForm('register'));
  }, [dispatch]);

  return (
    <AuthForm
      type="register"
      form={form}
      onChange={onChange}
      onSubmit={onSubmit}
    />
  );
};

export default RegisterForm;

* 이제 RegisterPage에서 사용 중이던 AuthForm을 RegisterForm으로 교체하자.

[pages/RegisterPage.js]

import React from 'react';
import AuthTemplate from '../components/auth/AuthTemplate';
import RegisterForm from '../containers/auth/RegisterForm';

const RegisterPage = () => {
  return (
    <AuthTemplate>
      <RegisterForm />
    </AuthTemplate>
  );
};

export default RegisterPage;

* 코드를 작성한 뒤 회원가입 폼에서 값을 입력할 때 리덕스에 값이 잘 반영되는지 확인해 보자.