이번 페이지에서는 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 /* 확장프로그램 입니다. */
./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'),
);
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;
./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;