이미지09
Coding Story/REACT

[ React ] 리액트 Redux 로 게시판 만들어보기

반응형

 

 

 

이번 포스팅에선 리덕스를 이용해 게시판의 기본적인 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 시켜놓는 원리이다.

 

 

 

실행 화면

포스팅 이미지 01

 

 

공부할 때 참고 많이 한 블로그

 

 

 

반응형