logo
$0으로 실시간 채팅을 만들 수 있을까?

$0으로 실시간 채팅을 만들 수 있을까?

마켓플레이스에서 대화할 수 없다

지난 달에 NFT 마켓플레이스에서 관심 있는 아이템을 발견했었다.

그런데 가격이 좀 애매해서 판매자와 흥정해보고 싶었는데, 사이트 내에서 연락할 방법이 따로 존재하지 않았다.. 굳이 하려면 Discord 서버를 찾아보거나 트위터에서 직접 DM을 보내야만 했다...

근데 거기까지 가면 이미 귀찮아서 안 하게 된다. 

"마켓플레이스 페이지를 떠나지 않고 바로 대화할 수 있으면?" 이라는 생각이 계속 남아 있었고, 그래서 사이드 프로젝트로 직접 만들어보기로 했다.

근데 문제가 하나 있다. 채팅 서비스를 만들려면 반드시 서버가 필요하다.

  • 메시지를 중계하는 서버
  • 사용자 인증을 관리하는 서버
  • 메시지를 저장하는 디비 서버

경험상, 사이드 프로젝트에 월 몇만원이라도 유지비가 발생하면 결국 나중에 내려버리게 되는 경험이 있었다

그래서 이번 플젝에서는 스스로 제약을 하나 걸었다. 

"서버 비용 $0"

서버 없는 실시간 통신?

WebRTC라는 기술이 있다. 이걸로 서버를 거치지 않고 직접(P2P) 데이터를 주고받을 수 있도록 할 수 있다. 텍스트든 음성이든 영상이든 서로 직접 교환하는 게 가능하다..!

그런데 함정, 단점이 하나 있다.

WebRTC를 쓰려면 "시그널링"이라는 단계가 필요하다. 두 브라우저가 서로를 찾고 연결을 맺기 위한 초기 정보를 교환하는 시그널링이란 단계가 필요하다는 것이다. 서버없는 구조를 원했지만 WebRTC 프로토콜 또한 구조상 어쨌든 중간 서버가 필요했다.

그래서 시그널링을 위해서... 무료로 사용가능한 서버 후보를 리서치해봤다.

  • Firebase: 무료 티어가 있었지만 Google 인프라에 종속되는 단점이 있다.
  • Nostr 공개 릴레이: Nostr는 트위터(X)의 탈중앙화 버전 같은 프로토콜인데, 누구나 무료로 사용할 수 있는 공개 서버가 운영되고 있다. 따로 내 서버를 호스팅하지 않고 무료로 사용가능하다.

나는 100% 무료인 Nostr를 골랐다. Trystero라는 라이브러리에서 Nostr 프로토콜도 쉽게 활용할 수 있는 구조여서 구현하기에 큰 부담도 되지 않을 거라 판단했다.

그런데 트레이드오프가 100% 없진 않았다. 전용 시그널링 서버와 비교하면 지연시간 500ms 정도는 느렸다..!

종단간 암호화

보통 일반적인 채팅앱은 세션을 서버가 발급하고 인증하지만, 나는 서버를 사용하는 옵션은 모두 배제해야 했다. 

대신에 가용 가능한 방식이 "종단간 암호화"였다. 종단간 암호화란 클라이언트가 공개키/암호키 방식으로 직접 암호화/복호화 하며 데이터를 주고받는 방식이다.

먼저 이 종단간 방식으로 구현하려면 개인키/공개키를 개인이 생성하거나 소유하고 있어야 하는데, 내 서비스는 이미 Web3에서 동작하는 웹앱이었다.

즉 블록체인을 사용하는 사람이라면 월렛이라는 특수한 형태의 개인키를 모두 소유하고 있기 때문에, 이걸 그대로 사용하면 개인의 신원 증명 뿐만 아니라 메시지 암호/복호화에도 활용할 수 있었다.

아무튼 채팅방에 입장할 때 양쪽 모두 자신의 지갑으로 서명을 하고, 상대방이 보낸 서명을 공개키로 검증하는 방식으로 디자인했다.

const message = `I am the owner of room ${roomAddress} at ${timestamp}`;
const signature = await wallet.signMessage(message);
// 상대방은 이 signature를 검증해서 방 주인이 맞는지 확인

이렇게 큰 틀은 완성시킬 수 있었으나 구현을 어느정도 진행하니 채팅방이라는 시스템에서 다뤄야할 엣지케이스가 꽤나 많았다. 

채팅방의 주인 입장에서 갑자기 누군가 접속하면? -> 수락/거절할 수 있어야 한다. 

신원 검증이 실패하면? 연결을 즉시 끊어야 한다. 

한쪽이 브라우저를 강제종료되면? 상대방 화면에서 세션이 정리가 되어야 한다. 

채팅 중에 네트워크가 끊기면?... 등등

결국 Peer의 상태를 명시적으로 관리하는 상태머신을 그려보고 설계까지 이어지게 됐다.

Verifying → Requesting → Pending → Chatting
               ↘ Rejected   ↘ Failed   ↘ Disconnected

서명검증(Verifying) → 채팅요청(Requesting) → 승인 대기(Pending) → 채팅중(Chatting)

+거절, 검증 실패, 연결 종료 같은 예외 분기까지. 총 8단계.

상대방이 오프라인일 때?

이런 식의 P2P로 동작하는 채팅방에는 근본적인 한계가 하나 있다. 

바로, 양쪽 모두 온라인 상태인 경우에만 서로 연결을 맺고 대화 가능한 상태가 되는 것이다...

그래서 만약 어느 한 쪽이라도 오프라인이면 대화가 불가능하기 때문에 댓글 처럼 메시지를 남길 수 있는 수단이 필요했다. 

그런데 댓글 또한 마찬가지로 DB와 서버 없이는 일반적으로 구현할 수 없다.

그래서 여기서 Nostr를 또다시 활용해보게 됐다. 시그널링 용도로만 쓰려던 Nostr을 댓글을 저장/조회하는 데도 활용하는 것이다. 

Nostr 프로토콜은 누구나 데이터를 발행하고 조회할 수 있는 구조라서, 댓글 저장소로 쓰는 게 가능했다.

다만 여기서도 Web3의 월렛을 이용했다. 댓글을 작성/수정/삭제 하기 전에 사용자의 월렛으로 메시지를 서명하여 댓글이 소유권이 보장되도록 구현했다.

+ 다만 Nostr은 데이터를 영구 저장해주지 않을 수 있다. Nostr 릴레이는 각자의 보존 정책이 있어서 며칠에서 몇 달 정도 유지된다. 이건 $0 제약 안에서는 감수할 수밖에 없었다.

채팅을 마켓플레이스 안으로

해당 프로젝트는 Opensea 또는 Blur 같은 NFT 마켓플레이스에 채팅 수단을 제공하기 위한 목적으로 시작한 프로젝트였다.

그런데 내 서비스를 사용하기 위해서 기존 호스팅 사이트 내부가 아닌 경로를 거쳐야 접근할 수 있다면 접근성이 떨어져서 아무도 사용하지 않을 게 분명했다...

이때 떠오른 답이 크롬익스텐션으로 배포하는 것이었다. 익스텐션은 한 번만 설치해두면 외부사이트의 DOM을 조작할 수 있기 때문에, 내 서비스로 연결시켜주는 버튼을 주입시킬 수 있었다

크롬익스텐션의 구동 환경.

일반적으로 Web3 사용자 대부분은 자신의 월렛을 Metamask 등의 크롬익스텐션에 등록해둔다. 만약 웹페이지 내에서 월렛 서명이 필요한 경우에 이러한 익스텐션을 거쳐서 사용자의 월렛을 활용할 수 있는 구조이다.

즉 사이트 내에서 사용자 월렛으로 데이터를 서명하려면 아래와 같은 순서를 따른다...!

웹사이트 -> Metamask(Provider) -> 월렛

그런데 문제는 내 서비스도 크롬익스텐션이라는 환경 내에서 실행되어야 했고, 일반적인 웹사이트와는 구동환경이 크게 다르다.

브라우저에서 일반적인 방식으로 접근하는 페이지는 Main 환경이라 불리며, 크롬익스텐션으로 동작하는 웹앱은 "Isolated"라는 분리된 구동환경이다. 

정리하면 아래와 같다.

  • Main 환경: 호스트 환경. 즉 사용자가 브라우저 내에서 띄워두고 메인으로 사용하는 페이지의 환경.
  • Isolated 환경: 크롬익스텐션 전용 환경.

Metamask 같은 월렛제공자 역할을 하는 크롬익스텐션은 당연 Isolated 환경에서 실행되며, Main 환경인 호스트 웹페이지에서만 이것과 소통이 가능한 구조이다.

그런데 내 서비스 또한 Isolated 환경에서 동작하는 익스텐션이기 때문에 Main 환경인 호스트와만 소통이 가능하다는 제약이 따라오게 됐다.

즉, 익스텐션끼리 소통하는 게 원초적으로는 불가능하기 때문에, 내 익스텐션에서 Metamask를 통한 사용자 월렛에 도달하는 게 불가능한 구조였다.

그래서 이리저리 방안을 찾아보았는데, 정공법으로 Main 환경에 프록시 역할을 하는 스크립트를 심는 게 가능했다.

즉 익스텐션인 내 서비스가 Main 호스팅 환경을 거쳐서 Metamask와 통신하는 구조를 만드는 게 이론적으로는 가능했다. 

내 익스텐션 ←→ Background Service Worker ←→ ISOLATED world ←→ MAIN world
                                                                ↕
                                                          호스트 페이지 지갑

위 구조를 통해서 사용자의 서명이 필요할 때 내 서비스에서 Background, ISOLATED, MAIN을 거쳐 지갑에 도달하고 다시 역순으로 결과를 받아내는 구조이다.

꽤 복잡했지만... 이론적으로 가능한 설계였다

그런데 크롬익스텐션 개발 자체가 익숙치 않아서, 기술적으로 구현이 가능한 것과 불가능한 것의 경계를 이해하는 데에서 막히는 상황이 정말 많았다...

최종 비용

  • WebRTC 시그널링 (Nostr): $0
  • 댓글 저장 (Nostr): $0
  • P2P 데이터 전송: $0
  • STUN 서버 (Google 공개): $0
  • 합계: $0

스스로 걸어둔 "서버비용 $0"라는 제약이 나중에 기술적 의사결정에 많은 영향을 줬다. 

서버를 호스팅하지 않기 위해 우회방법을 매번 고민하게 만들었다...

시그널링? -> Nostr 프로토콜. 

사용자 인증? -> Web3 서명. 

댓글 저장? -> Nostr 프로토콜.

서버비용을 없애려다보니 따라오는 트레이드오프도 당연 존재했다.

WebRTC는 양방향 통신이니 서로가 온라인이어야 연결맺음이 가능하고, 무료로 가용 가능한 Nostr 서버는 어찌 됐든 빌려쓰는 것이기 때문에 데이터 보존보존 기간은 각각의 서버 정책에 의존해야 했다. 

결국 서버비용 $0 제약 아래에서 모두 감수해야 했다.

결과와 느낀점

Chrome Web Store: 클릭

GitHub: 클릭

서버비용 $0, 스스로 걸어둔 제약 아래에서 방향을 이리저리 틀어가며 답을 찾아야했다. 막막하고 골아픈 지점이 꽤 많았지만, 그만큼 새로운 시도도 많이 해볼 수 있어서 꽤 즐거웠다

Written on

2026-03-30 07:17

Comments