이번 포스팅에선 리덕스를 이용해 게시판의 기본적인 Create, Read, Update, Delete 구현을 다룬다.
공부하며 간단히 만들어본거라 설명이 미약한 점 참고해주시길.
먼저 프로젝트를 만들고 리덕스 환경을 준비하고 실행한다.
( 이전에 react-redux 프로젝트를 생성했기에 two 를 붙여 생성했음 ).
// project 생성
C:\> create-react-app react-redux-two
// project 로 이동
C:\> cd react-redux-two
// 사용할 redux 와 react-redux 설치
C:\react-redux-two> yarn add redux react-redux
// project 실행
C:\react-redux-two> yarn start
메인 directory 의 구조는 다음과 같이 구현된다.
src
┕ component
┕ BoardList.js
┕ BoardNew.js
┕ container
┕ Container.js
┕ module
┕ boardReducer.js
┕ rootReducer.js
┕ App.js
┕ index.js
package.json
먼저 리듀서부터 코드를 살펴보자.
주석을 같이 참고하며 살펴보면 이해하기 수월할 것 이다.
src/module/boardReducer.js
// Action Type
const MODE_REMOVE = 'REMOVE';
const MODE_SAVE = 'SAVE';
const MODE_SELECT_ROW = 'SELECT_ROW';
// Action Create Function
export const boardSave = (saveData) => ({
type: MODE_SAVE,
saveData: {
boardId: saveData.boardId,
boardTitle: saveData.boardTitle,
boardContent: saveData.boardContent
}
});
export const boardRemove = (boardId) => ({
type: MODE_REMOVE,
boardId: boardId
});
export const boardSelectRow = (boardId) => ({
type: MODE_SELECT_ROW,
boardId: boardId
});
// initState
const initialState = {
boards: [
{
boardId: 1,
boardTitle: '제목1',
boardContent: '내용내용내용1'
},
{
boardId: 2,
boardTitle: '제목2',
boardContent: '내용내용내용2'
},
{
boardId: 3,
boardTitle: '제목3',
boardContent: '내용내용내용3'
},
{
boardId: 4,
boardTitle: '제목4',
boardContent: '내용내용내용4'
},
{
boardId: 5,
boardTitle: '제목5',
boardContent: '내용내용내용5'
}
],
lastId: 5,
selectRowData: {}
}
// Reducer
export default function boardReducer(state=initialState, action) {
switch(action.type) { // 클릭한 boardId 를 가지지 않은 data 만 return
case MODE_REMOVE:
return {
...state, boards: state.boards.filter(row =>
row.boardId !== action.boardId)
};
case MODE_SAVE:
if(action.saveData.boardId === '') { // boardId 가 없다면 신규 데이터 저장
return {
lastId: state.lastId+1,
boards: state.boards.concat({
...action.saveData,
boardId: state.lastId+1
}),
selectRowData: {}
};
} else { // boardId 가 있다면 기존 데이터 수정
return { ...state, boards: state.boards.map(data => data.boardId === action.saveData.boardId ? {...action.saveData}: data), selectRowData: {} };
}
case MODE_SELECT_ROW:
return { // 클릭한 셀의 boardId 를 가진 state 만 찾아서 return
...state,
selectRowData: state.boards.find(row => row.boardId === action.boardId)
};
default:
return state;
}
}
Action Type 은 Action 의 Type 을 선언해 놓은 녀석들이고,
Action Create Function 은 dispatch 를 통해 명령이 떨어지면,
선언 되어진 Action Type 을 가지고 Action 객체를 생성한다.
Reducer 에선 생성된 Action 을 참조해 Type 에 맞게 Store 의 State 에 변경을 준다.
이 때 Reducer 의 초기 state 값은 initialState 을 참조한다.
Reducer 가 뭔지 아직 헷갈린다면 아래의 링크 참고.
- 링크1
- 링크2
src/module/rootReducer.js
import { combineReducers } from 'redux';
import boardReducer from './boardReducer';
const rootReducer = combineReducers({
boardReducer
});
export default rootReducer;
combineReducers 는 여러 개의 reducer 를 하나로 합쳐 내보내는 기능을 수행하는데
현재는 하나의 Reducer 라 필요없지만 여러 개의 Reducer 를 가진 project 가 대부분이기에 익숙해지도록 구현했다.
결국 지금은 하나의 boardReducer 를 rootReducer 의 className 으로 내보낸다.
리듀서는 됬고, 이러한 리듀서를 적용해 사용한 파일들을 순서대로 살펴보자.
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './module/rootReducer';
// redux devTool
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
// reducer 와 devTool 을 가진 store 생성
const store = createStore(rootReducer, devTools);
// Provider 는 자식 컴포넌트 App 이 store 의 state 를 사용할 수 있도록 해준다.
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
index.js 에서는 이전 게시글에서 다룬 것과 별 다른 추가점이 없다.
store 를 만들고 Provider 를 통해 하위 컴포넌트 App 에게 사용할 수 있게 해준다.
src/App.js
import React from 'react';
import Container from './container/Container';
function App() {
return (
<div>
<Container />
</div>
)
}
export default App;
App.js 도 Container.js 컴포넌트를 렌더링 하는 것 외엔 뭐 없다.
src/container/Container.js
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import BoardList from '../component/BoardList';
import BoardNew from '../component/BoardNew';
import { boardRemove, boardSave, boardSelectRow } from '../module/boardReducer';
function Container() {
// State
let [inputData, setInputData] = useState({
boardId: '',
boardTitle: '',
boardContent: ''
});
// 함수형 컴포넌트에서 dispatch 를 사용할 수 있게 해줌
const dispatch = useDispatch();
// onRemove 와 onSave 는 Action 을 dispatch 하는 함수
const onRemove = (boardId) => dispatch(boardRemove(boardId));
const onSave = (saveData) => dispatch(boardSave(saveData));
// reducer state 의 selectRowData field 를 가져온 뒤 subscribe(구독)
const {selectRowData} = useSelector(state => state.boardReducer);
// User Function
const onRowClick = (boardId) =>
{
// dispatch 를 하고,
dispatch(boardSelectRow(boardId));
// inputData 에 selectRowData 의 값을 반영
if(JSON.stringify(selectRowData) !== '{}') {
setInputData({
boardId: selectRowData.boardId,
boardTitle: selectRowData.boardTitle,
boardContent: selectRowData.boardContent
})
}
}
const changeInput = (e) => {
setInputData({
...inputData,
[e.target.name]: e.target.value
})
}
const resetForm = () => {
setInputData({
boardId: '',
boardTitle: '',
boardContent: ''
})
}
// reducer state 의 boards field 를 가져온뒤 subscribe(구독)
const {boards} = useSelector(state => state.boardReducer);
return (
<div>
<div>
<table border="1">
<tbody>
<tr align="center">
<td width="50">번호</td>
<td width="100">제목</td>
<td width="200">내용</td>
</tr>
{
boards.map(row =>
(
<BoardList
key={row.boardId}
boardId={row.boardId}
boardTitle={row.boardTitle}
boardContent={row.boardContent}
onRemove={onRemove}
onRowClick={onRowClick}
/>
))
}
</tbody>
</table>
</div>
<div>
<BoardNew
onSave={onSave}
changeInput={changeInput}
inputData={inputData}
resetForm={resetForm}
/>
</div>
</div>
);
}
export default Container;
Container.js 컴포넌트는 View 컴포넌트들이 Reducer 과 통신간에 일어날 로직 등을 구현한 컴포넌트다.
다리역할을 해주는 이벤트 트리거 역할을 해주는 셈이다.
코드 블럭 별로 살펴보자.
// State
let [inputData, setInputData] = useState({
boardId: '',
boardTitle: '',
boardContent: ''
});
// 함수형 컴포넌트에서 dispatch 를 사용할 수 있게 해줌
const dispatch = useDispatch();
// onRemove 와 onSave 는 Action 을 dispatch 하는 함수
const onRemove = (boardId) => dispatch(boardRemove(boardId));
const onSave = (saveData) => dispatch(boardSave(saveData));
먼저 useState(hook) 을 이용해 inputData State 를 선언하고, setState 기능의 setInputData 를 선언해놓는다.
Counter.js 컴포넌트는 함수형 컴포넌트인데,
이러한 함수형 컴포넌트에서도 리듀서의 dispatch 를 사용할 수 있게 useDispatch 를 이용.
boardRemove() 액션 생성 함수를 dispatch 하는 함수 onRemove 와,
boardSave() 액션 생성 함수를 dispatch 하는 함수인 onSave 선언.
dispatch 의 인자값은 Action 이 들어가야 하는데
boardRemove() 와 boardSave() 는 액션을 만들어주는 액션 생성 함수이기에 가능.
// User Function
const onRowClick = (boardId) =>
{
// dispatch 를 하고,
dispatch(boardSelectRow(boardId));
// inputData 에 selectRowData 의 값을 반영
if(JSON.stringify(selectRowData) !== '{}') {
setInputData({
boardId: selectRowData.boardId,
boardTitle: selectRowData.boardTitle,
boardContent: selectRowData.boardContent
})
}
}
const changeInput = (e) => {
setInputData({
...inputData,
[e.target.name]: e.target.value
})
}
const resetForm = () => {
setInputData({
boardId: '',
boardTitle: '',
boardContent: ''
})
}
View Component 들이 사용할 사용자 함수 세 개 선언.
// store 의 state 를 가져와 boards 필드를 할당 후 subscribe(구독)
const {boards} = useSelector(state => state.boardReducer);
return (
<div>
<div>
<table border="1">
<tbody>
<tr align="center">
<td width="50">번호</td>
<td width="100">제목</td>
<td width="200">내용</td>
</tr>
{
boards.map(row =>
(
<BoardList
key={row.boardId}
boardId={row.boardId}
boardTitle={row.boardTitle}
boardContent={row.boardContent}
onRemove={onRemove}
onRowClick={onRowClick}
/>
))
}
</tbody>
</table>
</div>
<div>
<BoardNew
onSave={onSave}
changeInput={changeInput}
inputData={inputData}
resetForm={resetForm}
/>
</div>
</div>
);
그 후 boards 의 값 들과 onRemove(), onRowClick() 을 인자로 넘겨 BoardList.js 컴포넌트에서 사용할 수 있도록 하며 이를 렌더링한다.
BoardNew.js 컴포넌트로 렌더링하며 인자값으로는 액션 생성 함수를 dispatch 하는 onSave() 함수와 사용자 함수 세 개를 보낸다.
그럼 먼저 BoardList.js 부터 이동해 살펴보자.
src/component/BoardList.js
import React from 'react';
function BoardList({ boardId, boardTitle, boardContent, onRemove, onRowClick }) {
return (
<tr>
<td onClick={() => onRowClick(boardId)}>{boardId}</td>
<td onClick={() => onRowClick(boardId)}>{boardTitle}</td>
<td onClick={() => onRowClick(boardId)}>{boardContent}</td>
<td><button onClick={() => onRemove(boardId)}>삭제</button></td>
</tr>
)
}
export default BoardList;
BoardList.js 는 데이터를 뿌려주는 목록의 역할을 하는 컴포넌트인데,
부모 컴포넌트 ( Container.js ) 로 부터 받은 onRowClick() 함수를 각 셀 마다 onClick 으로 주었다.
이는 각 셀을 클릭할 때마다 boardId 를 가지고 Action 을 생성해 dispatch 할 수 있도록 한다.
onRemove() 도 마찬가지.
src/component/BoardNew.js
import React from 'react';
function BoardNew({ onSave, changeInput, inputData, resetForm }) {
const saveBtnClick = (e) => {
e.preventDefault();
onSave(inputData);
resetForm();
}
return (
<div>
<form onSubmit={saveBtnClick}>
<div>
제목 : <input type="text" name="boardTitle" onChange={changeInput} value={inputData.boardTitle} />
</div>
<div>
내용 : <input type="text" name="boardContent" onChange={changeInput} value={inputData.boardContent} />
</div>
<input type="hidden" name="boardId" onChange={changeInput} value={inputData.boardId} />
<button type="submit" >신규 게시글 저장</button>
</form>
</div>
)
};
export default BoardNew;
BoardNew.js 컴포넌트는 입력 폼의 기능을 수행하는 컴포넌트다.
돌아가는 원리가 어떻냐면 BoardList.js 에서 셀을 클릭하지 않았다면 현 Store 의 state 에는 초기값만이 들어가있을테고
input 에 값을 입력후 저장하면 현 데이터만을 가지고 Action 을 생성후 onSave() 함수를 통해 dispatch 한다.
수정의 경우라면 BoardList.js 에서 셀을 클릭해 해당 boardId 를 가지고 Action 을 생성해 dispatch 했을 테고,
이로 인해 store 의 state 에 selectRowData: {} 안에 해당 boardId 를 가진 데이터가 들어가게 된다.
이 때 Container.js 에서 Store 의 state 에 구독을 해놨기 때문에 변경 값을 감지하고 다시 BoardNew.js 컴포넌트를 리렌더링하고
인자로 inputData 를 보내는데 각 input 마다 inputData 안의 데이터를 setting 시켜놓는 원리이다.
실행 화면
'Coding Story > REACT' 카테고리의 다른 글
[ React ] 리액트 Saga + Toolkit ( 미들웨어 사가, 리덕스 툴킷 ) (1) | 2020.10.26 |
---|---|
[ React ] 리액트 리덕스 툴킷 redux-toolkit (3) | 2020.10.22 |
[ React ] 리액트 리덕스(Redux) 사용해보기 (0) | 2020.10.22 |
[ React ] 리액트 리덕스(Redux)란? (0) | 2020.10.22 |
[ React ] 리액트 Axios (0) | 2020.10.22 |