[React] Blog 회원가입 구현

자몽·2021년 8월 4일
4

React

목록 보기
4/7
post-thumbnail
post-custom-banner

1편. Blog 회원가입 구현

✅ 사용한 기술

  • api 연동
  • redux 사용
  • redux-saga 사용

본 포스팅은 리액트를 다루는 기술[김민준(velopert)] 책을 보고 따라 만들면서 이해가 가지 않았던 부분들을 해결하면서 주석으로 추가 내용을 덧붙인 포스팅입니다.
참고: https://thebook.io/080203/

Github: https://github.com/OseungKwon/practice-react/tree/main/blog/front2

✅ 구성

중요한 부분만 넣었기에, 일부 빠진 부분이 있으니, 빠진 부분은 위의 thebook 링크 또는 github 링크를 참고하기 바란다.

로그인/회원가입 인증 Form(주로 css 다움)

  • componenets>auth>AuthForm.js

회원가입 Form(주로 이벤트, 액션 다룸)

  • containers>auth>RegisterForm.js

redux, redux-saga

  • modules>auth.js
  • moduels>index.js
  • moduels>loading.js

redux-saga

  • lib>createRequestSaga.js

로그인 페이지

  • pages>RegisterPage.js

✅ 코드

📕 componenets>auth>AuthForm.js

// 회원가입, 로그인 Form
// 조금 더 세세하게 css 다룸
import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import Button from '../common/Button';
import { Link } from 'react-router-dom';
// WhiteBox 바로 안에 있게되는 AuthFormBolck
const AuthFormBlock = styled.div`
  h3 {
    margin: 0;
    color: ${palette.gray[8]};
    margin-bottom: 2rem;
  }
`;
// input태그 모두
const StyledInput = styled.input`
  font-size: 1rem;
  border: none;
  border-bottom: 1px solid ${palette.gray[5]};
  outline: none;
  width: 100%;
  padding-bottom: 0.5rem;
  margin-bottom: 1rem;
`;
// Link로 회원가입이면->로그인으로 넘어갈 수 있음
const Footer = styled.div`
  text-align: right;
  a {
    color: ${palette.gray[6]};
    text-decoration: underline;
    &:hover {
      color: black;
    }
  }
`;
// 제출하는 버튼
const ButtonWithMarginTop = styled(Button)`
  margin-top: 2rem;
  margin-bottom: 1rem;
`;

// 능동적으로 컴포넌트 관리 위해, props로 type를 받아
// 로그인, 회원가입을 하나의 Form으로 만든다.
const textMap = {
  login: '로그인',
  register: '회원가입',
};
// 로그인 시, type=>auth, form=>login 받음
const AuthForm = ({ type, form, onChange, onSubmit }) => {
  const text = textMap[type];
  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>
          {text}
        </ButtonWithMarginTop>
      </form>
      <Footer>
        {type === 'register' ? (
          <Link to="/login">로그인</Link>
        ) : (
          <Link to="/register">회원가입</Link>
        )}
      </Footer>
    </AuthFormBlock>
  );
};

export default AuthForm;

📙 containers>auth>RegisterForm.js

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

const RegisterForm = () => {
  const dispatch = useDispatch();
  const { form, auth, authError } = useSelector(({ auth }) => ({
    form: auth.register,
    auth: auth.auth,
    authError: auth.authError,
  }));
  const onChange = (e) => {
    const { value, name } = e.target;
    dispatch(
      changeField({
        form: 'register',
        key: name,
        value,
      }),
    );
  };
  const onSubmit = (e) => {
    e.preventDefault();
    const { username, password, passwordConfirm } = form;
    if (password !== passwordConfirm) {
      return;
    }
    dispatch(register({ username, password }));
  };

  useEffect(() => {
    dispatch(initializeForm('register'));
  }, [dispatch]);

  useEffect(() => {
    if (authError) {
      console.log('오류 발생');
      console.log(authError);
      return;
    }
    if (auth) {
      console.log(`회원가입 성공`);
      console.log(auth);
    }
  }, [authError, auth]);

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

export default RegisterForm;

📘 modules>auth.js

// 회원 인증 Form  Redux
import { createAction, handleActions } from 'redux-actions';
// immer
import produce from 'immer';
// redux-saga 관련
import createRequestSaga, {
  createRequestActionTypes,
} from '../lib/createRequestSaga';
import { takeLatest } from 'redux-saga/effects';
// 모든 rest api 가져오기
import * as authAPI from '../lib/api/auth';

// 회원인증 onChange, onSubmit 관련
const CHANGE_FIELD = 'auth/CHANGE_FIELD';
const INITIALIZE_FORM = 'auth/INITIALIZE_FORM';

// 회원가입 관련 saga
// 비구조화 할당 통해서 createRequestActionTyle에서의
// [type, SUCCESS, FAILURE]을 [REGISTER, REGISTER_SUCCESS, REGISTER_FAILURE]로
const [REGISTER, REGISTER_SUCCESS, REGISTER_FAILURE] =
  createRequestActionTypes('auth/REGISTER');

// 로그인 관련 saga
const [LOGIN, LOGIN_SUCCESS, LOGIN_FAILURE] =
  createRequestActionTypes('auth/LOGIN');

// onChange
export const changeField = createAction(
  CHANGE_FIELD,
  ({ form, key, value }) => ({
    form,
    key,
    value,
  }),
);
// Form 초기 상태
export const initializeForm = createAction(INITIALIZE_FORM, (form) => form);

// 회원가입
export const register = createAction(REGISTER, ({ username, password }) => ({
  username,
  password,
}));

//  로그인
export const login = createAction(LOGIN, ({ username, password }) => ({
  username,
  password,
}));

// [회원가입 사가]
// register === client.post('/api/auth/register', { username, password });
const registerSaga = createRequestSaga(REGISTER, authAPI.register);

// [로그인 사가]
// login === client.post('/api/auth/login', { username, password });
const loginSaga = createRequestSaga(LOGIN, authAPI.login);

// redux-saga는 "제너레이터 함수"를 사용해 비동기 작업을 관리
export function* authSaga() {
  // 기존에 진행중이던 작업이 있다면 취소,
  // 가장 마지막으로 실행된 작업만 수행
  yield takeLatest(REGISTER, registerSaga);
  yield takeLatest(LOGIN, loginSaga);
}

// 초기화
const init = {
  register: {
    username: '',
    password: '',
    passwordConfirm: '',
  },
  login: {
    username: '',
    password: '',
  },
  auth: null,
  authError: null,
};

const auth = handleActions(
  {
    [CHANGE_FIELD]: (state, { payload: { form, key, value } }) =>
      produce(state, (draft) => {
        draft[form][key] = value;
      }),
    [INITIALIZE_FORM]: (state, { payload: form }) => ({
      ...state,
      [form]: init[form],
    }),
    [REGISTER_SUCCESS]: (state, { payload: auth }) => ({
      ...state,
      authError: null,
      auth,
    }),
    [REGISTER_FAILURE]: (state, { payload: error }) => ({
      ...state,
      authError: error,
    }),
    [LOGIN_SUCCESS]: (state, { payload: auth }) => ({
      ...state,
      authError: null,
      auth,
    }),
    [LOGIN_FAILURE]: (state, { payload: error }) => ({
      ...state,
      authError: error,
    }),
  },
  init, // state에 딱히 값이 없으면 init이 대신 들어감
);
export default auth;

📗 auth>index.js

import { combineReducers } from 'redux';
import { all } from 'redux-saga/effects';
import auth, { authSaga } from './auth';
import loading from './loading';

const rootReducer = combineReducers({
  auth,
  loading,
});
// all 함수는 여러 사가를 합쳐주는 역할을 한다.
export function* rootSaga() {
  yield all([authSaga()]);
}

export default rootReducer;

📕 auth>loading.js

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

const START_LOADING = 'loading/START_LOADING'; // 로딩 시작
const FINISH_LOADING = 'loading/FINISH_LOADING'; // 로딩 끝

// 로딩 시작
export const startLoading = createAction(
  START_LOADING,
  (requestType) => requestType, // requestType에 뭐가 들어가는지
);

// 로딩 끝
export const finishLoading = createAction(
  FINISH_LOADING,
  (requestType) => requestType,
);
const init = {};

const loading = handleActions(
  {
    [START_LOADING]: (state, action) => ({
      ...state,
      [action.payload]: true,
    }),
    [FINISH_LOADING]: (state, action) => ({
      ...state,
      [action.payload]: false,
    }),
  },
  init,
);
export default loading;

📙 lib>createRequestSaga.js

import { call, put } from 'redux-saga/effects';
import { startLoading, finishLoading } from '../modules/loading';

export const createRequestActionTypes = (type) => {
  const SUCCESS = `${type}_SUCCESS`;
  const FAILURE = `${type}_FAILURE`;
  return [type, SUCCESS, FAILURE];
};

// type에는 REGISTER, LOGIN 들어감
// request에는 authAPI.register, authAPI.login 들어감
export default function createRequestSaga(type, request) {
  // request: 요청
  const SUCCESS = `${type}_SUCCESS`;
  const FAILURE = `${type}_FAILURE`;

  // 제너레이터 함수 사용
  return function* (action) {
    // 파라미터로 action을 받아 오면 action의 정보를 조회할 수 있음
    // put=> 특정 액션을 디스패치해줌
    yield put(startLoading(type)); // 로딩 시작
    try {
      // call은 Promise를 반환하는 함수를 호출하고, 기다릴 수 있음
      // request(action.payload)와 같은 의미
      const response = yield call(request, action.payload); // response: 응답
      // 파라미터로 전달한 객체를 payload로 설정
      yield put({
        type: SUCCESS,
        payload: response.data,
      });
    } catch (e) {
      yield put({
        type: FAILURE,
        payload: e,
        error: true,
      });
    }
    yield put(finishLoading(type)); // 로딩 끝
  };
}

📘 pages>RegisterPage.js

// 회원가입 페이지
import React from 'react';
import AuthTemplate from '../components/auth/AuthTemplate';
import RegisterForm from '../containers/auth/RegisterForm';

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

export default RegisterPage;

✅ 동작 과정

composeWithDevTools을 통해 본 redux의 흐름이다.
1. 회원가입 시, register 부분의 username, password, passwordConfirm이 저장된다.
2. onSubmit을 하면, 해당 정보가 서버로 POST된다.
이런 과정을 loading을 통해 확인할 수 있다.(auth/REGISTER : true)
3. 서버로 보내진 회원가입 데이터는 JWT로 저장된다.
4. 과정을 마치면 loading은 (auth/REGISTER : false)가 된다.

추가 ++
.
5. 같은 아이디/비밀번호로 회원가입을 시도하면, 서버에 이미 있는 아이디/비밀번호이기에,
에러가 발생한다.

2편. 회원인증 과정 이해하기

https://velog.io/@wkahd01/회원인증-이해하기

profile
꾸준하게 공부하기
post-custom-banner

0개의 댓글