grep

Engineering

여기어때 고객 상담 시스템 구축 — SendBird와 metaData 기반 상태 관리

엘라Ella(이다빈) / 서비스웹개발팀여기어때

2025년 11월 11일

원문에서 보기 ↗

1. 시작하며

안녕하세요, 여기어때컴퍼니 서비스웹개발팀의 엘라입니다.

이번 글에서는 새롭게 도입한 고객 상담 시스템 프로젝트에서 겪은 기술적 고민과 해결 과정을 공유합니다.

이번 프로젝트의 목표는 고객이 카카오 채널이 아닌 여기어때 앱/웹에서 바로 상담받을 수 있는 환경을 만드는 것이었습니다.

이를 위해 SendBird SDK를 중심으로 실시간 상담 시스템 을 설계하고, Salesforce 연동으로 상담 이력 관리와 상태 기반 처리를 구현했습니다.

또한 웹뷰 형태로 구조를 설계하여 다양한 서비스 환경에서도 재사용 가능하도록 했습니다.

다만, SendBird SDK를 실제 서비스에 녹여내면서 예상보다 고민할 지점이 많았습니다.

특히 커스텀 UI , 상태 기반 분기 처리 , 상담 연결/종료 시점에 맞춘 시스템 메시지 노출 , Salesforce 상태와의 동기화 같은 부분은 SDK에서 제공하는 데이터만으로는 부족해, 결국 SendBird의 메타데이터(metaData) 기능을 적극적으로 활용해 문제를 풀어나가야 했어요.

본격적으로 메타데이터 기반 확장 이야기를 하기 전에,

먼저 SendBird에서 실제로 어떤 흐름으로 채팅이 구성되는지 간단히 짚고 넘어가겠습니다.

2. SendBird SDK 기본 흐름 정리

(초기화 → 채널 생성/조회 → 메시지 로딩 → 실시간 이벤트 핸들링)

이번 프로젝트에서 가장 먼저 필요했던 건 안정적인 초기화와 연결 관리 , 그리고 일관된 메시지 처리 흐름이었습니다.

2–1. SDK 초기화 및 연결 관리

앱이 로드되면 가장 먼저 SendBird SDK를 초기화합니다.

그다음 재연결, 연결 해제 등의 이벤트를 계속 감지해서 채팅 경험이 끊기지 않도록 관리해야 했어요.

// SDK 초기화
const sendbird = SendbirdChat.init({ appId: process.env.SENDBIRD_APP_ID! });

// 연결 상태 핸들러 등록
const handler = new ConnectionHandler();
handler.onConnected = () => console.log('연결 성공');
handler.onReconnectStarted = () => console.log('재연결 시도');
sendbird.addConnectionHandler('default', handler);

이 흐름은 단순해 보이지만, 상담 UI는 실시간성이 중요하기 때문에

연결이 끊겼다가 다시 살아날 때 메시지가 중복되거나 UI가 튀지 않도록 후처리가 필요합니다.

이 부분이 뒤에서 설명할 중복 제거·정렬 로직과도 연결돼요.

2–2. 메시지 이벤트 수신 구조 (GroupChannelHandler)

SendBird SDK는 이벤트 기반으로 메시지를 전달합니다.

하지만 실제 서비스에서는 이 이벤트 그대로 UI에 반영하면 문제가 많습니다.

대표적인 문제가 바로 중복 메시지.

재연결 이벤트나 빠르게 연속 수신된 이벤트 때문에 동일 메시지가 두 번 들어와서

화면에 같은 메시지가 반복 표시되는 일이 없도록 applyMessages라는 훅을 만들어 관리했습니다.

handler.onMessageReceived = (_channel, message) => {
  applyMessages(message);  // 중복 제거 + 최신 메시지만 반영
  channel.markAsRead();
};

applyMessages에서는 다음을 처리해요

이 구조 덕분에 상담 중 재연결되더라도 메시지가 터지거나 순서가 뒤틀릴 일이 없어졌습니다.

2–3. 메시지 로딩 (초기 로딩 + 추가 로딩)

초기 채팅방 로딩에서는 이전 메시지를 불러오고, 채널 메타데이터를 함께 수신하여 상담 상태를 판단합니다.

const query = ch.createPreviousMessageListQuery({ limit: LIMITED_MESSAGE_COUNT });
const msgs = await query.load();
applyMessages(msgs);

setMetaData(await ch.getAllMetaData());

이 metaData가 바로 SendBird 확장 포인트 이자,

이번 글의 핵심이 되는 구조예요.

3. 왜 metaData가 필요했나?

SendBird가 기본적으로 제공하는 메시지 정보만으로는

저희가 필요한 상담 상태 기반 로직을 구현하기엔 한계가 있었습니다.

예를 들어:

이러한 정보는 SendBird SDK 메시지 자체엔 존재하지 않고 ,

그렇다고 각 메시지에 customType으로 넣기도 애매합니다.

(상태는 메시지의 속성이 아니라 “채팅방”의 속성에 더 가깝기 때문)

그래서 선택한 방법이 SendBird의 채널 메타데이터(metaData).

즉, SendBird의 부족한 상태 관리 능력을 메타데이터로 보완 한 셈입니다.

4. 메타데이터 기반 상태 제어 흐름

4–1. 상담방 최초 진입 시 로직

방에 처음 들어오면:

  1. 이전 메시지 로드
  2. metaData 로드
  3. metaData.type, metaData.caseStatus 등을 바탕으로 초기 UI 분기
  4. 필요 시 Salesforce에 임시 케이스 생성
const meta = await ch.getAllMetaData();
setMetaData(meta);

if (!meta.type) {
  const tempCase = await createTempCase(...);
  setTempCaseInfo(tempCase);
}

그리고 metaData에 아직 connectingMessage가 없다면,

상담 연결 안내 메시지를 시스템 타입으로 전송하고

metaData에 “connectingMessage”: “Y”를 넣어 재중복 발생을 차단합니다.

4–2. 상담 상태에 따라 메시지/컴포넌트를 분기

메타데이터 값 예시:

metaData를 감지해 UI와 시스템 메시지를 제어합니다.

if (metaData.caseStatus === 'Waiting' && !metaData.connectingMessage) {
  await sendMessage({ message: '상담사와 연결되었습니다.', customType: 'state-stamp' });
  channel.updateMetaData({ connectingMessage: 'Y' }, true);
}

이 메타데이터 기반으로 다음과 같은 UI 분기가 가능해졌습니다.

SDK만으로는 할 수 없던 기능들을

메타데이터를 통해 자연스럽게 확장한 구조라고 보면 됩니다.

5. 커스텀 메시지 & 버튼 UI

SendBird SDK는 기본 제공 UI가 있지만, 실제 서비스에 넣기엔 제약이 꽤 커서

저희는 대부분 customType 기반의 커스텀 UI로 구현했습니다.

예시:

이 모든 UI의 노출 여부도 metaData를 기준으로 결정합니다.

6. 마무리하며 — 앞으로의 개선 방향

SendBird SDK는 기본적으로 채팅 기능을 제공하는 데 매우 안정적이지만,

서비스 레벨에서 커스텀 기능을 구현하기에는 확장성이 아쉬운 부분도 있었습니다.

그 한계를 이번 프로젝트에서는 메타데이터 기반 상태 관리 로 풀어냈고,

덕분에 상담 흐름을 유연하게 제어하면서도 UI 일관성을 확보할 수 있었습니다.

향후에는 실제 예약 상세, 예약 취소, 쿠폰등 상담에서 더많은 기능을 고도화 계획이 있습니다~

많은 기대 부탁드립니다.