[trello-clone 05] redux, redux-saga 연결

Subin·2020년 4월 7일
0

trello-clone

목록 보기
5/6

이번 페이지에서는 redux와 redux-saga를 react에 연결해보도록 할게요.

redux에 대한 기초개념은 아래의 링크에서 확인해보세요
https://react.vlpt.us/redux/

redux-saga에 대한 기초개념은 아래의 링크에서 확인해보세요
https://velog.io/@jeonghoheo/Redux-Saga%EC%9D%98-%EA%B8%B0%EB%B3%B8

설치부터 시작하겠습니다.

yarn add redux react-redux redux-saga
yarn add redux-devtools-extension /* 확장프로그램 입니다. */

redux, redux-saga 초기 설정

./src 폴더에 store, reducers, sagas 폴더를 만들어 줍니다.

각각의 폴더에 index.js를 만들어 주세요. 차례대로 살펴보겠습니다.

./src/reducers/index.js

import { combineReducers } from 'redux';
//combineReducers 헬퍼 함수는 서로 다른 리듀싱 함수들을 값으로 가지는 객체를 받아서 createStore에 넘길 수 있는 하나의 리듀싱 함수로 바꿔줍니다.

const rootReducer = combineReducers({});

export default rootReducer;

./src/sagas/index.js

import { all, call } from 'redux-saga/effects';

function* rootSaga() {
  yield all([]);
} // rootSaga를 만들어 줍니다.

export default rootSaga;

./src/store/index.js

import { createStore, applyMiddleware } from 'redux';
//createStore는 Store를 만들어주는 역할을 해준다. applyMiddleware는 redux에 middleware를 추가해주는 역할을 해준다.
import { composeWithDevTools } from 'redux-devtools-extension'; // reudx 액션, 디스패치 같은 상태를 확인할 수 있는 역할
import createSagaMiddleware from 'redux-saga'; // saga를 쓰기 위해 불러줌
import rootReducer from '../reducers';
import rootSaga from '../sagas';

const sagaMiddleware = createSagaMiddleware(); // saga 미들웨어를 생성합니다.

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(sagaMiddleware)),
);

sagaMiddleware.run(rootSaga); // saga 실행

export default store;

./src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import store from './store'; // store를 불러옴

import App from './App';

ReactDOM.render(
  <Provider store={store}> // Provider를 통해서 store 접근
    <BrowserRouter>
      <React.StrictMode>
        <App />
      </React.StrictMode>
    </BrowserRouter>
  </Provider>,
  document.getElementById('root'),
);

01 user signup 추가하기

reducer, saga 작업부터 먼저 시작할게요.

./src/reducers/user.js

//ACTION
export const SIGN_UP_REQUEST = 'SIGN_UP_REQUEST';
export const SIGN_UP_SUCCESS = 'SIGN_UP_SUCCESS';
export const SIGN_UP_FAILURE = 'SIGN_UP_FAILURE';

// INITIAL
const INITIAL_STATE = {
  isLoggingIn: false,
  loginError: '',
  isSigningUp: false,
  signUpError: '',
  me: null,
};

// REDUCER
const reducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case SIGN_UP_REQUEST:
      return applySignUpRequest(state, action);
    case SIGN_UP_SUCCESS:
      return applySignUpSuccess(state, action);
    case SIGN_UP_FAILURE:
      return applySignUpFailure(state, action);
    default: {
      return state;
    }
  }
};

const applySignUpRequest = (state, action) => {
  return {
    ...state,
    isSigningUp: true,
    signUpError: '',
    me: null,
  };
};

const applySignUpSuccess = (state, action) => {
  return {
    ...state,
    isSigningUp: false,
    me: action.payload, // me에 값이 들어오면 성공입니다.
  };
};

const applySignUpFailure = (state, action) => {
  return {
    ...state,
    isSigningUp: false,
    signUpError: action.error,
    me: null,
  };
};

export default reducer;

./src/reducers/index.js

index 파일에 추가 해줄게요

import { combineReducers } from 'redux';

import user from './user';

const rootReducer = combineReducers({ user });

export default rootReducer;

./src/sagas/user.js

import { all, fork, takeLatest, call, put } from 'redux-saga/effects';
import axios from 'axios';

import {
  SIGN_UP_SUCCESS,
  SIGN_UP_FAILURE,
  SIGN_UP_REQUEST,
} from '../reducers/user'; // sign up과 관련된 action type들을 가지고 옵니다.

function signAPI(data) {
  return axios.post(`http://localhost:4000/auth/signup`, data, {
    withCredentials: true,
  });
}

function* sign(action) {
  try {
    const result = yield call(signAPI, action.data);
    yield put({
      type: SIGN_UP_SUCCESS,
      payload: result.data,
    });
  } catch (e) {
    yield put({
      type: SIGN_UP_FAILURE,
      error: e,
    });
  }
}

function* watchSign() {
  yield takeLatest(SIGN_UP_REQUEST, sign);
}

function* todoUserSaga() {
  yield all([fork(watchSign)]);
}

export default todoUserSaga;

./src/sagas/index.js

index 파일에 추가해줄게요

import { all, call } from 'redux-saga/effects';

import user from './user';

function* rootSaga() {
  yield all([call(user)]);
}

export default rootSaga;

기본 세팅은 여기까지 입니다. 흐름을 이해하기 힘드시다면 댓글로 남겨주세요.


./src/components/SignInput.js

import React, { useState, useEffect } from 'react';
import { Redirect } from 'react-router-dom';
import styled from 'styled-components';
import { useSelector, useDispatch } from 'react-redux'; 
import { SIGN_UP_REQUEST } from '../reducers/user';

const Container = styled.div`
  position: relative;
  top: 120px;

  width: 100%;
  height: 100%;
`;

const SignContainer = styled.div`
  position: relative;
  margin: 0 auto;
  height: 100%;
  width: 300px;
  background-color: #ddd;
`;

const Input = styled.input`
  height: 2rem;
  width: 80%;
  font-size: 1.5rem;
`;

const Button = styled.button`
  height: 2rem;
  width: 100%;
`;

const SignInput = () => {
  const [email, setEmail] = useState('');
  const [nick, setNick] = useState('');
  const [password, setPassword] = useState('');
  const [isRedirect, setIsRedirect] = useState(false);

  const { me, signUpError } = useSelector((state) => state.user); // me의 값과 signupError의 값으로 회원가입 여부를 확인 합니다.
  const dispatch = useDispatch();

  useEffect(() => {
    if (me) {
      setIsRedirect(true);
      alert('회원 가입 완료');
       // 성공적으로 회원가입이 되었을 때 me에 값이 들어오게 됩니다. 그리고 to='/' 으로 redirect 됩니다.
    }
    if (signUpError !== '') {
      alert('회원 정보 에러');
    }
  }, [me, signUpError, dispatch]);
  const onClick = () => {
    dispatch({
      type: SIGN_UP_REQUEST,
      data: {
        email,
        nick,
        password,
      },
    }); // dispatch로 sign up 요청을 합니다.
  };
  return (
    <>
      {isRedirect ? (
        <Redirect
          to={{
            pathname: '/',
          }}
        />
      ) : (
        <Container>
          <SignContainer>
            <Input
              value={email}
              placeholder="email"
              onChange={(e) => {
                setEmail(e.target.value);
              }}
            />
            <Input
              value={nick}
              placeholder="nickname"
              onChange={(e) => {
                setNick(e.target.value);
              }}
            />
            <Input
              value={password}
              type="password"
              placeholder="password"
              onChange={(e) => {
                setPassword(e.target.value);
              }}
            />
            <Button onClick={onClick}>회원가입</Button>
          </SignContainer>
        </Container>
      )}
    </>
  );
};

export default SignInput;

02 user login 추가하기

./src/reducers

//ACTION
export const SIGN_UP_REQUEST = 'SIGN_UP_REQUEST';
export const SIGN_UP_SUCCESS = 'SIGN_UP_SUCCESS';
export const SIGN_UP_FAILURE = 'SIGN_UP_FAILURE';

// 추가 Login ACTION
export const LOG_IN_REQUEST = 'LOG_IN_REQUEST';
export const LOG_IN_SUCCESS = 'LOG_IN_SUCCESS';
export const LOG_IN_FAILURE = 'LOG_IN_FAILURE';

// INITIAL
const INITIAL_STATE = {
  isLoggingIn: false,
  loginError: '',
  isSigningUp: false,
  signUpError: '',
  me: null,
};

// REDUCER

const reducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case SIGN_UP_REQUEST:
      return applySignUpRequest(state, action);
    case SIGN_UP_SUCCESS:
      return applySignUpSuccess(state, action);
    case SIGN_UP_FAILURE:
      return applySignUpFailure(state, action);
    case LOG_IN_REQUEST:
      return applyLoginRequest(state, action);
    case LOG_IN_SUCCESS:
      return applyLoginSuccess(state, action);
    case LOG_IN_FAILURE:
      return applyLoginFailure(state, action);
    default: {
      return state;
    }
  }
};

/* 중략 */

// 추가
const applyLoginRequest = (state, action) => {
  return {
    ...state,
    isLoggingIn: true,
    loginError: '',
    me: null,
  };
};

const applyLoginSuccess = (state, action) => {
  return {
    ...state,
    isLoggingIn: false,
    me: action.payload,
  };
};

const applyLoginFailure = (state, action) => {
  return {
    ...state,
    isLoggingIn: false,
    loginError: action.error,
    me: null,
  };
};

export default reducer;

./src/sagas/user.js

import { all, fork, takeLatest, call, put } from 'redux-saga/effects';
import axios from 'axios';

import {
  SIGN_UP_SUCCESS,
  SIGN_UP_FAILURE,
  SIGN_UP_REQUEST,
  LOG_IN_SUCCESS,
  LOG_IN_FAILURE,
  LOG_IN_REQUEST,
} from '../reducers/user';

/*중략*/

// 추가
function loginAPI(data) {
  return axios.post(`http://localhost:4000/auth/login`, data, {
    withCredentials: true,
  });
}

function* login(action) {
  try {
    const result = yield call(loginAPI, action.data);

    yield put({
      type: LOG_IN_SUCCESS,
      payload: result.data,
    });
  } catch (e) {
    yield put({
      type: LOG_IN_FAILURE,
      error: e,
    });
  }
}

function* watchLogin() {
  yield takeLatest(LOG_IN_REQUEST, login);
}

function* todoUserSaga() {
  yield all([fork(watchSign), fork(watchLogin)]);
}

export default todoUserSaga;

./src/components/LoginInput.js

import React, { useState, useEffect } from 'react';
import { Redirect } from 'react-router-dom';
import styled from 'styled-components';
import { useSelector, useDispatch } from 'react-redux';
import { LOG_IN_REQUEST } from '../reducers/user';

const Container = styled.div`
  position: relative;
  top: 120px;

  width: 100%;
  height: 100%;
`;

const SignContainer = styled.div`
  position: relative;
  margin: 0 auto;
  height: 100%;
  width: 300px;
  background-color: #ddd;
`;

const Input = styled.input`
  height: 2rem;
  width: 80%;
  font-size: 1.5rem;
`;

const Button = styled.button`
  height: 2rem;
  width: 100%;
`;

const LoginInput = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isRedirect, setIsRedirect] = useState(false);
  const { me, loginError } = useSelector((state) => state.user);
  const dispatch = useDispatch();

  useEffect(() => {
    if (me) {
      if (me.token) {
        setIsRedirect(true);
        // me.token 값이 들어오면 home으로 redirect 됩니다.
      }
    }
    if (loginError !== '') {
      alert('회원정보가 올바르지 않습니다.');
    }
  }, [me, loginError, dispatch]);

  const onClick = () => {
    dispatch({
      type: LOG_IN_REQUEST,
      data: {
        email,
        password,
      },
    });
  };

  return (
    <>
      {isRedirect ? (
        <Redirect
          to={{
            pathname: '/',
            state: { id: '123' },
          }}
        />
      ) : (
        <Container>
          <SignContainer>
            <Input
              value={email}
              placeholder="email"
              onChange={(e) => {
                setEmail(e.target.value);
              }}
            />
            <Input
              value={password}
              type="password"
              placeholder="password"
              onChange={(e) => {
                setPassword(e.target.value);
              }}
            />
            <Button onClick={onClick}>로그인</Button>
          </SignContainer>
        </Container>
      )}
    </>
  );
};

export default LoginInput;

./src/components/Header.js

import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import Cookies from 'js-cookie';
import { useSelector } from 'react-redux';

const Container = styled.div`
  position: absolute;
  display: flex;
  top: 0;
  left: 0;
  width: 100%;
  height: 50px;
  background-color: #ddd;
`;

const InnerContainer = styled.div`
  position: relative;
  top: 15px;
  display: flex;
  left: 25%;
  margin-right: 20px;
`;

const Header = () => {
  const [isToken, setIsToken] = useState(Boolean(Cookies.get('token')));
  const { me } = useSelector((state) => state.user);
  useEffect(() => {
    if (me) {
      if (me.token) {
        setIsToken(true);
        // me.token이 있을 때 setIsToken이 true가 됨
      }
    }
  }, [me]);
  return (
    <>
      <Container>
        <InnerContainer>
          <Link to="/"></Link>
        </InnerContainer>
        {isToken ? (
          <>
            <InnerContainer>
              <Link to="/board">Board</Link>
            </InnerContainer>
            <InnerContainer>
              <Link>
                <div>로그아웃</div>
              </Link>
            </InnerContainer>
          </>
        ) : (
          <>
            <InnerContainer>
              <Link to="/login">로그인</Link>
            </InnerContainer>
            <InnerContainer>
              <Link to="/sign">회원가입</Link>
            </InnerContainer>
          </>
        )}
      </Container>
    </>
  );
};

export default Header;
profile
정확하게 알고, 제대로 사용하자

0개의 댓글