React

 

 

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 이해하기

강의 :

https://www.inflearn.com/course/%EB%85%B8%EB%93%9C%EB%B2%84%EB%93%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC/unit/48813?tab=curriculum

 

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 이펙트 알아보기

강의 :

https://www.inflearn.com/course/%EB%85%B8%EB%93%9C%EB%B2%84%EB%93%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC/unit/48814?tab=curriculum

 

 

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 알아보기

강의 :

https://www.inflearn.com/course/%EB%85%B8%EB%93%9C%EB%B2%84%EB%93%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC/unit/48815?tab=curriculum

 

 

//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와 연결하기

 

강의 : https://www.inflearn.com/course/%EB%85%B8%EB%93%9C%EB%B2%84%EB%93%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC/unit/48816?tab=curriculum

 

 

 

 

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와 연결하기

 

강의 :

https://www.inflearn.com/course/%EB%85%B8%EB%93%9C%EB%B2%84%EB%93%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC/unit/48816?tab=curriculum

 

 

 

 

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. 액션과 상태 정리하기

 강의 :

https://www.inflearn.com/course/%EB%85%B8%EB%93%9C%EB%B2%84%EB%93%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC/unit/48817?tab=curriculum

 

 

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 점검하기

 

강의 :

https://www.inflearn.com/course/%EB%85%B8%EB%93%9C%EB%B2%84%EB%93%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC/unit/48818?tab=curriculum

 

 

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 작성하기

강의 :

https://www.inflearn.com/course/%EB%85%B8%EB%93%9C%EB%B2%84%EB%93%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC/unit/48819?tab=curriculum

 

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 작성하기

 

강의 :

https://www.inflearn.com/course/%EB%85%B8%EB%93%9C%EB%B2%84%EB%93%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC/unit/48820?tab=curriculum

 

 

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>

 

 

 

 

 

 

 

 

react

 

about author

PHRASE

Level 60  라이트

지속적인 자기 발전에 전념하라. 상대방의 눈을 보라

댓글 ( 4)

댓글 남기기

작성