figma

LiveGraph: Figma의 실시간 (새 탭에서 열림)

Figma는 실시간 협업 제품에 필요한 데이터를 안정적으로 제공하기 위해 Postgres 위에 GraphQL 기반의 실시간 데이터 계층인 LiveGraph를 구축했다. LiveGraph는 프론트엔드가 선언적으로 데이터를 구독하면 데이터베이스 복제 스트림을 읽어 밀리초 단위로 변경 사항을 반영한다. 이를 통해 수동 이벤트 메시지와 클라이언트 상태 동기화의 복잡성을 줄이고, 대규모 실시간 데이터 구독을 지원한다.

Figma에서 실시간 데이터가 필요한 이유

  • 협업자가 파일을 추가하거나 권한을 변경하면 다른 사용자 화면에도 새로고침 없이 즉시 반영되어야 한다.
  • 따라서 서버에서 데이터를 한 번 가져오는 것만으로는 부족하며, 클라이언트가 현재 관심 있는 데이터의 변경 사항을 계속 받아야 한다.
  • 인프라 팀의 목표는 제품 개발자가 데이터 전파 방식이나 WebSocket 세부 구현을 직접 관리하지 않고도 실시간 뷰를 만들 수 있게 하는 것이었다.

기존 방식의 한계

  • 초기에는 React 프론트엔드가 Ruby HTTP 엔드포인트에서 필요한 데이터를 한 번에 받아 Redux 전역 상태에 저장했다.
  • 데이터 변경 시 백엔드 코드에서 관련 클라이언트에 보낼 이벤트 메시지를 직접 작성하고, 프론트엔드는 WebSocket으로 이벤트를 받아 상태를 갱신했다.
  • 사용자와 데이터 규모가 커지면서 모든 데이터를 한 번에 로드하기 어려워졌고, 데이터를 점진적으로 불러오면서 다음 문제가 발생했다.
    • 특정 데이터가 메모리에 항상 존재한다는 보장이 없어짐
    • 여러 제품 영역이 같은 데이터를 사용할 때 데이터 로딩 책임이 불분명해짐
    • 이벤트를 어느 시점에 보내고 받아야 하는지 관리하기 어려워짐
  • 단순한 “새 파일 생성” 이벤트와 달리 권한 변경은 다른 리소스의 가시성까지 연쇄적으로 바꿀 수 있어 이벤트 설계가 복잡했다.
  • 데이터베이스 쓰기 순서와 실시간 메시지의 송수신 순서가 항상 일치한다는 보장도 없었다.
  • 그 결과 클라이언트 상태가 서버 상태의 올바른 부분집합을 반영하지 못하는 일관성 버그가 발생했다.

GraphQL 기반 Live Query 선택

  • Figma는 개발자가 실시간 데이터 구독을 선언적으로 정의할 수 있는 일반적인 프레임워크가 필요하다고 판단했다.
  • GraphQL을 인터페이스로 사용하면 필요한 데이터와 관계를 쿼리로 표현하고, 시스템이 해당 데이터를 자동으로 가져오고 최신 상태로 유지할 수 있다.
  • 여기서 말하는 GraphQL 구독은 일반적인 이벤트 스트림 구독과 다르다.
    • GraphQL의 전통적인 subscription은 이벤트 메시지를 전달하는 방식에 가깝다.
    • LiveGraph가 목표로 한 것은 쿼리 결과 자체를 계속 갱신하는 “Live Query” 방식이다.
  • 프론트엔드는 GraphQL과 유사한 쿼리를 보내고, 서버는 결과를 JSON 트리로 반환한다.
  • 서버에는 엔터티와 관계를 정의하는 스키마 및 그래프의 일부를 조회할 수 있는 뷰가 존재한다.

기존 실시간 데이터베이스 대신 자체 구축한 이유

  • Figma는 이미 Postgres를 대규모로 운영하고 있었기 때문에 Firebase나 RethinkDB 같은 별도의 실시간 데이터베이스로 이전할 수 없었다.
  • LiveGraph는 새로운 저장소가 아니라 기존 Postgres 위에 동작하는 쿼리 엔진이 되어야 했다.
  • Figma의 Multiplayer 시스템은 파일 단위의 쓰기와 충돌 해결을 담당하지만, LiveGraph는 여러 데이터의 조회와 실시간 동기화를 담당한다.
  • Hasura, Prisma, PostGraphile 등 GraphQL 기술도 검토했지만, 대규모 동시 구독을 핵심 요구사항으로 설계된 것은 아니었다.
  • Figma는 실시간 구독 수가 많아질수록 데이터베이스 부하가 커지는 폴링 방식도 피하고자 했다.
    • 폴링은 쿼리마다 주기를 정해야 한다.
    • 구독 수가 늘어나면 동일한 쿼리가 반복 실행되어 데이터베이스 부하가 증가한다.
    • 폴링보다 변경 발생 시점에 가까운 낮은 지연 시간을 확보하기 어렵다.

데이터베이스 복제 스트림 기반 설계

  • LiveGraph는 주기적으로 데이터를 다시 조회하는 대신 Postgres의 데이터베이스 복제 로그를 추적한다.
  • 복제 스트림에서 변경 사항을 읽으면 실제 데이터베이스 변경을 감지한 뒤 구독 중인 쿼리 결과를 갱신할 수 있다.
  • 이 방식은 폴링보다 빠른 업데이트 지연 시간을 제공한다.
  • 다만 LiveGraph가 데이터베이스의 전체 변경량을 읽어야 하므로, 대규모 환경에서는 확장성이 중요하다.
  • Figma는 여러 데이터베이스 샤드의 변경 사항을 여러 머신에 분산 처리할 수 있는 구조를 고려했다.
  • 최종적으로 LiveGraph를 자체 구축한 이유는 Figma의 협업 기능에서 실시간 데이터가 핵심 기능이며, 이러한 요구사항이 경쟁력으로 이어질 수 있다고 판단했기 때문이다.

실용적인 결론

실시간 UI를 구축할 때 클라이언트별 수동 이벤트와 전역 상태 갱신에 의존하면 데이터 규모와 기능 복잡도가 커질수록 일관성 문제가 발생하기 쉽다. 기존 Postgres를 유지해야 하고 대규모 구독이 필요하다면, GraphQL 기반 선언적 쿼리와 데이터베이스 변경 스트림을 결합하는 방식이 폴링이나 수동 이벤트보다 확장성과 유지보수성 측면에서 유리하다.