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)
댓글 남기기