GitpleLive React Sample Application

Gitple에서 새롭게 런칭한 GitpleLive 채팅 API를 사용하여 스터디 프로젝트로 Sample Application을 만들어보았습니다.

React에 Typescript를 사용하여 개발했습니다.

1. 개발 범위

  • 소규모 그룹 단체 채팅방
  • 채널 만들기/수정/참여/메타정보 입력,삭제
  • 사용자 만들기/삭제/메타정보 입력,삭제
  • 메시지 보기/수정/삭제/메타정보 입력,삭제

2. 사용 기술

  • React + Typescript
  • React Router
  • React Bootstrap
  • Recoil (상태 관리 Library)
  • Lodash (JavaScript utility library)
  • MQTT (경량형 메시지 프로토콜)

3. GitpleLive 채팅 API 서비스

  • GitpleLive는 SaaS 형태의 채팅 API & SDK를 제공하는 채팅앱 개발용 Kit 입니다.

4. 사용 라이브러리

  • gitpleLive Restful API 와 GitpleLive SDK(mqtt 이벤트 처리부를 SDK형태로 제작)를 사용 SDK는 Typescript + MQTT로 제작하였고 mqtt 모듈을 포함하여 npm pack명령어를 통해 tgz파일로 배포하였습니다.
  • browserify와 jscompress(https://jscompress.com, UglifyJS 3과 babel-minify를 사용)통해 브라우저에서도 지원이 가능한 javascript 파일도 제작했습니다. package.json에 gitplelive sdk를 추가하여 사용했습니다. 브라우저 기반에선 script tag를 통해 사용이 가능합니다.
1
"gitplelive": "file:./gitplelive-v0.0.6.tgz"

5. 기본 설정

A-1. Recoil (전역 상태관리 라이브러리 리코일을 사용하기 위해서 npm install recoil 또는 yarn add 를 실행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {
RecoilRoot
} from 'recoil';

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);

root.render(
<RecoilRoot>
<App/>
</RecoilRoot>
);

reportWebVitals();
  • /src/index.tsx

Recoil을 사용하기 위해 RecoilRoot를 사용하여 태그를 감싸 하위 컴포넌트들이 Recoil 객체를 사용할 수 있도록 합니다.

A-2. Recoil 대상에 대한 state와 model 설정

채팅의 상태값을 나타내는 StatusModel을 Recoil로 사용해봤습니다.

1
2
3
4
5
export interface StatusModel {
user_id: string,
name: string,
profile_url: string,
}
  • /src/models/status.model.ts

A-3. Recoil의 상태 값을 가질 State 설정

1
2
3
4
5
6
7
8
9
10
11
import { atom } from 'recoil'
import { StatusModel } from '../models/status.model'

export const statusState = atom<StatusModel>({
key: 'chat/status',
default: {
user_id: "",
name: "",
profile_url: "",
}
})
  • /src/recoil/chat.ts

B. React Router

라우팅 시스템을 이용하기 위해 BrowserRouter, Routes, Route 모듈을 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, {useEffect} from "react";
import {BrowserRouter, Routes, Route} from "react-router-dom";
import "./App.css";
import ChatApp from './components/ChatApp'
import Profile from './components/Profile'
import {useRecoilState} from "recoil";
import {StatusModel} from "./models/status.model";
import {statusState} from "./recoil/chat";
import AdminCT from "./components/admin/AdminCT";
import SearchCT from "./components/admin/SearchCT";

function App() {
const [myStatus, setMyStatus] = useRecoilState<StatusModel>(statusState)

// ...(중간 코드 생략)

return (
<BrowserRouter>
<Routes>
<Route path="/" element={<ChatApp />}></Route>
<Route path="/profile" element={<Profile />}></Route>
<Route path="/admin" element={<AdminCT />}></Route>
<Route path="/search" element={<SearchCT />}></Route>
</Routes>
</BrowserRouter>
)
}

export default App;

5. 컴포넌트 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├── src/
│ ├── components/
│ │ ├── admin/
│ │ │ ├── AdminChannel.tsx
│ │ │ ├── AdminCT.tsx
│ │ │ ├── AdminMessage.tsx
│ │ │ └── SearchCT.tsx
│ ├── Channel.tsx
│ ├── ChannelList.tsx
│ ├── Chat.tsx
│ ├── ChatApp.tsx
│ ├── ChatCT.tsx
│ ├── Header.tsx
│ ├── Message.tsx
│ ├── Pofile.tsx
│ └── SIgnCT.tsx

6. 비동기 통신을 위한 모듈 파일 구성

1
2
3
4
5
├── services
│ ├── channel.service.ts
│ ├── gitpleLive.service.ts
│ ├── message.service.ts
│ └── user.services..ts

A. channel,message,user.uservices.ts 등에서 http-common.ts을 import하여 axios(비동기 통신 라이브러리)를 사용할 수 있습니다.

1
2
3
{
"proxy": "https://{APIURL}"
}

B. package.json 에서 proxy로 api url을 지정했습니다. 이럴 경우 axios를 사용할 때 기본 URL을 생략하고 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
import axios from "axios";
export default axios.create({
baseURL: "/v1",
headers: {
"Content-type": "application/json",
"ORGANIZATION_API_KEY": process.env.REACT_APP_ORGANIZATION_API_KEY || '',
"APP_ID": process.env.REACT_APP_APP_ID || '',
"APP_API_KEY": process.env.REACT_APP_APP_API_KEY || '',
}
});

C. ‘.env’파일에 선언되어있는 REACT_APP_* 환경변수를 사용하여 필요한 Key, ID등을 할당 했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import http from "../modules/http-common";
import {MessageCreateModel, MessageExecuteModel, MessageModel} from "../models/message.model";
import {fdatasync} from "fs";

class MessageDataService {

get(channel_id: string, params: {} = {}) {
return http.get<MessageModel>(`/group/channels/${channel_id}/messages`,
{params});
}
create(channel_id: string, data: MessageCreateModel) {
return http.post(`/group/channels/${channel_id}/messages`, data);
// return http.post(`/messages/create/channels/${id}`, data);
}

// (중간 코드 생략)
}
export default new MessageDataService();

D. API별로 *.service.ts 파일을 생성하여 기능 구현.

7. 플로우

A. 로그인 페이지 (SignCT.tsx)

로그인 페이지

제일 처음 보는 로그인 페이지입니다. 로그인 기능과 사용자를 생성,수정,삭제할 수 있는 기능, 메타 데이터를 등록할 수 있는 곳 입니다.

B. 채팅 페이지 (ChatCT.tsx)

채팅 페이지

좌측 채널 리스트를 선택하여 채팅방에 입장 할 수 있고 text, image 메시지를 보낼 수 있다. 본인의 글은 삭제가 가능하며 메시지를 보낼 땐 API를 호출하고 메시지를 받을 땐 미리 제작한 gitplelive sdk를 사용합니다.

메시지를 보내는 방법은 api를 호출하면 되는 간단한 방법이므로 sdk로 이벤트를 받는 방법과 메시지에 담겨 있는 read_reciept 타임스탬프를 통해 메시지별로 몇 명이 읽었는지 알아내는 방법에 대해서 알아보기로 하겠습니다.

채팅 영역의 구조는 아래와 같습니다.

1
2
3
4
5
├── ChatCT.tsx/
│ ├── ChannelList.tsx/
│ │ ├── Channel.tsx/
│ ├── Chat.tsx/
│ │ └── Message.tsx/

<채팅 이벤트를 받는 부분>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
useEffect(() => {
const userInfo: GitpleLive.UserInfo = {
user_id: myStatus.user_id
};

const gitpleLive = getGitpleLive();

gitpleLive.connectUser(userInfo);

gitpleLive.on('connect', () => {
console.log('GitpleLive Connect');
});

gitpleLive.on('disconnect', () => {
console.log('GitpleLive Disconnect');
});

gitpleLive.on('error', (data) => {
console.log('GitpleLive Error:', data);
});

gitpleLive.on("group:message_send", (channel: GitpleLive.Channel, message: any) => {
console.log('=== group:message_send ===');
console.log("channel:", channel);
console.log("message:", message);
setSendMessageEventModel({
category: "group:message_send",
app_id: "",
message: message,
});
});

/// (이하 생략)
}
  • 미리 만들어 놓은 GitpleLive SDK를 통해 이벤트를 수신합니다. 수신시 setSendMessageEventModel(useState)를 통해 객체를 변화 시키고 hook으로 감지하여 메시지 리스트에 추가 합니다.

<읽은 카운트를 표시하는 부분>

Message.tsx 에서 unread(읽지 않은 사용자 수)를 계산 한다. 채널 정보에 있는 read_receipt(읽기 영수증) 과 message의 생성 시간을 비교하여 계산한다

1
2
3
4
5
    "read_receipt": {
"user_1": 1663057055113,
"user_2": 1662009830269,
"user_3": 1663643430480
}
  • read_receipt의 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const renderUnread = (message: MessageModel) => {
if (!mySelChannel.read_receipt) {
return (
<></>
)
}

let unread: number = Object.keys(mySelChannel.read_receipt).length;

for (let writer in mySelChannel.read_receipt) {
if (message.created_at <= mySelChannel.read_receipt[writer]) {
unread--;
}
}

return (
<Badge pill bg="secondary" className="p-1 m-1">
{unread}
</Badge>
)
}

이상 gitplelive api와 gitplelive sdk를 이용해 리액트 샘플 앱을 만드는데 중요하다고 생각했던 부분에 대해서 기술 하였습니다.

리액트를 class기반이 아닌 function 컴포넌트 기반으로 hook과 recoil을 사용해보며 새로운 지식을 습득하였습니다. 더불어 간단하지만 typescript 기반으로 mqtt를 사용하는 sdk를 만들었고 browserify 모듈을 사용하여 브라우저에서도 쓸 수 있는 번들링 기법도 알게 되었습니다.

읽어 주셔서 감사합니다.

공유하기