앞서서 정리한 React 기반 게시판을 Redux로 구현한다.

React 기반 게시판에서 글을 수정할 경우,

사용자가 선택한 행을 부모에게 알리고, 부모는 이것을 받아서 입력 폼으로 전송하는 방식으로 구현하였다.

사용자가 입력을 완료하고 저장하면, 다시 입력한 내용을 부모에게 전송해서 부모의 state 변수에 저장한다.

정리하면, 데이터 저장소(state)가 부모(App.js)에게 있기 때문에 항상 부모를 거쳐서 모든 기능이 구현되어야 한다.


여기서 정리한 내용은 React 게시판 만들기 시리즈의 두번째 내용이다.

   1. React 게시판(CRUD) 만들기

   2. React + Redux 게시판(CRUD) 만들기

   3. React(Redux) + Firebase 게시판(CRUD) 만들기


데이터 저장소와 데이터를 관리하는(CRUD) 기능(함수)등을 한 곳에 두고

각 컴포넌트들은 각자의 기능에 맞추어, 이 함수만 호출하도록 작성한다면 깔끔하고 쉬운 프로그램을 개발 할 수 있을 것이다.

이러한 기능을 제공하는 라이브러리 중에 많이 사용하는 Redux로 앞서 제작한 게시판을 간단하게 구현한다.

Redux 기본 문법과 이해는 자료가 많으니 검색해 보길 바라고, 여기서는 Redux로 구현하면서 사용법을 간단하게 정리한다.

소스는 Github에서 받을 수 있다.


먼저, create-react-app로 새로운 프로젝트를(redux_board) 생성하거나

앞서 정리한 예제를 수정하면서 따라한다.

create-react-app redux_board

cd redux_board


Redux를 설치한다.

npm install --save redux react-redux


create-react-app로 생성된 index.js 파일을 열어서 다음과 같이 문장을 추가한다.

import React from 'react';
import ReactDOM from 'react-dom';

import { createStore } from 'redux';
import { Provider } from 'react-redux';

import App from './App';
import board_reducer from './App_reducer';

let store = createStore(board_reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());

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

index.js

데이터 입출력과 관련된 모든 기능은 App_reducer.js 파일에 구현되어 있고,

이상의 코드는 이 App_reducer.js 파일을 Redux의 문법에 맞추어 App 전체에서 사용할 수 있도록 등록하는 것이다.


App_reducer.js 파일은 Redux의 reducer로 다음과 같이 데이터 관리와 관련된 기능을 구현해 둔다.

Reducer에는 지켜야 하는 형식이 있어서 복잡해 보이는데,

중요한 것은 데이터를 저장하는 state와 이를 관리하는 board_reducer 함수 이다 [라인 42].

state는 별도로 선언하지 않고,

board_reducer 함수 파라미터에서 initialState[라인 23]로 초기값을 지정하면서 사용한다 [라인 42].

initialState는 Json으로 구성되어 앞서의 React 게시판에서 데이터를 저장하는 state 구조와 동일하다.

최대 글 번호를 가지는 maxNo, 게시물 데이터를 보관하는 boards, 데이터를 수정하기 위해 현재 선택한 글 정보를 가지는 selectedBoard로 구성하였다.


board_reducer 함수에 대해서 정리하면

board_reducer 함수에서 모든 처리가 이루어지고,

파라미터로 제공되는 action의 종류(type)에 따라 어떤 처리(CRUD)를 할 것 인지를 구현한다.

action 종류(type)는 4가지의 상수로(const)로 구현되어 있다 [라인 1~4].

글 리스트를 제공하는 BOARD_LIST [라인 4],

사용자가 신규로 작성하거나 수정한 내용을 저장하는 BOARD_SAVE [라인 1],

수정하기 위해 글을 선택하는 BOARD_READ [라인 3],

글을 삭제하는 BOARD_REMOVE로 CRUD를 구성하였다 [라인 2].


App_reducer.js 파일 외부에서는 board_reducer 함수를 호출하는 것이 아니고,

액션 종류에 따른 각각의 함수 board_list, board_save, board_read, board_remove를 호출해서 사용한다.

이 함수를 호출하면 지정된 action type이 같이 파라미터와 같이 제공된다.


board_list는 그냥 전체 글 리스트를 반환 하기 때문에 파라미터 없이 action type만 지정한다 [라인 21].
(의미상 선언한 것으로 사용하지 않는다.)

board_save은 파라미터로 저장할 게시글 정보(data)가 필요하고 [라인 6~9],

board_read와 board_remove은 수정하거나 삭제할 글번호(brdno)가 필요하다 [라인 11~14, 16~19].

따라서 각각 라인처럼 필요한 정보를 파라미터로 지정하고

board_reducer에서는 action.data, action.brdno로 파라미터 값을 가지고 와서 사용한다.


board_reducer에서 구현한 각 액션의 기능은 모두 각 컴포넌트에서 사용한 코드들을 모은 것이다.

게시물과 관련된 CRUD 코드들을 board_reducer에 모아서 사용하는 것이다.


좀더 상세하게 정리하면,

글 리스트는 CRUD 개념상 정의한 것으로 여기서는 사용하지 않으니 넘어간다.

글 리스트 구현은 함수로 뭔가를 처리하는 것이 아니고,

state의 boards 변수의 전체 값을 가지고 와서 출력하는 것이기 때문에 구현하지 않는다.


글 저장은(BOARD_SAVE) 글 번호(brdno)의 값이 있으면 수정이니 

boards의 모든 행을 검사해서(map), 글 번호가 같은 게시물이면 새로운 게시물(data)를 반환하고 그렇지 않으면 기존 게시물(row)을 반환해서 새로운 배열을 생성한다 [라인 52] .

선택한 행은(selectedBoard) {}로 초기화 하고, 기존 state 값(…state)과 같이 반환한다 [라인 52] .

state에 변수가 3개 있으니, maxNo를 같이 반환하는 것과 같다 [라인 52] .

이 코드는 다음과 같이 작성해도 된다.

        return {maxno: state.maxno, boards: boards.map(생략), selectedBoard: {} };

변수가 많을 경우 이와 같이 모든 변수를 나열하는 것 보다 ...state로 작성하는 것이 좋다 (버그 방지).


글 번호(brdno)의 값이 없으면 [라인 49],

신규라 기존 게시물 데이터(boards)에 새로운 게시물을(data) 추가(concat)해주고, 글 번호(maxNo)를 1 증가 시켜 놓는다 [라인 50] .


글 삭제는(BOARD_REMOVE)는 게시물 데이터 (boards)에서 삭제할 글 번호에 해당하는 행을 찾아서 지우는 방식이 아니고,

삭제할 게시글이 아닌 게시물만 모아서(fiter) 배열로 다시 생성하는 방식으로 구현한다 [라인 54] .

조건에 부합하는 데이터(fiter)만 모아서 다시 배열을 만드는 방식이다.

성능 등의 여러가지 이유로 이 방식이 권장되고 있다.


글 수정을 위한 선택(BOARD_READ)은 주어진 글번호(brdno)에 맞는 게시글을 찾아서(find) selectedBoard로 지정하면 된다 [라인 58] .

나머지(maxNo, boards)는 ...state로 지정해서 반환한다.


React로 작성된 코드를 Redux로 바꾸는 것은

지금까지 정리한 App_reducer.js의 내용이 핵심이고, 다른 컴포넌트에서는 호출해서 사용만 하면 된다.

App_reducer.js에서는 저장할 데이터(state)의 초기값 지정(initialState)과 역할별 기능을 board_reducer에 구현하면 된다.


데이터와 데이터를 관리하는 기능들을 모두 리듀서 파일에 작성하기 때문에 App.js 파일에서는 다음과 같이 깔끔하게 정리된 코드를 볼 수 있다.

데이터를 저장하고, 삭제하고, 선택하는 기능들이 모두 리듀서 파일로 이동하고,

App.js에서는 게시글 리스트를 적절하게 출력하고, 주요 컴포넌트를 호출한다 [라인 14, 25].


코드 하단에 있는 mapStateToProps에서는 App 컴포넌트에서 사용할 변수 (데이터)를 선언한다 [라인 35~39].

여기서는 게시물 데이터만 가져와서 출력하기 때문에

Reducer의 state.boards를 boards로 받아서[라인 37] App 컴포넌트로 넘겨 준다(connect) [라인 41].

Reducerr에 있는 변수(boards)를 가지고 올 때에는 이런식으로 선언해서 가시고 오고 [라인 35~41]

this.props(.boards)로 해당 변수를 이용한다 [라인 9].


다음으로 글 항목(BoardItem)을 살펴보면, 코드는 다음과 같다.

부모로부터 받은 게시물 하나(this.props.row)를 TR 테그를 생성해서 적절하게 값을 출력한다.

글 항목(BoardItem)은 게시글을 출력하고,

사용자가 행을 선택하면(Click) 선택된 행 정보를 알려주는 역할만 하기 때문에 기존에도 코드가 별로 없었고, Redux에서도 큰 변경이 없다.


수정사항이 별로 없어서, 자바스크립트 문법을 다양하게 적용해 봤다.

제목을 클릭하면 글 수정, 삭제(X) 버튼을 클릭하면 삭제하도록 이벤트 핸들러를 작성한다.

기존에는 각각의 onClick에 [this.함수]방식으로 작성해서 Click 이벤트에 이벤트 핸들러를 연결했다.

이번에는 이벤트 핸들러를 화살표(=>) 함수로 작성하고,

화살표 함수에서 다른 함수를 호출하는 방식으로 구현했다 [라인 17, 20].

HTML에서 자바스크립트 함수를 직접 작성하는 것으로,

제목을 클릭하면 화살표 함수로 작성된 이벤트 핸들러에서 글번호(brdno)를 파리미터로 handleUpdateForm 함수를 호출한다 [라인 17].

handleUpdateForm 함수에서 실제로 데이터를 저장하는 리듀스(App_reducer)에 있는 board_read를 호출한다 [라인 7].

글 삭제는 이러한 과정을 생략하고 리듀스(App_reducer)에 있는 board_remove를 호출해서 데이터를 삭제한다 [라인 20].

하나는 함수를 거쳐서 실행한 것이고 하나는 직접적으로 실행한 것이다.

코드를 다양하게 구현할 수 있다는 걸 보여주기 위해 작성한 코드이다.


이상에서 주의해야 할 것은 기존의 기능들이 사라지고,

리듀스(App_reducer)에 구현해 놓은 함수 중에서 필요한 것을(board_read, board_remove) 선택해서 선언하고 [라인 4],

필요에 따라 호출하면 된다는 것이고 [라인 8, 20],

그냥 함수를 호출하는 것이 아니고, 부모에게서 받았다는 의미로 this.props.dispatch를 이용해서 호출한다는 것이다.


BoardForm에서는 글쓰기와 글 수정 기능이 구현되었다.

앞서의 예제에서 하나로 정리하지 않고 2가지 방식으로 정리한 글쓰기 코드(App5, App6)를 여기에서 정리하였다.

따라서 글쓰기 코드와 설명이 앞서 예제의 내용과 조금 더 차이가 있다 (둘을 겹합).


먼저, 사용자가 입력한 글 제목(brdtitle)과 작성자 이름(brdwriter)은 onChange 이벤트를 이용하여BoardForm 컴포넌트의 자체 state에 저장한다 [라인 15~17].

사용자가 저장 버튼을 클릭하면 [라인 33], 리듀서(App_reducer)에 구현해 놓은 board_save()함수를 호출하여 저장한다 [라인 19].

호출할때, 사용자가 입력한 값을 가지고 있는 내부 state 변수를 파라미터로 넘기고,

리듀서의 board_save()에서 boards 배열에 이 파라미터를 새로운 행으로 추가하면 관련 컴포넌트가 갱신되면서 행(TR-BoardItem)이 추가되어 화면에 출력된다.

입력 상자(input)가 내부 state 변수의[라인 7] 값을 바라보고 있기 때문에 [라인 31, 32]

setState로 각 변수의 빈 값을 가진 initialSelectedBoard를 지정해서 초기화 시켰다.

입력 상자의 값이 지워진다.


글 수정은 사용자가 수정할 행을 선택하면 board_read 함수가 실행되면서 selectedBoard에 선택된 행의 정보가 저장된다 [라인 39~43].

BoardForm에서는 mapStateToProps를 이용해서 리듀서(App_reducer)의 selectedBoard를 가지고 와서 화면에 출력한다.

BoardItem에서와 동일하게 mapStateToProps에서는 컴포넌트에서 사용할 리듀서의 변수들을 선언해서 사용한다.

사용할 때에는 this.props.selectedBoard로 사용하면 되고,

여기서는 props가 바뀔 때 발생하는 이벤트인 componentWillReceiveProps 를 이용해서 작성했다 [라인 24].

사용자가 선택할 때 마다 selectedBoard의 내용이 바뀌고,

이때마다 selectedBoard를 바라보고 있는 BoardForm의 componentWillReceiveProps의 이벤트가 실행된다 [라인 24].

componentWillReceiveProps에서 현재 선택된 행을(selectedBoard) 내부 state에 넣어주면 [라인 25],

state를 바라보고 있는 입력상자에 값들이 출력되어 사용자가 수정하게 된다 [라인 31, 32 ].

그리고, 내부 state에 있는 글번호(brdno) 값의 유무로 글 수정과 새 글쓰기가 구분되어 처리 되도록 리듀서에 작성하였다.

수정일 경우에는 selectedBoard의 글번호를 받아서 내부 state에 글번호가 있게 되고 [라인 25],

신규일 경우에는 initialSelectedBoard로 초기화 되어 글번호가 없다 [라인 21].


이상으로 Redex 사용 예제를 정리하였다.

앞서서 React만으로 구현한 예제 코드와 비교하면서 살펴보면 Redex가 얼마나 좋은지 쉽게 이해 될 것이다.

한마디로 다시 정리하면, 코드 복잡도가 줄어들어 버그나 유지 보수에 많은 이점이 있다.


그리고, 이번에는 이 편리한 것을 더 편리하게 사용하게 해주는 redux-actions을 적용한다.

리듀스에서 함수 선언시 역할(type)과 사용할 파라미터를 지정하는 것은 불편한 작업이고,

board_reducer함수에서 switch문의 사용은 여러가지 문제로 사용이 추천되지 않고 있다.


이것을 수정하는 것으로, 먼저 다음과 같이 redux-actions를 설치한다.

소스는 Github의 step2 브랜치(branch)를 다운로드 받으면 된다.


npm install --save redux-actions


설치한 redux-actions에서 사용할 createAction, handleActions 함수를 가지고 온다.

import { createAction, handleActions } from 'redux-actions'; const BOARD_SAVE = 'SAVE'; const BOARD_REMOVE = 'REMOVE'; const BOARD_READ = 'READ'; const BOARD_LIST = 'LIST'; export const board_save = createAction(BOARD_SAVE); export const board_remove = createAction(BOARD_REMOVE, brdno => brdno); export const board_read = createAction(BOARD_READ); export const board_list = createAction(BOARD_LIST); const initialState = { ~~생략 ~~ }; export default handleActions({ [BOARD_SAVE]: (state, { payload: data }) => { let boards = state.boards; ~~생략 ~~ }, [BOARD_REMOVE]: (state, { payload: brdno }) => { let boards = state.boards; return {...state, boards: boards.filter(row => row.brdno !== brdno), selectedBoard: {} }; }, [BOARD_READ]: (state, { payload: brdno }) => { let boards = state.boards; return {...state, selectedBoard: boards.find(row => row.brdno === brdno) }; } }, initialState);

App_reducer.js

createAction은 이상의 코드에서 작성된 것과 같이 그냥 역할(type)만 지정해서 호출하면 해당 역할을 하는 함수가 생성된다.

글번호(brdno)를 파라미터로 받는 경우 board_remove에서 사용한 것처럼 표시해도 되고, 다른 함수들처럼 생략해도 된다.


board_reducer 함수는 handleActions으로 바꾸고

Switch 문 대신에 Json 형식으로 각 역할에 따라 화살표(=>) 함수를 지정해 주면 된다.

데이터가 저장된 state는 파라미터로 제공받고,

글 저장을 위해 필요한 게시글 정보(data),

삭제와 선택을 위해 필요한 게시글 번호(brdno)와 같은 파라미터는 payload 변수의 멤버로 받아서 사용한다.


기존에는 하나의 함수에서 SWITCH문으로 각각의 기능을 처리해서

하나의 boards만 선언하면 되었지만,

handleActions에서는 각각의 함수로 구현하기 때문에 let boards = state.boards가 모든 함수에 사용되었다.

그 외의 코드는 모두 동일하다.


전체 코드(App_reducer.js)를 보면 훨씬 간단하게 작성된 것을 알 수 있다.







'Node.js > React' 카테고리의 다른 글

React 학습자료  (0) 2019.01.23
React(Redux) + Firebase 게시판(CRUD) 만들기  (2) 2018.11.04
React + Redux 게시판(CRUD) 만들기  (1) 2018.11.04
React 게시판(CRUD) 만들기 1  (4) 2018.10.28
React 게시판(CRUD) 만들기 2  (10) 2018.10.28
  1. ㅇㅇ 2020.11.26 17:47

    좋은 글 감사합니다.

+ Recent posts