create-react-app 으로 프로젝트를 셋팅하고 scss로 기본적인 컴포넌트 디자인과 react-router-dom을 사용해서 컴포넌트 링크를 연결시켜준 상태이다.
redux를 사용해서 포스팅의 추가, 읽기, 수정, 삭제 기능을 구현하려고 한다.
필요한 모듈을 먼저 설치해준다.
yarn add redux react-redux redux-devtools-extension redux-thunk
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;
concat 과 filter 메서드를 사용해서 포스팅을 추가하고 삭제하는 기능을 구현했다. 두 메서드 모두 사본을 반환하기 때문에 원래 상태에 영향을 주지 않는다.
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를 적용해줬다.
개발자 도구를 확인해 보면 잘 적용이 된 걸 확인할 수 있다.
이제 컴포넌트에서 리듀서를 사용해 포스팅을 추가하는 기능을 구현해보자!
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 컴포넌트로 돌아와 보면 포스팅이 잘 추가된걸 확인할 수 있다!
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을 클릭하면 해당하는 포스팅의 제목과 내용을 모달로 보여주고 수정하는 기능을 구현해 보자.