Blogrow Project Day 02

thisisyjin·2022년 5월 19일
0

Dev Log 🐥

목록 보기
11/23

Blogrow Project

📝 DAY 02 - 220519

  • redux-saga / Axios (API 연동)
  • 회원가입 구현
  • 로그인 구현

API 연동

axios 설정

  1. axios를 사용하여 API 연동

✅ axios

  • 가장 많이 사용중인 HTTP 클라이언트.
  • HTTP 요청을 Promise 기반으로 처리.
  1. 리덕스에서 비동기 작업을 쉽게 관리하기 위해 redux-saga를 이용.

패키지 설치

$ yarn add axios redux-saga

axios 인스턴스 생성

🔻 예시

const instance = axios.create({
  baseURL: 'https://some-domain.com/api/', // API 주소
  headers: { 'X-Custom-Header': 'foobar' }, // 헤더
  timeout: 1000, // 비동기 (setTimeout)
});

아래와 같이 코드를 작성함.

src/lib/api/client.js 생성

import axios from 'axios';

const client = axios.create();

/* 예시

// API 주소 다른곳으로 사용시
client.defaults.baseURL = 'https://external-api-server.com/'

// 헤더 설정
client.defaults.headers.common['Authorization'] = 'Bearer a1b2c3d4';

// 인터셉터 설정
axios.intercepter.response.use({
    respoonse => {
        return response;
    },
    error => {
        return  Promise.reject(error);
    }
})

*/

export default client;
  • axios 인스턴스를 만들면 나중에 API 클라이언트에 공통된 설정을 쉽게 넣어줄 수 있음.
  • 인스턴스를 만들지 않으면 모든 요청에 대해 설정하게 되므로, 다른 API 서버 사용시 곤란해질 수 있음.

프록시 설정

현재 백엔드는 localhost:4000 (4000포트)에 열려있고,
프론트엔드는 localhost:3000 (3000포트)에 열려있기 때문에
별도의 설정 없이 API 호출시 오류가 발생함.

✅ CORS(Cross Origin Request) 오류

  • 네트워크 요청시 주소가 다른 경우 발생하는 오류.
  • 다른 주소에서도 API를 호출할 수 있도록 서버쪽 코드를 수정해야 함.
  • 리액트 프로젝트의 경우에는 배포 후에는 같은 호스트에서 제공할 것이므로 이러한 설정이 불필요함.
    -> 대신 proxy 라는 기능을 사용할 것.

🔺 프록시(proxy)란?

  • 웹팩 개발 서버에서 지원하는 기능.
  • 개발 서버로 요청하는 API들을 우리가 프록시로 정해둔 서버로 전달해주고,
    응답(response)를 웹 어플리케이션에서 사용 가능하게 해줌.

CRA 에서 proxy를 설정해주려면, package.json을 수정해준다.

{
  "name": "blog-frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    ...
  },
    // 🔻 추가한 것
  "proxy": "http://localhost:4000/"
}

API 함수 작성

  • 회원 인증에 필요한 API를 사용하기 쉽도록 함수화하여 파일로 작성.

src/lib/api/auth.js 생성

import client from './client';

export const login = ({ username, password }) =>
  client.post('/api/auth/login', { username, password });

export const register = ({ username, password }) =>
  client.post('/api/auth/register', { username, password });

export const check = () => client.get('/api/auth/check');

redux-saga

1. loading 리덕스 모듈 생성

modules/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,
);

export const finishLoading = createAction(
  FINISH_LOADING,
  (requestType) => requestType,
);

const initialState = {};

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

export default loading;

2. 루트 리듀서에 등록

modules/index.js 수정

import { combineReducers } from 'redux';
import auth from './auth';
import loading from './loading';

const rootReducer = combineReducers({
  auth,
  loading,
});

export default rootReducer;

3. createRequestSaga 함수

📌 redux-saga는 제네레이터 함수 (function*())를 이용한 리덕스 미들웨어이다.
-> 자세한 내용은 공식 문서참조 문서를 참조!

  • 참고) 스토어에 미들웨어 적용시 createStore의 두번째 인자로 applyMiddleware를 넣어줌
const store = createStore(modules, applyMiddleware(logger, sagaMiddleware));

src/lib/createRequestSaga.js

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

// call = 첫인자에 두번째 인자를 전달하여 호출함
// put = 새 액션을 디스패치 -> 액션생성함수(Payload)
export default function createRequestSaga(type, request) {
  // type은 요청 성공 or 실패 여부.
  // request는 함수형태. (여기에 API 요청 - axios.get이 들어갈듯)
  const SUCCESS = `${type}_SUCCESS`;
  const FAILURE = `${type}_FAILURE`;

  return function* (action) {
    yield put(startLoading(type)); // 로딩 시작
    try {
      const response = yield call(request, action.payload);
      yield put({
        type: SUCCESS,
        payload: response.data,
      });
    } catch (e) {
      yield put({
        type: FAILURE,
        payload: e,
        error: true,
      });
    }
    yield put(finishLoading(type));
  };
}

🔺 yield 뒤에 오는 함수들 - call, put, delay
call: 첫번째 파라미터로 전달한 함수에 그 뒤에 있는 파라미터들은 전달하여 호출
put: 새 액션을 dispatch 함.
delay: ms 단위로 대기
+) 그 외에도 takeEvery, takeLastest 등이 있음

4. auth 리덕스 모듈에서 API 사용

  • 방금 만든 createRequestSaga 함수를 이용하여 auth 모듈에서 API를 사용할 수 있게 구현.
  • 우선 총 6개의 액션 타입을 추가로 더 선언해야 함.
    (기존에 있던 CHANGE_FIELD, INITIALIZE_FORM에서 추가함)
  1. REGISTER
  2. REGISTER_SUCCESS
  3. REGISTER_FAILURE
  1. LOGIN
  2. LOGIN_SUCCESS
  3. LOGIN_FAILURE

-> 6개를 선언해야 하므로, 같은 작업이 반복됨.
createRequestActionTypes 라는 함수를 선언하여 리팩토링.

⚡️ createRequestActionTypes 함수

createRequestSaga.js 수정

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

...

auth.js 수정

import { createRequestActionTypes } from '../lib/createRequestSaga';

...

const [REGISETR, REGISETR_SUCCESS, REGISETR_FAILURE] = createRequestActionTypes('auth/REGISTER');
const [LOGIN, LOGIN_SUCCESS, LOGIN_FAILURE] = createRequestActionTypes('auth/LOGIN');
  • 배열 구조분해 할당을 사용하여 간단하게 코드를 정리함.

5. API를 위한 사가 생성

-> saga란, 제너레이터 함수를 의미함.
-> createRequestSaga() 함수가 return function*()을 하므로, 사가를 생성하는 것임.

modules/auth.js 수정

import { createAction, handleActions } from 'redux-actions';
import produce from 'immer';
import { takeLatest } from 'redux-saga/effects';
import createRequestSaga, {
  createRequestActionTypes,
} from '../lib/createRequestSaga';
import * as authAPI from '../lib/api/auth';

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

const [REGISTER, REGISTER_SUCCESS, REGISTER_FAILURE] =
  createRequestActionTypes('auth/REGISTER');
const [LOGIN, LOGIN_SUCCESS, LOGIN_FAILURE] =
  createRequestActionTypes('auth/LOGIN');

export const changeField = createAction(
  CHANGE_FIELD,
  ({ form, key, value }) => ({ form, key, value }),
  // form: register, login / key: username, password, passwordConfirm
);
export const initializeForm = createAction(INITIALIZE_FORM, (form) => form);
// register, login

export const register = createAction(REGISTER, ({ username, password }) => ({
  username,
  password,
}));

export const login = createAction(LOGIN, ({ username, password }) => ({
  username,
  password,
}));

// 🔻 saga 생성

// 첫번째 인자 = type (REGISTER/LOGIN) , 두번째 인자 = request (액션생성함수)
const registerSaga = createRequestSaga(REGISTER, authAPI.register);
const loginSaga = createRequestSaga(LOGIN, authAPI.login);
export function* authSaga() {
  yield takeLatest(REGISTER, registerSaga);
  yield takeLatest(LOGIN, loginSaga);
}

const initialState = {
  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; // ex> state.register.username
      }),
    [INITIALIZE_FORM]: (state, { payload: form }) => ({
      ...state,
      [form]: initialState[form],
      authError: null,
    }),
    [REGISTER_SUCCESS]: (state, { payload: auth }) => ({
      ...state,
      authError: null,
      auth, // auth: 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,
    }),
  },

  initialState,
);

export default auth;

🔺 redux-saga의 takeLatests 함수

기존에 진행 중이던 작업이 있다면 취소 처리하고 가장 마지막으로 실행된 작업만 수행.
예>takeLatest(DECREASE_ASYNC, decreaseSaga)
DECREASE_ASYNC 액션에 대해서 기존에 진행 중이던 작업이 있다면 취소 처리하고
가장 마지막으로 실행된 작업에 대해서만 decreaseSaga()함수를 실행한다.


5. rootSaga 생성

rootReducer과 마찬가지로 modules/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;

combineReducers()의 경우에는 리듀서함수(auth, loading) 자체를 담은 객체를 인자로 넣어줬다.

반면에 all() 함수의 인자로는 사가를 호출한 후(=결과값) 배열에 담아 인자로 넣어준다.

6. store에 미들웨어 적용

src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import rootReducer, { rootSaga } from './modules';


// 🔻 1. sagaMiddleware 생성
const sagaMiddleware = createSagaMiddleware();
// 🔻 2. 두번째 인자에 applyMiddleware(sagaMiddleware) 넣어줌 
const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(sagaMiddleware)),
);

// 🔻 3. sagaMiddleware.run(rootSaga) 로 실행
sagaMiddleware.run(rootSaga);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
);

🛠 미들웨어 적용 과정

  1. const sagaMiddleware = createSagaMiddleware();
  2. const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(sagaMiddleware))
  3. sagaMiddleware.run(rootSaga)

이제 리덕스 관련 코드는 모두 끝!
회원가입 기능 구현 + 로그인 기능 구현 필요.


회원가입 구현

containers/auth/RegisterForm.js 수정

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

const RegisterForm = () => {
  const dispatch = useDispatch();
  const { form, auth, authError } = useSelector(({ auth }) => ({
    // auth 모듈의 state를 불러옴
    form: auth.register,
    auth: auth.auth,
    authError: auth.authError,
  }));

  const onChange = (e) => {
    const { value, name } = e.target;
    dispatch(
      changeField({
        form: 'register',
        key: name,
        value, // value: e.target.value
      }),
    );
  };

  // 🔻 폼 제출시 - password와 passwordConfirm이 일치하는지 확인 후 register 액션 디스패치
  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);
    }
  }, [auth, authError]);

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

export default RegisterForm;

TEST

  • 회원가입 구현이 잘 되었는지 테스트.
  • 우선 서버도 yarn start:dev로 실행시켜주고,
    frontend도 yarn start로 실행시켜줘야 함.

회원가입 성공이라고 뜸.

만약 회원가입 버튼을 한번 더 누르면?

-> 이전에 백엔드에서 작성했던 로직에 의해 409(Conflict) 에러가 발생함.

  • 백엔드 로직 다시보기

src/api/auth/auth.ctrl.js

// 1. 회원 가입 (등록)
export const register = async (ctx) => {
  // request body 검증
  const schema = Joi.object().keys({
    username: Joi.string().alphanum().min(3).max(20).required(),
    password: Joi.string().required(),
  });

  const result = schema.validate(ctx.request.body);

  if (result.error) {
    ctx.status = 400;
    ctx.body = result.error;
    return;
  }

  const { username, password } = ctx.request.body;
  try {
    // username 이미 존재하는지 체크
    const exists = await User.findByUsername(username);
    if (exists) {
      ctx.status = 409; // conflict
      return;
    }

    const user = new User({
      username,
    });
    await user.setPassword(password);
    await user.save(); // DB에 저장

    ctx.body = user.serialize();
    const token = user.generateToken();
    ctx.cookies.set('access_token', token, {
      maxAge: 1000 * 60 * 60 * 24 * 7, // 7d
      httpOnly: true,
    });
  } catch (e) {
    ctx.throw(500, e);
  }
};

mongoDB 확인

mongoDB Compass에서 blog DB에 users 컬렉션을 살펴보자.

-> 맨 아래에 방금 가입을 누른 유저 정보가 보인다! db에도 잘 등록된 것.


user 모듈 생성

  • 사용자의 상태를 담을 users 모델을 생성.
  • 새로고침 이후 임시 로그인 처리를 해주기 위함. (TEMP_SET_USER 액션)
  • takeLatest / 사가 생성 (createRequestSaga 모듈 사용)
import { createAction, handleActions } from 'redux-actions';
import { takeLatests } from 'redux-saga/effects';
import * as authAPI from '../lib/api/auth';
import createRequestSaga, {
  createRequestActionTypes,
} from '../lib/createRequestSaga';

const TEMP_SET_USER = 'user/TEMP_SET_USER';
const [CHECK, CHECK_SUCCESS, CHECK_FAILURE] =
  createRequestActionTypes('user/CHECK');

export const tempSetUser = createAction(TEMP_SET_USER, (user) => user);
export const check = createAction(CHECK);

// saga 생성
const checkSaga = createRequestSaga(CHECK, authAPI.check); //  client.get('/api/auth/check')
export function* userSaga() {
  yield takeLatests(CHECK, checkSaga);
} // userSaga를 rootSaga로 합쳐줌

const initialState = {
  user: null,
  checkError: null,
};

export default handleActions(
  {
    [TEMP_SET_USER]: (state, { payload: user }) => ({
      ...state,
      user,
    }),
    [CHECK_SUCCESS]: (state, { payload: user }) => ({
      ...state,
      user,
      checkError: null,
    }),
    [CHECK_FAILURE]: (state, { payload: error }) => ({
      ...state,
      user: null,
      checkError: error,
    }),
  },
  initialState,
);

루트 리듀서 + 루트 사가에 포함

modules/index.js

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

const rootReducer = combineReducers({
  auth,
  loading,
  user,
});

export function* rootSaga() {
  yield all([authSaga(), userSaga()]);
}

export default rootReducer;

RegisterForm 수정

  • 회원가입 성공 후, check 모듈을 호출하여 로그인 상태인지 확인.

containers/auth/RegisterForm.js 수정

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

const RegisterForm = () => {
  const dispatch = useDispatch();
  const { form, auth, authError, user } = useSelector(({ auth, user }) => ({
    // auth 모듈의 state를 불러옴
    form: auth.register,
    auth: auth.auth,
    authError: auth.authError,
    // 🔻 user 모듈의 state를 불러옴 (state.user.user)
    user: user.user,
  }));

  const onChange = (e) => {
    const { value, name } = e.target;
    dispatch(
      changeField({
        form: 'register',
        key: name,
        value, // value: e.target.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);
      // 🔻 check (액션 생성함수)를 dispatch (payload는 없음)
      dispatch(check());
    }
  }, [auth, authError, dispatch]);

  // 🔻 user 값이 잘 설정되었나 체크
  useEffect(() => {
    if (user) {
      console.log('check API 성공');
      console.log(user);
    }
  }, [user]);

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

export default RegisterForm;

redux devTool을 보면 아래와 같이 user 안에 값이 들어가 있다면 성공!

회원가입 성공시 홈(/)으로 이동

  • 라우트를 이동시키면 됨.
  • history 객체를 사용.
    -> withRouter로 컴포넌트를 감싸주면 됨.
  • react-router-domuseNavigate() 함수를 이용함.

containers/auth/RegisterForm.js 수정

import { useNavigate } from 'react-router-dom';

const navigate = useNavigate();

useEffect(() => {
  if (user) {
    // user 값이 있으면? -> 회원가입 성공 -> 홈으로 이동시켜야함
    navigate('/'); // 홈으로 이동
  }
}, [navigate, user]);
  • 회원가입 성공 시 / 경로로 이동하게 됨.


로그인 구현

containers/auth/LoginForm.js 수정

import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm, login } from '../../modules/auth';
import AuthForm from '../../components/auth/AuthForm';
import { check } from '../../modules/user';
import { useNavigate } from 'react-router-dom';

const LoginForm = () => {
  const dispatch = useDispatch();
  const { form, auth, authError, user } = useSelector(({ auth, user }) => ({
    form: auth.login, // { username, password}
    auth: auth.auth,
    authError: auth.authError,
    user: user.user,
  }));

  const onChange = (e) => {
    const { value, name } = e.target;
    dispatch(
      changeField({
        form: 'login',
        key: name,
        value, // value: e.target.value
      }),
    );
  };

  const onSubmit = (e) => {
    e.preventDefault();
    const { username, password } = form;
    // LOGIN 액션 디스패치
    dispatch(login({ username, password }));
  };

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

  // 로그인 실패, 성공 여부
  useEffect(() => {
    if (authError) {
      console.log('오류 발생');
      console.log(authError);
    }
    if (auth) {
      console.log('로그인 성공');
      console.log(auth);
      // check
      dispatch(check()); // CHECK 액션 디스패치
    }
  }, [auth, authError, dispatch]);

  // 로그인 성공시 -> 홈으로 이동
  const navigate = useNavigate();
  useEffect(() => {
    if (user) {
      navigate('/');
    }
  }, [navigate, user]);

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

export default LoginForm;
  • check 모듈을 불러옴 (user.js)
  • useEffect로 로그인 실패/성공 여부 나타냄. (user 값이 존재하는지)
  • useNavigate 를 이용. (로그인 성공시 / 경로로 이동)

회원 인증 에러 처리

  • 로그인 / 회원가입시 발생하는 에러를 처리해줌.

🔐 1. 로그인시
-> 비밀번호 불일치시 에러

Login component 수정

  • 에러 발생시 에러 메시지 UI 렌더링
  • 데이터는 props로 전달받음. (error 여부)

components/auth/AuthForm.js 수정

import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import { Link } from 'react-router-dom';
import Button from '../common/Button';

...

const ErrorMessage = styled.div`
  color: #ff0000;
  text-align: center;
  font-size: 0.85rem;
  margin-top: 1rem;
`;

const AuthForm = ({ type, form, onChange, onSubmit, error }) => {
  const text = textMap[type];
  return (
    <AuthFormBlock>
      <h3>{text}</h3>
      <form onSubmit={onSubmit}>
        <StyledInput
          autoComplete="username"
          name="username"
          placeholder="아이디"
          onChange={onChange}
          value={form.username}
        />
        <StyledInput
          type="password"
          autoComplete="new-password"
          name="password"
          placeholder="비밀번호"
          onChange={onChange}
          value={form.password}
        />
        {type === 'register' && (
          <StyledInput
            autoComplete="new-password"
            name="passwordConfirm"
            placeholder="비밀번호 확인"
            type="password"
            onChange={onChange}
            value={form.passwordConfirm}
          />
        )}
        // 🔻 error이 true일때만 ErrorMessage를 띄움.
        {error && <ErrorMessage>비밀번호가 일치하지 않습니다.</ErrorMessage>}
        <ButtonWithMarginTop teal fullWidth>
          {text}
        </ButtonWithMarginTop>
      </form>
      <Footer>
        {type === 'login' ? (
          <Link to="/register">회원가입</Link>
        ) : (
          <Link to="/login">로그인</Link>
        )}
      </Footer>
    </AuthFormBlock>
  );
};

export default AuthForm;

Login container 수정

  • AuthForm 에게 error이라는 props를 전달해줌.
  • useState로 'error'라는 상태 추가함.

containers/auth/LoginForm
-> 컨테이너에서 에러를 처리하는 것이 훨씬 쉬움.

import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm, login } from '../../modules/auth';
import AuthForm from '../../components/auth/AuthForm';
import { check } from '../../modules/user';
import { useNavigate } from 'react-router-dom';

const LoginForm = () => {
  // 🔻 state 추가 
  const [error, setError] = useState(false);
	
 		 ...

  // 로그인 실패, 성공 여부
  useEffect(() => {
    if (authError) {
      console.log('오류 발생');
      console.log(authError);
      setError(true);
    }
    if (auth) {
      console.log('로그인 성공');
      console.log(auth);
      // check
      dispatch(check()); // CHECK 액션 디스패치
    }
  }, [auth, authError, dispatch]);

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

export default LoginForm;
  • 잘못된 비밀번호 입력시 ErrorMessage 가 렌더링.
* * *

👥 2. 회원가입시
-> 입력창이 비어있을 때 / password와 passwordConfirm이 일치하지 않을 때 / username이 중복될 때 에러

Register container 수정

containers/auth/RegisterForm.js 수정

import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, initializeForm, register } from '../../modules/auth';
import AuthForm from '../../components/auth/AuthForm';
import { check } from '../../modules/user';
import { useNavigate } from 'react-router-dom';

const RegisterForm = () => {
  const [error, setError] = useState(null);

  const dispatch = useDispatch();
  const { form, auth, authError, user } = useSelector(({ auth, user }) => ({
    // auth 모듈의 state를 불러옴
    form: auth.register,
    auth: auth.auth,
    authError: auth.authError,
    // user 모듈의 state를 불러옴
    user: user.user,
  }));

  const onChange = (e) => {
    const { value, name } = e.target;
    dispatch(
      changeField({
        form: 'register',
        key: name,
        value, // value: e.target.value
      }),
    );
  };

  const onSubmit = (e) => {
    e.preventDefault();
    const { username, password, passwordConfirm } = form;
    // 인풋 하나라도 비어있으면
    if ([username, password, passwordConfirm].includes('')) {
      setError('빈 칸을 모두 입력하세요.');
      return;
    }
    if (password !== passwordConfirm) {
      // 비밀번호 확인 불일치시
      setError('비밀번호가 일치하지 않습니다.');
      dispatch(changeField({ form: 'register', key: 'password', value: '' })); // password 인풋을 빈칸으로 만듬
      dispatch(
        changeField({ form: 'register', key: 'passwordConfirm', value: '' }),
      );
      return;
    }
    dispatch(register({ username, password })); // REGISTER 액션 디스패치
  };

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

  // 회원가입 실패/성공 처리
  useEffect(() => {
    if (authError) {
      // 계정명이 이미 존재할 때 - 409 Conflict
      if (authError.response.status === 409) {
        setError('이미 존재하는 계정명입니다.');
        return;
      }
      // 그 외 이유
      setError('회원가입 실패');
      return;
    }
    if (auth) {
      console.log('회원가입 성공');
      console.log(auth);
      dispatch(check());
    }
  }, [auth, authError, dispatch]);

  // user 값이 잘 설정되었나 체크
  useEffect(() => {
    if (user) {
      console.log('check API 성공');
      console.log(user);
    }
  }, [user]);

  const navigate = useNavigate();
  useEffect(() => {
    if (user) {
      // user 값이 있으면? -> 회원가입 성공 -> 홈으로 이동시켜야함
      navigate('/'); // 홈으로 이동
    }
  }, [navigate, user]);

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

export default RegisterForm;

Register components 수정

components/auth/AuthForm.js

{error && <ErrorMessage>{error}</ErrorMessage>}

error 문구가 달라져야 하므로 위와 같이 변경함.

-> 하나라도 빈칸이 있을 경우


-> password, passwordConfirm 칸이 비워짐

dispatch(changeField({ form: 'register', key: 'password', value: '' })); // password 인풋을 빈칸으로 만듬
dispatch(
        changeField({ form: 'register', key: 'passwordConfirm', value: '' }),
      );

changeField 액션을 실행시켜 value를 ''로 만들어버렸기 때문.


-> 이미 존재하는 계정명인 경우

profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글