WebRTC는 개인간 연결(Peer to Peer)을 기본으로 하기 때문에

실행되는 단말기(PC, 휴대폰등)가 공인 IP를 가지거나 같은 네트워크(공유기) 안에서 서로 인식 할 수 있어야 한다.

하나는 공유기 안에 있고, 다른 하나는 공유기 밖에 있다면 통신을 할 수 없다.

즉, 앞서서 정리한 화상채팅용 WebRTC 예제는 같은 공유기 내에서만 실행된다.

 

이 경우 각 단말기는 공인 IP를 가진 서버(Server)를 경유해서 통신해야 하고,

coturn서버는 WebRTC가 이렇게 통신할 수 있도록 중계 서버 역할을 해주는 오픈 소스 프로그램이다.

(이런 서버를 Turn 서버라고 한다. Turn / Stun의 개념은 인터넷으로 쉽게 찾을 수 있다.)

 

coturn 서버의 설치 절차는 간단하고 쉽지만 (설치 자료를 쉽게 찾을 수 있다),

리눅스에서만 사용할 수 있고,

외부에서 접근하기 위해 공인 IP를 가진 서버에 설치해야 하기 때문에 공유기를 사용하는 환경(집)에서는 한계가 있다. 

 

따라서, 여기에서는 coturn 설치 절차나 사용 방법을 정리하지 않고,

다음과 같이 공유기(집)를 사용하는 환경에서 coturn 서버를 설치해서 운영하기 위해 필요한 것들을(링크자료) 간단하게 정리한다.

  1. WSL 설치: Linux 설치를 위해
  2. 공유기 사용시 포트포워딩: 공인 IP 사용
  3. coturn 설치 후 테스트
  4. 예제와 연동

공인 IP를 가진 리눅스 서버가 있는 경우 1번과 2번은 넘어가도 무방하다.

 

1. WSL 설치

coturn을 사용하기 위해서는 리눅스가 필요한데,

Windows 10 에서는 WSL(Windows Subsystem for Linux)로 쉽게 설치해서 사용할 수 있다.

도커(Docker), VirtualBox 같은 가상화 소프트웨어를 사용해도 좋지만,

개인적인 호기심과 저사양 노트북을 쓰는 관계로 WSL을 사용해서 coturn 서버를 구축했다.

WSL의 설치는 여기에서 제공하는 내용을 따라서 진행하면 된다.

Ubuntu 설치시 로그인을 요청하는데, 계정이 없다면 로그인 창을 그냥 닫아도 설치된다.

이 내용의 하단에 있는 도커는 설치 하지 않아도 되고, WSL로 우분투(UBUNTU)를 설치한다.

 

WSL의 단점은 WSL로 설치한 운영체제의 IP가 재부팅 할 때 마다 변경된다는 것이다.

이 문제는 이 글에서 정리한 스크립트를 실행해서 해결한다.

 

WSL로 우분투를 설치한 경우 텔넷(telnet)이 기본으로 설치되어 있지 않다.

개인적으로 윈도우즈 콘솔(cmd, Terminal) 화면을 좋아하지 않아서, WSL등으로 리눅스를 실행하고 텔넷으로 접속해서 사용하는 것을 선호한다.

이경우 SSH등을 지원하게 설치해서(apt install openssh-server) 사용하는 것이 좋다.


2. 공유기 사용시 포트포워딩

다음으로, 외부에서 WSL로 설치한 리눅스에 접속할 수 있도록 지정해야 한다.

[공유기 포트포워딩] 인터넷을 검색해보면 쉽게 자료를 구할 수 있다.

사용하는 공유기에 따라 설정이 다르기 때문에 제조사를 포함해서 검색하면 변경 방법을 찾을 수 있다.

 

Coturn 서버외에 화상 채팅을 사용하려는 사용자들이 접속해서 채팅을 시작할 수 있도록 도와주는 웹 서버도 외부에서 접속해야 한다.

이 웹 서버(WerbRTC 예제 참조)도 같이 리눅스 서버에서 실행시키고 외부에서 접근할 수 있게 한다.

그리고 다음과 같이 웹 서버에서 사용하는 3000과 443 포트를 지정하고,

Coturn 서버에서 사용하는 3478과 5349 포트를 지정해 준다.

다음 리스트에서 포트 외에 지정된 [내부 IP 주소]는 WSL이 실행된 노트북(PC)의 내부 IP이다.


포트 포워딩이 잘 되었는지 확인하려면, NodeJS로 만든 WebRTC 예제를 다운 받아서 리눅스에 설치한다.

NodeJS를 설치하고 WebRTC 예제를 복사해서 실행하면 된다.

이 예제를 내부 IP가 아닌 공인 IP로 접속할 수 있으면 포트 포워딩이 잘 이루어진 것이다.

coturn 서버를 설치하고 잘 실행되는지 확인하려면 WebRTC 예제가 있어야 한다.

 

자신의 외부(공인) IP는 다음과 같이 포털에서 ip로 검색하면 된다.

 

3. coturn 설치 후 테스트

 

Coturn 설치는 인터넷 자료가 많으니 검색해 보거나, 다음 자료를 참고하면 된다.

          buttercoconut.xyz/247/

          meetrix.io/blog/webrtc/coturn/installation.html

          www.omegaduck.com/2019/08/12/sturn-turn-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%84%B1/

설치시 완성된 서비스를 제공하는 것이 아니기 때문에 SSL(TLS)을 설치하지 않아도 되고,

도메인도 사용할 필요 없이 공인 IP만 지정해서 사용한다.


Coturn 설치 후 설치가 잘 되었는지, turnutils_uclient 를 이용하여 다음과 같이 확인한다.

     turnutils_uclient -t -u 계정 -w 비밀번호 공인IP

공인 IP 대신에 내부 IP를 지정해서, coturn 서버는 잘 설치되었는데, 포트 포워딩이나 방화벽 문제로 외부에서 접속이 안되는지 확인하는 하는 용도로도 사용할 수 있다.

또는 ICE 사이트에서 다음 그림과 같이 확인 할 수 있다.

설치한 coturn 서버 정보를 입력한 후에 [Gather candidates]를 누르면, 설치가 제대로 된 경우 그림과 비슷한 결과를 얻을 수 있다.

위 그림과 다른 결과가 나온다면 coturn 설정이나 방화벽 설정 등을 다시 확인해야 한다.

 

4. 예제와 연동

앞서 정리한 WebRTC 예제를 설치하고

설치한 Turn 서버 정보를 입력기 위해, main.js 파일에서 다음과 같이 수정한다.


var pcConfig = {
  'iceServers': [
          {"urls":["turn:공인IP:3478?transport=tcp"],"username":"계정","credential":"비밀번호"}
  ]
};


function createPeerConnection() {
  try {
    pc = new RTCPeerConnection(pcConfig);
~ 생략 ~~
}

설치한 coturn 서버의 정보를 pcConfig 변수에 지정하고, 이 pcConfig를 WebRTC를 실행할때(new RTCPeerConnection) 지정한다.

 

이 WebRTC 예제는 구글 예제를 기반으로 작성한 것으로, 구글의 WebRTC 설명용으로 작성된 코드로 짐작되지만 사용하지 않는 코드가 있다.

예로 main.js 파일에서 requestTurn로 검색되는 코드는 사용하지 않는 코드로 Turn 서버 접속에 문제를 만들 수 있다.

코드를 확인해서 삭제하는 것이 좋다.

 

이제 하나는 공유기 안에서 WebRTC 서버에 접속하고, 하나는 휴대폰으로(둘다 휴대폰) 접속해서 화상 통화가 잘 실행되는지 확인한다.

 

 

WebRTC는 구글에서 공유한 웹 기반 커뮤니케이션 라이브러리로, 별도의 설치 없이 웹 브라우저에서 화상채팅을 할 수 있는 기능을 제공한다.

WebRTC는 화상채팅, 음성채팅 등을 쉽게 제작할 수 있도록 도와주며,몇 번의 검색만으로 다음과 같이 다양한 설명과 예제 소스를 구할 수 있다.

      WebRTC 개념 및 각종 개념 설명

      화상채팅 예제

      WebRTC 각종 예제

대부분이 개념 설명과 코드 사용법을 중심으로 설명했는데,

여기에서는 개념 설명 없이 구글에서 제공하는 예제에 여러 가지 기능을 추가하는 방식으로 정리한다.

WebRTC는 제법 어려운(낯선) 내용들이 많고,

NodeJS, SSL, Soeket.IO, HTML, CSS 등의 부가적인 것에 대해서도 알아야 한다.

여기에서 간단하게 예제를 구현해 보면서 WebRTC 개발에 대한 재미를 느낀 뒤에,

본격적으로 관련된 깊이 있는 내용들을 찾아보면 좋을 것이다.

하지만, 최소한의 기본 개념은 있어야 하니,

WebRTC 개념 및 각종 개념에 대한 설명은 한번 쯤 읽어보고 다음 내용을 따라 하는 것이 좋다.

 

  1. 화상 채팅 예제로 익히는 WebRTC - 기본 예제
  2. 화상 채팅 예제로 익히는 WebRTC - 기능 추가
  3. WebRTC 외부에서 사용하기 – coturn

 

구글의 WebRTC 예제는 다음과 같이 기초부터 하나씩 기능을 추가하여 6가지 단계로 구성되어 있다.

  • 1단계는 카메라에서 비디오를 추출하는 예제,
  • 2단계는 RTCPeerConnection을 이용하여 대화상대에게 비디오를 제공하는 예제 (네트워크없이)
  • 3단계는 RTCDataChannel을 이용하여 데이터를 주고 받는 예제
  • 4단계는 서버를 이용하여 메시지를 주고 받는 예제 (signaling)
  • 5단계는 네트워크 상에서 비디오를 주고 받는 예제
  • 6단계는 이미지(데이터)를 공유(제공)하는 예제 이다.

1~3 단계는 네트워크로 상대와 연결하는 것 없이 혼자 실행하는 것으로

다운로드 받은 파일을 탐색기에서 실행해서(웹 브라우저로) 확인할 수 있다.

해당 예제의 설명서에는 크롬(Chrome)의 플러그인인 Web Server를 설치해서 사용하는 방법을 설명하는데,

1~3 단계는 그냥 파일로 실행하며 관련 코드를 확인하고 인터넷으로 해당 설명을 찾아보는 것으로 충분할  것 같다.

4~6단계는 네트워크를 기반으로 하는 예제이기 때문에 NodeJS를 설치해서 웹으로 실행해야 한다.

 

3단계와 6단계는 데이터를 주고 받는 RTCDataChannel에 대한 것으로, 

처음 시작하는 사람에게는 RTCPeerConnection 만으로도 충분이 어렵기 때문에 여기서는 정리하지 않는다.

2단계에서 사용된 RTCPeerConnection은 Peer라는 단어에서 알 수 있겠지만

대화 상대 둘이서 직접 연결해서 대화에 필요한 이미지를 주고 받도록 하는 클래스로

WebRTC로 화상채팅을 구현하기 위해서 꼭 알아야 하는 클래스이다.

 

정리하면,

카메라에서 비디오를 추출하고(getUserMedia - 1단계), 

대화 상대를 찾아서 (signaling - 4단계)

네트워크로 연결하여 (RTCPeerConnection - 2단계)

비디오를 주고 받는 예제로 위 단계를 종합한 예제가 5단계의 예제이다.

즉, 5단계의 예제를 실행하면,

두 대의 장치에서 이미지를 주고 받으며 화상채팅이 되는 것을 볼 수 있다.

따라서, 여기에서는 이 5단계의 예제에 기능을 추가하는 방식을 정리한다.


주의: 예제는 크롬 브라우저에서만 작동한다. 

IE, Edge(이전 버전, 최신 edge는 크롬기반으로 작동한다)를 제외하고는 WebRTC를 지원하지만, 

예제는 크롬에서만 작동한다. 다른 웹 브라우저에서 실행하려면 별도의 처리가 필요하다.

 

이 외에도 구글의 예제는 몇 가지 부족한 것 들이 있다.

먼저 WebRTC는 기본적으로 네트워크 연결시 SSL이 있어야 한다.

localhost로 간단하게 비디오 화면을 가져오는 것 외에,

네트워크로 연결해서 데이터를 주고 받으며 화상 채팅을 하려면 SSL이 있어야 한다.

 

두 번째, 대화 상대를 찾기 위해 사용하는 서버가 한번만 연결 하도록 작성되어서,

한번 채팅을 하고 나면 계속 서버를 재가동해야 한다.

프로그램을 조금이라도 수정하고 확인하려면 계속 서버를 재가동해야 한다.

 

마지막으로, 해당 예제는 이미지만 주고 받는 화상채팅으로 오디오는 제공되지 않는다.

 

이상의 3가지 기능을 보강하는 방법은 코드 몇 줄만 추가하면 되는 것으로

이렇게 기능을 보강하다 보면 WebRTC 예제(코드)와 친해지고 개발 능력을 향상 시킬 수 있을 것이다.

 

본격적으로 이상의 부족한 부분을 보강을 하기 전에,

구글 예제 중 5단계 예제부터 실행 한다.

 

먼저 Github에서 소스를 다운 받아서 적절한 디렉토리에 설치한다.

git 명령어로 설치하거나 [Download ZIP]을 선택해서 압축 파일을 받아서 설치한다.

주의: 서버가 NodeJS로 구축되어 있기 때문에 NodeJS를 설치한다.

NodeJS는 홈페이지에서 자신의 운영체제에 맞추어 다운로드 받아서 실행하면 된다.

 

콘솔창(CMD)에서 예제 5(step-05)가 있는 디렉토리로 이동해서 파일을 확인하고

npm i (install) 명령어로 예제 실행에 필요한 라이브러리들을 설치한다.

실행이 끝나면 탐색기에서 node_modules 폴더에 라이브러리들이 설치된 것을 확인할 수 있다.

 

콘솔창에서 node index.js 을 실행해서 웹 서버를 실행한다.

아무런 메시지도 없이 다음과 같이 커서가 깜박이면 제대로 실행된 것이다.

 

크롬을 실행해서 주소창에 http://localhost:8080을 입력한다.

다음 그림과 같이 카메라 사용 권한 요청화면에서 [허용]을 선택한다.

 

그림과 같이 자신의 모습이(?) 웹 브라우저에 나타나는 것을 볼 수 있다. 

하지만 다음과 같이 다른 컴퓨터나 휴대폰으로 접속하면, 화면이 나타나지 않는다.

    http://서버 IP:8080

참고: 서버 IP는 NodeJS를 실행한 컴퓨터의 콘솔창에서 ipconfig or ifconfig를 입력하면 확인할 수 있다.

 

localhost가 아닌 IP를 사용하는 경우에는 자신의 컴퓨터에서도 화면이 나타나지 않는다.

카메라에서 이미지를 못가지고 오는 것으로, SSL을 통하지 않으면 카메라(WebRTC)를 이용할 수 없다.

이제, 이러한 문제들을 해결하고, 기능을 추가하는 방법에 대해서 정리한다 (다음 페이지).

 

참고사항: step-05 폴더에 있는 5 단계 예제의 주요 파일은 다음과 같다.

다음 파일들을 모두 수정하니, 어떤 파일인지, 어떤 코드가 있는지 알고 있어야 한다.

  • index.js - NodeJS로 실행한 웹 서버 및 Signaling
  • index.html - 사용자가 웹으로 접근했을때, 보여주는 웹페이지. 화상채팅 화면.
    • js/main.js - index.html 파일에는 간단한 html 태그만 있고, RTCPeerConnection과 같은 실제 화상채팅을 하는 클라이언트 코드가 모두 여기 있다.
    • css/main.css - index.html 에서 사용할 디자인 내용이 작성되어 있다.

 

 

 

  1. 화상 채팅 예제로 익히는 WebRTC - 기본 예제
  2. 화상 채팅 예제로 익히는 WebRTC - 기능 추가
  3. WebRTC 외부에서 사용하기 – coturn

 

여기에서는 앞서 정리한 구글 WebRTC 예제에 다음 4가지 사항을 추가한다.

  1. SSL 적용
  2. 서버 기능 보강
  3. 오디오 추가
  4. 조금 나은 디자인

기존 예제에 추가된 소스는 GitHub에서 받을 수 있다.

 

먼저, NodeJS에 SSL을 적용하기 위해, SSL을 생성한다.

SSL은 HTTP를 암호화 해서 사용하기 위한 것으로 자세한 것은 찾아보길 바라고,

제대로 된 SSL은 유료로 구매해야 하지만

여기서는 개발을 위해 제공되는 OpenSSL로 간단하게 만들어서 사용한다.

OpenSSL을 설치하는 방법은 여기에서 정리하지 않으니 찾아보고 설치한다.



OpenSSL로 개인키(private.pem) 파일을 생성한다.

         openssl genrsa 1024 > private.pem

그리고, 개인키와 쌍이 되는 공개키를(public.pem) 다음과 같이 생성한다.

         openssl req -x509 -new -key private.pem > public.pem

탐색기로 개인키(private.pem)와 공개키(public.pem) 파일이 생성된 것을 확인 할 수 있다.

 

공개키를 생성하면, 국가, 도시 등의 여러 가지 값을 입력하라고 한다.

개발용 SSL이니 아무 값이나 입력하면 된다.

 참고: 여기서는 간단하게 키만 생성해서 사용하고, 제대로된 사설 인증서를 만드는 방법은 검색해서 익혀두면 유용하다.

 

생성한 인증서 파일을 구글 예제 폴더에 복사한다.

index.js 파일과 같은 폴더에 복사하고, index.js 파일을 적당한 편집기로 수정한다.

index.js는 다음과 같이 8080 포트의 http로 접속하도록 작성되어 있다.

var http = require('http');
var socketIO = require('socket.io');

var fileServer = new(nodeStatic.Server)();
var app = http.createServer(function(req, res) {
  fileServer.serve(req, res);
}).listen(8080);

이 내용을 다음과 같이 앞서 생성한 키 파일을 이용해서 https, 3000포트로 접속하도록 수정한다.

//var http = require('http');
var socketIO = require('socket.io');

const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('./private.pem'),
  cert: fs.readFileSync('./public.pem')
};

var fileServer = new(nodeStatic.Server)();
let app = https.createServer(options, (req,res)=>{
  fileServer.serve(req, res);
}).listen(3000);

console.log('Started chating server...');

기존에는 http://127.0.0.1:8080으로 접속했고,

이제는 https://127.0.0.1:3000으로 접속하면 된다.

그리고, "Started chating server..." 문장을 출력해서 서버가 가동된 사실을 알 수 있도록 한다.

주의: 실제로는 127.0.0.1로 접속하지 않고, 서버(PC)에 할당된 사설 IP(예: 192.168.35.154)나 공인 IP로 접속한다.

 

콘솔창에서 node index.js를 입력해서 실행하면, 다음과 같이 서버가 실행된 것을 알 수 있다.

PC나 휴대폰의 크롬 브라우저로 https://서버IP:3000으로 접속하면 다음과 같은 경고창이 실행된다.

정상 SSL이 아니라는 의미로, 웹 페이지 하단의 [고급] 버튼을 선택한다.

다음 페이지에서 "192.168.35.154(안전하지 않음)(으)로 이동"을 선택해서 서버 접속을 계속한다.

다음과 같이 PC와 휴대폰으로 접속하면, 비디오(만) 화상채팅이 실행된다.

 

두 번째 예제 보강 작업으로 서버 기능을 보강한다.

구글 예제에서 socket.io를 이용해 구현한 네트워크 연결 부분은 아주 기초적이고 단순한 구조로

두 대의 클라이언트에서 실행된 WebRTC가 서로 RTCPeerConnection로 연결할 수 있도록 도와 주는 역할을 한다(signaling).

 

현재의 예제는 채팅을 종료(웹브라우저를 닫거나 갱신-F5)하면 다시 채팅을 할 수 없다.

서버를 재가동하고 다시 접속해야만 한다.

주의: 서버 파일(index.js) 대부분과 클라이언트(main.js)의 네트워크 연결 관련 부분의 코드는 모두 socket.io로 작성되었다. socket.io에 대해서 잘 모른다면 socketio nodejs로 검색해서 관련 자료를 읽어보는 것이 좋다.

socketio nodejs 채팅방으로 검색 결과의 예제와 여기서 사용된 예제가 비슷한 것이 많으니 관련 내용을 읽어보고 구글 예제를 보강해 보는 것도 좋다.

 

다음 코드는 클라이언트의 각종 message를 받아서 전달(broadcast)하는 기존 코드(회색)에

message가 bye이면 채팅방(foo)을 비우는(leave) 코드를 추가한 것이다.

즉, 화상 채팅 중에 한 사람이라도 채팅방을 나가면(bye) 채팅방을 비우도록 한 것이다.

  socket.on('message', function(message) {
    log('Client said: ', message);
    
    if (message==="bye" && socket.rooms['foo']) {
        io.of('/').in('foo').clients((error, socketIds) => {
            if (error) throw error;

            socketIds.forEach(socketId => {
                io.sockets.sockets[socketId].leave('foo');
            });
        });
    }
    socket.broadcast.emit('message', message);
  });

다음 코드에서 확인 할 수 있지만,

다시 빈 채팅방에 입장한 순서로 연결해서 화상 채팅을 하게 된다.

참고: bye라는 메시지는 클라이언트(main.js 파일)에서 웹 브라우저를 닫거나 해당 채팅 웹 페이지를 떠날 때(onbeforeunload 이벤트시) 채팅 종료를 알리도록 작성된 메시지이다.

채팅방 이름인 foo는 클라이언트에 변수명으로 고정되어 있다 (구글 예제에서 그렇게 작성) .

제대로 된 채팅 프로그램이면 채팅방 이름이 bye란 메시지와 같이 전송되게 했을 것이다.

추가로 구현하면 예제를 이해하는데 도움이 될 것이다.

 

추가적으로 create or join 메시지의 기존 코드에 다음과 같이(빨간색),

첫 사용자가 입장하면 채팅방을 생성한다는 의미로 created,

두번째 사용자가 입장했다는 의미로 joined를 콘솔창에 출력하도록 한다.

  socket.on('create or join', function(room) {
    log('Received request to create or join room ' + room);

    var clientsInRoom = io.sockets.adapter.rooms[room];
    var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
    log('Room ' + room + ' now has ' + numClients + ' client(s)');

    if (numClients === 0) {
      socket.join(room);
      log('Client ID ' + socket.id + ' created room ' + room);
      socket.emit('created', room, socket.id);
      console.log('created');
    } else if (numClients === 1) {
      log('Client ID ' + socket.id + ' joined room ' + room);
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room, socket.id);
      io.sockets.in(room).emit('ready');
      console.log('joined');
    } else { // max two clients
      socket.emit('full', room);
    }
  });

위 코드는 이해를 하고 있는 것이 좋을 것 같아서 간단하게 정리한다.

create or join 메시지시 파라미터(room)에 채팅방 이름이(foo)이 같이 전송되어 온다.

해당 채팅방의 정보(clientsInRoom) 중에서 사용자 수가 0명이면 (numClients === 0),

해당 접속자(socket)를 지정된 채팅방(room)의 생성자로 참여(join)시키고,

해당 접속자에게 created란 메시지를 전달한다(emit).

해당 접속자는 메시지를 수신하면 방 생성자 또는 방장 등의 의미를 가지도록 설정한다.

다시 다른 사용자가 접속하면 접속자수는 현재 1이기 때문에 (numClients === 1),

해당 접속자에게 joined란 메시지와 이제 화상 채팅을 실행할 준비를(ready) 하라고 메시지를 전달한다(emit).

 

서버를 재가동하고, 다시 접속하면 화상 채팅을 할 수 있다.

이제 서버 재가동없이 한 사람이 채팅을 종료하고 다시 접속(둘다)하면 화상 채팅을 다시 할 수 있다.

양쪽에서 웹 페이지 갱신(F5)을 하면 확인 할 수 있다.

 

참고: 다소 어렵지만 한사람이 채팅방을 나가면, 채팅방을 비우기 보다는 남은 사람은 그대로 두고 새로 입장하는 사람과 채팅할 수 있도록 하는 것이 좋다.

 

세번째로 오디오가 전송 되도록 한다.

오디오가 전송되도록 하기 위해서

클라이언트에서(main.js) WebRTC가 비디오 정보를 가지고 오도록 하는 다음의 기존 코드를 수정한다.

audio가 false로 되어 있다.

navigator.mediaDevices.getUserMedia({
  audio: false,
  video: true
})
.then(gotStream)
.catch(function(e) {
  alert('getUserMedia() error: ' + e.name);
});

화상 채팅을 위해 비디오 정보를 가지고 오는 getUserMedia의 파라미터에 지정된 audio 옵션을 다음과 같이 true로 지정한다.

  const mediaOption = {
    audio: true,
    video: {
      mandatory: {
        maxWidth: 160,
        maxHeight: 120,
        maxFrameRate: 5,
      },
      optional: [
        { facingMode: 'user' },
      ],
    },
  };
  
navigator.mediaDevices.getUserMedia(mediaOption)

오디오 설정외에 인터넷에서 검색한 비디오의 다양한 옵션도 추가로 작성하였다.

추가한 옵션은 이름에서 어느 정도 의미를 알 수 있을 것이고, 자세한 내용은 여기서 정리하지 않는다.

 

다시 채팅을 시작하면, 오디오가 전송되는 것을 확인할 수 있다.

다만, 두 개의 클라이언트가 가까이 있는 경우 심한 잡음(?)을 들을 수 있다.

 

이제 마지막으로 조금더 그럴듯하게 디자인을 변경해 본다.

디자인을 변경하기 위해서는 CSS를 알아야 한다.

자세한 내용은 정리하지 않으니 따로 찾아보고,

index.html 파일의 head에 다음과 같이 메타 태그를(빨간색) 추가한다.

이 코드는 모바일 화면을 위해 추가하는 것이다.

<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
  <title>Realtime communication with WebRTC</title>
  <link rel="stylesheet" href="/css/main.css" />

</head>

css/main.css 파일의 내용을 다음과 같이 수정한다.

body {
  font-family: sans-serif;
}

#videos {
  width: 100%;
  max-width: 500px;
}

#localVideo {
  max-width: 100%;
  width: 100%;
  float:left;
  border: 1px solid;
}

#remoteVideo {
  max-width: 100%;
  width: 100%;
  float:left;
  border: 1px solid;
  display: none;
}

.localVideoInChatting {
  width: 100px !important;
  position: absolute;
}
.remoteVideoInChatting {
  display: inline-block !important;
}

위 CSS에서 localVideoInChattingremoteVideoInChatting 클래스(CSS의 클래스)를 기억해야 한다.

이 CSS의 내용은 채팅을 시작하기 전에는 내 화면(#localVideo)만 크게 보이고(width:100%),

상대 화면(#remoteVideo)은 보이지 않도록(display: none;)하는 것이다.

 

그러다 화상채팅이 시작되면(handleRemoteStreamAdded - 다음 코드)

내 화면(#localVideo)에 localVideoInChatting 클래스를 추가하여(add) 내 화면을 작게 만들고,

상대 화면(#remoteVideo)은 remoteVideoInChatting 클래스를 추가하여 크게 잘 보이도록 한다.

이 기능을 위해 다음 코드의 빨간색으로 표시된 부분을 추가하면 된다.

function handleRemoteStreamAdded(event) {
  console.log('Remote stream added.');
  remoteStream = event.stream;
  remoteVideo.srcObject = remoteStream;

  remoteVideo.classList.add("remoteVideoInChatting");
  localVideo.classList.add("localVideoInChatting");
}

function handleRemoteHangup() {
  remoteVideo.classList.remove("remoteVideoInChatting");
  localVideo.classList.remove("localVideoInChatting");
    
  console.log('Session terminated.');
  stop();
  isInitiator = false;
}

대화가 종료되면(handleRemoteHangup)

추가한 클래스들을 제거(remove)하여 원래데로 되돌린다.

 

이상으로 구글의 WebRTC 예제에 4가지를 추가하여 제법 그럴듯한 화상채팅 프로그램을 만들었다.

기존 예제에 추가된 소스는 GitHub에서 받을 수 있다.

 

제대로 화상채팅 프로그램을 만들기 위해서는 NodeJS Socket.IO, CSS를 알아야 하고,

WebRTCRTCPeerConnection를 알아야 하는 것을 알 수 있다.

그리고 signaling, STUN, TURN등의 용어를 사용하지 않았지만

예제 코드에 사용되어 있으니 확인하면서 알아 두어야 한다.

 

제대로된 화상채팅을 만들려면, 아직 많은 기능을 구현해야한다 (특히, 채팅방).

하나씩 기능을 추가하면서 제대로된 화상채팅 프로그램을 제작하다 보면 WebRTC에 대해서 잘 알게 되고

멋진 프로그램을 제작하게 될 것이다.

 

그리고 WebRTC를 이용하면,

화상채팅 외에 여러 명이 채팅 하는 컨퍼런스, 방송 등 미디어와 관련된 많은 것을 구현할 수 있다.

사용해보진 않았지만 WebRTC 기반의 Janus, Licode, Medooza, Jitsi 등을 이용하면 더 쉽게 구현할 수 있다고 한다.

 

 

Playground는 서울 시내에서 아이랑 갈만한 

실내외 놀이터, 박물관/미술관, 도서관등의 장소(약 3,400군데)들을 모아서, 

일목요연하게 보여 주는 웹 사이트로 

주말 같은 여유 시간에 아이랑 어디를 가야 할지 고민하는 부모들을 위해 

개인의 경험을 토대로 제작하였다.

수 많은 날을 검색으로 지새우며 놀러 갈 곳을 찾아 다니다 

집 주변에 괜찮은 시설이 제법 많다는 것을 알게 되어 정리하면서 시작하였다.

개발은 2단계로 나누어서 진행하고 있다.

현재는 1단계 개발을 완료한 상태로 서울 시내의 시설과 장소 데이터를 모아서 DB로 구축하고,

내 주변의 시설과 장소에 대한 리스트(맵)를 보여주는 것에 집중하였다.

현재는 개발 버전이라 가격이 싼 웹 호스팅을 사용해서 SSL을 적용하지 못했다.

(정확하게는 가격이 싸서 적용되지 않는 것이고, 싼 호스팅이라 동접이 많아지면 속도가 ...)

SSL이 되어야 웹 브라우저에서 제공하는 현재 위치를 찾는 기능(navigator.geolocation)을 사용할 수 있다.

따라서 나의 주변이라기 보다는 내가 지정한 위치 주변의 시설과 장소를 조회할 수 있게 구현하였다.

 

2단계는 각자의 경험을 공유하는 추천코스이다.

개인이 괜찮다고 추천하는 시설과 장소들을 묶어서 소개하는 기능으로 ,

개인의 경험을 공유하고, 의견을 공유하는 기능으로 구현하고 있다.

회원 관리 기능은 구현되어 있지만 (github에서 소스로 확인가능),

SSL이 적용되지 않아서 [개인정보보호법] 준수 문제로 운영에 배포되어 있지 않다.

현재는 제작자(관리자)만 작성 가능하다.

 

이 사이트의 소스는 Nodejs와 MariaDB를 기반으로 제작한 것으로

github에서 다운로드 받아서 샘플 데이터와 같이 실행 가능하지만

구축된 데이터는 공유하지 않는다.

데이터는 계속 구축 중으로, 

데이터 양이 너무 많아 현재는 서울만 제공하며 

원 데이터의 출처는 Playground 사이트github에 공유되어 있다.


다음으로 주요 기능에 대한 사용법을 정리한다.

 

먼저, [우리 동네] 메뉴를 실행하면 다음과 같이 지도(Daum map)가 실행된다.

기본 위치는 광화문이고, 

마커를 마우스(PC일경우)나 손가락(스마트폰)으로 움직여서 위치를 지정한다.

지도를 클릭해서 마커를 이동 시킬 수 있다.

SSL이 적용되지 않아 현재 위치를 자동으로 찾는 기능은 사용할 수 없지만

지도 좌측에 있는 [주소로 위치 검색] 기능을 이용해 이동할 수 있다.

지도 좌측에 있는 [주소로 위치 검색]을 선택하면, 그림과 같은 다이얼로그 창이 실행된다.

이 창에서 찾을 주소나 시설명을 입력하면 된다.

주소 검색기능은 Daum에서 제공하는 기능으로,

검색된 리스트에서 해당하는 장소를 클릭하면 위치(마커)가 이동된다.

위치를 지정하고 [2Km내에서 검색] 버튼을 클릭하면

다음 그림과 같이 지정된 위치의 주변에 있는 실내외 놀이터, 박물관, 도서관등의 위치가 색깔별로 표시된다.

조회된 데이터양이 많을 경우,

상단의 실내외 놀이터, 박물관, 도서관 옵션 기능을 이용해서

보고 싶은 종류만 볼 수 있다.

옵션 좌측에 있는 리스트 버튼을 클릭해서 시설과 장소 리스트를 보면서 선택할 수 있다.

지도에 표시된 마커(색깔별 원)를 클릭해서 상세한 정보를 볼 수도 있다.

 

다음은 [추천 코스] 기능이다.

특정 장소를 중심으로 갈만한 장소들을 묶어서 하나의 코스로 정리하여 보여 주는 기능으로

아직은 개발 중인 기능이며,

실행하면 다음 그림과 같이 리스트가 출력된다.

각 항목을 클릭하면 다음과 같은 상세 페이지가 실행된다.

상세 페이지는 추천하는 장소에 대한 리스트 아이콘, 소개글, 위치를 나타내는 지도로 구성되어 있다.

장소에 대한 리스트 아이콘이나 지도의 마커를 클릭하면

다음 그림처럼 각 장소에 대한 상세 정보를 확인할 수 있다.

지도의 좌측 중앙에 있는 빨간색 지도 아이콘을 클릭하면

현재 보고 있는 위치를 중심으로 2Km내에 있는 다른 장소들을 볼 수 있는

[우리 동네]로 이동한다.

 

아직은 개발 중이고, 기능이 제한적이지만

도움이 되는 사용자가 많았으면 좋겠다.

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

nineBatis (9batis)  (2) 2017.08.16
1. Node.js 프로젝트 생성 및 개발 준비  (0) 2017.08.05
2. MariaDB 기반 CRUD 게시판 만들기  (23) 2017.08.05
3. 게시판 예제 보강  (4) 2017.08.05
4. GitHub에서 Node.js 소스 가져오기  (0) 2017.08.05


ES6(ECMAScript6)

- http://web-front-end.tistory.com/21

- https://cimfalab.github.io/deepscan/2016/07/ecmascript-6

- https://www.w3schools.com/js/js_es6.asp

- https://www.w3schools.com/js/js_es5.asp


React & Redux

- https://velopert.com/3613

- https://www.inflearn.com/course/react-%ea%b0%95%ec%a2%8c-velopert/

- https://www.inflearn.com/course/reactjs-web/

- https://reactjs.org/

- React + Redux 플로우의 이해


Router (특히 로그인 상태에 따른 처리)

https://reacttraining.com/react-router/web/guides/quick-start


Firebase

- https://firebase.google.com/docs/?hl=ko


React, Redux & Firebase

- https://chanspark.github.io/2017/12/06/Firebase-%EA%B3%B5%EB%B6%80.html

- https://www.youtube.com/watch?v=mwNATxfUsgI

- https://www.youtube.com/watch?v=z0QpTl5-0Bk

- https://www.youtube.com/channel/UCW5YeuERMmlnqo4oq8vwUpg


이상의 기초를 가지고 만든 CRUD 게시판

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

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

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


디자인

- https://material-ui.com/ or https://material-ui.com/premium-themes/

- material-ui로 만든 템플릿 https://github.com/gujc71/project9_react

- http://react-toolbox.io/#/


이상의 기초를 가지고 만든 응용 웹

- Firebase 기반 메신저 앱 - DirectTalk9 (Web과 PC 버전)


분석해 보면 좋을 예제

- https://github.com/firebase/friendlyeats-web

- https://github.com/web-pal/react-trello-board

- https://react.rocks/


라이브러리

- https://github.com/brillout/awesome-react-components

- http://khan.github.io/react-components/


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

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  (11) 2018.10.28

React로 작성한 이전 예제는 게시판 데이터를 배열에 저장하기 때문에 다른 사람은 볼 수 없고, 웹 브라우저를 갱신(F5)하면 초기화 되었다.

이번에는 Firebase를 이용하여 실제 게시판처럼 데이터를 서버에 저장해서 데이터가 사라지지 않고 다른 사람과 공유할 수 있도록 구현한다.


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

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

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

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


Firebase는 구글에서 제공하는 서버 관련 기능들로 gmail계정으로 간편하게 사용할 수 있다.

NodeJS에서 Firebase를 사용하는 방법은 이 문서에 정리되어 있다.

Firebase의 콘솔 사용법도 같이 정리 되었으니 이전 문서를 확인하고,

여기서는 Firebase의 여러 서비스 중 데이터를 저장하는 Cloud FireStore를 이용한다.

여기서 만드는 게시판은 로그인기능이 없기 때문에 아무나 글을 쓰고 저장할 수 있도록 Cloud FireStore를 테스트 모드로 설정하고, 관련 설정은 이 문서를 참고하면 된다.


배열이 아닌 Cloud FireStore에 사용자가 작성한 게시물을 저장한다.

이외에 Firebase에 대한 사용법은 Firebase 문서를 참고하고, 이 문서 중에서 Cloud FireStore 관련 문서(데이터 가져오기, 데이터 저장등)를 읽어 보는 것이 좋다.


React로 작성한 이전 게시판 예제Nodejs 기반으로 작성한 Firebase 게시판 예제를 합치는 것으로,

다음과 같이 App_reduce.js 파일의 내용만 Firebase에 맞추어 조금만 수정하면 작성할 수 있다.

그전에 다음과 같이 Firebase와 미들웨어를 설치 한다.

npm install --save firebase

npm install --save redux-thunk

redux-thunk를 설치하고, redux-thunk를 사용할 수 있도록 다음과 같이 index.js 파일을 수정한다.

import thunk from 'redux-thunk';

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

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

index.js

그리고, Firestore.js 파일을 생성하여 다음과 같이 작성한다.

import firebase from 'firebase';

var config = {
    apiKey: "AIzaSyDbsU_",
    authDomain: "fir-example-.firebaseapp.com",
    databaseURL: "https://fir-example-.firebaseio.com",
    projectId: "fir-example-",
    storageBucket: "fir-example-.appspot.com",
    messagingSenderId: ""
};
firebase.initializeApp(config);
var firestore = firebase.firestore();

const settings = {timestampsInSnapshots: true};
firestore.settings(settings);

export default firestore;


Firestore.js

React에서 Firestore에 접속하기 위한 정보로 config의 내용을 채워야 한다.

config의 내용은 Firebase 콘솔에서 가져올 수 있고, 가져오는 방법은 이전 문서를 참고하면 된다.

Firestore에 접속한 연결은 firestore로 외부로 보내지고 (export ),

다음 코드에서처럼 firestore로 사용된다 [라인 2, 7, 23, 32, 40].


다음과 같이 App_reduce.js에 firebase_board_list, firebase_board_remove, firebase_board_save 3개의 함수를 추가한다.

3 개의 파일은 Firebase(FireStore)의 게시판 데이터를 가지고 오는 리스트, 선택한 게시물을 삭제하는 삭제, 게시글을 수정하거나 새로 등록하는 글 저장 함수이다.

먼저, firebase_board_list()는 FireStore에서 데이터를 가지고 와서 기존의 state.boards에 넣어주는 기능을 한다 [라인 5].

작성된 날짜(brddate)를 기준으로 최근 데이터가 먼저 오게(desc) 데이터들을 가지고 와서 (get) [라인7 ]

데이터의 개수 만큼 반복(forEach)해서 별도의 배열(rows)에 넣어주고 [라인 10],

화면에 출력하기 위해 board_list() 함수를 호출해서 Redux로 보관한다 [라인 15].

데이터의 개수 만큼 반복하는 것은 전송 받은 데이터 리스트 구조가 JSon 배열이라 필요한 정보만 추출(doc.data() – 게시물 하나)해서 보관하기 위한 것이다 [라인 11].

전송 받은 데이터를 그대로 배열에 넣어서 사용해도 된다 (대신 관련한 코드를 좀더 수정해야 한다.).

이전 예제에서는 board_list()함수를 사용하지 않았는데, 여기서는 state.boards에 데이터를 저장하는 역할을 한다.

개념상 Redux로 저장된 state.boards의 데이터를 가지고 오는 것이 되어야 할 것 같지만 반대로 저장하는 역할을 한다.

즉, firebase_board_list()로 서버에서 데이터를 가지고 오고, board_list()로 state에 저장하는 구조이다.


firebase_board_remove()는 주어진 글번호로(brdno) FireStore에 있는 데이터를 삭제(delete)하는 명령어를 실행한다 [라인 20].

삭제하고 나면(then) [라인 23],

화면에 출력하기 위해 데이터를 가지고 있는 state.boards에서 제거하기 위해 board_remove() 함수를 호출한다 [라인 24].

board_remove()는 기존 코드와 동일하다.


firebase_board_save()함수는 수정인지 신규 등록인지 구분해서 [라인 31]

수정이면 update [라인 10],

신규 등록이면 set으로 데이터를 저장한다 [라인 35].

수정이든 신규등록이든 저장하고 나면(then), board_save() 함수를 호출해서 state.boards의 값을 수정하여 화면에 반영되게 한다 [라인 37, 41].

board_save()의 코드도 기존 코드와 동일하다.


사용자가 행을 선택하면 호출하는 board_read()는 데이터 변경이 없기 때문에 수정없이 그대로 사용한다.


initialState 에서 선언했던 최대 글번호(maxNo)를 제거한다 [라인 47].

Firestore에 저장하기 때문에 Firestore에서 제공하는 문서 번호(id)를 글번호로 사용한다 [라인 33].


이상으로 App_reducer.js 파일을 이렇게 수정한 뒤,

기존에 각 컴포넌트에서 사용한 board_remove(), board_save() 함수를 사용한 각 컴포넌트에서 firebase_ 을 앞에 붙여 추가한 3개의 함수를 사용하도록 하고,

글 리스트의 경우에는 state.boards 를 사용하기 전에 firebase_board_list()를 호출하도록 수정한다.


글리스트(BoardList) 먼저 수정하면 다음과 같다.

App.js에 작성했던 리스트 코드를 분리하여 별도의 컴포넌트로 제작하였다.

이해를 위해 App.js에 그대로 작성했지만, React에서는 기능별로 세분화해서 컴포넌트로 사용하는 것이 권장된다.


글 리스트를 구현 할 것이기 때문에 Reducer에 작성해 놓은 firebase_board_list()를 사용한다고 선언한다(import) [라인 2].

컴포넌트가 생성되는 시점에 발생하는 이벤트인 componentDidMount에서 firebase_board_list()를 실행한다 [라인 6].

firebase_board_list()를 호출하면 Reducer의 state.boards에 게시글들이 채워지고

이것을 바라보고 있는 this.props.boards에 전달되면서 화면에 출력된다 [라인 9].

this.props.boards는 앞서 정리한 데로 mapStateToProps으로 연결하였다 [라인 16~23].


각 게시물을 출력하는 BoardItem은 데이터 삭제를 위해 board_remove() 대신에 firebase_board_remove()를 호출한다.

import { board_read, firebase_board_remove } from './App_reducer'

const BoardItem = ({row, inx, board_read, firebase_board_remove}) => (
    <tr>
        <td>{inx}</td>
        <td><a onClick={() => { board_read(row.brdno) } }>{row.brdtitle}</a></td>
        <td>{row.brdwriter}</td>
        <td>{row.brddate}</td>
        <td><a onClick={() => { firebase_board_remove(row.brdno) }}>X</a></td>
    </tr>
);

const mapDispatchToProps = dispatch => ({
  board_read: brdno => dispatch(board_read(brdno)),
  firebase_board_remove: brdno => dispatch(firebase_board_remove(brdno))
})

export default connect(null, mapDispatchToProps)(BoardItem)

BoardItem.js

코드의 다양성을 보여 주기 위해 BoardItem에서는 기존과 다른 방법으로 App_reducer의 함수를 호출하도록 했다.

BoardList 컴포넌트에서는 import문에서 사용할 함수를 선언하고 다음과 같이 사용했다.

         this.props.dispatch(firebase_board_list());

firebase_board_remove도 이렇게 사용해도 되지만 mapDispatchToProps 를 사용해서

App_reducer에서 사용할 함수를 다시 선언하고 BoardItem에 연결(connect)할 때 파라미터를 넘기는 방식으로 사용한다.

이렇게 사용하면 this.props.dispatch를 생략하고 firebase_board_remove() 함수만 호출해서 사용할 수 있다.


[ 참고 ] connect에서 첫번째 파라미터는 mapStateToProps로 컴포넌트에서 사용할 변수들을 선언한다. BoardItem에서는 사용할 변수가 없어서 null로 지정하였다. mapDispatchToProps 는 사용할 함수를 선언하는 것이다.


글 쓰기 폼(BoardForm)에서는 board_save() 대신에 firebase_board_save로 바꾸어 주면 된다.

import { firebase_board_save } from './App_reducer'

class BoardForm extends Component {
   
    handleSave = () => {
        this.props.dispatch(firebase_board_save(this.state));
        this.setState (this.initialSelectedBoard);
    }
}

BoardForm.js

BoardForm에서는 this.props.dispatch로 호출했다.

mapDispatchToProps와 this.props.dispatch 중 적절한 방식으로 작성하면 되지만 mapDispatchToProps가 많이 사용된다.

(개인적으로 귀찮아서...)


이상으로 간단하게 Redux 예제를 Firebase Cloud Firestore로 구현하였다.

기존 코드에 Firestore의 CRUD를 처리하는 함수를 추가하는 것으로 간단하게 구현하였다.

즉, React(Redux)로 사용자의 PC에서 화면에 데이터를 출력하고 관리하는 기능을 하도록 하고,

Firestore로 서버에 데이터를 저장하고 관리하는 기능을 하도록 서로의 역할을 나눠서 사용한다.


다른 브라우저로 접속해서 데이터가 동일하게 출력되는 것을 확인 할 수 있다.


Firestore(Cloud Firestore)는 Realtime database와 같이 실시간 데이터베이스 이다.

서버의 데이터에 변경이 생기면 현재 접속한 클라이언트에 실시간으로 데이터가 보내진다.

실시간으로 동기화 된다는 것으로, 이 기능을 이용하여 많은 예제들이 Fireabse 기반 메신저를 제작하였다.

상세한 설명은 Firebase 문서에서 확인할 수 있다.


이번에는 앞서 작성한 예제를 실시간 데이터 방식으로 작성한다.

소스는 Github의 step2 브랜치에서 받을 수 있다.

변경 방법을 간단하게 정리하면, 데이터 변경 사항을 글 리스트에서 받아서 state.boards에 반영하여 보여주는 식으로 작성한다.

코드에서 보듯이 글 리스트(firebase_board_list)에서 거의 모든 기능이 구현된다.


이전에는 get() 메소드를 사용했지만, onSnapshot()을 사용하고 [라인 4],

전송 받은 데이터의 개수(forEach) 만큼 반복했다.

이전과 가장 큰 차이는 전송된 데이터의 종류(change.type)이다 [라인 7, 11, 14].

현재 데이터(행)가 신규 추가인지(add), 수정 인지(modified), 삭제 인지를(removed) 나타낸다.

이 종류에 따라 board_save()나 [라인 9, 12] board_remove()를 호출해서[라인 15] state.boards의 데이터를 관리한다.


그리고, firebase_board_remove(), firebase_board_save()에서

처리 후 콜백(then)으로 board_save()나 board_remove()를 호출할 필요가 없어져 제거한다 [라인 25, 35, 37].

이 코드들을 모두 리스트에 작성하였다 [라인 9, 12, 15] .


그리고 onSnapshot으로 호출된 콜백 함수는 함수가 종료 되어도 계속 실행되기 때문에 서버의 데이터 변경 정보를 계속 수신해서 반영한다.

다른 사람이 작성한 것도 모두 실시간으로 제공된다.


 주의  onSnapshot으로 호출된 콜백 함수는 계속 실행되기(Listening) 때문에 필요하지 않을 경우에는 사용을 중지해야 한다.

중지 방법은 Firestore 문서에서 [리스너 분리]항목을 참조하면 된다.


이상으로 React, Redux, Firebase 순으로 기초적인 게시판 만들기를 정리하였다.

이해를 위해 Table 테그로 디자인 없이 작성하였는데,

그림과 같이 Material-ui와 같은 React용 디자인 라이브러리를 사용하면 제법 쓸만한 SPA(Single Page Application) 웹사이트를 만들 수 있다.

게시판에 대한 개념이 이해가 되었으면,

Google's Material Design을 준수하는 Material-ui등의 디자인 라이브러리를 적용하여 다음과 같이 사용하면 된다.


이상의 게시판에 기능을 보강하고

Fireabse 인증(로그인) 기능, Material-ui 등을 추가하여 Project9 템플릿을 제작하였다.

실제 프로젝트를 쉽게 할 수 있도록 만든 템플릿으로 (작성된 코드를 Copy & Paste 하여 개발하기 위한 예제들)

이상의 개념을 가지고 응용하는 방법을 이해하는데 도움을 줄 수 있으니 확인해보면 도움이 될 것이다.


Project9 템플릿을 기반으로 DirectTalk9 이라는 실시간 메신저를 웹 버전으로 제작하였고,

다시 이것을 Electron을 이용하여 PC 버전으로 제작하였다.

React Native를 쓰면 모바일까지….



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

React 학습자료  (0) 2019.01.23
React + Redux 게시판(CRUD) 만들기  (1) 2018.11.04
React 게시판(CRUD) 만들기 1  (4) 2018.10.28
React 게시판(CRUD) 만들기 2  (11) 2018.10.28

앞서서 정리한 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

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) 값을 키(필드)로 사용한다.


+ Recent posts