서론
현재 근무하는 회사 서비스의 웹 소켓 기반의 채팅 기능이 전반적으로 하자가 많아서 카카오톡이나 다른 여러 채팅 API를 끌어다가 사용하자는 의견이 나오고 있고, 실제로 11월 초에 한번 회의를 하기로했다. 그 중 센드버드를 채택할 것 같은 가능성이 짙어 센드버드를 사용해보고 장/단점들을 파악해보고자 한다.
대서사가 진행될 예정으로
센드버드의 간단한 API 사용 예제만 참고하고 싶은 분들은
5. 센드버드의 활용 부터 보시기 바랍니다.
★지적은 언제나 감사합니다★
기존 문제점
현재 사내 서비스 내부의 웹 소켓 채팅은 크게 다음과 같은 문제점이 있다고 생각했다.
1. 미래를 생각하지 않고 단순 현 상황에서의 기능이 돌아가게만 작성되어 있다. 점차 적응하다보니 문제 하나가 터지면 여기저기 다 뜯어야했다. 코드 간의 의존관계가 너무 높아서 유지보수가 너무 힘들다.
2. 고객이 메세지를 보냈을 때 렌더링이 너무 오래걸린다. 이 문제를 해결하기 위해 단순 프론트에서 선 렌더링으로 코드를 바꿔 작성하고 싶었으나, 프론트단 또한 마찬가지로 유지보수가 힘든 의존관계가 높은 코드들이 덕지덕지 붙어있어서 다 뜯어내어 해결해야했다.
3. 소켓 서버의 테스트 환경 구축이 전혀 되어있지 않다. 테스트 서버 환경을 구축하기 위해서는 사내 업무를 마비시켜야만 가능했다. 소켓 서버에는 사내 ERP로의 통신도 포함되어 있었다. 사내 전반적인 상담이나 기타 업무는 해당 ERP를 통해 진행이 되는 것 같았다.
카카오톡의 선 렌더링 방식
대표적으로 카카오톡을 보면, 왼쪽 사진은 데이터 연결을 끊고 메세지를 보냈고, 오른쪽 사진은 데이터가 연결됐을 때이다.
사진에는 나타나지 않았지만 메세지를 보내는 데 장애가 생겼을 때 새로고침과 X버튼이 같이 있는 화면도 접해본 적이 있을 것이다.
짐작컨데, 다음과 같은 동작을 수행하지 않을까 생각해본다.
1. 메세지를 보냈을 때 바로 클라이언트 단에서 렌더링을 처리함
2. 왼쪽 사진은 데이터 연결이 되지 않기 때문에 서버 단으로 메세지 전송이 불가능함. 그렇기에 1 표시도 나타나지 않고 새로고침+X도 나타나지 않음.
3. 오른쪽 사진은 클라이언트 단에서 렌더링을 수행하며, 서버로 메세지를 보냄. 서버에서 메세지를 정상적으로 받았기 때문에 1 표시를 클라이언트에게 보여줌.
4. 만약 데이터는 넘어갔는데 장애가 발생하여 서버 측에서 받지 못하였다면, 클라이언트에게 새로고침+X버튼을 제공하여 장애 발생 상황을 클라이언트가 인식하고 유연하게 대처할 수 있게 함.
사내 렌더링 방식
위의 문제점을 제기했듯이, 선 렌더링방식을 도입하고 싶어도 다 뜯어내지 않는 이상 불가능에 가깝다고 판단했다. 도입하지 못한다고 해도 어떤 문제점들이 있어서 사내 렌더링이 느리게 동작할까 살펴봐야했다.
소켓 서버 내부에서 고객에게서 메세지를 받았을 때의 로직은 다음과 같았다.
1. 메세지를 받음
2-1. 내부에서 비즈니스 로직을 수행할 때 필요한 쿼리 작성(mongo)
2-2. ERP 서버에 쿼리 최초 쿼리 전송(메세지 받았다)
3-1. 고객이 접속해 있는 서비스 서버에 리턴 메세지를 전송 후 리스트 갱신 요청을 보냄
3-2. 해당 방 번호에 대한 렌더링해줘야 할 데이터 row 전송
4-1. ERP 서버에 고객에게서 온 메세지에 대한 동작 완료 로그처리 요청 보냄.
우선 3-1부터 이해가 가지 않았는데, 굳이 리턴 메세지를 전송한 후 다시 갱신 요청을 보내는 이유를 전혀 알지 못했다.
이 단에서 데이터를 전송해서 렌더링했어도 0.5초 ~ 1초정도 렌더링 시간을 단축시킬 수 있다고 생각했다.
또한 소켓 서버에서 동시간대에 많은 요청이 몰리면 그만큼 처리 속도가 지연되어 렌더링 시간이 길게는 2~3초까지 지연되는 것을 보고 반드시 개선하겠노라 생각했다.
센드버드의 활용
그러던 중 완전 뒤엎고 새로운 채팅 서비스를 도입해보자 라는 의견이 많아져서, 기존 것을 반드시 갈아 엎겠노라고 강경하게 의견을 제시하시는 PM에 의해 채팅 api 중 이름있는 센드버드를 사용해볼 기회가 생겼다.
우선, 사용하려면 APP_ID와 API_TOKEN이 필요한데, 이는 가입한 후 대시보드에서 Settings에서 확인할 수 있다.
현재 회사의 서비스 환경이 Nest와 Next를 사용하고 있기 때문에 센드버드 docs를 뒤져 개발 환경에 맞는 사용법을 익혀야했고, 공식 가이드를 따라해보기로 했다.
1. SDK를 설치하고 아래 사용할 변수들을 생성해준다.
$ npm install --save @sendbird/chat sendbird-platform-sdk-typescript
해당 변수들은 프로젝트 규모가 더 커지기 전에 따로 빼서 관리할 생각이다.
const APP_ID = "YOUR_APP_ID_FROM_DASHBOARD";
const API_TOKEN = "YOUR_MASTER_API_TOKEN_FROM_DASHBOARD";
const serverConfig = new sendbird.ServerConfiguration("https://api-{app_id}.sendbird.com", { "app_id": APP_ID })
const configuration = sendbird.createConfiguration({ baseServer : serverConfig });
const APINAME = new sendbird.APINAME(configuration);
내가 사용하려고 하는 디렉토리 구조와 커스텀하여 실험해본 코드는 아래와 같다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SendbirdUserController } from './sendbird.user.controller';
import { SendbirdUserService } from './sendbird.user.service';
@Module({
controllers: [SendbirdUserController],
exports: [SendbirdUserService],
providers: [SendbirdUserService],
imports: [
TypeOrmModule.forFeature([]),
]
})
export class SendbirdUserModule {}
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, Req } from '@nestjs/common';
import { ApiBadRequestResponse, ApiOkResponse, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { SendbirdUserService } from './sendbird.user.service';
import { CreateUserDto } from './dto/sendbird.user.dto';
import { SendbirdDashBoardUserCreateResponse, SendbirdDashBoardUserListResponse } from './resopnse/sendbird.user.response';
import { SendbirdBadRequestResponse400105, SendbirdBadRequestResponse400111 } from '../errorresponse/sendbird.error.response';
@ApiTags('sendbird')
@Controller('/api/v1/sendbird/user')
export class SendbirdUserController {
constructor
(
private sendbirdUserSerivce: SendbirdUserService,
){}
@ApiOperation({ description: '대쉬보드 유저 리스트 전체' })
@ApiOkResponse({ type: SendbirdDashBoardUserListResponse })
@ApiBadRequestResponse({ type:SendbirdBadRequestResponse400105 })
@Get('userList')
async getUserList() {
return await this.sendbirdUserSerivce.userList();
}
@ApiOperation({ description: '대쉬보드 유저 생성' })
@ApiOkResponse({ type: SendbirdDashBoardUserCreateResponse })
@ApiBadRequestResponse({ type: SendbirdBadRequestResponse400111 })
@Post('createUser')
createUser(@Body() createUserDto: CreateUserDto) {
return this.sendbirdUserSerivce.createUser(createUserDto);
}
}
import { Injectable } from '@nestjs/common';
import * as sendbird from 'sendbird-platform-sdk-typescript';
import { ConfigService } from '@nestjs/config';
import { CreateUserDto } from './dto/sendbird.user.dto';
@Injectable()
export class SendbirdUserService {
APP_ID: string;
API_TOKEN: string;
serverConfig: sendbird.ServerConfiguration<{app_id: string}>;
configuration: sendbird.Configuration;
userAPI: sendbird.UserApi;
constructor(
private readonly configService: ConfigService,
){
this.APP_ID = this.configService.get('SENDBIRD_APP_ID');
this.API_TOKEN = this.configService.get('SENDBIRD_API_TOKEN');
this.serverConfig = new sendbird.ServerConfiguration(`https://api-${this.APP_ID}.sendbird.com`, { "app_id": this.APP_ID });
this.configuration = sendbird.createConfiguration({ baseServer: this.serverConfig });
this.userAPI = new sendbird.UserApi(this.configuration);
}
/**
*
*/
async userList() {
try {
const users = await this.userAPI.listUsers(this.API_TOKEN, '', 10);
console.log(users.users);
return users.users;
} catch (error) {
throw new sendbird.HttpException('userList');
}
}
/**
*
* @param userId
* @param nickname
* @param profileUrl
*/
async createUser (createUserDto: CreateUserDto) {
const userData: sendbird.CreateUserData = {
userId: createUserDto.userId,
nickname: createUserDto.nickname,
profileUrl: createUserDto.profileURL
}
try {
const user = await this.userAPI.createUser(this.API_TOKEN, userData)
return user;
} catch(error) {
throw new sendbird.HttpException('createUser');
}
}
}
Response들은 상태 코드에 따른 Response를 객체화한 것 뿐이다.
또한 차후 보안을 위해 APP_ID와 API_TOKEN은 env 환경변수에 셋팅해두고 configService를 주입하여 사용하였다.
userList는 내 대시보드에 존재하는 유저 리스트를 조회하는 것 같았고, createUser는 유저를 생성하는 api였다.
진행방향
우선 메세지 봇이 있는 것과, 파일 첨부 api를 따로 지원한다는 점에서는 합격점을 주고 싶다. 비즈니스 로직과 일맥상통하는 부분이 바로 이 부분이다.
다음 포스팅에서는 어느정도 UI도 구현하여 채팅 버튼을 눌렸을 때 채팅방이 개설되고, 메세지를 간단하게 주고받을 수 있는 정도의 구현 단계를 거쳐 돌아오도록 하겠다.
To-Do-List
1. 현재 회사의 비즈니스 로직과 맞아 떨어지게 돌아가는지를 파악해야한다. 내 생각에는 회사 ERP를 걷어낼 수 없기 때문에 채팅을 주고받는 위주의 api를 확인해봐야할 것 같다.
2. docs를 보면 공개 채널, 그룹 채널을 만들 수 있게 되어있던데, 단순 1:1 문의 기능만 필요한 입장에서 활용가능성이 있는지 알아봐야한다.
3. 단순 센드버드 자체의 장단점을 파악해보고 싶다.
2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!