Redux 연동하기
버전이 다르기 때문에 소스가 강좌와 다를 수 있다.
버전
next: 13.0.4
antd: 5.0.1
소스 : https://github.dev/braverokmc79/node-bird-sns
25. redux-thunk 이해하기
강의 : https://www.inflearn.com/course/노드버드-리액트-리뉴얼/unit/48812?tab=curriculum
$ npm i redux-thunk
설치후
다음과 같이 미들웨어로 적용해 주면 끝이다.
여기서는 로그 미들워어 를 추가적으로 적용했다.
store/configureStore.js
import thunkMiddleware from 'redux-thunk';
const loggerMiddleware = ({ dispatch, getState }) => (next) => (action) => {
console.log("loggerMiddleware :", action);
return next(action);
}
사용방법은 다음과 같이 dispatch(loginRequestAction());
보내고 axios 로 백엔드와 통신후 성공시 loginSuccessAction(res.data) 와 loginFailureAction(err) 를 처리해 주면 된다.
reducer/user.js
export const initialState = {
isLoggedIn: false,
me: null,
signUpdata: {},
loginData: {}
}
export const loginAction = (data) => {
return (dispatch, action) => {
dispatch(loginRequestAction());
axios.post('/api/login')
.then((res) => {
dispatch(loginSuccessAction(res.data));
})
.catch((err) => {
dispatch(loginFailureAction(err));
})
}
// return {
// type: "LOG_IN",
// data
// }
}
export const loginRequestAction = (data) => {
return {
type: "LOG_IN_REQUEST",
data
}
}
export const loginSuccessAction = (data) => {
return {
type: "LOG_IN_SUCCESS",
data
}
}
export const loginFailureAction = (data) => {
return {
type: "LOG_IN_FAILURE",
data
}
}
export const logoutAction = () => {
return {
type: "LOG_OUT"
}
}
export const logoutRequestAction = () => {
return {
type: "LOG_OUT_REQUEST"
}
}
export const logoutSuccessAction = () => {
return {
type: "LOG_OUT_SUCCESS"
}
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'LOG_IN':
return {
...state,
isLoggedIn: true,
me: action.data
}
case 'LOG_OUT':
return {
...state,
isLoggedIn: false,
me: null
}
default:
return state;
}
}
export default reducer;
26. saga 설치하고 generator 이해하기
강의 :
store/configureStore.js
import { createWrapper } from 'next-redux-wrapper';
import { compose, applyMiddleware, legacy_createStore as createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import reducer from '../reducers';
import rootSaga from '../saga';
const loggerMiddleware = ({ dispatch, getState }) => (next) => (action) => {
console.log("loggerMiddleware : ", action);
return next(action);
};
const configureSotre = () => {
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware, loggerMiddleware];
const enhancer = process.env.NODE_ENV === 'production'
? compose(applyMiddleware(...middlewares))
: composeWithDevTools(applyMiddleware(...middlewares));
const store = createStore(reducer, enhancer);
store.sagaTask = sagaMiddleware.run(rootSaga);
return store;
};
const wrapper = createWrapper(configureSotre, {
debug: process.env.NODE_ENV === 'development,'
});
export default wrapper;
// import Reducer from './_reducers';
// import { applyMiddleware, legacy_createStore as createStore } from 'redux';
// import promiseMiddleware from 'redux-promise';
// import ReduxThunk from 'redux-thunk';
// const createStoreWithMiddleware = applyMiddleware(promiseMiddleware, ReduxThunk)(createStore);
// const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
// createStoreWithMiddleware(Reducer, devTools)
saga/index.js
export default function* rootSaga() {
}
generator
const gen =function* (){
console.log(1);
yield;
console.log(2);
yield;
console.log(3);
yield 4;
}
undefined
const generator=gen();
undefined
generator.next();
let i=0;
const gen=function* (){
while(true){
yield i++;
}
}
const g=gen();
g.next();
{value: 0, done: false}
g.next();
{value: 1, done: false}
27. saga 이펙트 알아보기
강의 :
store/configureStore.js
import { createWrapper } from 'next-redux-wrapper';
import { compose, applyMiddleware, legacy_createStore as createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import reducer from '../reducers';
import rootSaga from '../saga';
const loggerMiddleware = ({ dispatch, getState }) => (next) => (action) => {
console.log("loggerMiddleware : ", action);
return next(action);
};
const configureSotre = () => {
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware, loggerMiddleware];
const enhancer = process.env.NODE_ENV === 'production'
? compose(applyMiddleware(...middlewares))
: composeWithDevTools(applyMiddleware(...middlewares));
const store = createStore(reducer, enhancer);
store.sagaTask = sagaMiddleware.run(rootSaga);
return store;
};
const wrapper = createWrapper(configureSotre, {
debug: process.env.NODE_ENV === 'development,'
});
export default wrapper;
// import Reducer from './_reducers';
// import { applyMiddleware, legacy_createStore as createStore } from 'redux';
// import promiseMiddleware from 'redux-promise';
// import ReduxThunk from 'redux-thunk';
// const createStoreWithMiddleware = applyMiddleware(promiseMiddleware, ReduxThunk)(createStore);
// const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
// createStoreWithMiddleware(Reducer, devTools)
saga/index.js
import { all, fork, call, take, put, delay, debounce, throttle, takeLatest } from 'redux-saga/effects';
import axios from 'axios';
//1-1.로그인 처리
function logInAPI(data) {
return axios.post('/api/login', data);
}
// const l = login({ type: "LOG_IN_REQUEST", data: { id: 'test@gmail.com' } });
// l.next();
// l.next();
//1-2.로그인 처리
function* login(action) {
//put 을 dispatch
//call 은 동기 함수 호출
//fork 는 비동기 함수 호출
try {
const result = yield call(logInAPI, action.data);
yield put({
type: 'LOG_IN_SUCCESS',
data: result.data
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data
});
}
}
//1-3 로그인 처리
function* watchLogIn() {
//LOG_IN 실행 될때 까지 기다리겠다.
yield take('LOG_IN_REQUEST', login);
}
//2-1.로그아웃 처리
function logOutAPI() {
return axios.post('/api/logout');
}
//2-2.로그아웃 처리
function* logOut() {
//put 을 dispatch
//call 은 동기 함수 호출
//fork 는 비동기 함수 호출
try {
const result = yield call(logOutAPI);
yield put({
type: 'LOG_OUT_SUCCESS',
data: result.data
});
} catch (err) {
yield put({
type: "LOG_OUT_FAILURE",
data: err.response.data
});
}
}
//2-3 로그아웃 처리
function* watchLogOut() {
yield take('LOG_OUT_REQUEST', logOut);
}
//3-1.
function addPostAPI(data) {
return axios.post('/api/post', data);
}
//3-2.
function* addPost(action) {
try {
const result = yield call(addPostAPI, action.data);
yield put({
type: 'ADD_POST_SUCCESS',
data: result.data
});
} catch (err) {
yield put({
type: "ADD_POST_FAILURE",
data: err.response.data
});
}
}
//3-3
function* watchAddPost() {
yield take('ADD_POST_REQUEST', addPost);
}
//all 하면 한방에 배열로 적은 함수들이 실행처리 된다.
//fork , call 로 실행한다. all 은 fork 나 call 을 동시에 실행시키도록 한다.
export default function* rootSaga() {
yield all([
fork(watchLogIn),
fork(watchLogOut),
fork(watchAddPost)
]);
}
28. take, take 시리즈, throttle 알아보기
강의 :
//1-3 로그인 처리
function* watchLogIn() {
//LOG_IN 실행 될때 까지 기다리겠다.
yield take('LOG_IN_REQUEST', login);
}
yield take('LOG_IN_REQUEST', login); 은 한번 밖에 실행이 안된다.
따라서, whie 반복문 처리를 해야 하는데, 이것을 수행하는 것이 takeEvery 함수이다.
//2-3 로그아웃 처리
function* watchLogOut() {
while (true) {
yield takeEvery('LOG_OUT_REQUEST', logOut);
}
}
실수로 여러번 클릭하는 것 방지로 마지막것만 실행 takeLatest
//3-3
function* watchAddPost() {
yield takeLatest('ADD_POST_REQUEST', addPost);
}
실수로 여러번 클릭하는 것 방지로 첫번째것만 실행 takeLeading
//3-3
function* watchAddPost() {
yield takeLeading('ADD_POST_REQUEST', addPost);
}
throttle 은 10초 동안 한번만 전송
function* watchAddPost() {
yield throttle('ADD_POST_REQUEST', addPost, 10000);
}
saga/index.js
import { all, fork, call, take, put, takeEvery, takeLatest, takeLeading, throttle, delay } from 'redux-saga/effects';
import axios from 'axios';
//1-1.로그인 처리
function logInAPI(data) {
return axios.post('/api/login', data);
}
// const l = login({ type: "LOG_IN_REQUEST", data: { id: 'test@gmail.com' } });
// l.next();
// l.next();
//1-2.로그인 처리
function* login(action) {
//put 을 dispatch
//call 은 동기 함수 호출
//fork 는 비동기 함수 호출
try {
//const result = yield call(logInAPI, action.data);
yield delay(1000);
yield put({
type: 'LOG_IN_SUCCESS',
data: result.data
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data
});
}
}
//1-3 로그인 처리
function* watchLogIn() {
//LOG_IN 실행 될때 까지 기다리겠다.
yield takeLatest('LOG_IN_REQUEST', login);
}
//2-1.로그아웃 처리
function logOutAPI() {
return axios.post('/api/logout');
}
//2-2.로그아웃 처리
function* logOut() {
//put 을 dispatch
//call 은 동기 함수 호출
//fork 는 비동기 함수 호출
try {
//const result = yield call(logOutAPI);
yield delay(1000);
yield put({
type: 'LOG_OUT_SUCCESS',
// data: result.data
});
} catch (err) {
yield put({
type: "LOG_OUT_FAILURE",
data: err.response.data
});
}
}
//2-3 로그아웃 처리
function* watchLogOut() {
while (true) {
yield takeEvery('LOG_OUT_REQUEST', logOut);
}
}
//3-1.
function addPostAPI(data) {
return axios.post('/api/post', data);
}
//3-2.
function* addPost(action) {
try {
//const result = yield call(addPostAPI, action.data);
yield delay(1000);
yield put({
type: 'ADD_POST_SUCCESS',
data: result.data
});
} catch (err) {
yield put({
type: "ADD_POST_FAILURE",
data: err.response.data
});
}
}
//3-3
function* watchAddPost() {
yield throttle('ADD_POST_REQUEST', addPost, 2000);
}
//all 하면 한방에 배열로 적은 함수들이 실행처리 된다.
//fork , call 로 실행한다. all 은 fork 나 call 을 동시에 실행시키도록 한다.
export default function* rootSaga() {
yield all([
fork(watchLogIn),
fork(watchLogOut),
fork(watchAddPost)
]);
}
29. saga 쪼개고 reducer와 연결하기
saga/index.js
import { all, fork } from 'redux-saga/effects';
import postSaga from './post';
import userSaga from './user';
export default function* rootSaga() {
yield all([
fork(postSaga),
fork(userSaga),
]);
}
saga/user.js
import { all, fork, call, take, put, takeEvery, takeLatest, takeLeading, throttle, delay } from 'redux-saga/effects';
import axios from 'axios';
//1-1.로그인 처리
function logInAPI(data) {
return axios.post('/api/login', data);
}
// const l = login({ type: "LOG_IN_REQUEST", data: { id: 'test@gmail.com' } });
// l.next();
// l.next();
//1-2.로그인 처리
function* login(action) {
//put 을 dispatch
//call 은 동기 함수 호출
//fork 는 비동기 함수 호출
try {
console.log("2. 미들웨어로 사가 로그인 호출 : ", action);
//const result = yield call(logInAPI, action.data);
yield delay(1000);
yield put({
type: 'LOG_IN_SUCCESS',
data: action.data
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data
});
}
}
//1-3 로그인 처리
function* watchLogIn() {
//LOG_IN 실행 될때 까지 기다리겠다.
console.log("2. watchLogIn ");
yield takeLatest('LOG_IN_REQUEST', login);
}
//2-1.로그아웃 처리
function logOutAPI() {
return axios.post('/api/logout');
}
//2-2.로그아웃 처리
function* logOut() {
//put 을 dispatch
//call 은 동기 함수 호출
//fork 는 비동기 함수 호출
try {
//const result = yield call(logOutAPI);
yield delay(1000);
yield put({
type: 'LOG_OUT_SUCCESS',
// data: result.data
});
} catch (err) {
yield put({
type: "LOG_OUT_FAILURE",
data: err.response.data
});
}
}
//2-3 로그아웃 처리
function* watchLogOut() {
while (true) {
yield takeLatest('LOG_OUT_REQUEST', logOut);
}
}
//all 하면 한방에 배열로 적은 함수들이 실행처리 된다.
//fork , call 로 실행한다. all 은 fork 나 call 을 동시에 실행시키도록 한다.
//call 은 동기 함수 호출
//fork 는 비동기 함수 호출
export default function* userSaga() {
yield all([
fork(watchLogIn),
fork(watchLogOut)
])
}
saga/post.js
import { all, fork, put, throttle, delay, takeLatest } from 'redux-saga/effects';
import axios from 'axios';
//3-1.
function addPostAPI(data) {
return axios.post('/api/post', data);
}
//3-2.
function* addPost(action) {
try {
//const result = yield call(addPostAPI, action.data);
yield delay(1000);
yield put({
type: 'ADD_POST_SUCCESS',
data: action.data
});
} catch (err) {
yield put({
type: "ADD_POST_FAILURE",
data: err.response.data
});
}
}
//3-3
function* watchAddPost() {
//yield throttle('ADD_POST_REQUEST', addPost, 3000);
yield takeLatest('ADD_POST_REQUEST', addPost);
}
export default function* postSaga() {
yield all([
fork(watchAddPost)
]);
}
components/LoginForm.js
~
import { useDispatch, useSelector } from 'react-redux';
~
const LoginForm = () => {
const { isLoggingIn } = useSelector((state) => state.user);
~
const onSubmitForm = useCallback(() => {
console.log("1.로그인 onSubmitForm dispatch ");
dispatch(loginRequestAction({ id, password }));
}, [id, password]);
<ButtonWrapper >
<Button type="primary" htmlType='submit' loading={isLoggingIn} >로그인</Button>
<Link href="/signup" ><Button>회원가입</Button></Link>
</ButtonWrapper>
~
~
};
export default LoginForm;
실행처리
1) 로그인 버튼 클릭
<Button type="primary" htmlType='submit' loading={isLoggingIn} >로그인</Button>
2) loginRequestAction 호출
const { isLoggingIn } = useSelector((state) => state.user);
const onSubmitForm = useCallback(() => {
console.log("1.로그인 onSubmitForm dispatch ");
dispatch(loginRequestAction({ id, password }));
}, [id, password]);
3) reducer 와 saga 동시에 호출 한다.
로그인 LOG_IN_REQUEST 호출
~
//1-3 로그인 처리
function* watchLogIn() {
//LOG_IN 실행 될때 까지 기다리겠다.
console.log("2. watchLogIn ");
yield takeLatest('LOG_IN_REQUEST', login);
}
~
4) reducer
export const loginRequestAction = (data) => {
return {
type: "LOG_IN_REQUEST",
data
}
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'LOG_IN_REQUEST':
console.log("3. 리듀서 LOG_IN_REQUEST : ", action);
return {
...state,
isLoggingIn: true
}
case 'LOG_IN_SUCCESS':
console.log("4. 리듀서 LOG_IN_REQUEST : ", action);
return {
...state,
isLoggingIn: false,
isLoggedIn: true,
me: { ...action.data, nickname: 'macaronics' }
}
실행을 처리를 하면 리듀서가 사가보다 먼저 실행 처리 된다.
1.로그인 onSubmitForm dispatch
3. 리듀서 LOG_IN_REQUEST :
{type: 'LOG_IN_REQUEST', data: {…}}
2. 미들웨어로 사가 로그인 호출 :
{type: 'LOG_IN_REQUEST', data: {…}}
4. 리듀서 LOG_IN_REQUEST
5. 미들웨어로 사가 로그인 호출 : {
30. saga 쪼개고 reducer와 연결하기
강의 :

saga/index.js
import { all, fork } from 'redux-saga/effects';
import postSaga from './post';
import userSaga from './user';
export default function* rootSaga() {
yield all([
fork(postSaga),
fork(userSaga),
]);
}
31. 액션과 상태 정리하기
강의 :

reducers/index.js
import { HYDRATE } from 'next-redux-wrapper';
import { combineReducers } from 'redux';
import user from './user';
import post from './post';
//(이전상태,액션) => 다음 상태
const rootReducer = combineReducers({
index: (state = {}, action) => {
switch (action.type) {
case HYDRATE:
console.log(' HYDRATE ', action);
return { ...state, ...action.payload };
default:
return state;
}
},
user,
post
});
export default rootReducer;
reducers/post.js
const initialState = {
mainPosts: [
{
id: 1,
User: {
id: 1,
nickname: '마카로닉스'
},
content: '첫 번째 게시글 #해시태그 #익스프레스',
Images: [
{ src: "https://cdn.pixabay.com/photo/2022/12/06/00/25/beach-7637946_960_720.jpg" },
{ src: "https://cdn.pixabay.com/photo/2022/11/22/10/37/house-7609267_960_720.jpg" },
],
Comments: [{
User: {
nickname: 'nero',
},
content: "우와 개정판이 나왔군요.~"
},
{
User: {
nickname: 'hero',
},
content: "얼른 사고 싶어요"
},
]
},
{
id: 2,
User: {
id: 1,
nickname: '마카로닉스'
},
content: '첫 번째 게시글 #해시태그 #익스프레스',
Images: [
{ src: "https://cdn.pixabay.com/photo/2014/08/01/00/08/pier-407252_960_720.jpg" },
{ src: "https://cdn.pixabay.com/photo/2015/01/28/23/35/hills-615429_960_720.jpg" },
{ src: "https://cdn.pixabay.com/photo/2014/11/27/10/29/mountain-547363_960_720.jpg" }
],
Comments: [{
User: {
nickname: 'nero',
},
content: "우와 개정판이 나왔군요.~"
},
{
User: {
nickname: 'hero',
},
content: "얼른 사고 싶어요"
},
]
},
{
id: 3,
User: {
id: 1,
nickname: '마카로닉스'
},
content: '첫 번째 게시글 #해시태그 #익스프레스',
Images: [
{ src: "https://cdn.pixabay.com/photo/2022/12/06/00/25/beach-7637946_960_720.jpg" },
],
Comments: [{
User: {
nickname: 'nero',
},
content: "우와 개정판이 나왔군요.~"
},
{
User: {
nickname: 'hero',
},
content: "얼른 사고 싶어요"
},
]
},
],
imagePaths: [],
addPostLoading: false,
addPostDone: false,
addPostError: null,
addCommentLoading: false,
addCommentDone: false,
addCommentError: null
}
export const ADD_POST_REQUEST = 'ADD_POST_REQUEST';
export const ADD_POST_SUCCESS = 'ADD_POST_SUCCESS';
export const ADD_POST_FAILURE = 'ADD_POST_FAILURE';
export const ADD_COMMENT_REQUEST = 'ADD_COMMENT_REQUEST';
export const ADD_COMMENT_SUCCESS = 'ADD_COMMENT_SUCCESS';
export const ADD_COMMENT_FAILURE = 'ADD_COMMENT_FAILURE';
export const addPost = (data) => ({
type: ADD_POST_REQUEST,
data
});
export const addComment = (data) => ({
type: ADD_COMMENT_REQUEST,
data
});
const dummyPost = {
id: 2,
content: '더미데이터',
User: {
id: 1,
nickname: '마카로닉스'
},
Images: [],
Comments: []
}
const reducer = (state = initialState, action) => {
switch (action.type) {
//글작성
case ADD_POST_REQUEST:
return {
...state,
addPostLoading: true,
addPostDone: false,
addPostError: null
};
case ADD_POST_SUCCESS:
return {
...state,
mainPosts: [dummyPost, ...state.mainPosts],
addPostLoading: false,
addPostDone: true
}
case ADD_POST_FAILURE:
return {
...state,
addPostLoading: false,
addPostError: action.error
}
//댓글 작성
case ADD_COMMENT_REQUEST:
return {
...state,
addCommentLoading: true,
addCommentDone: false,
addCommentError: null
};
case ADD_COMMENT_SUCCESS:
return {
...state,
addCommentLoading: false,
addCommentDone: true
}
case ADD_COMMENT_FAILURE:
return {
...state,
addCommentLoading: false,
addCommentError: action.error
}
default:
return state;
}
}
export default reducer;
reducers/user.js
export const initialState = {
logInLoading: false,//로그인 시도중
logInDone: false,
logInError: null,
logOutLoading: false, //로그아웃 시도중
logOutDone: false,
logOutError: null,
signUpLoading: false, //회원가입 시도중
signUpDone: false,
signUpError: null,
me: null,
signUpdata: {},
loginData: {}
}
export const LOG_IN_REQUEST = "LOG_IN_REQUEST";
export const LOG_IN_SUCCESS = "LOG_IN_SUCCESS";
export const LOG_IN_FAILURE = "LOG_IN_FAILURE";
export const LOG_OUT_REQUEST = "LOG_OUT_REQUEST";
export const LOG_OUT_SUCCESS = "LOG_OUT_SUCCESS";
export const LOG_OUT_FAILURE = "LOG_OUT_FAILURE";
export const SIGN_UP_REQUEST = "SIGN_UP_REQUEST";
export const SIGN_UP_SUCCESS = "SIGN_UP_SUCCESS";
export const SIGN_UP_FAILURE = "SIGN_UP_FAILURE";
export const FOLLOW_REQUEST = "FOLLOW_REQUEST";
export const FOLLOW_SUCCESS = "FOLLOW_SUCCESS";
export const FOLLOW_FAILURE = "FOLLOW_FAILURE";
export const UNFOLLOW_REQUEST = "UNFOLLOW_REQUEST";
export const UNFOLLOW_SUCCESS = "UNFOLLOW_SUCCESS";
export const UNFOLLOW_FAILURE = "UNFOLLOW_FAILURE";
const dummyUser = (data) => ({
...data,
nickname: '마카로닉스',
id: 1,
Posts: [],
Followings: [],
Followers: []
});
export const loginRequestAction = (data) => {
return {
type: LOG_IN_REQUEST,
data
}
}
export const logoutRequestAction = () => {
return {
type: LOG_OUT_REQUEST
}
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case LOG_IN_REQUEST:
console.log("3. 리듀서 LOG_IN_REQUEST : ", action);
return {
...state,
logInLoading: true,
logInDone: false,
logInError: null
}
case LOG_IN_SUCCESS:
console.log("4. 리듀서 LOG_IN_REQUEST : ", action);
return {
...state,
logInLoading: false,
logInDone: true,
me: dummyUser(action.data)
}
case LOG_IN_FAILURE:
return {
...state,
logInLoading: false,
logInError: action.error,
}
//로그 아웃
case LOG_OUT_REQUEST:
return {
...state,
logOutLoading: true,
logOutDone: false,
logOutError: null,
}
case LOG_OUT_SUCCESS:
return {
...state,
logOutLoading: false,
logOutDone: true,
me: null
}
case LOG_OUT_FAILURE:
return {
...state,
logOutLoading: false,
logOutError: action.error
}
//회원가입
case SIGN_UP_REQUEST:
return {
...state,
signUpLoading: true,
signUpDone: false,
signUpError: null,
}
case SIGN_UP_SUCCESS:
return {
...state,
signUpLoading: false,
signUpDone: true,
}
case SIGN_UP_FAILURE:
return {
...state,
signUpLoading: false,
signUpError: action.error
}
default:
return state;
}
}
export default reducer;
saga/index.js
import { all, fork } from 'redux-saga/effects';
import postSaga from './post';
import userSaga from './user';
export default function* rootSaga() {
yield all([
fork(postSaga),
fork(userSaga),
]);
}
saga/post.js
import { all, fork, put, throttle, delay, takeLatest } from 'redux-saga/effects';
import axios from 'axios';
import {
ADD_POST_REQUEST, ADD_POST_SUCCESS, ADD_POST_FAILURE,
ADD_COMMENT_REQUEST, ADD_COMMENT_SUCCESS, ADD_COMMENT_FAILURE
} from '../reducers/post'
//3-1.
function addPostAPI(data) {
return axios.post('/api/post', data);
}
//3-2.
function* addPost(action) {
try {
//const result = yield call(addPostAPI, action.data);
yield delay(1000);
yield put({
type: ADD_POST_SUCCESS,
data: action.data
});
} catch (err) {
yield put({
type: ADD_POST_FAILURE,
error: err.response.data
});
}
}
//3-3
function* watchAddPost() {
//yield throttle('ADD_POST_REQUEST', addPost, 3000);
yield takeLatest(ADD_POST_REQUEST, addPost);
}
//댓글
function addCommentAPI(data) {
return axios.Comment('/api/comment', data);
}
function* addComment(action) {
try {
//const result =yield call(addCommentAPI, action.data);
yield delay(1000);
yield put({
type: ADD_COMMENT_SUCCESS,
data: action.data
});
} catch (err) {
yield put({
type: ADD_COMMENT_FAILURE,
error: err.response.data
});
}
}
function* watchAddComment() {
yield takeLatest(ADD_COMMENT_REQUEST, addComment);
}
export default function* postSaga() {
yield all([
fork(watchAddPost),
fork(watchAddComment)
]);
}
saga/user.js
import { all, fork, put, takeLatest, delay } from 'redux-saga/effects';
import axios from 'axios';
import {
LOG_IN_REQUEST, LOG_IN_SUCCESS, LOG_IN_FAILURE,
LOG_OUT_REQUEST, LOG_OUT_SUCCESS, LOG_OUT_FAILURE,
SIGN_UP_REQUEST, SIGN_UP_SUCCESS, SIGN_UP_FAILURE,
} from '../reducers/user';
//1-1.로그인 처리
function logInAPI(data) {
return axios.post('/api/login', data);
}
// const l = login({ type: "LOG_IN_REQUEST", data: { id: 'test@gmail.com' } });
// l.next();
// l.next();
//1-2.로그인 처리
function* login(action) {
//put 을 dispatch
//call 은 동기 함수 호출
//fork 는 비동기 함수 호출
try {
console.log("2. 미들웨어로 사가 로그인 호출 : ", action);
//const result = yield call(logInAPI, action.data);
yield delay(1000);
yield put({
type: LOG_IN_SUCCESS,
data: action.data
});
console.log("5. 미들웨어로 사가 로그인 호출 : ", action);
} catch (err) {
yield put({
type: LOG_IN_FAILURE,
error: err.response.data
});
}
}
//1-3 로그인 처리
function* watchLogIn() {
//LOG_IN 실행 될때 까지 기다리겠다.
console.log("2. watchLogIn ");
yield takeLatest(LOG_IN_REQUEST, login);
}
//2-1.로그아웃 처리
function logOutAPI() {
return axios.post('/api/logout');
}
//2-2.로그아웃 처리
function* logOut() {
//put 을 dispatch
//call 은 동기 함수 호출
//fork 는 비동기 함수 호출
try {
//const result = yield call(logOutAPI);
yield delay(1000);
yield put({
type: LOG_OUT_SUCCESS,
// data: result.data
});
} catch (err) {
yield put({
type: LOG_OUT_FAILURE,
error: err.response.data
});
}
}
//2-3 로그아웃 처리
function* watchLogOut() {
while (true) {
yield takeLatest(LOG_OUT_REQUEST, logOut);
}
}
//3. 회원가입
function signUpAPI() {
return axios.post('/api/signup');
}
function* signUp() {
try {
//const result = yield call(signUpAPI);
yield delay(1000);
//throw new Error('');
yield put({
type: SIGN_UP_SUCCESS,
// data: result.data
});
} catch (err) {
yield put({
type: SIGN_UP_FAILURE,
error: err.response.data
});
}
}
function* watchSignUp() {
while (true) {
yield takeLatest(SIGN_UP_REQUEST, signUp);
}
}
//all 하면 한방에 배열로 적은 함수들이 실행처리 된다.
//fork , call 로 실행한다. all 은 fork 나 call 을 동시에 실행시키도록 한다.
//call 은 동기 함수 호출
//fork 는 비동기 함수 호출
export default function* userSaga() {
yield all([
fork(watchLogIn),
fork(watchLogOut)
])
}
32. 바뀐 상태 적용하고 eslint 점검하기
강의 :
components/AppLayout
~
const AppLayout = ({ children }) => {
const { me } = useSelector((state) => state.user);
~
{me ? <UserProfile /> : <LoginForm />}
components/CommentForm.js
import { Button, Form, Input } from 'antd';
import React, { useCallback, useEffect } from 'react';
import useInput from '../hooks/useInput';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { ADD_COMMENT_REQUEST } from '../reducers/post';
const CommentForm = ({ post }) => {
const dispatch = useDispatch();
const id = useSelector((state) => state.user.me?.id);
const { addCommentDone } = useSelector((state) => state.post);
const [commentText, onChangeCommentText, setCommentText] = useInput('');
useEffect(() => {
if (addCommentDone) {
setCommentText('');
}
}, [addCommentDone]);
const onSubmitComment = useCallback(() => {
console.log(post.id, commentText);
dispatch({
type: ADD_COMMENT_REQUEST,
data: {
content: commentText,
postId: post.id,
userId: id
}
})
}, [commentText, id]);
return (
<Form onFinish={onSubmitComment}>
<Input.TextArea value={commentText} onChange={onChangeCommentText} rows={4} />
<Button type="primary" htmlType='submit' style={{ marginTop: 10 }}>삐악</Button>
</Form>
);
};
CommentForm.propType = {
post: PropTypes.object.isRequired
}
export default CommentForm;
components/LoginForm,js
~
const LoginForm = () => {
const dispatch = useDispatch();
const { logInLoading } = useSelector((state) => state.user);
const [email, onChangeEmail] = useInput('');
const [password, onChangePassword] = useInput('');
const onSubmitForm = useCallback(() => {
console.log(email, password);
dispatch(loginRequestAction({ email, password }));
}, [email, password]);
~
components/PostForm.js
import React, { useCallback, useEffect, useRef } from 'react';
import { Form, Input, Button } from 'antd';
import { useSelector, useDispatch } from 'react-redux';
import { addPost } from '../reducers/post';
import useInput from '../hooks/useInput';
const PostForm = () => {
const { imagePaths, addPostDone } = useSelector((state) => state.post);
const dispatch = useDispatch();
const imageInput = useRef();
const [text, onChangeText, setText] = useInput('');
useEffect(() => {
if (addPostDone) {
setText('');
}
}, [addPostDone])
const onSubmit = useCallback(() => {
dispatch(addPost(text));
setText("");
}, [text]);
~
components/UserProfile.js
import React, { useCallback } from 'react';
import { Card, Avatar, Button } from 'antd';
import { useDispatch, useSelector } from 'react-redux';
import { logoutRequestAction } from './../reducers/user';
const UserProfile = () => {
const dispatch = useDispatch();
const { me, logOutLoading } = useSelector((state) => state.user);
const onLogOut = useCallback(() => {
dispatch(logoutRequestAction(false));
}, []);
return (
<Card
actions={[
<div key="twit">짹짹<br />{me.Posts.length}</div>,
<div key="followings">팔로잉<br />{me.Followings.length}</div>,
<div key="follower">팔로워<br />{me.Followers.length}</div>,
]}
>
~
hook/useinput.js
import { useCallback, useState } from 'react';
export default (initialValue = null) => {
const [value, setValue] = useState(initialValue);
const handler = useCallback((e) => {
setValue(e.target.value);
}, []);
return [value, handler, setValue];
}
pages/Profile.js
~
import { useSelector } from 'react-redux';
import FollowList from './../components/FollowList';
import NicknameEditForm from './../components/NicknameEditForm';
const Profile = () => {
const { me } = useSelector((state) => state.user);
~
pages/SignUp.js
~
import { SIGN_UP_REQUEST } from './../reducers/user';
import { useDispatch, useSelector } from 'react-redux';
const ErroMessage = styled.div`
color:red;
`;
const SignUp = () => {
const dispatch = useDispatch();
const { signUpLoading } = useSelector((state) => state.user);
const [email, onChangeEmail] = useInput('');
~
eslint 추가
npm i -D babel-eslint eslint-config-airbnb eslint-plugin-import npm i -D eslint-plugin-react-hooks npm i -D eslint-plugin-jsx-a11y
.eslintrc
{
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"env":{
"browser":true,
"node":true,
"es6":true
},
"extends":[
"airbnb",
],
"plugins": [
"import",
"react-hooks"
],
"rules": {
"jsx-a11y/label-has-associated-control": "off",
"jsx-a11y/anchor-is-valid": "off",
"no-console": "off",
"no-underscore-dangle": "off",
"react/forbid-prop-types": "off",
"react/jsx-filename-extension": "off",
"react/jsx-one-expression-per-line": "off",
"object-curly-newline": "off",
"linebreak-style": "off"
// "arrow-body-style": "off",
// "comma-dangle": "off",
// "consistent-return": "off",
// "operator-linebreak": "off"
}
}
33. 게시글, 댓글 saga 작성하기
강의 :
components/user.js
~
export const CHANGE_NICKNAME_REQUEST = "CHANGE_NICKNAME_REQUEST";
export const CHANGE_NICKNAME_SUCCESS = "CHANGE_NICKNAME_SUCCESS";
export const CHANGE_NICKNAME_FAILURE = "CHANGE_NICKNAME_FAILURE";
~
//닉네임변경
case CHANGE_NICKNAME_REQUEST:
return {
...state,
changeNicknameLoading: true,
changeNicknameDone: false,
changeNicknameError: null,
}
case CHANGE_NICKNAME_SUCCESS:
return {
...state,
changeNicknameLoading: false,
changeNicknameDone: true,
}
case CHANGE_NICKNAME_FAILURE:
return {
...state,
changeNicknameLoading: false,
changeNicknameError: action.error
}
~
components/post.js
~
const dummyComment = (data) => ({
id: shortId.generate(),
content: data,
User: {
id: 1,
nickname: '마카로닉스'
}
});
const reducer = (state = initialState, action) => {
switch (action.type) {
~
case ADD_COMMENT_SUCCESS: {
const postIndex = state.mainPosts.findIndex((v) => v.id === action.data.postId);
const post = state.mainPosts[postIndex];
post.Comments = [dummyComment(action.data.content), ...post.Comments];
const mainPosts = [...state.mainPosts];
mainPosts[postIndex] = post;
return {
...state,
mainPosts,
addCommentLoading: false,
addCommentDone: true
}
}
~
34. 게시글 삭제 saga 작성하기
강의 :
reducer/users.js
~
export const ADD_POST_TO_ME = 'ADD_POST_TO_ME';
export const REMOVE_POST_OF_ME = 'REMOVE_POST_OF_ME';
const dummyUser = (data) => ({
...data,
nickname: '마카로닉스',
id: 1,
Posts: [{ id: 1 }],
Followings: [{ nickname: '부기초' }, { nickname: 'Chanho Lee' }, { nickname: 'neue zeal' }],
Followers: [{ nickname: '부기초' }, { nickname: 'Chanho Lee' }, { nickname: 'neue zeal' }]
});
~
//게시글 수 숫자 증가
case ADD_POST_TO_ME:
return {
...state,
me: {
...state.me,
Posts: [{ id: action.data }, ...state.me.Posts]
}
};
//게시글 수 숫자 감소
case REMOVE_POST_OF_ME:
return {
...state,
me: {
...state.me,
Posts: state.me.Posts.filter((v) => v.id !== action.data),
}
}
~
saga/post.js
~
import { ADD_POST_TO_ME, REMOVE_POST_OF_ME } from '../reducers/user';
//게시글 삭제
function removePostAPI(data) {
return axios.post('/api/removepost', data);
}
function* removePost(action) {
try {
yield delay(1000);
yield put({
type: REMOVE_POST_SUCCESS,
data: action.data
});
yield put({
type: REMOVE_POST_OF_ME,
data: action.data
})
} catch (err) {
yield put({
type: REMOVE_POST_FAILURE,
error: err.response.data
});
}
}
function* watchRemovePost() {
yield takeLatest(REMOVE_POST_REQUEST, removePost);
}
~
export default function* postSaga() {
yield all([
fork(watchAddPost),
fork(watchAddComment),
fork(watchRemovePost)
]);
}
component/PostForm.js
const { imagePaths, addPostDone, addPostLoading } = useSelector((state) => state.post);
~
<Button type="primary" htmlType='submit' style={{ float: 'right' }} loading={addPostLoading} >글작성</Button>














댓글 ( 4)
댓글 남기기