모듈 설치

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

image.png

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를 적용해줬다.

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

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를 사용해서 최적화를 시켜준다.

image.png

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에 전달해준다.

image.png
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컴포넌트에 상태를 전달해준다.
image.png

오른쪽 상단에 + 버튼을 클릭하면 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 액션을 사용해 포스팅을 추가해 준다.

image.png

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

image.png

다시 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

image.png

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

마무리

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

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