[ React Blog ] - Redux 연동 & 추가, 삭제기능 구현

승진·2019년 10월 7일
1

React Blog

목록 보기
2/3

모듈 설치

create-react-app 으로 프로젝트를 셋팅하고 scss로 기본적인 컴포넌트 디자인과 react-router-dom을 사용해서 컴포넌트 링크를 연결시켜준 상태이다.

redux를 사용해서 포스팅의 추가, 읽기, 수정, 삭제 기능을 구현하려고 한다.
필요한 모듈을 먼저 설치해준다.

yarn add redux react-redux redux-devtools-extension redux-thunk

  • redux: 자바스크립트 앱을 위한 상태 컨테이너
  • react-redux: 리액트 앱과 리덕스를 연결시켜주는 모듈
  • redux-devtools-extenstion: 리덕스의 상태 추적을 브라우저에서 할 수 있게 해주는 확장모듈
  • redux-thunk: 액션 대신 함수를 반환, 액션 디스패치시 딜레이 발생, 특정조건 시 디스패치 등을 지원하는 미들웨어

STORE

store 폴더에 액션과 리듀서를 정의해준다.

action / index.js

// 액션 타입 정의
export const ADD_POSTING = 'ADD_POSTING';
export const EDIT_POSTING = 'EDIT_POSTING';
export const DELETE_POSTING = 'DELETE_POSTING';

let nextId = 2;

// 액션 생성 함수
export const addPosting = (title, description) => {
    return {
        type: ADD_POSTING,
        post: {
            id: nextId++,
            title,
            description
        }
    };
}

export const editPosting = (id) => {
    return {
        type: EDIT_POSTING,
        id
    }
}

export const deletePosting = (id) => {
    return {
        type: DELETE_POSTING,
        id
    }
}

액션 타입과 함수를 만들고 리듀서 파일에 가져와 리듀서를 만들어준다.

reducers / posts.js

import { ADD_POSTING, DELETE_POSTING } from "../actions"

const initialState = [
    {
        id: 1,
        title: ' This is First Post',
        description: '포스팅 1',
    }
]

const posts = (state = initialState, action) => {
    switch (action.type) {
        case ADD_POSTING:
            return state.concat(action.post);
        case DELETE_POSTING:
            return state.filter(post => post.id !== action.id)
        default:
          return state;
      }
};

export default posts;

concatfilter 메서드를 사용해서 포스팅을 추가하고 삭제하는 기능을 구현했다. 두 메서드 모두 사본을 반환하기 때문에 원래 상태에 영향을 주지 않는다.

reducers / posts.js

import { combineReducers } from 'redux';
import posts from './posts';

const rootReducer = combineReducers({
    posts
});

export default rootReducer;

루트 리듀서를 만들어주고 이제 만든 리듀서를 적용해주자

src / index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import './static/scss/styles.scss';
import App from './App';
import * as serviceWorker from './serviceWorker';

import {BrowserRouter} from 'react-router-dom';
// Redux
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunkMiddleware from 'redux-thunk';

import rootReducer from './store/reducers';

const middleware = applyMiddleware(thunkMiddleware);
const store = createStore(rootReducer, composeWithDevTools(middleware));


ReactDOM.render( 
    <Provider store={store}>
    <BrowserRouter >
        <App />
    </BrowserRouter>
    </Provider>,
    document.getElementById('root')
);

serviceWorker.unregister();

스토어를 생성해서 리듀서를 적용해주고 composeWithDevTools에 thunk를 적용해줬다.

개발자 도구를 확인해 보면 잘 적용이 된 걸 확인할 수 있다.

ADD POSTING

이제 컴포넌트에서 리듀서를 사용해 포스팅을 추가하는 기능을 구현해보자!

Item.jsx

import React from 'react';
import { MdModeEdit } from "react-icons/md";
import { MdDelete } from "react-icons/md";

const Item = React.memo(function Item({ post, date, onToggle}) {

    return (
        <div className="itemContainer">
            <h2 className="id">{post.id}</h2>
            <h2 className="title">{post.title}</h2>
            <h2 className="date">{date}</h2>
            <h2 className="action edit">
                <MdModeEdit className="icon"/>
                <MdDelete className="icon" onClick={onClick}/>
            </h2>
        </div>
    )
})

export default Item;

Item은 포스팅의 리스트 목록을 보여주는 컴포넌트로 post를 전달받아 컨텐츠를 보여준다.
React.memo를 사용해서 최적화를 시켜준다.

Posts.jsx

import React from "react";
import Item from "../container/Item";

const Posts = ({onCreate, onToggle, posts}) => {
  return (
    <div className="postContainer">
      <div className="post-nav">
        <h2 className="id">ID</h2>
        <h2 className="title">T</h2>
        <h2 className="date">Date</h2>
        <h2 className="action">Action</h2>
      </div>
        {posts.map(post => (
           <Item key={post.id} post={post} onCreate={onCreate} onToggle={onToggle} date={now}/>
        ))}
    </div>
  );
};

export default Posts;

Item 컴포넌트를 렌더해서 보여주는 컴포넌트로 posts를 전달 받아서 Itme에 전달해준다.


Home.jsx

import React, {useCallback} from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addPosting, deletePosting } from "../store/actions";
import Posts from './Posts';
import { Link } from 'react-router-dom'
import { MdAdd } from "react-icons/md";

const Home = () => {

    const posts = useSelector(state => state.posts);
    const dispatch = useDispatch();

    const onCreate = (title, des) => dispatch(addPosting(title, des));
    const onToggle = useCallback(id => dispatch(deletePosting(id), [dispatch]));

    return (
        <main>
            <div className="homeContainer">
                <h1>POST</h1>
                <Link to="/new" className="new">
                    <MdAdd/>
                </Link>
            </div>
            <Posts posts={posts} onCreate={onCreate} onToggle={onToggle}/>
        </main>
    )
}

export default Home;

useSelector, useDispatch를 사용해서 컴포넌트에서 리듀서를 불러와 Posts컴포넌트에 상태를 전달해준다.

오른쪽 상단에 + 버튼을 클릭하면 New 컴포넌트로 이동해 새로운 포스팅을 작성할 수 있다.

New.jsx

import React, { useState } from "react";
import { useSelector, useDispatch } from 'react-redux';
import { addPosting } from "../store/actions";
import { Link } from "react-router-dom";
import { IoMdArrowRoundBack } from "react-icons/io";
import { MdGetApp } from "react-icons/md";

const New = () => {
  // redux
  const posts = useSelector(state => state.posts);
  const dispatch = useDispatch();

  const onCreate = (title, des) => dispatch(addPosting(title, des));

  // input
  const [inputs, setInputs] = useState({
    title: '',
    des: ''
  });
  const {title, des} = inputs;

  const onChange = (e) => {
    const {value, name} =  e.target;
    setInputs({
      ...inputs,
      [name] : value
    })
  }

  const onSubmit = (e) => {
    e.preventDefault();
    onCreate(title, des);
  }

  return (
    <div className="newContainer">
        <form onSubmit={onSubmit}>
        <div className="btn">
        <Link to="/">
          <IoMdArrowRoundBack className="back" />
        </Link>
        <button onSubmit={onSubmit}><MdGetApp className="save"/></button>
      </div>
        <div className="titleContainer">
          <input
            type="text"
            name="title"
            autoComplete="off"
            required
            value={title}
            onChange={onChange}
          />
          <label htmlFor="name" className="label-name">
            <span className="content-name">Title</span>
          </label>
          </div>
          <div className="desContainer">
            <textarea name="des" cols="30" rows="10" required value={des} onChange={onChange}></textarea>
          </div>
        </form>
    </div>
  );
};

export default New;

리듀서를 불러와주고, 두개의 input을 useState()를 사용해서 관리해준다.
title, des의 value값을 addPosting 액션을 사용해 포스팅을 추가해 준다.

오른쪽 상단에 업로드 아이콘을 클릭 하면 포스팅이 추가되고

다시 Home 컴포넌트로 돌아와 보면 포스팅이 잘 추가된걸 확인할 수 있다!

DELETE POSTING

Home.jsx 에서 전달해주는 onToggle 함수가 deletePosting 리듀서 함수를 전달해주고

const onToggle = useCallback(id => dispatch(deletePosting(id), [dispatch]));

Item 컴포넌트에서 전달받아 쓰레기통 아이콘을 클릭하면 Confirm 모달이 뜨고 대답에 따라 해당하는 포스트를 삭제하는 기능을 구현했다.

Item.jsx

const Item = React.memo(function Item({ post, date, onToggle}) {

    const [open, setOpen] = useState(false);

    const remove = () => {
        onToggle(post.id)
    }

    const onClick = () => {
        setOpen(true);
    }

    const close = () => {
        setOpen(false);
    }

    return (
        <div className="itemContainer">
            <h2 className="id">{post.id}</h2>
            <h2 className="title">{post.title}</h2>
            <h2 className="date">{date}</h2>
            <h2 className="action edit">
                <MdModeEdit className="icon"/>
                <MdDelete className="icon" onClick={onClick}/>
            </h2>
            <Confirm post={post} open={open} remove={remove} close={close}/>
        </div>
    )
})

export default Item;

Confirm.jsx

import React from 'react';

const Confirm = ({ open, close, post, remove }) => {
    return (
        <React.Fragment>
            {
            open ?
            <React.Fragment>
            <div className="confirm-container">
                <div className="confirm">
                    <p>Delete this posting ?</p>
                <div className="btn">
                    <button onClick={() => remove()}>Yes</button>
                    <button onClick={close}>No</button>
                </div>
                </div>
            </div>
            </React.Fragment>
            :
            null
            }
        </React.Fragment>
    )
}

export default Confirm

Yes를 누르면 해당하는 포스트가 삭제된다.

마무리

Redux를 사용해서 상태관리를 하는 것은 복잡하지만 적용하고 나면 편리하게 상태를 전달하며 관리할 수 있다는 장점이 있다.
매번 리덕스를 적용할 때 마다 적용 방법을 찾아보며 하게 되는데 스스로 새로운 걸 만들어 보는게 리덕스를 이해하는데 있어서 많은 도움이 되는것 같다.
아직은 기능에 맞는 함수를 작성하는데 어려움이 좀 있지만 잘 작동할 때의 뿌듯함에 재미를 느끼며 만들게 된다ㅎ

다음 포스팅에서는 item을 클릭하면 해당하는 포스팅의 제목과 내용을 모달로 보여주고 수정하는 기능을 구현해 보자.

profile
Front-end 개발 공부일지

0개의 댓글