FrontEnd

Firebase로 실시간 데이터 동기화하기

뎁희 2024. 7. 14. 13:56

개인 프로젝트의 기능 중 일부로 Firebase를 이용한 실시간 통신으로 채팅 기능을 구현했다. 전체적인 기획 중에서는 일부이지만 현재 배포된 버전에서는 채팅 기능을 위주로 마무리된 상태이다. 그 이유는 채팅 구현이 생각보다 쉽지 않았기 때문이다. 그래도 소박하게나마 채팅을 구현할 수 있었던 이유는 Firebase가 제공하는 기능들 덕분이었는데 이번 글에서는 이 편리함이 어떻게 제공될 수 있었는지에 대해 적어보려고 한다.


Firebase Realtime Database

Google에서 제공하는 클라우드 기반 NoSQL 데이터베이스이다. 무료이고 사용법이 간단하여, 이전에 진행했던 투두리스트나 쇼핑몰 프로젝트의 데이터베이스로 주로 사용했었다. 여러 가지 특징이 있지만, 주제와 관련하여 가장 큰 특징은 이름에서 나타나듯이 데이터가 실시간으로 클라이언트와 동기화된다는 점이다.

지금까지의 통신은 실시간이 아니었나?

실시간 동기화란 클라이언트와 서버가 연결을 유지한 채로, 양방향으로 데이터를 실시간으로 주고받을 수 있음을 의미한다.

 

일반적으로 웹 사이트를 이용해 쇼핑을 하거나 어떤 서비스를 이용할 때, 서버 통신이 이루어진다. 서버에 요청을 보내고, 서버가 응답을 보내주며 상호작용을 할 수 있는데, 이를 실시간 통신처럼 느낄 수도 있을 것 같다. 그래서 `Firebase Realtime Database`라는 이름이 처음에는 크게 와닿지 않았었다. (그냥 당연해 보였다.)

 

실시간 통신은 혼자서 이용하는 것보다 여러 사람이 함께 이용하는 서비스를 생각하면 이해가 쉽다. 예를 들어, 나와 다른 사람이 같은 화면을 보고 있을 때, 누군가 어떤 행동을 취하더라도 그 결과가 즉시 모든 사용자에게 동일하게 보여야 한다. '요청을 보내고 응답을 받는다'보다 '어떤 요청이 없어도 응답을 받는다'고 표현할 수 있겠다.


양방향 통신 vs 단방향 통신

우리는 지금까지 서버와 데이터를 주고받았다고 생각했는데 양방향/단방향이 왜 나뉘는 것일까? 웹 서비스는 HTTP 프로토콜을 통해 서버는 요청에 대한 응답을 보내줄 뿐, 클라이언트에게 어떤 요청을 하지 않는다. 즉, 요청 -> 응답 순서를 따르는 단방향 통신을 하게 된다. 서버는 클라이언트가 요청하지 않으면 어떤 데이터도 보내지 않는다.

 

그렇다. HTTP 프로토콜을 이용한 서비스는 양방향 통신이 이루어지지 않는다. 그러므로 채팅 기능을 HTTP로 구현하는 것에는 한계가 있다. 사용자 A가 서버에 요청을 하면, 서버는 사용자 B가 요청하지 않아도 B에게 응답을 보내줄 수 있어야 하기 때문이다. 물론 아예 불가능한 것은 아니지만, 폴링(polling)과 같은 방법을 이용해 주기적으로 서버에 요청을 보내서 응답을 받을 수 있도록 해야 하는데, 이는 비효율적이다.

 

채팅 외에도 양방향 통신이 필요한 케이스는 많다. 게임, 주식 거래 현황 등 즉시성을 요구하는 데이터를 다루는 서비스에서는 필수적이며, 폴링만으로는 한계가 있다.


WebSocket

웹소켓은 HTTP 프로토콜의 한계를 해결하기 위해 등장했다. 이는 Firebase의 주요 장점 중 하나인 양방향 통신을 지원할 수 있게 해주는 프로토콜이다.

특징

  • HTTP 프로토콜(Stateless)을 업그레이드 한 웹 프로토콜
  • 양방향 통신으로 클라이언트와 서버가 모두 데이터 전송
  • 한번 연결 설정 후 지속적으로 연결 유지(Stateful)

WebSocket 연결 과정

웹소켓을 이용하기 위해 HTTP와 다른 요청 방법이 있는 것은 아니다. HTTP 프로토콜 요청을 보낼 때 "나 WebSocket으로 업그레이드할래~"라고 서버에게 메시지를 전달하고, 서버가 이에 응하면 그때부터 양방향 통신이 시작된다.

  1. HTTP handshake
    클라이언트가 HTTP 요청을 보내 서버에게 WebSocket으로 업그레이드를 요청한다.
    이때 HTTP 요청에는 `Upgrade`헤더가 포함되어 있다.
  2. Response: 서버가 승인하면 WebSocket 연결이 설정된다.
  3. Data: 연결이 설정되면 양방향으로 데이터를 주고받는다.

연결 결과

프로젝트의 Request-Response 결과를 통해 파이어베이스도 내부적으로 웹소켓 통신을 하고 있다는 것을 알 수 있다. 이미지에 표시된 `Status Code: 101 Switching Protocols`가 웹소켓으로 업그레이드를 승인했다는 서버의 응답 코드이다.


Firebase는 WebSocket과 언제 연결되었을까?

위 이미지를 통해 Firebase Realtime Database도 WebSocket을 이용한다는 것을 알았다. 내 프로젝트에서 어느 순간 연결이 되었다는 것인데 그 시점이 궁금했다. 

 

1. 데이터베이스 인스턴스 생성

const db = getDatabase(app);

파이어베이스 계정으로 얻은 정보들을 이용해 app을 초기화하고, 그 값으로 db 인스턴스를 생성한다. 단지 인스턴스를 생성해 초기화한다고 해서 연결이 되지는 않았다.

 

2. 데이터 참조 및 동기화 시작

const userRef = ref(db, 'users');
const chatRoomRef = ref(db, 'lines');
const messagesRef = ref(db, 'messages');

특정 데이터 객체에 접근하려면 참조가 필요한데 참조하는 값을 이용해 데이터를 읽거나 쓰는 시점에 WebSocket이 연결을 위한 HTTP 요청을 시도한다.

 

파이어베이스 계정을 생성하고, 데이터베이스를 만든 뒤 제공받은 key를 이용해 참조로 연결하는 것만으로 실시간 양방향 통신을 위한 준비가 간단히 끝났다. 단, 이벤트 발생에 따라 수신을 즉각 받을 수 있도록 구독 함수 설정이 필요하다. 

 

구독 함수

onValue, onChildAdded, onChildChanged, onChildRemoved
export const addMessageListener = (chatRoomId: string, callback: (newMessages: Message[]) => void) => {
  const chatRoomMessagesRef = child(messagesRef, chatRoomId);
  onValue(chatRoomMessagesRef, (snapshot) => {
    if (snapshot.exists()) {
      const messages: Message[] = snapshot.val();
      callback(Object.values(messages));
    }
  });

  //리스너 해제
  return () => off(chatRoomMessagesRef);
};

파이어베이스는 콜백 함수를 실행할 수 있는 구독 함수를 제공한다. 그 함수를 이용해 참조할 데이터베이스에 구독을 설정해 두면 이벤트가 감지될 때마다 실시간으로 수신받을 수 있다. 


WebSocket을 직접 사용한다면?

프로젝트를 처음 배포한 후 Firebase의 한계를 많이 느꼈기 때문에, 이 프로젝트를 다른 서버와 DB를 이용해 다시 만든다면 어떨까 하는 생각을 했다. 특히 채팅 기능을 구현하기 위해 Firebase와 비교해서 WebSocket이 어떻게 동작하는지 간단히 알아보았다.

 

[Server]

  • WebSocket 라이브러리를 사용하여 WebSocket 서버 생성
  • 연결 후 상태에 따라 처리할 콜백 함수 정의

[Client]

  • WebSocket 인스턴스를 생성: 서버의 WebSocket 경로 적용
  • 이벤트 핸들러를 이용해 콜백 함수 등록

이렇게만 보면 간단해 보이지만, 이는 단지 연결과 이벤트 등록 과정일 뿐이다. 채팅방이나 메시지 처리 등 별도의 CRUD API가 추가되면 작업량이 많아질 것이다.


Firebase vs WebSocket 직접 사용

간편함을 얻고 자유도를 잃을 것이냐 vs 간편함을 잃고 자유도를 얻을 것이냐의 차이이다. UI 컴포넌트를 구현할 때도 라이브러리를 사용하면 편리하게 원하는 기능을 구현할 수 있지만, 원하는 대로 커스터마이징 하기 어렵다. 마찬가지로 Firebase를 사용하면 채팅을 손쉽게 구현할 수 있지만, 고도화된 기능이나 커스터마이징에는 제약이 생긴다.

 

예를 들어, Firebase는 이벤트 수신에 따라 콜백 함수를 이용해 바로 DB에 값을 추가하거나 수정할 수 있으며, 클라이언트와 서버가 자동으로 동기화된다. 반면, WebSocket은 이 과정도 수동으로 관리해주어야 한다. 수동으로 관리하는 것이 불편해 보일 수 있지만, 피그마나 노션처럼 클라우드를 이용해 실시간 협업을 할 때, 데이터 충돌을 어떻게 처리할 것인가와 같은 정책을 더 정밀하게 설정할 수 있다. 또한, 프로젝트가 작고 소박한 경우에는 Firebase가 제공하는 보안 수준으로 충분하지만, 실시간으로 주고받는 데이터가 중요한 경우 더 강력한 보안 설정이 필요할 수도 있다.

 

반대로, Firebase의 장점 중 하나는 오프라인 지원이다. WebSocket은 자체적으로 오프라인 지원을 하지 않기 때문에 연결이 끊기면 재연결 로직과 오프라인 상태에서의 데이터 처리를 직접 다루어야 한다. Firebase는 이를 보완할 수 있도록 설계되어 있다. 오프라인 상태에서 로컬에 캐싱하여 사용하다가 다시 연결되었을 때, 자동으로 동기화된다. WebSocket으로 이 부분을 보완하려면 네트워크 연결 상태를 확인하고, 연결이 끊기면 어떻게 할 것인지, 다시 연결되었을 때는 어떻게 할 것인지 모두 직접 설정해주어야 하는 번거로움이 있다.

 

어떤 것을 선택할지는 프로젝트의 규모나 요구사항에 따라 다르겠지만, 간단한 서비스에서는 Firebase의 오프라인 지원 기능도 큰 장점이 될 것이다.


채팅 SDK

 

센드버드 - 채팅과 영상통화 개발의 완벽한 솔루션

채팅, 음성 통화, 영상 통화 개발을 위한 센드버드의 API와 네이티브 SDK, 그리고 UI 키트. 높은 트래픽의 사용자의 참여를 극대화 하기 위한 혁신적인 채팅, 고객상담, 라이브 커머스 서비스를 가

sendbird.com

 

관심 있는 채팅 서비스들이 센드버드(Sendbird)라는 채팅 SDK를 쓴다는 것을 알고 있어 관심 있게 보던 서비스이다. 센드버드도 웹소켓을 기반으로 개발되었다고 한다. 이를 활용하여 실시간 통신 기능을 제공하는 고도화된 기능을 추상화한 라이브러리 같은 것이라고 볼 수 있다. 파이어베이스가 가진 장점에 웹소켓의 장점을 더하고 거기에 부가적인 기능들이 또 제공된다. 

 

처음 이 서비스를 알았을 때에는 가격을 보고 가능할까? 정말 이 가격이 괜찮을까? 했던 기억이 난다. 하지만 채팅 기능을 구현하면서 채팅방, 참여자, 메시지, 참여한 채팅방 관리 등 실시간 통신 문제뿐만 아니라 관련된 데이터가 너무 많아 헷갈리고 관리가 어려워 개발 속도가 느려질 때가 많았다. 센드버드가 제공하는 기능들을 보면, 왜 이런 서비스가 등장했는지 백번 천 번 이해가 되고, 금액적인 부담이 있을 수 있지만 개발 리소스를 고려하면 충분히 고려할 만하다는 생각이 들었다.

 


반성으로 마치며

채팅 통신에 대해 알아본 후, Firebase를 선택한 이유를 다시 한번 생각해 보는 시간을 가졌다. 이 글은 어느새 프로젝트 회고이자 반성문이 되었지만, 변명하자면 프로젝트를 시작할 당시 나에겐 선택의 여지가 없었다. 개인 프로젝트였고, 백엔드를 구성할 수 없었기 때문에 Firebase가 최선의 선택이었다. 그러나 지금 돌아보면, 채팅 기능을 구현하기 위해 Firebase는 최적의 선택이었다고 생각된다. 다만 Firebase를 단순히 쉽게 구축할 수 있는 저장소로만 생각하고, 실시간이라는 장점을 제대로 인지하지 못한 것 같다. 만약 이 점을 인지했다면, 채팅을 선택했을 때 더 고민해보고 다른 서비스를 선택하거나, 웹소켓을 경험해보고 싶은 백엔드 개발자를 구하는 등 다른 방향으로 진행했을지도 모른다는 생각이 든다.