As i wish

[React SNS] React SNS 만들기 - 4 - 2 (redux saga 적용) 본문

React JS

[React SNS] React SNS 만들기 - 4 - 2 (redux saga 적용)

어면태 2019. 8. 12. 12:54

안녕하세요. 엄티입니다.

계속해서 React 를 사용해서 SNS 를 만들어 보겠습니다.

 

프로젝트 소개와 프로젝트 구성 요소는 제 전 포스팅을 참고해주세요.

React SNS 만들기 - 1

 

[React SNS] React SNS 만들기 - 1

안녕하세요. 엄티입니다. 오늘은 React 를 사용하여 간단한 SNS 를 만들어 볼껀데요. 제 포스팅은 '제로초' 님의 강의를 바탕으로 복습용으로 작성됩니다. 자세한 설명을 원하면 밑에 강의를 참조해 주세요. '제로..

eomtttttt-develop.tistory.com

참고로 제 포스팅은 '제로초' 님의 SNS 만들기 강좌 복습 포스팅입니다.

'제로초' 님의 React NodeBird SNS

 

React로 NodeBird SNS 만들기 - 인프런

리액트&넥스트&리덕스&리덕스사가&익스프레스 스택으로 트위터와 유사한 SNS 서비스를 만들어봅니다. 끝으로 검색엔진 최적화 후 AWS에 배포합니다. 중급 웹 개발 프레임워크 및 라이브러리 서비스 개발 Front End Back End Javascript React 온라인 강의

www.inflearn.com

 

앞서 포스팅에 이어서 redux-saga 를 적용해보겠습니다.

 

redux-saga 란?

 

[React SNS] React SNS 만들기 - 4 - 1 (redux saga 란, airbnbStyle applied)

안녕하세요. 엄티입니다. 계속해서 React 를 사용해서 SNS 를 만들어 보겠습니다. 프로젝트 소개와 프로젝트 구성 요소는 제 전 포스팅을 참고해주세요. React SNS 만들기 - 1 [React SNS] React SNS 만들기 - 1..

eomtttttt-develop.tistory.com

 

먼저 saga 를 프로젝트에 적용해보겠습니다.

미들웨어에 적용해주면 되는데, 일전에 chrome에서 redux 를 디버깅 하기 위해 미들웨어를 적용한 적이 있습니다.

그와 비슷합니다.

 

$ npm i redux-saga axios

redux-saga 를 사용하기 위해 모듈을 설치해주고요 axios 는 서버와 통신 (REST API 사용)을 하기 위함입니다.

 

그 다음 pages/_app.js

import React from 'react';
import Head from 'next/head';
import PropTypes from 'prop-types';
import withRedux from 'next-redux-wrapper'; // With redux in next
import { applyMiddleware, compose, createStore } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { Provider } from 'react-redux';

import reducers from '../reducers';
import rootSaga from '../sagas';
import AppLayout from '../components/AppLayout';

const NodeBird = ({ Component, store }) => {
  // Using Provider link react with redux
  return (
    <Provider store={store}>
      <Head>
        <title>NodeBird</title>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/antd/3.20.5/antd.css"/>
      </Head>
      <AppLayout>
        <Component />
      </AppLayout>
    </Provider>
  );
};

NodeBird.propTypes = {
  Component: PropTypes.elementType.isRequired,
  store: PropTypes.object.isRequired,
};

const configureStore = (initialState, options) => {
  const sagaMiddleware = createSagaMiddleware();
  const middlewares = [sagaMiddleware];
  const enhancer = process.env.NODE_ENV === 'production'
    ? compose(applyMiddleware(...middlewares))
    : compose(
      applyMiddleware(...middlewares),
      !options.isServer && typeof window.__REDUX_DEVTOOLS_EXTENSION__ !== 'undefined' ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f,
    );
  const store = createStore(reducers, initialState, enhancer);
  sagaMiddleware.run(rootSaga);
  return store;
};

export default withRedux(configureStore)(NodeBird);

이런식으로 store 에 middleware 를 적용해 줍니다.

그 다음 saga 를 만들어야 겠죠?

 

위와 같이 sagas 라는 폴더를 만들고 파일을 생성해줍니다.

reducers 랑 비슷합니다. index 를 만들고 그 안에 post, user 에 관련된 saga 들을 생성해줍니다.

 

sagas/index.js

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

import user from './user';
import post from './post';

export default function* rootSaga() {
  yield all([
    fork(user),
    fork(post),
  ]);
}

sagas/user.js

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

import axios from 'axios';
import * as actions from '../reducers/user';

function loginApi() {
  return axios.post('/login');
}

function* login({ payload }) {
  try {
    const { loginData } = payload;
    console.log('Login data', loginData);
    // yield call(loginApi);
    yield delay(2000);
    yield put({
      type: actions.LOG_IN_SUCCESS,
    });
  } catch (error) {
    console.error('Login error. ', error);
    yield put({
      type: actions.LOG_IN_FAILURE,
      error,
    });
  }
}

function logoutApi() {
  return axios.post('/logout');
}

function* logout() {
  try {
    // yield call(logoutApi);
    yield delay(2000);
    yield put({
      type: actions.LOG_OUT_SUCCESS,
    });
  } catch (error) {
    console.error('Logout error. ', error);
    yield put({
      type: actions.LOG_OUT_FAILURE,
      payload: {
        error,
      },
    });
  }
}

function signUpApi() {
  return axios.post('/signup');
}

function* signUp({ payload }) {
  try {
    const { signUpData } = payload;
    console.log('Sign up data', signUpData);
    // yield call(signUpApi);
    yield delay(2000);
    yield put({
      type: actions.SIGN_UP_SUCCESS,
    });
  } catch (error) {
    console.error(error);
    yield put({
      type: actions.SIGN_UP_FAILURE,
      error,
    });
  }
}

function* watchLogin() {
  yield takeEvery(actions.LOG_IN_REQUEST, login);
  yield takeEvery(actions.LOG_OUT_REQUEST, logout);
}

function* watchSignUp() {
  yield takeEvery(actions.SIGN_UP_REQUEST, signUp);
}

export default function* userSaga() {
  yield all([
    fork(watchLogin),
    fork(watchSignUp),
  ]);
}

 

먼저 로그인 및 회원가입 로직에 필요한 사가들을 만들었는데요. 로그인만 먼저 보시겠습니다.

왜냐하면 비동기 처리하는 로직은 다 비슷비슷하기 때문이죠. userSaga 가 all 를 이용하여 watchLogin 을 등록하고 watchLogin 이 takeEvery 를 이용하여 action 을 dispatch 합니다. LOGIN_IN_REQUEST action 이 dispatch 되면 login 이라는 함수를 실행하겠죠?

login은 payload 를 통하여 action 을 dispatch 할 때에 parameter 를 받습니다. 

그 후 delay 를 주죠. 이는 원래를 call 을 이용하여 loginApi 를 비동기로 부르는 부분을 간이적으로 구현한거죠.

그 다음 성공/실패 여부에 따라 action 을 dispatch 해주게 됩니다.

 

어렵지 않죠...?

 

이에 따라 바뀐 reducers 에 모습도 확인해 보겠습니다.

 

reducers/user.js

const mockUser = {
  nickname: 'MockUserRedux',
  Post: [],
  Followings: [],
  Followers: [],
  id: 1,
};

export const initialState = {
  isLoggedIn: false, // 로그인 여부
  isLoggingOut: false, // 로그아웃 시도중
  isLoggingIn: false, // 로그인 시도중
  logInErrorReason: '', // 로그인 실패 사유
  isSignedUp: false, // 회원가입 성공
  isSigningUp: false, // 회원가입 시도중
  signUpErrorReason: '', // 회원가입 실패 사유
  me: null, // 내 정보
  followingList: [], // 팔로잉 리스트
  followerList: [], // 팔로워 리스트
  userInfo: null, // 남의 정보
};

// Action types
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 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';

// Actions creators
export const signUpRequestAction = data => ({
  type: SIGN_UP_REQUEST,
  payload: {
    signUpData: data,
  },
});

export const loginRequestAction = data => ({
  type: LOG_IN_REQUEST,
  payload: {
    loginData: data,
  },
});

export const logoutRequestAction = () => ({
  type: LOG_OUT_REQUEST,
});

// reducers
export default (state = initialState, action) => {
  const { type, payload, error } = action;

  switch (type) {
    case SIGN_UP_REQUEST: {
      return {
        ...state,
        isSigningUp: true,
        isSignedUp: false,
        signUpErrorReason: '',
      };
    }
    case SIGN_UP_SUCCESS: {
      return {
        ...state,
        isSigningUp: false,
        isSignedUp: true,
      };
    }
    case SIGN_UP_FAILURE: {
      return {
        ...state,
        isSigningUp: false,
        signUpErrorReason: error,
      };
    }
    case LOG_IN_REQUEST:
      return {
        ...state,
        isLoggingIn: true,
        logInErrorReason: '',
      };
    case LOG_IN_SUCCESS:
      return {
        ...state,
        isLoggingIn: false,
        isLoggedIn: true,
        me: mockUser,
        isLoading: false,
      };
    case LOG_IN_FAILURE:
      return {
        ...state,
        isLoggingIn: false,
        isLoggedIn: false,
        logInErrorReason: error,
        me: null,
      };
    case LOG_OUT_REQUEST:
      return {
        ...state,
        isLoggingOut: true,
      };
    case LOG_OUT_SUCCESS:
      return {
        ...state,
        isLoggedIn: false,
        isLoggingOut: false,
        me: {},
      };
    case LOG_OUT_FAILURE:
      return {
        ...state,
        isLoggingOut: false,
      };
    default:
      return state;
  }
};

비동기 action 들은 다음과 같이 ACTION_REQUEST, ACTION_SUCCESS, ACTION_FAILURE 로 적어주면 구분하기 편하겠죠? 이것은 자유 입니다. 여기서 한가지 주의할 점이 있는데 reducers 에도 LOG_IN_REQUEST 가 있다는 점이죠. 

즉, reducer와 saga 는 독립적으로 존재한다는 사실을 알 수 있는데요. 실제로 LOG_IN_REQUEST action 을 dispatch 해도 saga에서도 reducer에서도 둘다 잡히는것을 볼 수 있습니다.

이를 통하여 reducer 는 reducer대로 saga는 saga 대로 자신의 일을 처리할 수가 있죠.

 

components/LoginForm.js

import React, { useState, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Link from 'next/link';
import { Form, Input, Button } from 'antd';

import { loginRequestAction } from '../reducers/user';

// Custom hook
export const useInput = (initValue = null) => {
  const [value, setter] = useState(initValue);
  const handler = useCallback((e) => {
    setter(e.target.value);
  }, []);

  return [value, handler];
};

const LoginForm = () => {
  const dispatch = useDispatch();
  const { isLoggingIn } = useSelector(state => state.user);

  const [id, onChangeId] = useInput('');
  const [pass, onChangePass] = useInput('');

  const onSubmitForm = useCallback((e) => {
    e.preventDefault();
    dispatch(loginRequestAction({
      id,
      pass,
    }));
  }, [id, pass]);

  return (
    <>
      <Form onSubmit={onSubmitForm} style={{ padding: '10px' }}>
        <div>
          <label htmlFor="user-id">ID</label>
          <br />
          <Input name="user-id" required value={id} onChange={onChangeId} />
        </div>
        <div>
          <label htmlFor="user-pass">Password</label>
          <br />
          <Input name="user-pass" type="password" required value={pass} onChange={onChangePass} />
        </div>
        <div style={{ marginTop: '10px' }}>
          <Button type="primary" htmlType="submit" loading={isLoggingIn}>LogIn</Button>
          <Link href="/signup"><Button>SignUp</Button></Link>
        </div>
      </Form>
    </>
  );
};

export default LoginForm;

loginForm 도 변경해야하는데요 위처럼 reducers 에서 action creator 를 가져온 뒤 이를 dispatch 만 해주면 됩니다.

그럼 action creator 에 정의한 대로 saga 는 saga 대로 reducer 는 reducer 대로 각자의 일을 하게 됩니다.

 

위 프로젝트 같은 경우는 login 을 눌렀을 때(LOG_IN_REQUEST 를 dispatch 하였을 때) 에 reducer 는 isLoggingIn 을 true 로 만들어줘서 버튼에 로딩 효과를 주고 saga는 실제 서버에 요청을 하고 요청에 응답에 따라 action 을 dispatch 하게끔 구현하였습니다.

 

이와 마찬가지로 logout, signup 다 비슷하게 구현 됩니다.

 

components/UserProfile.js

import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Card, Avatar, Button } from 'antd';
import { logoutRequestAction } from '../reducers/user';

const UserProfile = () => {
  const dispatch = useDispatch();
  const { me, isLoggingOut } = useSelector(state => state.user);

  const onLogout = useCallback(() => {
    dispatch(logoutRequestAction());
  }, []);

  return (
    <Card
      actions={[
        <div key="twit">
          짹짹
          <br />
          {me.Post.length}
        </div>,
        <div key="following">
          팔로잉
          <br />
          {me.Followings.length}
        </div>,
        <div key="follower">
          팔로워
          <br />
          {me.Followers.length}
        </div>,
      ]}
    >
      <Card.Meta
        avatar={<Avatar>{me.nickname[0]}</Avatar>}
        title={me.nickname}
      />
      <Button onClick={onLogout} loading={isLoggingOut}>LogOut</Button>
    </Card>
  );
};

export default UserProfile;

 

pages/signup.js

import React, { useState, useEffect, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Form, Input, Checkbox, Button } from 'antd';
import Router from 'next/router';
import { signUpRequestAction } from '../reducers/user';

import { useInput } from '../components/LoginForm';

const Signup = () => {
  const dispatch = useDispatch();
  const { isSigningUp, me } = useSelector(state => state.user);
  // const [id, setId] = useState(''); // Using custom hook in bottom
  // const [nick, setNick] = useState(''); // Using custom hook in bottom

  const [id, onChangeId] = useInput('');
  const [nick, onChangedNick] = useInput('');

  const [pass, setPass] = useState('');
  const [passChk, setPassChk] = useState('');
  const [term, setTerm] = useState('');

  const [passError, setPassError] = useState(false);
  const [termError, setTermError] = useState(false);

  useEffect(() => {
    if (me) {
      alert('Move to mainpage because logged in.');
      Router.push('/');
    }
  }, [me && me.id]);

  const onSubmit = useCallback((e) => {
    e.preventDefault();

    if (pass !== passChk) {
      return setPassError(true);
    }

    if (!term) {
      return setTermError(true);
    }

    return dispatch(signUpRequestAction({
      id,
      pass,
      nick,
    }));
  }, [pass, passChk, term]);

  // const onChangeId = (e) => { // Using custom hook in bottom
  //   setId(e.target.value);
  // };

  // const onChangedNick = (e) => {
  //   setNick(e.target.value);
  // };

  const onChangePass = useCallback((e) => {
    setPass(e.target.value);
  }, []);

  const onChangePassChk = useCallback((e) => {
    setPassError(e.target.value !== pass);
    setPassChk(e.target.value);
  }, [pass]);

  const onChangeTerm = (e) => {
    setTermError(false);
    setTerm(e.target.checked);
  };

  return (
    <>
      <Form onSubmit={onSubmit} style={{ padding: 10 }}>
        <div>
          <label htmlFor="user-id">ID</label>
          <br />
          <Input name="user-id" required value={id} onChange={onChangeId} />
        </div>
        <div>
          <label htmlFor="user-nick">Nickname</label>
          <br />
          <Input name="user-nick" required value={nick} onChange={onChangedNick} />
        </div>
        <div>
          <label htmlFor="user-pass">Password</label>
          <br />
          <Input name="user-pass" type="password" required value={pass} onChange={onChangePass} />
        </div>
        <div>
          <label htmlFor="user-pass-chk">Password Check</label>
          <br />
          <Input name="user-pass-chk" type="password" required value={passChk} onChange={onChangePassChk} />
          {passError && <div style={{ color: 'red' }}>Please check password</div>}
        </div>
        <div>
          <Checkbox name="user-term" value={term} onChange={onChangeTerm}>
            Are you agree this term?
          </Checkbox>
          {termError && <div style={{ color: 'red' }}>You must agrre term</div>}
        </div>
        <div>
          <Button type="primary" htmlType="submit" loading={isSigningUp}>Register</Button>
        </div>
      </Form>
    </>
  );
};

export default Signup;

 

Add login, logout, signup Redux-saga

 

[4-10, 11, 12] Add login and register redux saga · eomttt/react-sns@59a80ac

Permalink Browse files [4-10, 11, 12] Add login and register redux saga Loading branch information... Showing 11 changed files with 261 additions and 94 deletions. +1 −1 front/components/AppLayout.js +10 −9 front/components/LoginForm.js +7 −9 front/compone

github.com

두 번째로 post 부분에 redux-saga를 적용해볼건데요. user 부분과 별다른게 없습니다.

 

sagas/post.js

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

import axios from 'axios';
import * as actions from '../reducers/post';

function addPostApi() {
  return axios.post('/postcard');
}

function* addPost({ payload }) {
  try {
    const { addPostData } = payload;
    console.log('Add post data. ', addPostData);
    // yield call(addPostApi);
    yield delay(2000);
    yield put({
      type: actions.ADD_POST_SUCCESS,
    });
  } catch (error) {
    console.error('Add post error. ', error);
    yield put({
      type: actions.ADD_POST_FAILURE,
      error,
    });
  }
}

function addCommentApi() {
  return axios.post('/addcomment');
}

function* addComment({ payload }) {
  try {
    const { addCommentData } = payload;
    console.log('Add comment data.', addCommentData);
    // yield call(addCommentApi);
    yield delay(2000);
    yield put({
      type: actions.ADD_COMMENT_SUCCESS,
      payload: {
        postId: addCommentData.postId,
      },
    });
  } catch (error) {
    console.error('Add comment error. ', error);
    yield put({
      type: actions.ADD_COMMENT_FAILURE,
      error,
    });
  }
}

function* watchAddPost() {
  yield takeLatest(actions.ADD_POST_REQUEST, addPost);
}

function* watchAddComment() {
  yield takeLatest(actions.ADD_COMMENT_REQUEST, addComment);
}

export default function* postSaga() {
  yield all([
    fork(watchAddPost),
    fork(watchAddComment)
  ]);

user 부분과 비슷하게 REQUEST, SUCCESS/FAILURE 순으로 이루어집니다.

 

reducers/post.js

export const initialState = {
  mainPosts: [{
    id: 1,
    User: {
      id: 1,
      nickname: 'MockUser',
    },
    content: 'First content',
    img: 'https://bookthumb-phinf.pstatic.net/cover/137/995/13799585.jpg?udate=20180726',
    Comments: [],
  }], // 화면에 보일 포스트들
  imagePaths: [], // 미리보기 이미지 경로
  addPostErrorReason: '', // 포스트 업로드 실패 사유
  isAddingPost: false, // 포스트 업로드 중
  postAdded: false, // 포스트 업로드 성공
  isAddingComment: false,
  addCommentErrorReason: '',
  commentAdded: false,
};

const dummyPost = {
  id: 2,
  User: {
    id: 1,
    nickname: 'MockUser',
  },
  content: 'Dummy POST',
  Comments: [],
};

const dummyComment = {
  id: 1,
  User: {
    id: 1,
    nickname: 'MockUser',
  },
  createdAt: new Date(),
  content: 'Dummy REPLY',
};


// Action types
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';

// Action creators
export const addPostRequest = data => ({
  type: ADD_POST_REQUEST,
  payload: {
    addPostData: data,
  },
});

export const addCommentRequest = data => ({
  type: ADD_COMMENT_REQUEST,
  payload: {
    addCommentData: data,
  },
});

// reducers
export default (state = initialState, action) => {
  const { type, payload, error } = action;
  switch (type) {
    case ADD_POST_REQUEST:
      return {
        ...state,
        isAddingPost: true,
        postAdded: false,
        addPostErrorReason: '',
      };
    case ADD_POST_SUCCESS:
      return {
        ...state,
        isAddingPost: false,
        postAdded: true,
        mainPosts: [dummyPost, ...state.mainPosts],
      };
    case ADD_POST_FAILURE:
      return {
        ...state,
        isAddingPost: false,
        addPostErrorReason: error,
      };
    case ADD_COMMENT_REQUEST:
      return {
        ...state,
        isAddingComment: true,
        commentAdded: false,
        addCommentErrorReason: '',
      };
    case ADD_COMMENT_SUCCESS:
      const postIndex = state.mainPosts.findIndex(v => v.id === payload.postId);
      const post = state.mainPosts[postIndex];
      const Comments = [...post.Comments, dummyComment];

      const mainPosts = [...state.mainPosts];
      mainPosts[postIndex] = { ...post, Comments };

      return {
        ...state,
        isAddingComment: false,
        commentAdded: true,
        mainPosts,
      };
    case ADD_COMMENT_FAILURE:
      return {
        ...state,
        isAddingComment: false,
        addCommentErrorReason: error,
      };
    default:
      return state;
  }
};

component 부분도 바꾸어 주어야겠죠?

 

components/PostForm.js

import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Form, Input, Button } from 'antd';

import * as actions from '../reducers/post';

const PostForm = () => {
  const dispatch = useDispatch();
  const { imagePaths, isAddingPost, postAdded } = useSelector(state => state.post);

  const [text, setText] = useState('');

  useEffect(() => {
    if (postAdded) {
      setText('');
    }
  }, [postAdded]);

  const onSubmitForm = (e) => {
    e.preventDefault();
    dispatch(actions.addPostRequest({
      text,
    }));
  };

  const onChangeText = (e) => {
    setText(e.target.value);
  };

  return (
    <Form style={{ margin: '10px 0 20px' }} encType="multipart/form-data" onSubmit={onSubmitForm}>
      <Input.TextArea maxLength={140} placeholder="How was your day?" value={text} onChange={onChangeText} />
      <div>
        <input type="file" multiple hidden />
        <Button>Upload Image</Button>
        <Button type="primary" htmlType="submit" loading={isAddingPost}>TWIT</Button>
      </div>
      <div>
        {
          imagePaths.map(v => (
            <div key={v}>
              <img src={`http://localhost:3000/${v}`} alt={v} />
              <div>
                <Button>Remove</Button>
              </div>
            </div>
          ))
        }
      </div>
    </Form>
  );
};

export default PostForm;

 

Add post Redux-saga

 

[4-13, 14] Add post with redux saga · eomttt/react-sns@6fb19a0

Permalink Browse files [4-13, 14] Add post with redux saga Loading branch information... Showing 5 changed files with 106 additions and 28 deletions. +1 −1 front/components/AppLayout.js +29 −7 front/components/PostForm.js +1 −1 front/pages/index.js +42 −17

github.com

 

마지막으로 댓글 부분을 추가하였는데요.

이또한 redux-saga를 적용했으니 함께 보면 좋을것 같습니다.

이미 앞서 언급한 코드인 saga/post.js, reducers/post.js 에서는 댓글부분이 적용되있으니 확인하면 될것 같습니다.

components 만 만들고 이에 대하여 action creator 만 dispatch 해주면 될것 같습니다.

 

components/CommentForm.js

import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useSelector, useDispatch } from 'react-redux';
import { Form, Input, Button } from 'antd';

import * as actions from '../reducers/post';

const CommentForm = ({ postId }) => {
  const dispatch = useDispatch();
  const { me } = useSelector(state => state.user);
  const { isAddingComment, commentAdded } = useSelector(state => state.post);
  const [text, setText] = useState('');

  useEffect(() => {
    if (commentAdded) {
      setText('');
    }
  }, [commentAdded]);

  const onSubmitForm = (e) => {
    e.preventDefault();
    
    if (!me) {
      return alert('Please login');
    }

    return dispatch(actions.addCommentRequest({
      postId,
      text,
    }));
  };

  const onChangeText = (e) => {
    setText(e.target.value);
  };

  return (
    <Form onSubmit={onSubmitForm}>
      <Form.Item>
        <Input.TextArea rows={4} value={text} onChange={onChangeText} />
      </Form.Item>
      <Button type="primary" htmlType="submit" loading={isAddingComment}>Comment</Button>
    </Form>
  );
};

CommentForm.propTypes = {
  postId: PropTypes.number.isRequired,
};

export default CommentForm;

 

components/CommentList.js

import React from 'react';
import PropTypes from 'prop-types';
import { Avatar, List, Comment } from 'antd';

const CommentList = ({ commentList }) => (
  <List
    header={`${commentList ? commentList.length : 0} 댓글`}
    itemLayout="horizontal"
    dataSource={commentList || []}
    renderItem={item => (
      <li>
        <Comment
          author={item.User.nickname}
          avatar={<Avatar>{item.User.nickname[0]}</Avatar>}
          content={item.content}
        />
      </li>
    )}
  />
);

CommentList.propTypes = {
  commentList: PropTypes.array.isRequired,
};

export default CommentList;

 

components/PostCard.js

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Card, Avatar, Icon, Button } from 'antd';

import CommentForm from './CommentForm';
import CommentList from './CommentList';

const PostCard = ({ post }) => {
  const [commentOpened, setCommentOpened] = useState(false);

  const onToggleComment = () => {
    setCommentOpened(prev => !prev);
  };

  return (
    <div>
      <Card
        key={+post.createdAt}
        cover={post.img && <img alt="example" src={post.img} />}
        actions={[
          <Icon type="retweet" key="retweet" />,
          <Icon type="heart" key="heart" />,
          <Icon type="message" key="message" onClick={onToggleComment} />,
          <Icon type="ellipsis" key="ellipsis" />,
        ]}
        extra={<Button>팔로우</Button>}
      >
        <Card.Meta
          avatar={<Avatar>{post.User.nickname[0]}</Avatar>}
          title={post.User.nickname}
          description={post.content}
        />
      </Card>
      {
        commentOpened && (
          <>
            <CommentForm postId={post.id} />
            <CommentList commentList={post.Comments} />
          </>
        )
      }
    </div>
  );
};

PostCard.propTypes = {
  post: PropTypes.shape({
    User: PropTypes.object,
    content: PropTypes.string,
    img: PropTypes.string,
    createdAt: PropTypes.object,
    id: PropTypes.number,
    Comments: PropTypes.array,
  }).isRequired,
};

export default PostCard;

 

한가지 주의할 점은 reducers/post.js 부분에 

    case ADD_COMMENT_SUCCESS:
      const postIndex = state.mainPosts.findIndex(v => v.id === payload.postId);
      const post = state.mainPosts[postIndex];
      const Comments = [...post.Comments, dummyComment];

      const mainPosts = [...state.mainPosts];
      mainPosts[postIndex] = { ...post, Comments };

      return {
        ...state,
        isAddingComment: false,
        commentAdded: true,
        mainPosts,
      };

요 부분인데 불변성을 위해서 다음과 같이 적용했습니다.

 

마지막으로 저의 깃허브 계정을 통하여 빠진 부분이나 에러나는 코드를 확인하면 될것 같습니다.

 

깃허브 react-sns

 

eomttt/react-sns

React sns test by react-nodebird(zeroCho). Contribute to eomttt/react-sns development by creating an account on GitHub.

github.com

 

여기까지 redux-saga 를 적용한 모습이 되겠습니다.

Comments