앞서서 정리한 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 게시판(CRUD) 만들기 1  (4) 2018.10.28
React 게시판(CRUD) 만들기 2  (11) 2018.10.28

개발자가 하면 안 좋은 일 중에

가장 첫번째는 준비 되지 않은 개발자가 팀장이 되는 것 일 것이다.

개발을 잘 하는 것과 좋은 리더 (팀장)이 되는 것은 당연이 다른 것이고, 많은 사람들이 말하는 것이다.

배틀스타 갤럭티카(Battlestar Galactica)라는 미드를 보면(시즌 2, 에피소드 17 -The Captain's Hand 참고)

유능한 엔지니어(개발자)가 준비 없이 리더가 되면 조직을 어떻게 말아 먹는지를 잘 보여준다.

그래도 현실에서는 나이 순, 입사순, 직책 순으로 한번 쯤 하게 되는 것 같다.


다음으로, 하지 말아야 하지만 가장 흔하게 하는 일이 신입 개발자 교육인 것 같다.

중소업체에서는 신입 개발자 교육을 관련 업무의 선배가 진행하게 되는데, 도제식으로 진행하는 경우가 많다.

군대의 사수/부사수처럼 진행하는 경우가 가장 흔할 것 같다.

모든 것이 낮선 신입에게 어떤 것을 가르쳐도 큰 효과가 없고, 여러가지 이유로 아예 기억 조차 못 하는 경우가 허다하다.

그러다 보면,

"아까 말해줬는데", "어제 말해줬는데", "왜 아직도 못하냐", "이런 것도 못하냐"며 짜증을 내게 되고 점점 감정이 격해진다.

가르치는 개발자는 짜증내는 빈도가 늘어나고,

교육 받는 개발자는 반감이 늘어나거나 기가 죽는 것을 많이 봤다.

그리고, 가르치는 사람의 입장에서는 아주 많은 지식을 전달하거나 어려운 지식을 전달하는 것이 아니라고 생각하기 때문에

"이런것도 못해요", "생각 좀 해요" 등의 언어 폭력이 발생한다.

개발을 잘 하는 것과 잘 가르치는 것은 아주 다르다.

전문 교육자가 가르쳐도 제대로 받아들이는 학생이 몇 명 없는데,

비교육자인 개발자가 어떻게 잘 가르칠 수 있을까?

개발자 컨퍼런스나 세미나에 가면 쉽게 볼 수 있는 장면이 전문 용어만 남발하며 혼자서 발표하는 것이다.


한 교수님이 후배 교수나 학생들에게 자주 하신 말씀이 있다.

강의 내용을 학생들이 이해하지 못하면 그건 내용을 제대로 설명하지 못한 교수 잘못이라고 하셨다.

그 분 말씀대로 모두 교수 잘못은 아니겠지만,

상대가 제대로 이해 하지 못했다면 내 설명에 문제가 없는지 고민해봐라는 뜻일 것이다.


교육에서 가장 중요한 건 자존감이라고 한다.

스스로 찾아서 노력할 수 있는 자존감이 중요하다고 하는데,

그 자존감도 무너뜨리고, 회사에 대한 마음도 무너뜨리는 것이 아닐까한다.

교육에 미숙한 선배와 업무에 미숙한 신입끼리 진행되는 교육속에 서로에 대한 부정적인 생각만 커지는 것 같다.


적응하지 못하고 떠난 1년차 개발자와

떠난 개발자를 대신해 입사한 신입 개발자를 교육하는 모습을 보며





'개발 이야기' 카테고리의 다른 글

왼쪽? 오른쪽?  (0) 2019.08.04
오만과 편견 또는 게으름  (0) 2018.11.23
두 마리 소 II  (0) 2018.09.11
배틀스타 갤럭티카(Battlestar Galactica)와 직장 생활  (2) 2018.06.13
4살과 국회의원  (0) 2018.04.12

React는 페이스북에서 제안된 사용자 인터페이스 제작을 위한 자바스크립트 라이브러리의 하나로,

싱글 페이지(SPA-Sigle Page Applications)나 모바일 애플리케이션의 개발에 유용하다.

이 React를 익히기 위해서

React를 이용하여 데이터 입출력(CRUD) 기능을 가진 게시판을 구현한다.

데이터베이스에 연결해서 구현하지 않고, 배열에 데이터를 저장하고 출력하는 방식으로 게시판을 구현한다.

React 기초 문법은 따로 정리하지 않으니, 관련 자료를 읽어보는 것을 좋다.

여기서 구현한 소스는 Github에서 다운로드 받을 수 있다.

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

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

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

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


React 게시판(CRUD) 만들기는 React 설치와 Github 예제를 실행해 보는 방법(만들기 1)과

실제로 이 게시판(CRUD) 예제를 만드는 과정(만들기 2)으로 정리한다.

       React 게시판(CRUD) 만들기 1

       React 게시판(CRUD) 만들기 2


먼저, NodeJS가 설치되어 있어야 한다.

yarn이나 npm중 편리한 것을 사용하면 되고, 여기에서는 npm으로 작성한다.

NodeJS와 npm 설치는 관련 자료가 많기 때문에 찾아보면 되고

여기서는 React 설치부터 시작한다.

운영체제 콘솔창(여기서는 윈도우 CMD)에서 다음 명령어로 create-react-app를 설치한다.

npm install -g create-react-app

create-react-app는 React 프로젝트를 쉽게 시작할 수 있도록 필요한 설정을 해주는 도구이다.

create-react-app를 설치한 뒤에 다음과 같이 create-react-app로 제작할 프로젝트를 생성한다.

여기에서는 react_board라는 이름으로 프로젝트를 생성하였다.


create-react-app react_board


create-react-app으로 React 앱 프로젝트를 생성하면

다음 그림과 같이 간단한 사용법이 출력된다.

yarn을 설치한 경우 npm이란 단어 대신 yarn으로 출력된다.

각 사용법은 따로 익혀두고 그림 하단의 2가지 명령어를 실행한다.

먼저, 생성한 프로젝트 폴더로 이동해서 (cd react_board)

웹 서버를 가동하면(npm start) 다음과 같이 웹 페이지가 실행된다.

웹 페이지에 설명되어 있듯이

src 폴더의 App.js 파일을 편집기로 열어서 다음 코드를 확인한다.

(Edit src/App.js and save to reload.)

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
}

export default App;

HTML 태그와 Javascript, React로 작성된 코드를 볼 수 있다.

Edit src/App.js and save to reload. 문장에서 글자를 원하는데로 수정하고 저장한 뒤에

웹브라우저로 확인하면 수정된 내용이 반영된 것을 확인할 수 있다.


여기까지 React를 실행하는 방법을 간단하게 정리하였다.

기초 문법은 따로 정리하지 않으니, 관련 자료를 읽어보는 것을 좋다.


구현할 게시판 예제는 다음과 같이 단계별로 제작한다.

    1. 데이터 출력 (글 리스트)

    2. 형식에 맞춰서 출력 (글 리스트)

    3. 데이터 입력 (새 글 작성)

    4. 데이터 수정과 삭제 (글 수정 / 삭제)

    5. 기능(컴포넌트)별 파일 구성


이상의 내용을 App.js 파일에 작성하면 되지만

작성 과정을 각각의 예제 파일로 남기기 위해 App1.js ~ App6.js 파일로 구성하고

react-router-dom을 이용하여 별도의 url로 제작하였다.


import React, { Component } from 'react';
import { Route, BrowserRouter as Router } from 'react-router-dom';

import App1 from './App1';
import App2 from './App2';
import App3 from './App3';
import App4 from './App4';
import App5 from './App5';
import App6 from './App6';

// simple list
class App extends Component {
render() {
return (
<Router>
<div>
<Route exact path="/" component={App1}/>
<Route exact path="/App1" component={App1}/>
<Route exact path="/App2" component={App2}/>
<Route exact path="/App3" component={App3}/>
<Route exact path="/App4" component={App4}/>
<Route exact path="/App5" component={App5}/>
<Route exact path="/App6" component={App6}/>
</div>
</Router>
);
}
}

export default App;

App.js

각 컴포넌트는 웹브라우저에서 다음과 같이 컴포넌트 이름으로 접속하여 실행할 수 있다.

       http://localhost:3000/App1

       ...

       http://localhost:3000/App6



이상으로 React 설치와 Github 예제를 실행해 보는 방법을 정리하였고,

다음으로 실제로 이 게시판(CRUD) 예제를 만드는 과정을 정리한다.

       React 게시판(CRUD) 만들기 1

       React 게시판(CRUD) 만들기 2




'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) 만들기 2  (11) 2018.10.28

React를 쉽게 익히기 위해서 데이터 입출력(CRUD) 기능을 가진 게시판을 구현한다.

React 만으로 구현하기 위해 배열에 데이터를 저장하고 출력하는 방식으로 게시판을 구현한다.


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

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

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

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


앞서 간단한 설치와 사용법을 정리하였고, 여기서는 다음과 같이 단계별로 제작한다.

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


    1. 데이터 출력 (글 리스트)

    2. 형식에 맞춰서 출력 (글 리스트)

    3. 데이터 입력 (새 글 작성)

    4. 데이터 수정과 삭제 (글 수정 / 삭제)

    5. 기능(컴포넌트)별 파일 구성


개인적으로 React는 아주 쉽게 (?) 배울 수 있는 라이브러리 이지만

자바스크립트(JavaScript), 특히 ES5 (ECMAScript 5) 이상(ES6)의 문법을 모른다면 배우기 아주 어려운 라이브러리라고 생각한다.

ES5와 ES6의 특징 정도는 알고 있는 것이 좋으니 웹 문서들을 찾아보길 바란다.

여기에서는 별도로 정리하지 않지만 쉽게 React를 익히기 위해 단계별로 가장 쉬운 것 부터 시작한다.


먼저, 주어진 배열의 내용을 출력하는 글 리스트부터 작성한다 (App1.js, App2.js).

앞서서 create-react-app로 생성하고 정리한 예제에서 App.js 파일의 내용을 지우고 다음과 같이 작성한다.

Github에서 다운받은 파일에서는 App1.js로 작성되어 있다.

작성 과정을 각각의 예제 파일로 남기기 위해 단계별로 진행한 내용을 App1.js ~ App6.js 파일로 작성하였지만,

따라 할 때에는 App.js에 코드를 추가하는 방식으로 작성하면 된다.

코드를 살펴보면 state 변수를 가장 먼저 선언하였다 [라인 4].

state 변수는 게시판 데이터를 배열로 가지는 boards 배열을 구성원(Json)으로 가지고 있다.

boards는 글번호(brdno), 작성자(brdwriter), 글제목(brdtitle), 작성일자(brddate)로 구성되고 2개의 데이터가 등록되어 있다 [라인 7~16].

데이터 베이스로 보면 4개의 필드와 2개의 행이 있는 것이다.


이 데이터(state)를 render()에서 출력한다 [라인 21~33].

render()는 React에서 화면을 생성하기 위해 실행하는 이벤트이다.

App 컴포넌트(Component)에 있는 state [라인 4]을 render()에서 사용하기 위해 this.state로 지정한다 [라인 22].

this는 자바스크립트에서 자기 자신(Component)을 의미한다.

this.state에 있는 것 중에서 하나를 가지고 올 때 사용하는 것이 대괄호({})로state에 있는 데이터(boards)를 boards에 저장한다 [라인 22].

대괄호({})는 state에 변수가 많을 때 편하게 사용하는 코드로 다음과 같이 사용해도 된다.

           const { boards } = this.state;

           const boards = this.state. boards;


 주의  const, let, var의 차이는 찾아보길……


가지고 온 데이터(boards)를 map() 메서드를 이용하여 2개 행의 글번호와 작성자를 묶어서 하나의 문자열(list)로 작성하였다 [라인 23~25].

이 값을 화면에 출력한다 [라인 29].


 주의  값을 출력할 때 대괄호({})가 사용되었다. 대괄호는 자바스크립트에서 함수의 범위가 되기도 하고 Json이기도 하지만, 변수내의 수 많은 변수 중 일부를 빼내서 사용하거나 [라인 22], React의 문법으로 태그(div) 사이에서 값을 출력할 때에도 사용된다. 여러 가지 의미로 사용되니 잘 기억해야 한다.

실행 결과는 다음과 같다.

정리하면,

render() 시작부터 return 이전(라인 22~6)까지는 자바스크립트의 영역이고

return 내부는 HTML (React 정의라 조금 다름) 영역으로 볼 수 있다.

HTML영역에서 자바스크립트를 사용할 때에는 대괄호({})를 사용한다.


이상의 코드는 다음과 같이 작성 할 수 있다.

    render() {
        const { boards } = this.state;

        return (
            <div>
            {
                boards.map(function(row){
                    return row.brdno + row.brdwriter ;
                })
            }
            </div>
        );
    }

App2.js

이전과 동일하게 글 번호와 작성자를 묶어서 출력하는 코드이다.

자바스크립트 코드 위치가 바뀌었고 대괄호({})를 사용한다.

데이터를 출력하는 코드는 render와 return 사이보다 return의 HTML 사이에서 많이 사용한다.


두번째 단계로 테이블(table) 테그로 게시판 리스트처럼 출력한다 (App3.js).

DIV 테그를 사용하는 것이 좋지만 CSS 설정 등으로 불필요한 코드 작성이 필요해, 다음과 같이 테이블 테그로 간단하게 작성하였다.

기존 코드에 테이블 테그를 사용하여 리스트 헤드 [라인 7~14]를 작성한다.

그리고, boards에 있는 데이터를 행(tr)으로 출력하도록 작성하였다 [라인 16~18].

다만, 각 행을 BoardItem이라는 컴포넌트를 이용하여 출력하게 작성한다 [라인 27].


BoardItem 컴포넌트에 row라는 변수로 boards의 행(row)을 하나씩 지정해서 넘겨주고 [라인 17]

BoardItem 컴포넌트에서는 이 row를 this.props로 받아서 사용한다.


 주의  컴포넌트 자신이 사용하는 것은 state이고, 부모로부터 받은 것은 props이다. 이 개념을 잘 이해하면 React의 주요 개념 절반을 이해한 것이다.

BoardItem 컴포넌트를 사용하는 것은 React의 특징으로 React에서는 모든 기능을 컴포넌트로 구현하여 사용한다.


이상으로 글 리스트 기능을 구현하면서 React의 컴포넌트 개념을 사용하였다.

React는 기능을 세분화해서 컴포넌트로 구현하는 특징이 있다.

그리고, 부모(호출하는) 컴포넌트가 자식(호출 받는) 컴포넌트에 값을 넘겨주고 받는 방법을 정리하였다.

세번째로 값을 입력받아서 저장하는 글 쓰기 기능을 구현한다 (App4.js).

글쓰기에서는 그림과 같이 글 제목과 작성자를 입력하고 저장(save) 버튼을 누르면 입력한 내용이 baords 배열에 저장되도록 구현한다.

글쓰기 기능을 구현하기 위해서는 HTML 컨트롤과 이벤트가 React와 연동되는 방법을 알아야 한다.

React(특히 이 예제)에서 가장 어려운 부분이라고 생각하니 코드의 의미에 대해서 제대로 이해하고 넘어가야 한다.


글쓰기 기능을 구현할 BoardForm 컴포넌트를 생성하고 [라인 21],

글쓰기에서 사용할 state 변수와 handleChange, handleSubmit 이벤트 핸들러를 작성하였다.

1 라인에 선언된 state는 App 컴포넌트에서 사용하는 state 이고, 22 라인에서 선언한 state는 BoardForm내부에서 사용하는 state이다.

state는 컴포넌트 내부에서 사용할 변수로 이름이 고정되어 있다.


두 이벤트 핸들러는 화살표(=>) 함수로 작성하였다.

화살표 함수가 아닌 전통적인 함수로 작성하면 bind등의 제법 복잡한 처리를 해야 한다.

handleChange는 사용자가 값을 입력할 때 마다(onChange 이벤트) 입력하는 값을 받아서 state 변수에 각 컨트롤의 이름(brdtitle, brdwriter)으로 저장한다 [라인 24].

handleChange핸들러의 e는 자바스크립트의 change 이벤트에서 파라미터로 넘어오는 Event를 의미하고 e.target은 현재 이벤트가 발생한 개체,

즉 값을 입력하는 입력상자를 의미한다.

두 개의 입력상자가 각각 brdtitle, brdwriter로 지정되어 있기 때문에 [라인 39, 40] 각각의 이름으로 변수가 생성되어 사용자가 입력한 값이 저장된다.

입력 받는 값이 글 제목(brdtitle)과 작성자(brdwriter)이므로, brdtitle, brdwriter로 저장된다.

즉,
    state = {
        brdtitle: 값,
        brdwriter: 값
    }
으로 저장된다.

저장시 "this.state.brdwriter=값" 이나 "this.state[brdwriter]=값" 으로 저장하지 않고 setState 함수를 사용하여 저장한다 [라인 25].

React의 규칙이니 준수해야 하고, 이렇게 하지 않으면 웹 브라우저 콘솔 등에 경고가 출력된다 (다음 그림과 전후 설명 참조).


라인 39 와 40 에서 입력 상자와 handleChange를 연결하였다.

연결시 handleChange에 this를 붙여 사용하는데, 컴포넌트 내의 변수나 함수(이벤트 핸들러)를 참조할 때에는 this를 붙여야 한다.


handleSubmit은 Form 태그가 값을 서버로 전송할 때 발생하는 이벤트를 처리하기 위한 핸들러이다 [라인 38].

즉, 사용자가 값을 입력하고 저장할 때 발생한다.

실제로 서버로 보낼 것이 아니기 때문에 preventDefault로 이벤트를 중지한다 [라인 22].

그리고 onSaveData 함수를 호출하여 데이터를 저장한다.


onSaveData()는 BoardForm 컴포넌트에 있지 않고 [라인 32],

부모인 App 컴포넌트에 있기 때문에 this.props.onSaveData()로 사용한다 [라인5, 15].

onSaveData()는 부모로 부터 파라미터(this.props)로 받았다 [라인 15].

부모로부터 받은 것은 값이든 함수이든 항상 props를 사용해야 한다 [라인 32].

그리고 저장할 값은 stae에 있으니 함수를 호출하면서 this.state를 넘겨준다 [라인 32].


부모 (호출하는) App 컴포넌트에서는 값을 입력 받을 적당한 위치에 BoardForm 컴포넌트를 생성한다 [라인 15].

컴포넌트의 생성은 HTML 태그처럼 <BoardForm/>로 작성하면 된다.

BoardForm를 생성하면서 파라미터로 handleSaveData() 함수를 onSaveData()라는 이름으로 넘겨준다.

이것을 자식(호출받는) BoardForm에서는 this.props.onSaveData()로 호출한다 [라인 32].

호출 받은 부모의 handleSaveData()에서는 setState를 이용하여 [라인 6] state에 있는 baords배열에 값을 추가(concat)한다 [라인 7].

baords배열에 concat으로 추가하고 이것을 baords라는 이름으로 저장하는 방식으로 작성한다.


저장시 글 번호(brdno)와 작성일자(brddate)을 생성한다[라인 7].

작성일자는 자바스크립트 Date 클래스로 현재 날짜를 입력하고, 글 번호는 state에 추가한 변수 maxNo의[라인 2] 값을 사용한다.

기본적으로 baords에 데이터 2 건이 있으므로 maxNo은 3의 값을 가지고 있고, 글을 추가한 후에 1 증가(++) 한 값(다음 글번호)을 저장한다.


 주의  웹 브라우저에서 이상의 예제인 App4를 실행하면 다음과 같은 경고를 볼 수 있다.



이 경고는 state에 값을 바로 대입한 코드를[라인 7] setState 함수로 수정하라는 것으로 다음과 같이 작성 한다.

    this.setState({
        maxNo: this.state.maxNo+1,
        boards: boards.concat({brdno: this.state.maxNo, brddate: new Date(), ...data })
    });

게시물(boards)을 추가하듯이 [라인 7],

maxNo의 값을 증가시키고, maxNo라는 이름으로 setState함수를 이용하여 저장한다.


실행해서 새로운 글이 잘 작성되는지 확인한다.


React의 값을 주고 받는 방식도 잘 기억해야 하지만,

자식(BoardForm)에서 작성한 값을 부모에게 보내어 저장하는 구조도 잘 기억해야 한다.

부모(App)의 state에 있는 boards에 모든 값을 저장하기 때문에 부모에게 사용자가 입력한 값을 전송한다.

부모의 state에 값을 저장하고,

이 state에 변경이 생기면

state(boards)의 값을 참조하는 또 다른 자식인 BoardItem에 값들이 자동으로 보내어져 추가된 글이 화면에 출력된다.

이 것이 React의 특징 중 하나이다.

일반적인 프로그래밍에서는 데이터를 추가하거나 수정하면 화면 갱신을 하도록 별도의 처리를 해야 하지만

React에서는 state에 변경사항이 생기면 관련 내용이 자동으로 반영된다.

익숙하지 않은 사람에게는 아주 헷갈리는 기능일 수 있다.


네 번째로 데이터를 수정하고 삭제하는 기능을 구현한다 (App5.js).


삭제 기능은 글을 추가하는 것과 같은 원리로 구현되니 상세한 설명은 생략하고 개념만 정리한다.

(직접 해보는 것이 실력 향상에 도움이 된다.)

삭제는 다음 그림처럼 글 리스트의 각 항목(BoardItem)에 삭제 버튼을 두고 [라인 37],

사용자가 삭제 버튼을 클릭하면 부모에 저장된 boards에서 해당 글을 삭제한다 [라인 3].

삭제 할 때는, 사용자가 선택한 글 번호(brdno)에 해당하는 글을 찾아서 삭제한다.

배열에서 값을 삭제하는 것은 fiter 사용이 권장된다 [라인 5].

다음 코드에서 handleRemove를 따라 다니면 보다 쉽게 코드를 이해 할 수 있을 것이다. (라인 37 > 24 > 26 > 17 > 3 > 5 순)


글 수정은 글 항목(행)들 중에 하나를 선택하면(handleSelectRow)

선택된 행의 값들을 사용자가 수정할 수 있도록 입력상자(BoardForm)에 뿌려주고,

사용자가 수정 후 저장 버튼을 클릭하면, 글번호의 값이 있으면 수정, 없으면 신규 등록으로 구현한다.

개념은 새글 작성과 동일하지만 글 수정은 제법 많은 변경과 추가가 이루어져 모든 코드를 글로 정리하기 어렵다. (실제로는 가장 어려운 부분이 여기다)

정리하더라도 읽으면서 혼란해지기 때문에 주요 코드와 개념만 정리한다.

나머지는 … (이 부분은 이해되지 않는 경우 넘어가도 된다. Redux에서 좀더 쉽고 간단하게 구현한다.)

handleSelectRow함수를 중심으로 살펴 보면, 그나마 보다 쉽게(?) 코드를 이해 할 수 있을 것이다.

사용자가 글 항목(행)들 중에 하나를 선택하면(handleSelectRow) [라인 58 => 50 => 38 => 25]

선택된 행의 값들을 사용자가 수정할 수 있도록 입력상자에 뿌려주어야 한다.

입력상자는 BoardForm에 있기 때문에 BoardForm을 부모(App) 컴포넌트가 알고 있어야 한다.

즉, 자식(BoardForm)의 핸들러를 가지고 있어야 한다.

각 컴포넌트의 핸들을 가지고 오는 속성 ref를 this.child에 보관하는 방법을 사용한다 [라인 34].


부모는 선택한 행의 데이터를 자식 (this.child)의 handleSelectRow를 호출하면서 파라미터로 넘겨준다 [라인 26].


입력 폼(BoardForm)에서는 부모로부터 받은 값을 그대로 state 변수에 넣어준다 [라인 76].

파라미터로 받은 값의 구조가 Json이기 때문에 이전처럼[라인 83~85] “변수: 값”의 구조로 지정하지 않고 한번에 넣어 준다 [라인 76].

이 state의 변수를 입력상자가 바로 보고 있도록 수정해서 [라인 92, 93] state 값이 변경되면 자동으로 바뀌도록 작성했다.

BoardItem과 같은 원리이다.


사용자가 수정 후 저장 버튼을 클릭하면 [라인 79]

부모로 데이터를 보내서 [라인 81]

글 번호(brdno)가 있으면 [라인 11] 수정, 없으면 신규 등록으로 구현한다.

신규 등록은 앞서 정리했고,

수정은 글 번호가 같은 행을 찾아서 (data.brdno === row.brdno) 행을 바꾸는 방식으로 구현한다 [라인 18].


마지막으로 사용자가 입력한 값은 지워 주는데

BoardForm에서 state 변수내의 값 변수들 값을 지워주면 화면에서도 지워진다.

BoardItem과 같은 원리로 입력 상자들이 state를 바라보고 있기 때문에 가능한 것이다.

React에서는 HTML 태그들이 자바스크립트에 바인드(Bind) 된 것처럼 작동한다.


이상으로 React의 기본적인 사용법을 게시판 예제로 정리하였다.

React 자체는 state, props만 알면 그렇게 어렵지 않지만

모든 기능을 세분화해서 컴포넌트로 작성하고, 값이 컴포넌트를 오가면서 코드가 아주 복잡해 지고 개념이 어려워 진다.

(App 컴포넌트의 리스트 부분도 BoardList로 별도로 제작해야 한다. 직접 해보길…)


값을 한 곳에 넣고 처리할 수 있도록 함수를 작성해 두고

이 함수만 각 컴포넌트에서 호출한다면 아주 쉽고 깔끔하게 코드를 작성 할 수 있을 것이다.

이러한 기능을 제공하는 라이브러리 중에 하나가 ReduxRedux예제는 별도로 작성하였기 때문에 여기에 정리하지 않는다.


마지막 예제는 각 컴포넌트를 별도의 파일로 작성하는 것이다 (App6.js).

코드 양이 많기 때문에 컴포넌트는 각각의 파일로 작성하고 export 를 이용하여 다른 파일에서 사용할 수 있도록 한다.

각 컴포넌트를 사용할 컴포넌트에서는 import로 가져와서 사용한다.


메인 컴포넌트인 App을 포함해서, BoardItem(BoardRow)과 BoardForm의 총 3개의 컴포넌트가 사용되었다.

따라서, 다음과 같이 각각의 파일을 작성하였다.

import React, { Component } from 'react';

class BoardRow extends Component {


}

export default BoardRow;

App6_BoardItem.js


import React, { Component } from 'react';


class BoardForm extends Component {

}

export default BoardForm;

App6_BoardForm.js

import React, { Component } from 'react';
import BoardForm from './App6_BoardForm';
import BoardItem from './App6_BoardItem';

class App6 extends Component {

    render() {
        const { boards, selectedBoard } = this.state;

        return (
            <div>
                <BoardForm selectedBoard={selectedBoard} onSaveData={this.handleSaveData}/>
                <table border="1">

                    {
                        boards.map(row =>
                            (<BoardItem key={row.brdno} row={row} onRemove={this.handleRemove} onSelectRow={this.handleSelectRow} />)
                        )
                    }
                    </tbody>
                </table>
            </div>
        );
    }
}

export default App6;

App6.js

주의 - BoardItem은 파일에서는 BoardRow로 작성하고, import 할 때에는 BoardItem로 이름 붙여 사용했다. 이렇게 사용해도 된다는 의미로 사용한 것이다.


마지막 예제에는 컴포넌트 별로 각각의 파일을 작성한 것 외에 몇 가지 React 기능을 더 추가해서 작성했다.

네 번째 예제(App5)는 React의 특징을 제대로 표현하지 못하였고, 값이 여러 컴포넌트를 오고 가면서 상당히 헷갈리는 예제다.

이 예제를 state를 이용하여 구현하였다.


부모(App6)에서 수정된 코드는 다음과 같다.

class App6 extends Component {

    state = {
        maxNo: 3,
        boards: [        ],
         selectedBoard:{}
    }
   
    handleSelectRow = (row) => {
        this.setState({selectedBoard:row});
    }
    ~~ 생략 ~~
    render() {
        const { boards, selectedBoard } = this.state;

        return (
            <div>
                <BoardForm selectedBoard={selectedBoard} onSaveData={this.handleSaveData}/>
                ~~ 생략 ~~
            </div>
        );
    }
}

행이 선택되면 handleSelectRow에서 선택된 행의 값을 모두 state의 selectedBoard에 저장한다.

이 selectedBoard을 입력폼(BoardForm)으로 전달한다.

선택된 행(selectedBoard)을 입력폼(BoardForm)의 shouldComponentUpdate 이벤트에서 처리하는 방식으로 구현한다.


이전 방식은 React의 특징인 state를 이용하여 컨트롤에 값을 보여주고,

사용자가 입력한 값을 state에 저장해서 구현했고,

이번에는 다음과 같이 shouldComponentUpdate 이벤트와 입력 상자에 대한 컨트롤(ref)을 이용해서 구현한다.

입력폼(BoardForm)에서는 컴포넌트가 업데이트 될때 발생하는 shouldComponentUpdate 이벤트에서 전달 받은 selectedBoard의 값을 화면에 출력한다 [라인 5].

shouldComponentUpdate와 같은 React의 이벤트는 이 문서를 참조하면 된다.


shouldComponentUpdate 이벤트에서 글 번호의 값이 있으면 [라인 7]

글 수정 상황이니 selectedBoard의 값을 입력상자에 넣어주고 [라인 13, 14]

값이 없는 것은 초기화 상태이니 입력 상자에 빈 문자열을 넣어준다 [라인 8, 9].


사용자가 값을 입력하면 이전에는 Chang 이벤트에서 값을 state에 보관해서 처리했지만 이번에는 ref를 사용하여 컨트롤을 참조하는 방식으로 구현했다 [라인 35, 36].

 주의  이 방식은 추천하는 방식이 아니지만 React의 이해(ref)를 위해 사용한 코드이다.


저장 버튼을 누르면 사용자가 입력한 값을 저장할 구조에 맞추어 Json 형식으로 생성한다 [라인 21~28].

글 번호(selectedBoard.brdno)가 있으면 수정이니 기존 데이터(selectedBoard)에서 글 번호와 작성일자를 가져오고 [라인 26, 27],

없으면 신규이니 사용자만 입력한 값만 Json으로 저장하면 된다.


이상으로 React를 이용한 게시판 제작을 5 단계로 구현하였다.

React는 특이한 방식으로 개발하여 익히는데 혼란을 주지만 개념을 잘 잡으면 쉽게 웹 개발을 할 수 있다.

더욱이 멀티 OS 개발을 쉽게 하는 React Native의 기본 문법이라 더욱 매력적이다.





'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

DirectTalk9 Web과 PC 버전은 이전에 작성한 Android 버전을(Firebase Firestore 예제) 변환한 예제로,

Web은 React (Redux)을 익히기 위해서

PC 버전은 Electron을 학습하기 위한 예제로 제작하였다.

특히, PC 버전은 Web 버전과 동일한 코드로 작성되었지만 PC 환경에 맞추어 수정하였다.

현재는 기본적인 채팅 기능 위주로 구현되었고, 계속 버그 수정 및 기능을 보강할 예정(?)이다.

디자인은 능력이 되지 않아서 Google's Material Design 가이드를 준수하는 material-ui.com을 사용하였다.


먼저, Web 버전을 설치하기 위해서는 NodeJS와 git이 설치되어 있어야 한다.

NodeJS와 git 설치는 찾아보길 바라고 여기에서 정리하지 않는다.


DirectTalk9 Web 버전은 다음과 같이 5 단계의 과정으로 설치를 진행한다.

1. 소스 다운로드: git clone https://github.com/gujc71/DirectTalk9_web.git
2. 관련 라이브러리 설치: npm install
3. Firestore.js에 접속 정보 입력
4. Firebase 관련 서비스 (Authentication, Storage, Firestore) 활성화
5. 실행: npm start


상세하게 정리하면, 콘솔(cmd) 창에서 github의 소스를 다운로드 받는다.

git clone https://github.com/gujc71/DirectTalk9_web.git

소스를 다운로드 받은 폴더로 이동(cd DirectTalk9_web) 해서 필요한 라이브러리들을 설치한다.

npm install

DirectTalk9 Web 버전은 React, Redux, Material-ui, ImageTool (image Resize)등이 사용되었고,

package.json 파일에서 설치된 라이브러리를 확인할 수 있다.


설치한 폴더(DirectTalk9_web) 하위의 src\reducer 폴더에 있는 Firestore.js 파일에

다음 그림과 같이 Firebase 설정 정보를 채워준다.

Firebase 설정 정보를 가져오는 방법은 이 문서나 문서내의 우측에 있는 동영상을 참고 하면 된다.

이전에 작성된 NodeJs 예제 문서를 참고해도 된다.


다음으로 Firebase console에 접속해서 Authentication, Storage, Firestore를 사용할 수 있도록 해야 한다.

Firebase는 Gmail 계정만 있으면 사용할 수 있다.

웹 브라우저에서 Firebase console에 접속해서 Gmail 계정으로 로그인하면 된다.


로그인을 위한 인증(Authentication)은 다양한 방식이 제공되는데

, "이메일/비밀번호"을 사용할 수 있도록 하고, 설정 방법은 이전에 작성한 Firebase 설정 문서를 참고하면 된다.

데이터를 저장할 Cloud Firestore는 [테스트 모드로 시작]을 선택하면 되고, 설정 방법은 이전에 작성한 Firebase 설정 문서를 참고한다.

파일을 저장할 Storage는 그림과 같이 콘솔의 왼쪽 메뉴에서 Storage를 선택하고, 화면 중앙에 나타나는 [시작하기] 버튼을 눌러주면 된다.


설정을 마쳤으면 콘솔 창에서 NodeJS 서버를 실행한다.

npm start

웹 브라우저에서 다음과 같은 화면이 실행된다.


처음에는 가입된 회원이 없으니 계정으로 사용할 이메일과 비밀번호를 입력하고,

"Create Account" 버튼을 눌러서 회원 가입을 하면 된다.

테스트 용이기 때문에 이메일 주소는 꼭 사용하는 이메일일 필요는 없다.

user1@test.com 와 같이 입력해도 된다.


로그인을 하고 나면 사용자 리스트(UserList) 화면이 나타난다.

왼쪽 메뉴에서 채팅방 리스트(Rooms)를 실행한다.


몇 개의 계정을 생성하고, 대화 상대를 선택해서 채팅을 하면 된다.


브라우저의 크기를 줄이면 반응형으로 변하는 화면을 볼 수 있다.





다음으로, PC 버전을 설치하는 방법은 Web 방식과 거의 유사하다.

1. 소스 다운로드: git clone https://github.com/gujc71/DirectTalk9_pc.git
2. 관련 라이브러리 설치: npm install
3. Firestore.js에 접속 정보 입력
4. Firebase 관련 서비스 (Authentication, Storage, Firestore) 활성화
5. 실행: npm run dev


다운로드 받는 주소가 DirectTalk9_web에서 DirectTalk9_pc로 바뀌고

라이브러리 설치 등은 모두 동일하다 (Electron 설치로 라이브러리 설치 시간이 더 많이 걸린다).

Firebase 서비스를 사용하기 위해 콘솔에서 지정하는 설정은 Web 버전에서 진행한 경우 별도로 지정하지 않아도 된다.


Web 버전과 가장 큰 차이점은 실행 방식이다.

Web 버전은 npm start로 시작하지만

PC 버전은 다운로드 받은 설정이 npm run dev로 실행하게 되어 있다.

npm run dev로 실행하면 웹과 응용프로그램이 동시에 실행되고,

npm run build로 빌드를 진행하고

npm run electron을 실행하면 응용프로그램만 실행된다.


 참고  React (특히 create-react-app)와 Electron을 같이 실행하는 방법은 설정이 복잡하다.

여기에서는 Christian Sepulveda이 제시한 방법을 사용했으니, 상세한 설정은 관련 자료를 읽어보면 된다.


실행 화면은 다음과 같다.

Web 버전에서는 왼쪽 메뉴로 구성했던 사용자, 채팅방, 계정 정보를

그림과 같이 탭(Tab)으로 제작하였다.

사용법은 웹 버전과 동일하지만 다음 그림처럼 대화 상대별로 채팅창을 실행할 수 있다.

Web 버전에서는 웹 브라우저내의 다이알로그로 구성했기 때문에 하나의 채팅창만 실행할 수 있고,

PC 버전에서는 별창(new Window)으로 구현하여 동시에 여러 채팅창을 실행 할 수 있다.


이상으로 DirectTalk9의 Web버전과 PC 버전을 설치하는 방법을 정리하였다.

Android 버전과는 채팅 할 수 없다.

Android 버전은 메시지 전송 시간을 서버 시간을 사용하고(serverTimestamp),

Web과 PC 버전은 클라이언트(JS의 new Date) 시간을 사용하기 때문에 오류가 발생한다.

Web과 PC 버전에서 서버 시간을 사용하도록 작성하면 되지만 Firebase 설치와 설정이 필요해서 제외하였다.

이외에도 몇 가지 버그가 있고, 구현해야 될 기능도 많지만 제법 그럴듯하게 작동하는 예제(?)로,

응용해서 사용한다면 개발에 도움이 될 것 같다.


작성된 데이터 Cloud Firestore에 저장하고, 구조는 Android 버전과 동일하다.


먼저 사용자(users)와 채팅방(rooms) 정보를 저장하는 컬렉션(collection)으로 구성하였다.

사용자(users)에는 사용자 고유 식별값(uid)과 계정(이메일, userid), 상태 메시지(usermsg), 사용자 이름(usernm)으로 구성한 일종의 회원 테이블이다.

토큰(token)은 Firebase에서 제공하는 것으로 FCM(Firebase Cloud Message)을 사용하기 위해 필요한 필드로

Andoid 버전에서는 사용하지만 Web과 PC 버전에서는 아직 구현하지 않아서 사용하지 않는다.

로그인과 회원에 대한 기본 정보는 Firebase 인증(Authentication)으로 처리하지만

Firebase 인증으로 처리되지 않는 정보들을 users 컬렉션에서 저장하고 관리한다.



채팅방(rooms)은 채팅 정보를 관리하는 컬렉션으로,

마지막 메시지(lastmessage), 채팅방 참여자(users)의 필드로 구성된다.

메시지(messsge)는 사용자가 작성한 메시지로 각 채팅방의 하위  컬렉션으로 작성되고,

메시지 문장(msg), 메시지 종류(텍스트, 이미지, 파일), 작성시간(timestamp), 작성자(uid)로 구성했다.

메시지를 읽은 사용자(readusers)는 메시지별 안 읽은(unread) 사용자 수(참여자수-읽은 사용자수)를 보여주는데 사용한다.

마지막 메시지(lastmessage)와 관련된 필드는 메시지(messsge) 컬렉션과 동일한 구조를 가지고,

채팅방 리스트를 출력할 때 마지막 메시지 내용과 시간을 보여주기 위해 사용했다.

채팅방 참여자(users) 사용자의 고유값(uid) 값을 키(필드)로 사용한다.


많은 Android앱들이 죽지 않는 서비스를(Immortal Service) 이용하는데,

개발 중인 제품에 필요하여 인터넷을 검색해 사용해 보니 제대로 작동하지 않거나 관련 자료가 부족해서 직접 개발하였다 (해야 했다...).

국내외국 자료를 검색해서 찾은 결과들을 토대로 구현하였고,

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

(따라서, 충분하게 검증된 기술이 아니기 때문에 문제가 발생할 수 있으며, 관련 문제를 공유하거나 더 나은 방법을 공유해줬으면 하는 기대로 이 글을 작성합니다.)


Android의 죽지 않는 서비스는 일반적인 서비스를 startForeground로 실행하면 간단하게 구현 할 수 있다.

이 방법의 문제는 Notification을 이용하여 알림창에 표시해야만 한다는 것이다.

알림창을 제거하기 위해 다양한 방법들이 논의 되었지만 [참고],

Android Oreo 버전에서는 작동되지 않는다.


이 내용들을 토대로 운영체제가 서비스를 죽이면 다시 실행하는 방법으로 죽지 않는 서비스를 구현하였다.

다시 실행시키는 방법은 알람(Alarm)을 이용한다.

서비스가 종료되면(onDestroy) 1초 뒤에 알람이 실행되게 하고,

알람에서 해당 서비스를 다시 실행한다.

이때, Android Oreo에서는 서비스의 백그라운드 실행을 금지하기 때문에 문제가 발생한다.

따라서, Oreo 이전과 이후 버전으로 나누어서 구현한다.


Oreo 이전 버전은 startService로 해당 서비스를 실행하고,

Oreo 이후 버전은 한 단계를 더 거쳐서 구현한다.

Oreo에서는 서비스를 백그라운드(Background)에서 실행하는 것을 금지하기 때문에

포그라운드(Foreground)에서 실행해야 한다.

즉, 알람은 백그라운드이기 때문에 서비스를 실행할 수 없다.

따라서 알람에서 포그라운드인 startForegroundService(Notification)으로 서비스를 실행하고, 이 서비스 안에서 해당 서비스를 실행한다.

그리고, startForegroundService으로 실행한 서비스는 죽이는 방식으로 구현한다.

그림으로 정리하면 다음과 같다.


기본 개념을 코드로 구현하면,

먼저, 앱을 실행하고 종료(onDestroy)할 때 서비스(RealService)를 종료(stopService)한다.

public class MainActivity extends AppCompatActivity {
private Intent serviceIntent;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

if (RealService.serviceIntent==null) {
serviceIntent = new Intent(this, RealService.class);
startService(serviceIntent);
} else {
serviceIntent = RealService.serviceIntent;//getInstance().getApplication();
Toast.makeText(getApplicationContext(), "already", Toast.LENGTH_LONG).show();
}
}

@Override
protected void onDestroy() {
super.onDestroy();
if (serviceIntent!=null) {
stopService(serviceIntent);
serviceIntent = null;
}
}

MainActivity.java


무엇인가 백그라운드에서 서비스를 제공할 서비스 클래스는 RealService.java이다.

메신저 앱의 경우 메시지를 수신하는 등의 처리를 구현하는데, 여기서는 1분에 한번씩 시간을 알려주도록 (Notification) 했다.


가장 처음에는 앱(MainActivity)이 실행되기(Foreground) 때문에 RealService는 startService로 실행하면 된다.

다만, 앱이 다시 실행되었을때 충돌을 방지하기 위해 RealService에 serviceIntent를 static 변수로 두어 서비스 실행시 intent를 가지고 있도록 한다.

serviceIntent의 값이 있으면 이미 서비스가 실행 중이니 넘어가고 없을 때만 서비스를 실행한다.


앱을 종료(onDestroy)할 때 서비스(RealService)를 종료(stopService)하여

서비스의 종료(onDestroy) 이벤트가 실행되게 작성한다.

public void onDestroy() {
super.onDestroy();

serviceIntent = null;
setAlarmTimer();
Thread.currentThread().interrupt();

if (mainThread != null) {
mainThread.interrupt();
mainThread = null;
}
}

protected void setAlarmTimer() {
final Calendar c = Calendar.getInstance();
c.setTimeInMillis(System.currentTimeMillis());
c.add(Calendar.SECOND, 1);
Intent intent = new Intent(this, AlarmRecever.class);
PendingIntent sender = PendingIntent.getBroadcast(this, 0,intent,0);

AlarmManager mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
mAlarmManager.set(AlarmManager.RTC_WAKEUP, c.getTimeInMillis(), sender);
}

RealService.java

RealService 서비스가 종료(onDestroy)될때 마다 1초뒤 알람이 실행되게 작성한다(setAlarmTimer).

public class AlarmRecever extends BroadcastReceiver{

@Override
public void onReceive(Context context, Intent intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent in = new Intent(context, RestartService.class);
context.startForegroundService(in);
} else {
Intent in = new Intent(context, RealService.class);
context.startService(in);
}
}

}

AlarmRecever.java

알람(AlarmRecever)에서는

Android가 Oreo 이후 버전이면 (SDK_INT >= O)

Android에서 제공하는 죽지않는(Foreground) 서비스인 RestartService를 startForegroundService로 실행한다.

이전 버전은 RealService 서비스를 실행하면 끝이다.


Oreo 이후 버전은 startForegroundService로 실행한 RestartService의 코드는 다음과 같다.

public class RestartService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "default");
builder.setSmallIcon(R.mipmap.ic_launcher);
~~ 생략 ~~

Notification notification = builder.build();
startForeground(9, notification);

/////////////////////////////////////////////////////////////////////
Intent in = new Intent(this, RealService.class);
startService(in);

stopForeground(true);
stopSelf();

return START_NOT_STICKY;
}

RestartService.java

RestartService 의 코드는 Android에서 제공하는 죽지않는(Foreground) 서비스의 코드이다.

포그라운드(Foreground)로 실행되지만, Android 알림창에 표시되는 문제가 있다.

따라서, RestartService를 startForeground로 실행하고,

RealService를 실행한다 (startService).

그리고 실행된 RestartService를 stopForeground와 stopSelf를 실행하여 RestartService를 종료한다.

RestartService가 실행되고 종료되는 시간이 짧기 때문에 알림창에는 표시가 생기지 않는다.


이외에 부팅후 실행을 위한 코드(RebootRecever)도 있지만 알람과 동일한 코드라 생략한다.


이상의 죽지 않는 서비스를 구현하기 위해 필요한 2개의 서비스와 2개의 Recever를

AndroidManifest.xml 파일에 다음과 같이 등록해야 한다.

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<application
~~ 생략 ~~
<activity android:name="com.damonet.service9.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service android:name="com.damonet.service9.RealService"
~~ 생략 ~~
<service android:name="com.damonet.service9.RestartService"
~~ 생략 ~~
<receiver android:name="com.damonet.service9.AlarmRecever"/>

<receiver
android:name=".RebootRecever"
~~ 생략 ~~
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>

AndroidManifest.xml

추가적으로 부팅이 완료되면 서비스를 시작하기 위하여 부팅이 완료되었다는 것을 수신할 권한(permission)을 등록한다(RECEIVE_BOOT_COMPLETED).

또, Oreo API 28 부터는 FOREGROUND_SERVICE권한을 등록해야 포그라운드(Foreground) 서비스를 실행할 수 있다.


테스트 결과

Oreo 이전 버전에서는 큰 이상을 찾을 수 없었고,

Oreo에서는 2~5분 마다 서비스가 다시 실행되는 것을 확인 할 수 있었다.


---------------------------- 2019년 8월 19일 추가 ----------------------------

2~5분 마다 서비스가 다시 실행되는 문제는 절전 모드(doze) 문제로,

REQUEST_IGNORE_BATTERY_OPTIMIZATION를 이용하여 절전 모드를 사용하지 않는 예외 앱으로 처리하면 된다.

상세한 내용은 다른 자료를 참고 하고,

이 기능은 채팅 또는 통화 앱 등에만 허용하는 것으로, 메신저가 아니라면 웹 스토어 등록이 거절 될 수 있다 (참고).


먼저, AndroidManifest.xml에 다음 권한을 추가한다.

<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>

그리고, MainActivity.java 파일에 절전 모드를 해제하는 권한을 얻는 코드를 추가해 준다.

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

PowerManager pm = (PowerManager) getApplicationContext().getSystemService(POWER_SERVICE);
boolean isWhiteListing = false;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
isWhiteListing = pm.isIgnoringBatteryOptimizations(getApplicationContext().getPackageName());
}
if (!isWhiteListing) {
Intent intent = new Intent();
intent.setAction(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + getApplicationContext().getPackageName()));
startActivity(intent);
}

MainActivity.java

추가된 코드는 기존의 github에 추가되어 있고,

계속 테스트 중이지만 현재까지 성능 문제 없이 잘 사용하고 있다.




두명의 대리가 있었다.

160cm 언저리로 키도 비슷하고, 축구를 즐기는 취미도 비슷했다.

한 명은 7년이라는 직장 생활을 한 회사에서 보냈고

다른 한명은 다른 회사에서 1년 정도 일하다 경력으로 입사해 6년째 둘이 친구로 지냈다.

한 회사에만 있은 대리는 충청도라서 그런 거야 라는(?) 말과 함께 모든 것이 느리고 느긋하다.

다른 한 대리는 항상 적극적이고 경쟁적이기도 했다.

한 회사에서 시작해서 인지, 시작하는 모습을 기억하는 사람들과 함께 지나칠 정도로 친했고,

누가 무슨 말을 하든 예란 말과 함께 넘어가기 일 수 였다.

때로는 그 위의 과장이나 차장이 말을 심하게 할 때도 있지만 이내 웃으며 넘어갔다.

다른 한 대리는 누군가 자신에 대해 조금이라도 부정적이면 가시를 곤두세우는 성격이었다.

(나이가 약 20년 정도 차이 나는 나에게도 마찬가지였다.)

한명은 나무라면 그냥 자신의 잘못이고

한명은 나무라면 따질 건 다 따진 후에 자신의 잘못을 부정하는 것이 대부분이었다.

그래서 직원들이 둘을 대하는 태도가 달랐다.

한명은 편하고, 다른 한명은 모두 불편해 했다.

다른 한명은 다른 대리에게 그렇게 하지 말라고, 그러니 모두가 널 무시한다고 충고했지만

둘은 그렇게 각자의 성격대로 살았다.


어느 날, 팀장이 조금 더 회사에 있었던 대리를 조그만 프로젝트의 PM으로 임명했다.

그리고, 다른 대리는 다른 주임에게 저 친구가 있는 이상 모든 기회는 그 친구에게 먼저 갈 것이고,

이 회사에서 나는 두 번째 기회만 있다는 말을 하며, 친구가 소개한 회사로 떠나갔다.


6년간 많은 직원이 입사하고 떠나갔다.

그 속에 남아 있는 이 친구는 묵묵이 일을 했고, 앞으로도 그렇게 할 것이다.


과연 둘 중 어떤 대리가 회사에 더 도움이 되었고,

자신의 삶에 충실했는지? 더 나은 사람인지 알 수 없다.

참 다른 두 개의 개성과 삶이 있었다.



회사를 떠나고 내 생일이라고 내가 좋아하는 달달한 커피와 간식 세트 쿠폰을 보내온 걸 보면서….

'개발 이야기' 카테고리의 다른 글

오만과 편견 또는 게으름  (0) 2018.11.23
개발자가 하면 안 좋은 일  (2) 2018.10.28
배틀스타 갤럭티카(Battlestar Galactica)와 직장 생활  (2) 2018.06.13
4살과 국회의원  (0) 2018.04.12
두 마리 소  (0) 2017.10.13

Firebase는 Realtime Database외에 Cloud Firestore (beta)라는 클라우드 기반 데이터베이스 솔루션을 제공한다.

구글에 따르면 Cloud Firestore는 유연하고 확장 가능한 NoSQL 클라우드 데이터베이스로,

두 데이터 베이스에 대한 차이는 Firebase 문서에 잘 정리되어 있다.


이번에는 앞서 정리한 Realtime Database기반의 예제를 Cloud Firestore로 변환하여 구현한다.

두 데이터 베이스의 간단한 변환은 Node.js 예제로 정리하였고, 여기서 확인할 수 있다.

Node.js 예제Firebase 문서에서 알 수 있듯이 기본적인 입출력이나 사용법은 둘 다 비슷하다.

Node.js 예제에서 Cloud Firestore가 검색 기능과 정렬 기능이 더 뛰어난 것을 확인할 수 있지만,

저장 방식의 차이로 인해 꽤 다르게 구현해야 하는데,

이와 관련된 적절한 예제가 없어서,

보다 복잡한 예제인 Realtime Database 예제(database)를 Cloud Firestore로 구현하면서 이와 관련된 정리를 한다.

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


저장 방식에 있어서 Realtime Database는 Json 형태로 데이터를 저장하고,

Json 형태로 자식(Child) 데이터를 저장한다.

Cloud Firestore는 Json 형태로 데이터를 저장하지만

최상위 Json 노드를 컬렉션(collection)이라고 부르고 컬렉션에 저장하는 데이터를 문서(Document)로 부른다.

다시 문서 하위에 컬렉션(collection)을 생성하고 문서를 저장할 수 있다.

비슷한 것 같지만 사용상에 차이가 있고, 개인적으로 하위 문서 관리에는 Cloud Firestore의 컬렉션이 더 불편한 것 같다.

Realtime Database는 부모를 가지고 오면 자식(child)도 바로 사용할 수 있지만

Cloud Firestore는 부모를 가지고 오면 자식(collection)을 사용할 수 없다.

대신 필드 타입에 배열(Array)과 객체(Object)를 추가하여 비슷한 기능을 편하게 사용할 수 있다.


이상의 개념을 가지고 예제의 데이터 저장 방식을 정리하면 다음과 같다.

Realtime Database 예제의 데이터 저장은 다음 그림에서 회색 부분을 포함한 부분(빨간색 제외)이고

Cloud Firestore에서는 회색을 제외하고 빨간색으로 표시(추가)한 부분이 저장 구조를 의미한다.

Realtime Database 예제의 정확한 데이터 저장 구조는 이전 정리 자료에서 확인 할 수 있다.


Realtime Database 예제는 게시판(post) 예제로 (포스트가 게시물, 하나의 글이라고 보면 된다.)

Realtime Database에서는 사용자(users), 사용자별 포스트(user-posts), 전체 포스트(posts), 댓글(posts-comments)의 4가지 스키마(schema => table)로 개발 되었고,

Cloud Firestore는 사용자(users), 전체 포스트(posts)의 2가지 스키마로 구성하여 변환하였다.

Realtime Database는 검색 기능이 약해서 전체 포스트(posts)에서 특정 사용자의 포스트만 조회(Query)할 수 없다.

따라서, 사용자가 글을 작성하면 사용자별 포스트(user-posts)와 전체 포스트(posts)에 동시에 저장해야 한다 (구조가 동일하다).

Cloud Firestore에서는 검색 기능을 이용하여 사용자별 포스트를 구현할 수 있기 때문에 사용자별 포스트(user-posts)를 생략하였다.


사용자들이 글을 읽고 좋아요(like)를 누르는 것과 비슷한 별(star) 기능은 별을 클릭한 사용자 아이디(uid)를 저장하여 관리하는 방법으로 개발한다.

저장하는 방식에 있어서

Realtime Database에서는 포스트(Json) 하위의 자식 노드(Json)로 stars 필드를 정의하였다.

(그림에서 회색 부분의 stars – uid)

Cloud Firestore는 자식 노드의 개념이 하위 컬렉션(collection)인데,

사용이 조금 불편한것도 있지만

별을 클릭한 사용자 아이디와 같이 단순 정보를 저장하기에는 배열이 적당하기 때문에 stars 필드를 배열([])로 정의하였다.


마지막으로 댓글(posts-comments)은 Realtime Database처럼 별도의 컬렉션으로 구현해도 되지만

하위 컬렉션 사용법을 익히기 위해 Cloud Firestore에서는 문서의 하위 컬렉션으로 정의하였다.


이상으로 Realtime Database 예제를 Cloud Firestore로 변환하기 위해 필요한 기본적인 개념을 정리하였다.

이 내용을 이해해야만 코드를 제대로 이해하고 작성할 수 있다.

다음 코드는 이상의 개념을 구체적으로 구현한 것이니 꼭 이상의 내용을 이해하고 넘어가야 한다.



실제 변환을 진행하기 전에 3가지를 먼저 정리한다.

기존의 gradle 파일에서 다음과 같이 수정한다.

implementation 'com.google.firebase:firebase-database:16.0.1'

implementation 'com.google.firebase:firebase-firestore:17.1.0'

Realtime Database 라이브러리 대신에 Cloud Firestore 라이브러리를 등록하는 것으로

Cloud Firestore 라이브러리는 17.1.0 이상을 지정하는 것이 좋다.

17.1.0 부터 배열 타입 사용에 유용한 whereArrayContains 같은 함수를 사용할 수 있기 때문이다.

변환이 끝난뒤 Realtime Database 라이브러리를 제거하면 된다.


다음으로 데이터 베이스에 연결하는 방법의 차이를 기억한다.

private DatabaseReference mDatabase;
mDatabase = FirebaseDatabase.getInstance().getReference();


private FirebaseFirestore db;
db = FirebaseFirestore.getInstance();


기존에 mDatabase로 명명한 것을 구별하기 위해 Cloud Firestore에서는 db라고 이름 붙였다.


Firebase 콘솔에 접속해서 왼쪽 메뉴 중 Database를 실행한다.

Realtime Database와 Cloud Firestore를 선택하는 화면에서 Cloud Firestore를 선택한다.

다음 그림이 실행되면 편의를 위해 보안 규칙을 [테스트 모드로 시작]으로 하고 [사용설정]을 선택한다.

이제 Cloud Firestore를 사용할 준비가 되었다.


먼저, 글쓰기(NewPostActivity.java)부터 변환한다 (편의상 포스트라는 단어보다 "글"이라는 단어를 사용한다).

글쓰기에서는 사용자가 작성한 글 제목(title)과 내용(body)을 Firebase 실시간 데이터베이스에 저장하는 기능을 구현한다.


구현 절차는

사용자가 글을 작성하고 저장 버튼을 누르면

사용자의 아이디로 사용자의 이름을 서버에서 가지고 와서 게시물 내용과 같이 저장소(posts)에 저장한다.

(RDMBS는 사용자 아이디를 저장하지만 NoSQL에 계열에서는 이런 식으로 저장한다.)


Realtime Database는 다음과 같다.

    private void submitPost() {
        ~~ 생략 ~~
        final String userId = getUid();
        mDatabase.child("users").child(userId).addListenerForSingleValueEvent(
                new ValueEventListener() {
                    @Override
                    public void onDataChange(DataSnapshot dataSnapshot) {
                        User user = dataSnapshot.getValue(User.class);
                        if (user == null) {
                            ~~ 생략 ~~
                        } else {
                            // Write new post
                            writeNewPost(userId, user.username, title, body);
                        }
                            ~~ 생략 ~~
    }

    private void writeNewPost(String userId, String username, String title, String body) {
        String key = mDatabase.child("posts").push().getKey();
        Post post = new Post(userId, username, title, body);
        Map<String, Object> postValues = post.toMap();

        Map<String, Object> childUpdates = new HashMap<>();
        childUpdates.put("/posts/" + key, postValues);
        childUpdates.put("/user-posts/" + userId + "/" + key, postValues);

        mDatabase.updateChildren(childUpdates);
    }



Cloud Firestore에서는 다음과 같이 작성한다.

    private void submitPost() {
        ~~ 생략 ~
        final String userId = getUid();
        DocumentReference docRef = db.collection("users").document(userId);
        docRef.get().addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
            @Override
            public void onSuccess(DocumentSnapshot documentSnapshot) {
                User user = documentSnapshot.toObject(User.class);
                if (user == null) {
                        ~~ 생략 ~
                } else {
                    writeNewPost(userId, user.username, title, body);
                }
                        ~~ 생략 ~
    }

    private void writeNewPost(String userId, String username, String title, String body) {
        Post post = new Post(userId, username, title, body);
        db.collection("posts").add(post);
    }

데이터를 가지고 오는 함수나 구조 자체는 당연히 다르기 때문에 정리하지 않는다.

하지만 사용하는 개념은 비슷하다.

Realtime Database는 Users라는 자식(Table)의 특정 사용자 아이디(userId - uid)라는 자식(행)의 데이터를 가지고 와서 getValue로 클래스(User.class)화 하여 사용한다.

Cloud Firestore는 Users라는 컬렉션(Table)의 특정 사용자 아이디(userId - uid)라는 문서(행)의 데이터를 가지고 와서 toObject로 클래스(User.class)화 하여 사용한다.


실제 저장하는 부분에 큰 차이가 있는 것 같지만 Realtime Database는 다음과 같이 수정할 수 있다.

원 예제에서는 일종의 트렌젝션 처리를 위해 updateChildren를 사용한 것 같다.

    private void writeNewPost(String userId, String username, String title, String body) {
        String key = mDatabase.child("posts").push().getKey();
        Post post = new Post(userId, username, title, body);
        mDatabase.child("posts").child(key).setValue(post);
        mDatabase.child("user-posts").child(userId).child(key).setValue(post);

    }

Realtime Database는 posts라는 자식(Table)에 데이터(post)를 저장한다 (setValue).

Cloud Firestore는 posts라는 컬렉션(Table)에 데이터(post)를 저장한다 (add - set).


Realtime Database에서 getKey()로 행 번호를 미리 구하는 것은

같은 행 번호로 사용자별 포스트(user-posts), 전체 포스트(posts) 테이블에 저장하기 위한 방법으로

Cloud Firestore에서는 하나의 테이블에만 데이터를 저장하기 때문에 키를 먼저 구하는 방법을 사용하지 않았다.

Cloud Firestore에서도 키를 먼저 구할 수 있으며, 키를 구하는 각각의 코드는 다음과 같다.

    mDatabase.child("posts").push().getKey()

    db.collection("posts").document()


두 번째로 글 리스트(fragment)를 변환한다.

먼저 데이터를 조회하는 Query를 정리하면 다음과 같다.

최신글

 (RecentPostsFragment)

    public Query getQuery(DatabaseReference databaseReference) {
        Query recentPostsQuery = databaseReference.child("posts")
                .limitToFirst(100);
        return recentPostsQuery;
    }

     public Query getQuery(FirebaseFirestore databaseReference) {
        return databaseReference.collection("posts");
    }

내가 작성한 글

(MyPostsFragment)

    public Query getQuery(DatabaseReference databaseReference) {
        return databaseReference.child("user-posts").child(getUid());
    }

     public Query getQuery(FirebaseFirestore databaseReference) {
        return databaseReference.collection("posts").whereEqualTo("uid", getUid());
    }

 내가 작성한 인기 글

(MyTopPostsFragment)

    public Query getQuery(DatabaseReference databaseReference) {
        String myUserId = getUid();
        Query myTopPostsQuery = databaseReference.child("user-posts").child(myUserId)
                .orderByChild("starCount");
        return myTopPostsQuery;
    }

     public Query getQuery(FirebaseFirestore databaseReference) {
        String myUserId = getUid();
        return databaseReference.collection("posts").whereEqualTo("uid", getUid()).orderBy("starCount", Query.Direction.DESCENDING);
    }

최신 글(RecentPostsFragment)를 가지고 오는 것은 자식과 컬렉션의 차이일 뿐 동일하다.

Cloud Firestore에서는 갯수 제한(limit)을 두지 않았다.

기존 예제는 최신글이고 수정한 예제는 전체 글리스트가 된다.

기존 예제처럼 최신 글로 구현하려면 코드에서 limit (100)을 추가하면 된다.


Cloud Firestore의 내가 작성한 글(MyPostsFragment)에서는 조건(where)이 사용되었다.

Cloud Firestore는 다양한 조건절이 지원되며, Firebase 문서를 참고하면 된다.


내가 작성한 인기 글(MyTopPostsFragment)은  조건(where)에 정렬(orderBy)을 사용하였다.

Realtime Database은 오름차순만 지원되지만,

Cloud Firestore에서는 내림차순(DESCENDING)도 지원된다.

정렬과 개수 제한(limit)에 대한 자세한 내용은 Firebase 문서에 정리되어 있다.


이러한 Query를 실행해서 데이터를 출력하는 PostListFragment의 내용에는 큰 차이가 없다.

다만, Realtime Database FirebaseRecyclerAdapter를 사용했고

Cloud Firestore에서는 FirestoreAdapter를 사용한다.

FirebaseRecyclerAdapter는 Realtime Database 라이브러리와 같이 제공되지만

FirestoreAdapter는 그렇지 않다.

FirestoreAdapter는 Firestore 예제(Friendly Eats)에서 가지고 와야 한다.

사용법은 조금 차이가 있지만 거의 유사하기 때문에 여기에 따로 정리하지 않는다.


Friendly Eats 예제는 레스토랑 추천 앱으로 Cloud Firestore의 다양한 기능을 학습하기 위해 제작된 예제이다.


PostListFragment에서 리스트를 출력하는 것 외에

출력 후 사용자가 별(star)을 클릭하면 해당 글에 별을 주거나 취소할 수 있다.

이 기능을 Realtime Database는 다음과 같이 작성한다.

        ~~ 생략 ~
                viewHolder.bindToPost(model, new View.OnClickListener() {
                    @Override
                    public void onClick(View starView) {
                        DatabaseReference globalPostRef = mDatabase.child("posts").child(postRef.getKey());
                        DatabaseReference userPostRef = mDatabase.child("user-posts").child(model.uid).child(postRef.getKey());

                        onStarClicked(globalPostRef);
                        onStarClicked(userPostRef);
                    }
                });
        ~~ 생략 ~
    private void onStarClicked(DatabaseReference postRef) {
        postRef.runTransaction(new Transaction.Handler() {
            @Override
            public Transaction.Result doTransaction(MutableData mutableData) {
                Post p = mutableData.getValue(Post.class);
                if (p == null) {
                    return Transaction.success(mutableData);
                }

                if (p.stars.containsKey(getUid())) {
                    // Unstar the post and remove self from stars
                    p.starCount = p.starCount - 1;
                    p.stars.remove(getUid());
                } else {
                    // Star the post and add self to stars
                    p.starCount = p.starCount + 1;
                    p.stars.put(getUid(), true);
                }

                // Set value and report transaction success
                mutableData.setValue(p);
                return Transaction.success(mutableData);
            }
        ~~ 생략 ~


Cloud Firestore에서는 다음과 같이 작성한다.

viewHolder.bindToPost(post, new View.OnClickListener() {
    @Override
    public void onClick(View starView) {
        db.collection("posts").document(postKey).get()
                .addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
                    @Override
                    public void onSuccess(DocumentSnapshot documentSnapshot) {
                        Post post = documentSnapshot.toObject(Post.class);
                        if (post.stars.indexOf(getUid())==-1) {
                            post.stars.add(getUid());
                        } else {
                            post.stars.remove(getUid());
                        }
                        post.starCount = post.stars.size();
                        documentSnapshot.getReference().set(post);
            }
        });
    }
});

코드 차이가 커 보이지만 기본적인 개념은 동일하다.

사용자가 별을 클릭한 문서를 가지고 오도록 쿼리를 실행한다 (이상의 코드에서 파란색 표시).

둘다 문서 내용을 클래스(Post)로 변경한후

stars 필드를 Json으로 구현한 Realtime Database는 containsKey 로,

배열로 구현한 Cloud Firestore는 indexOf를 사용한다.

값을 수정하고 각각의 방법으로 저장하여 구현한다.

배열을 사용하기 때문에 별의 개수에 대한 연산 (+, -)을 할 필요가 없고,

사용자별 포스트(user-posts), 전체 포스트(posts)에 각각 저장하기 위해 두 번 실행할 필요가 없어서

Cloud Firestore가 더 간단하게 구현된 것 같이 보인다.

Cloud Firestore에서는 배열 외에도 HashMap등을 사용할 수 있도록 Object 타입도 있다.


마지막으로 글 상세(PostDetailActivity) 부분을 변환한다.

사용자가 선택한 글 내용을 가지고 오는 코드는 기존 변환 작업과 동일하게 진행된다.

글 상세에서는 2가지 주요한 코드가 사용되었다.

해당 글의 댓글을 가지고 오는 방법에 차이가 있다.


다음 코드에서 보듯이 Realtime Database는 댓글(post-comments)을 최상위 노드(테이블)에 두고 사용하였다.

mPostReference = FirebaseDatabase.getInstance().getReference()
        .child("posts").child(mPostKey);
mCommentsReference = FirebaseDatabase.getInstance().getReference()
        .child("post-comments").child(mPostKey);

Cloud Firestore도 이와 같이 구현하면 되는데

Realtime Database 예제에서 별점주기(starts)를 Cloud Firestore에서 배열로 구현하면서

하위 컬렉션(subcollection)을 이해하는 데 도움되기 위해 댓글(post-comments)을 게시글(posts)의 하위 컬렉션으로 구현하였다.

mPostReference = FirebaseFirestore.getInstance().collection("posts").document(mPostKey);
mAdapter = new CommentAdapter(mPostReference.collection("post-comments"));

사용된 이 코드를 연결해서 표현하면 다음과 같다.

     FirebaseFirestore.getInstance().collection("posts").document(mPostKey).collection("post-comments")

posts 컬렉션에 있는 mPostKey 문서에서 post-comments 컨렉션에 있는 내용을 모두 가지고 오라는 의미이다.

다시 의역하면

posts 테이블 문서 중 mPostKey에 등록된 댓글(post-comments)을 모두 가지고 오라는 의미가 된다.

이것을 다시 Realtime Database로 표현하면 다음과 같다.

    FirebaseDatabase.getInstance().getReference().child("posts").child(mPostKey).child("post-comments")


다음으로 특이한 코드는 댓글 리스트 데이터 전체를 서버에서 가지고 오는 것이 아니고,

처음에는 모두 가지고 오고, 이후에는 추가되거나 수정된 내용만 가지고 오도록 구현하는 것이다.

Realtime Database와 Cloud Firestore는 실시간 데이터 베이스로 현재 보고 있는 데이터의 수정된 내역이 바로 반영되어 보인다.

(에뮬레이터를 두 개 실행해서 같은 글을 보도록 하고 댓글을 추가하면 다른 쪽에 추가되는 것을 볼 수 있다.)


Realtime Database에서는 다음과 같이 작성되었다.

ChildEventListener childEventListener = new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
        Comment comment = dataSnapshot.getValue(Comment.class);

        mCommentIds.add(dataSnapshot.getKey());
        mComments.add(comment);
        notifyItemInserted(mComments.size() - 1);
    }

    @Override
    public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
        Comment newComment = dataSnapshot.getValue(Comment.class);
        String commentKey = dataSnapshot.getKey();

        int commentIndex = mCommentIds.indexOf(commentKey);
        if (commentIndex > -1) {
            mComments.set(commentIndex, newComment);

            notifyItemChanged(commentIndex);
        ~~ 생략 ~~
    }

    @Override
    public void onChildRemoved(DataSnapshot dataSnapshot) {
        String commentKey = dataSnapshot.getKey();

        int commentIndex = mCommentIds.indexOf(commentKey);
        if (commentIndex > -1) {
            mCommentIds.remove(commentIndex);
            mComments.remove(commentIndex);

            notifyItemRemoved(commentIndex);
        ~~ 생략 ~~
    }

Cloud Firestore에서는 다음과 같이 작성한다.

EventListener childEventListener = new EventListener<QuerySnapshot>() {
    @Override
    public void onEvent(@Nullable QuerySnapshot snapshots,
                        @Nullable FirebaseFirestoreException e) {
        if (e != null) {return;}
        String commentKey;
        int commentIndex;
        Comment comment;

        for (DocumentChange dc : snapshots.getDocumentChanges()) {
            switch (dc.getType()) {
                case ADDED:
                    comment = dc.getDocument().toObject(Comment.class);
                    mCommentIds.add(dc.getDocument().getId());
                    mComments.add(comment);
                    notifyItemInserted(mComments.size() - 1);
                    break;
                case MODIFIED:
                    comment = dc.getDocument().toObject(Comment.class);
                    commentKey = dc.getDocument().getId();
                    commentIndex = mCommentIds.indexOf(commentKey);
                    if (commentIndex > -1) {
                        mComments.set(commentIndex, comment);

                        notifyItemChanged(commentIndex);
                     ~~ 생략 ~~                   
                case REMOVED:
                    commentKey = dc.getDocument().getId();
                    commentIndex = mCommentIds.indexOf(commentKey);
                    if (commentIndex > -1) {
                        mCommentIds.remove(commentIndex);
                        mComments.remove(commentIndex);

                        notifyItemRemoved(commentIndex);
                     ~~ 생략 ~~                   
            }
        }
    }
};
listenerRegistration = query.addSnapshotListener(childEventListener);


Realtime Database에서는

글 하나를 추가(onChildAdded)하거나 수정(onChildChanged) 또는 삭제(onChildRemoved)하면

각각의 이벤트가 발생하고 해당 내용만 전송해서 처리한다.


Cloud Firestore에서는 하나의 이벤트에서 이벤트 타입(getType)이 추가(ADDED)인지 수정(MODIFIED)인지 삭제(REMOVED)인지를 판단해서 처리한다.

그외 코드는 거의 동일하다.


이상으로 Firebase realtime database로 제작된 게시판(post) 예제를 Cloud Firestore로 변환하는 방법을 정리하였다.

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


Firebase 실시간 데이터베이스(Realtime database) 예제를 쉽게 이해하기 위해 전체적인 개념을 정리하였고,

여기서는 주요 코드에 대한 설명을 정리한다.

여기에서는 Firebase 실시간 데이터베이스를 이용하여 구현한 코드(Android)들을 중심으로 정리한다.

Firebase 실시간 데이터베이스에 대한 구체적인 설명은 관련 문서를 한번 읽어 보길 바란다.


1. Firebase realtime database 실행과 테스트

2. Firebase realtime database 구조와 프로그램 구조

3. Firebase realtime database 코드

4. Firebase realtime database를 Cloud Firestore로


포스트(post) 예제는 데이터 베이스 단순 입출력 예제로 단순 게시판과 유사하기 때문에

편의상 포스트(post)라는 단어보다 [글]이라는 단어로 사용한다.

먼저, 글쓰기(NewPostActivity.java)부터 정리한다.

글쓰기에서는 사용자가 작성한 글 제목(title)과 내용(body)을 Firebase 실시간 데이터베이스에 저장하는 기능을 구현한다.

NewPostActivity.java

코드가 복잡해 보이지만

사용자가 저장 버튼을 누르면 전처리를 위한 submitPost()함수[라인 4]를 호출한다.

submitPost()에서는 사용자 이름을 조회해서[라인6] 없으면[라인 14] 오류를 발생시키고,

조회가 되면 실제 데이터를 저장하는 writeNewPost() 함수를 호출한다.

현재 글을 작성하는 사용자의 UID(고유번호) [라인 5]로 사용자 이름을 조회하는 것은 글을 저장할 때 같이 저장하기 위해서 이다.

이 방식은 좋은 방식이 아니지만,

Firebase 실시간 데이터베이스가 NoSQL로 RDBMS의 Join과 같은 기능이 없어서 이렇게 구현한 것 같다.

writeNewPost() 함수에서는 사용자가 입력한 값(title, body)을 사용자 이름, 사용자 UID와 함께Post 개체(모델)에 담아서 저장한다 [라인 45].


여기서 사용된 저장 방식은 관련 문서에 없는 내용으로 제법 유용한 팁을 제공해 준다.

먼저, 데이터가 중복되면 안되기 때문에 getKey()을 이용하여 고유한 값(Primary Key, Unique value)을 받아온다 [라인 44].

저장할 정보를 Post 개체(모델)로 생성하고 [라인 45],

생성한 Post 개체를 Map로 변환한다 [라인 46].

Firebase 실시간 데이터베이스는 Json형태(Map과 비슷)로 저장하기 때문이다.


다시 Map을 하나 더 생성한다 [라인 48].

이 맵은 작성한 글을 두 군데 동시에 저장하기 위해서 사용한 방법이다.

앞서서 데이터 베이스 구조에서 정리하였지만

사용자가 작성한 글은 전체 게시글(posts) [라인 49]과 작성자별 게시글(user-posts)에 [라인 50] 각각 저장한다.

이렇게 하는 것은 Firebase 실시간 데이터베이스가 검색 기능이 약하기 때문이다.

전체 게시글에 데이터를 저장하고, 지정된 사용자 글만 가져오도록 구현 할 수 없다(?).

따라서, 기능에 맞추어 데이터를 미리 가공해 두어야 한다.


전체 글은 posts 스키마 하위의 고유값(key) 아래에 저장하고 [라인 49]

사용자별 게시글은 user-posts 스키마 하위의 사용자별 UID 하위에 고유값(key) 아래에 저장한다 [라인 50]

사용자별 UID 하위에 저장해야 사용자별 글만 가져올 수 있다.

updateChildren으로 한번에 두 개의 스키마에 저장한다 [라인 52].


Firebase 콘솔로 접속해서 다음과 같이 저장된 결과를 볼 수 있다.

전체 글은 posts 스키마 하위의 고유값(key) 아래에 ,

사용자별 게시글은 user-posts 스키마 하위의 사용자별 UID 하위에 고유값(key) 아래에 저장된 것을 볼 수 있다.


저장 기능을 구현한 writeNewPost()의 내용은 다음과 같이 작성할 수 있다.

    private void writeNewPost(String userId, String username, String title, String body) {
        String key = mDatabase.child("posts").push().getKey();
        Post post = new Post(userId, username, title, body);
        mDatabase.child("posts").child(key).setValue(post);
        mDatabase.child("user-posts").child(userId).child(key).setValue(post);

}

이 내용은 Firebase 문서에 있는 방식으로 구현한 것으로 저장(setValue)을 두번 실행하도록 작성한 것이다.

데이터 계층은 슬래쉬(/)로 작성해도 되고 child로 지정해서 작성해도 된다.


간단해 보여서 좋아 보지만

예제가 이렇게 작성하지 않은 이유는 Transaction처리 때문으로 보여 진다.

위와 같이 작성하면, posts에 저장하고 user-posts에 저장하다 여러가지 이유로 문제가 생겨서 저장하지 못 할 경우

두 스키마 간에 불일치가 발생할 수 있다.

둘다 없거나 둘다 있어야지 하나만 저장되면 안되기 때문에 두 스키마에 저장할 경우에는 Transaction처리 해야한다.


Firebase에서는 Transaction처리를 지원하는데 (문서 하단 참조), Transaction의 의미가 RDMBS와는 조금 다른 것 같다.

이 부분은 좀더 사용해 보고 찾아보고 차후에 추가할 예정이다.

다음 예제에서는 Transaction을 사용하였다.


이번에는 글 리스트(PostListFragment)에 대해서 정리한다.

앞서 정리했지만, 글 리스트는 세가지가 존재하고 PostListFragment에 모든 기능이 구현되어 있다.

코드가 많아 보이지만, 주의 깊게 봐야할 코드는 FirebaseRecyclerAdapter와 Transaction 사용이다.

PostListFragment.java

리스트를 사용하기 위해 RecyclerAdapter를 사용하는데,

좀더 쉽게 사용할 수 있는 기능을 제공하는 FirebaseRecyclerAdapter가 사용되었고 [라인 17],

사용자가 게시물에 별표시(좋아요)를 눌렀을 떼, 관련 처리를 하기 위해Transaction을 사용하였다 [라인 68].

하나씩 정리하면 FirebaseRecyclerAdapter은 RecyclerAdapter와 대부분 동일하지만 데이터를 관리하는 기능을 좀더 단순화 시켰다.

RecyclerAdapter는 출력할 데이터들을 List나 ArrayList 형태의 변수로 보관하고 있지만

위 코드에서는 이것과 관련된 코드를 볼 수 없다.

RecyclerAdapter에서 화면에 출력할(onBindViewHolder) 때에는 [라인 26]

다음 코드처럼 현재 출력하는 데이터 순번(position)으로 List나 ArrayList 변수에서 해당 개체를 찾아서 처리한다.

          Post model = List.get(position)

FirebaseRecyclerAdapter에서는 onBindViewHolder의 마지막 파라메터에 현재 출력하려는 데이터 개체를 지정해서(Post model) 반환해 준다.

즉, Post 클래스에 값을 넣어서 model 개체로 제공된다 [라인 26].

이렇게 제공된 model의 값을 각각의 view(TextView등)에 넣어서 출력한다 [라인 49].

기본적인 값 출력은 별도의 파일인 ViewHolder의 bindToPost에 작성되어 있다[라인 49].


이렇게 쉽게 사용하기 위해, 데이터를 조회하도록 하는 부분에 차이가 있다.

Firebase는 데이터를 가지고 오는 조건에 ValueEventListener나 SingleValueEventListener를 사용한다.

여기에서는 그런 지정없이 FirebaseRecyclerAdapter 생성시에 데이터 조건을 FirebaseRecyclerOptions으로 지정하여 처리한다 [라인 11~17].

사용법의 차이일 뿐이니 기억해서 사용하면 된다.

다른 곳에서 값을 변경하면 자동으로 반영되는 것을 볼 때,

FirebaseRecyclerAdapter는 ValueEventListener를 사용하는 것 같다.

(SingleValueEventListener을 사용하면 한번만 호출하는 것으로 값이 변해도 반영되지 않는다.)


다음으로 주의 깊게 봐야 하는 것은 Transaction 처리 방법이다 [라인 68~].

사용자가 별 표시를 누르면,

사용자 uid를 해당 글의 stars 필드에 넣어주거나 [라인 83] 빼고 [라인 79]

별의 개수를 증가 [라인 82] 시키거나 감소[라인 78]시킨다.

문서에 따르면, 여러 사용자가 동시에 별 표시를 눌렀을 경우를 대비하여 구현하였다고 한다.

전체 게시글 [라인 57]과 사용자 게시글을 [라인 58]을 따로 Transaction처리하였는데

여기에 Transaction을 처리하는 것이 맞지 않나 라고 생각한다.


이외에 게시물 하나(행)을 선택하면 게시물 읽기로 가기 위해

행에 클릭 이벤트 핸들러를 작성하였다 [라인 31].

행은 viewHolder의 itemView을 의미한다.


마지막으로 글 읽기(PostDetailActivity.java)를 정리한다.

코드 양이 많아 게시글 읽기, 댓글 저장, 댓글 리스트로 기능에 따라 3가지로 나누어 정리한다.




PostDetailActivity.java


게시글 읽기는 전형적인 Firebase 실시간 데이터베이스의 데이터 가져오기 코드로 작성되었다.

글 리스트에서 사용자가 선택한 글번호(EXTRA_POST_KEY)를 받아와서 [라인 10],

Firebase 실시간 데이터베이스에서 해당 글 내용을 가지고 오도록 조건을 설정한다 [라인 16].

지정된 데이터는 ValueEventListener [라인 30]을 이용하여 받아서 화면에 출력하도록 [라인 34~38]

주어진 조건을[라인 16] 실행한다 [라인 52].


addValueEventListener를 사용하여 다른 곳에서 수정하여도, 수정된 내용이 반영되도록 하였다.

(수정 기능이 없으므로 Firebase 콘솔에서 수정해 보면 알 수 있다.)

addValueEventListener는 해당 Activity가 종료되어도 계속 작동하기 때문에 별도의 핸들러를 생성하여 [라인 16],

Activity가 종료될 때(onStop) 해당 핸들러를 종료시켜야 한다 [라인 69].


PostDetailActivity.java

댓글 저장은 (postComment) 게시글 저장과 동일한 방식으로 작성되었다.

해당 글 작성자의 UID로 [라인 2] 작성자의 이름을 가지고 와서 [라인 3~, 특히 라인 9]

Comment 모델에 필요한 정보(UID, 이름, 댓글)을 지정하여 개체를 생성하고 [라인 13]

이 개체(comment)를 setValue로 저장하였다 [라인 16].

일반적인 데이터 저장 방식이다.


작성자 이름은 한번만 호출해서 가져오면 되기 때문에 addListenerForSingleValueEvent가 사용되었다 [라인 4].


글 읽기의 마지막 기능으로 댓글 리스트가 구현되었다.

이 코드에서 재미 있는 코드는 childEventListener이다.

PostDetailActivity.java

ValueEventListener는 조회 중인 데이터에 변화가 생기면 전체 리스트를 다시 반환하지만

childEventListener는 변화가 생긴 데이터에 대하여만 반환하고 발생한 기능 이벤트 별로 처리한다.

즉, 어떤 글(데이터)이 추가 되거나(add), 수정(Changed) 또는 삭제(Removed)되면 해당 이벤트에 대하여 각각 발생 발생한다.

여기서는 새로운 댓글이 추가되면 onChildAdde가 실행되고 [라인 18]

추가된 글 내용이 반환된다 [라인 22].

추가된 글을 데이터를 관리하는 리스트에 추가하고 [라인 26, 27],

RecyclerView에 마지막에 행이 추가되었다고(notifyItemInserted()) 알려준다 [라인 28].

댓글이 수정되면 동일한 방식으로 onChildChanged가 실행되고 [라인 33],

데이터 관리하는 리스트에서 수정된 데이터를 찾아서 [라인 42] 수정하고 [라인 45]

RecyclerView에 지정된 행이 수정되었다고(notifyItemChanged()) 알려준다 [라인 48].


삭제도 같은 방식으로 작성되었다 [라인 56].

데이터의 위치가 바뀌면 발생하는 onChildMoved는 예제로 작성되지 않았다 [라인 79].


childEventListener는 처리가 발생한 데이터에 대하여 구체적인 기능에 따라 처리하기 때문에 효율적으로,

보다 상세한 기능 설명은 관련 문서를 읽어보는 것이 좋다.

ValueEventListener는 모든 데이터에 대하여 처리하므로 비효율적이지만 코딩 양이 적은 장점이 있다.

이런 것이 왜 Firebase 실시간 데이터베이스(Realtime database)라고 하는지에 대한 이유가 될 것 같다.


이외에 조금 특이한 작성법은 mCommentIds와 mComments로, 2 개의 List를 사용한 것이다.

mComments는 Comment형으로 선언되었고[라인 8] 댓글 정보를 저장하는 리스트이다 [라인 27].

mCommentIds는 문자열(String)로 선언되었고[라인 7] 댓글 번호를(getKey) 가지고 있다 [라인 26].


수정(onChildChanged)과 삭제(onChildRemoved) 이벤트가 발생되었을때, 

수정된 행의 정보만 가지고 있기 때문에 수정된 행이 mComments의 몇 번째 글인지 알 수가 없다.

몇 번째 인지 알아야 mComments에서 수정하거나 삭제하고 RecyclerView도 갱신할 수 있다.

몇 번째 인지 알기 위해서는 반환된 정보에 있는 글번호(key)로 mComments의 개수만큼 돌면서 같은 값을 찾으면 된다.

다소 복잡한 코드를 작성해야 하는데,

이러한 코드를 줄이기 위해 mComments와 동일한 순서로 mCommentIds에 글 번호를 저장하고

mCommentIds의 indexOf로 찾은 위치가 mComments에서의 위치인 것이 된다.

mCommentIds는 문자열로 구성되니 indexOf를 사용할 수 있지만,

mComments는 Comment로 구성되어 indexOf와 같은 검색 기능을 사용할 수 없기 때문에 이렇게 구현 한 것 같다.




구글에서는 Firebase에서 제공하는 다양한 기능을 문서예제로 제공하고 있다.

제공되는 예제를 이해 하면서 Firebase 사용법 뿐만 아니라 Android 개발에 유용한 방법 등을 익힐 수 있다.

여기에서는 Firebase 예제 중에서 realtime database 예제를 쉽게 이해 할 수 있도록 정리하였다.

Firebase realtime database 예제는 데이터 입출력(CRUD) 예제로 일종의 게시판 예제로,

보다 쉽게 이해하기 위해 다음과 같이 3단계로 나누어 정리하였다.


1. Firebase realtime database 실행과 테스트

2. Firebase realtime database 구조와 프로그램 구조

3. Firebase realtime database 코드

4. Firebase realtime database를 Cloud Firestore로


Firebase 실시간 데이터베이스의 기본 개념이나 사용법에 대해서는 따로 정리하지 않는다.

관련된 상세한 설명은 구글의 문서Node.js로 정리한 내용을 참고하면 된다.


먼저, 구글에서 제공하는 예제 소스를 GitHub에서 다운로드 받는다.

CMD 창에서 clone https://github.com/firebase/quickstart-android.git 로 받아도 되고

그림과 같이 Git GUI 도구를 이용해도 된다.

Android studio에서 받아도 되지만,

Android studio로 받는 경우 여러가지 설정과 라이브러리 다운로드 등이 자동으로 진행되면서 시간도 오래 걸리고 제대로 작동하지 않다.


다운받은 예제는 다음 그림과 같이 하나의 예제가 아니고 여러 개의 예제로 구성되어 있다.

각 폴더를 클릭해 보면, 각각의 폴더에 Android 프로젝트에 필요한 파일(gradle등)이 있는 것을 볼 수 있다.

각 폴더가 하나의 예제이다.


이 예제 중에서 database가 실시간 데이터베이스(Realtime database) 예제이므로,

Android studio에서 database 폴더을 연다.



예제를 열면 다음 그림의 하단과 같이 오류가 발생하는 것을 볼 수 있다.

이 오류는 Firebase와 연동하지 못해서 발생하는 오류 (google-services.json 파일이 없음)이다.


상세한 설정 방법은 Firebase 메신저 예제 설정의 중간 내용을 참고 하면 된다.

Firebase를 처음 사용하는 경우에는 Firebse 서버에서 설정이 필요한데,

이 문서의 내용 인증과 실시간 데이터 베이스 관련 설정을 참고하면 된다.


간단하게 정리하면 Tools > Firebase 메뉴를 눌러서 연동한다.


Firebase와 연동후 실행하면 다음 그림과 같이 로그인 화면이 실행된다.

가입된 회원이 없기 때문에 아이디와 비번을 입력한 후에 회원 가입(Sign Up) 버튼을 선택한다.


회원 가입과 동시에 로그인 되고, 다음 그림과 같이 글 리스트 화면이 실행된다.

최신글 (Recent), 내가 작성한 글(My Posts),  내가 작성한 인기 글(My Top Posts)이 TabLayout으로 개발되어 있다.


하단의 플로팅 버튼을 눌러서 다음 그림과 같이 새글을 작성한다.

제목과 게시글 내용을 작성한 뒤에 하단의 플로팅 버튼을 눌러서 저장한다.

글이 저장된 후, 화면이 최신글 리스트로 이동한다.


작성된 글 리스트에서 글 하나를 선택하면,

선택된 글의 상세 내용을 볼 수 있고, 댓글(comment)을 작성할 수 있다.


이상으로 Firebase 실시간 데이터베이스(Realtime database) 예제를 실행시켜 보았다.

제법 많은 기능이 있는 것 같지만 다음과 같이 간단한 게시판 구조로 구현되어 있다.

Fireabse 콘솔로 접근해 보면

사용자(users), 사용자별 글(user-posts), 작성된 글(posts)이 생성되어 데이터가 저장된 것을 볼 수 있다.

사용자별 작성 글(user-posts)과 전체 작성 글(posts)은 동일한 구조와 데이터를 가진다.

사용자별 작성 글(user-posts)은 한 사용자가 작성한 모든 글을 저장하는 스키마이고,

전체 작성 글(posts)은 사용자에 관계없이, 사이트내의 모든 글을 저장하는 스키마이다.

사용자가 하나의 글을 작성하면 사용자별 작성 글(user-posts)과 전체 작성 글(posts)에 저장하고,

사용자별 글(user-posts)은 사용자가 작성한 글이기 때문에 사용자 UID 별로 작성된 글(posts)을 저장한다.

(사용자 UID는 사용자 고유 번호를 의미한다.)

Firebase 실시간 데이터 베이스는 검색 기능이 거의 없기 때문에 이러한 방식으로 데이터를 저장한다.


Android Studio에서 파일을 살펴보면, Espresso 기반의 UI 테스트 파일을 볼 수 있다.


newPostTest 파일을 선택하고, 마우스 오른쪽 버튼을 클릭해서 Run newPostTest을 실행한다.

그림처럼 테스트에 실패하는 경우도 있고, 별 문제없이 테스트가 진행되는 경우도 있다.

위 그림과 같이 실패한 경우에는, 메시지 창(run)에서 오류가 발생한 [newPostTest: 78]을 클릭해서 오류가 발생한 부분으로 이동한다.


해당 코드의 앞에 다음과 같이 2초 정도 후에(Thread.sleep(2000)) 코드가 실행되도록 작성한다.

사용하는 컴퓨터의 사양이 느리면 UI 실행이 느려서 오류가 발생한다.

따라서 2초 정도 지연 시간을 준 것이다.

다시 테스트를 진행하면, 자동으로 랜덤하게 사용자가 생성되고 글이 작성되는 것을 볼 수 있다.


Espresso의 사용법은 쉬우니, 이 동영상을 참고하거나 자료를 찾아보면 된다.

간단하게 정리하면,

Run > Record Espresso Test 메뉴를 실행하고,

해당 앱에서 여러가지 실행을 하면 자동으로 기록해서 테스트 코드(newPostTest)가 작성된다.


테스트 후에 정상적으로 실행하려고 해도 테스트로 실행된다.

Android Studio 우측 상단에 있는 실행 버튼 옆의 선택 상자에서 app을 선택하고 실행하면 제대로 실행이 된다.


이상으로 Firebase 예제 중에서 realtime database 예제를 실행하는 방법을 정리하였다.

다음으로, 예제의 database 구조와 프로그램 구조, 코드 특성 등을 정리한다.

보안(Rule)등과 관련된 내용은 여기에서 정리하지 않으니 Git 예제 설명이나 Firebase 문서를 참고하면 된다.









+ Recent posts