본 포스팅은 리액트를 다루는 기술[김민준(velopert)] 책을 보고 따라 만들면서 이해가 가지 않았던 부분들을 해결하면서 주석으로 추가 내용을 덧붙인 포스팅입니다.
참고: https://thebook.io/080203/
Github: https://github.com/OseungKwon/practice-react/tree/main/blog/front2
중요한 부분만 넣었기에, 일부 빠진 부분이 있으니, 빠진 부분은 위의 thebook 링크 또는 github 링크를 참고하기 바란다.
// 회원가입, 로그인 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;
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;
// 회원 인증 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;
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;
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;
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)); // 로딩 끝
};
}
// 회원가입 페이지
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. 같은 아이디/비밀번호로 회원가입을 시도하면, 서버에 이미 있는 아이디/비밀번호이기에,
에러가 발생한다.