SiliconValley_survivor

SiliconValley_survivor

Frontend Figma, 실시간 캔버스

도형은 즉시 움직이고 문서는 안전하게 합쳐지도록 좌표 계산과 렌더링, 공동 편집의 책임을 나누는 Figma형 실시간 캔버스 설계.

SiliconValley_survivor's avatar
SiliconValley_survivor
Oct 01, 2026
∙ Paid

Introduction & Requirements

두 명의 디자이너가 같은 파일을 열었습니다.

한 사람은 버튼을 오른쪽으로 옮기고, 다른 사람은 그 버튼의 배경색을 바꿉니다. 화면에는 상대방의 커서와 선택 영역이 보이고, 각자의 변경도 빠르게 반영됩니다.

여기까지만 보면 실시간 캔버스는 도형의 좌표를 WebSocket으로 전달하는 기능처럼 보입니다.

그런데 사용자가 화면을 두 배로 확대했다고 해보겠습니다.

마우스를 오른쪽으로 100px 움직였을 때 도형도 문서 좌표에서 100만큼 이동해야 할까요. 확대된 화면에서는 같은 마우스 이동이 문서 안에서 더 짧은 거리에 해당해야 합니다.

이번에는 버튼을 옮기는 동안 다른 사용자가 버튼의 부모 프레임을 회전시켰습니다.

브라우저가 처음 드래그를 시작할 때의 좌표를 그대로 저장하면 버튼이 예상하지 못한 위치로 이동할 수 있습니다.

그다음 사용자가 실행 취소를 누릅니다.

자신이 옮긴 위치만 되돌리고 싶은데, 이전 문서 전체를 복원하면서 동료가 변경한 배경색까지 사라집니다.

마지막으로 네트워크가 끊깁니다.

내 화면에서는 도형이 움직였고 동료 화면에도 잠깐 보였습니다. 하지만 서버에는 최종 위치가 저장되지 않았을 수 있습니다.

이 문제들은 렌더링 속도 하나로 해결되지 않습니다.

실시간 캔버스에서는 세 가지 결과를 구분해야 합니다.

내 입력에 대한 즉각적인 반응, 다른 사용자에게 보여주는 작업 과정, 서버가 확정한 문서 변경입니다.

이번 글에서는 Figma형 공동 디자인 편집기를 설계합니다. 도형과 텍스트가 있는 캔버스를 중심으로 화면 이동과 확대, 선택과 드래그, 공동 편집, 실행 취소와 재연결을 하나의 흐름으로 연결하겠습니다.

핵심은 캔버스를 빠르게 그리는 것만이 아닙니다.

사용자가 어디를 가리키고 있는지 정확하게 계산하고, 지금 보이는 움직임이 어떤 상태인지 구분하며, 여러 사람이 편집해도 문서 구조와 작업 이력이 망가지지 않게 만드는 것입니다.

Functional Requirements

  • 사용자는 프레임, 도형, 텍스트와 이미지를 생성하고 선택, 이동, 크기 변경, 회전과 삭제를 수행할 수 있어야 합니다.

  • 사용자는 캔버스를 이동하고 확대하거나 축소하면서 같은 객체를 정확하게 선택하고 편집할 수 있어야 합니다.

  • 여러 사용자가 같은 문서를 편집하고 상대방의 커서, 선택 영역과 작업 중인 미리보기를 확인할 수 있어야 합니다.

  • 사용자는 자신의 작업을 실행 취소하거나 다시 실행할 수 있어야 하며, 다른 사용자의 후속 변경을 조용히 덮어쓰지 않아야 합니다.

  • 연결이 끊겼을 때 로컬 변경을 구분해 보존하고, 다시 연결되면 서버의 최신 문서와 미확정 작업을 확인할 수 있어야 합니다.

Non-Functional Requirements

  • 현재 페이지에 노드 2만 개가 있고 화면 주변의 렌더링 후보가 약 1,000개인 시험 환경에서, 드래그 입력의 시각적 반응을 p95 50ms 이내로 목표합니다.

  • 60Hz 화면에서 프레임 간격 약 16.7ms를 기준으로 렌더링 비용을 관리하고, 입력 처리와 장면 계산이 메인 스레드를 장시간 점유하지 않도록 합니다.

  • 정상적인 네트워크 환경에서는 원격 드래그 미리보기 전달을 p95 250ms 이내, 편집 완료 후 서버 저장 확인을 p95 500ms 이내로 목표합니다.

  • 같은 작업의 재전송이 중복 변경을 만들지 않아야 하며, 오래된 이벤트가 더 최신의 확정 상태를 되돌리지 않아야 합니다.

  • 문서 데이터, 미확정 작업과 GPU 리소스에 각각 상한을 두고, 키보드와 보조 기술로 주요 편집 작업을 수행할 수 있어야 합니다.

이 수치는 설계를 위한 목표입니다. 모든 기기와 모든 문서에서 같은 결과를 보장한다는 뜻은 아닙니다.

특히 원격 미리보기와 저장 확인의 지연 목표는 정상적인 연결 상태를 전제로 합니다. 오프라인에서는 빠른 저장 완료 표시를 만드는 대신, 아직 서버에 저장하지 못했다는 사실을 정확하게 보여줘야 합니다.

Out of Scope

  • 모든 벡터 효과와 파일 형식을 완벽하게 지원하는 그래픽 엔진 전체를 구현하지 않습니다.

  • 여러 사용자가 같은 문단을 동시에 수정하는 일반적인 리치 텍스트 협업 알고리즘은 직접 구현하지 않습니다.

  • 임의의 플러그인 실행과 디자인 자동 생성 모델은 제외하고, 문서 편집과 렌더링 및 동기화 경계에 집중합니다.

Traffic & Capacity Assumptions

실시간 편집기는 일일 페이지 조회 수보다 동시에 편집하는 사용자와 문서별 참여자 수가 중요합니다.

피크 시간에 편집자 5만 명이 접속해 있고, 문서당 평균 5명이 함께 작업한다고 가정하겠습니다. 동시에 활성화된 문서는 약 1만 개입니다.

사용자 한 명이 평균적으로 초당 0.5개의 확정 편집 배치를 만든다면 서버는 초당 약 2만 5,000개의 변경을 처리합니다.

여기서 편집 배치는 마우스 이동 이벤트 하나가 아닙니다. 한 번의 드래그를 끝내거나 여러 도형의 색상을 함께 바꾸는 것처럼 사용자가 하나의 작업으로 인식하는 단위입니다.

Peak editors

50,000

Average committed batches per editor

0.5 per second

Total committed batches

25,000 per second

편집 배치 하나의 평균 전송 크기를 700바이트로 가정하면 서버로 들어오는 변경 데이터는 초당 약 17.5MB입니다.

같은 문서의 다른 사용자 4명에게 전달한다면 변경 이벤트의 전송량은 초당 약 70MB가 됩니다.

Mutation ingress

25,000 x 700 bytes
= 17.5MB per second

Mutation fanout

25,000 x 4 recipients x 700 bytes
= 70MB per second

프로토콜 비용, 복제와 재전송은 제외한 값입니다. 대량 붙여넣기나 복잡한 벡터 편집은 평균보다 훨씬 큰 배치를 만들 수 있으므로 별도의 크기 제한도 필요합니다.

커서와 작업 미리보기도 무시할 수 없는 트래픽입니다

커서와 드래그 미리보기는 확정 편집보다 더 자주 바뀔 수 있습니다.

전체 사용자의 활동과 전송 제한을 반영한 평균을 사용자당 초당 2개, 이벤트 크기를 160바이트로 가정하겠습니다.

같은 문서의 다른 사용자에게 보내는 양은 초당 약 64MB입니다.

Presence fanout

50,000 editors
x 2 events per second
x 4 recipients
x 160 bytes

= 64MB per second

문서당 참여자가 5명에서 100명으로 늘어나면 같은 계산을 그대로 적용할 수 없습니다. 한 사람의 이벤트를 전달해야 하는 대상이 크게 늘기 때문입니다.

따라서 문서 변경과 존재 정보의 전송 정책을 분리합니다.

오래된 커서 위치는 버릴 수 있지만, 확정된 삭제 이벤트를 같은 방식으로 버리면 문서가 달라집니다.

브라우저 메모리는 노드 수만으로 계산하지 않습니다

페이지에 노드 2만 개가 있고 직렬화한 노드 정보가 평균 600바이트라면 데이터만 약 12MB입니다.

하지만 실제 브라우저에는 문자열과 객체, 공간 인덱스, 계산된 경계와 GPU 리소스도 존재합니다.

이미지도 중요합니다.

4,096 × 4,096 이미지 한 장을 픽셀당 4바이트로 표현하면 약 64MiB입니다.

Decoded image estimate

4096 x 4096 x 4 bytes
= 64MiB

이런 이미지 20개가 모두 해당 크기의 픽셀 데이터로 유지된다면 그것만 약 1.25GiB입니다. 추가 버퍼나 텍스처 형식에 따른 차이는 별도로 봐야 합니다.

따라서 네트워크에서 받은 압축 이미지 크기만 보고 메모리 예산을 정하지 않습니다.

화면에 필요한 해상도, 디코드된 이미지의 수명, GPU 텍스처의 재사용과 제거 정책이 필요합니다.

Core Entities & Data Modeling

캔버스에 보이는 사각형은 단순한 픽셀 묶음이 아닙니다.

어느 페이지에 속하는지, 어떤 프레임의 자식인지, 앞뒤 순서는 무엇인지, 위치와 크기는 얼마인지, 어떤 색과 이미지를 사용하는지 알아야 합니다.

이런 객체와 계층을 표현하는 구조를 Scene Graph라고 부릅니다. 여기서는 문서의 노드를 관계로 연결하고, 그 정보로 실제 장면을 계산하는 모델로 사용합니다.

Document
    |
    +-- Page A
    |      |
    |      +-- Frame
    |             |
    |             +-- Rectangle
    |             +-- Text
    |
    +-- Page B
           |
           +-- Image

각 노드는 화면에 표시되는 순서와 관계없이 안정적인 ID를 가집니다. 배열의 15번째 항목이라는 이유로 객체를 식별하지 않습니다.

앞에 다른 도형이 들어와도 기존 객체의 정체성은 유지되어야 하기 때문입니다.

노드의 속성을 수정 단위로 나눕니다

다음은 수동 배치 노드의 저장 정보를 설명하기 위한 예시입니다.

SceneNode

nodeId = node_8
pageId = page_2
kind = rectangle

parentId = frame_3
orderKey = k7
placementRevision = 4

localMatrix = [1, 0, 0, 1, 140, 80]
width = 240
height = 64
geometryRevision = 12

fill = blue
opacity = 1
appearanceRevision = 9

`parentId`는 어떤 부모 아래에 배치되는지 나타냅니다.

`orderKey`는 같은 부모 안에서 앞뒤 순서를 정하는 값입니다.

`localMatrix`는 부모 좌표계에서 노드가 어떻게 이동하고 회전하며 확대되어 있는지를 표현합니다. 여섯 값은 2차원 아핀 변환을 나타냅니다.

위치와 크기는 하나의 기하 정보 묶음으로 취급합니다. 사용자가 크기를 바꾸는 작업에서 위치만 반영되고 크기는 이전 값으로 남는 상태를 피하기 위해서입니다.

반면 색상 변경은 별도의 속성 묶음으로 둘 수 있습니다. 그러면 한 사용자가 위치를 바꾸는 동안 다른 사용자가 색상을 바꾸더라도 불필요하게 같은 충돌로 취급하지 않을 수 있습니다.

문서 전체의 버전 하나를 모든 편집의 조건으로 사용하면, 멀리 떨어진 도형의 색상 변경 때문에 내 이동 작업까지 거절될 수 있습니다.

문서 원본과 화면 계산 결과를 구분합니다

부모 프레임이 회전하면 자식의 화면 위치도 바뀝니다.

그렇다고 모든 자식의 로컬 좌표를 저장소에서 다시 수정할 필요는 없습니다. 부모와 자식의 변환을 연결해 화면 위치를 계산할 수 있습니다.

이 설계에서는 사용자가 정한 값과 그 값에서 계산되는 결과를 구분합니다.

사용자가 정한 위치, 크기, 계층, 텍스트와 레이아웃 규칙은 문서 데이터입니다.

월드 좌표의 경계, 화면에 보이는 영역, 선택 핸들의 위치와 GPU 버퍼는 계산하거나 다시 만들 수 있는 결과입니다.

Authored Document
        |
        v
Layout and Transform Evaluation
        |
        v
World Bounds and Render Data

문서가 같더라도 사용자의 확대율과 화면 크기는 다릅니다. 따라서 화면 좌표를 문서의 최종 원본으로 저장하지 않습니다.

브라우저 안에서도 상태를 분리합니다

이번 설계에서는 다섯 종류의 상태를 구분합니다.

Confirmed Document는 서버가 확정한 문서입니다.

Pending Operations는 아직 서버 결과가 확인되지 않은 로컬 변경입니다.

Gesture Preview는 현재 드래그나 크기 조절 중에만 사용하는 임시 결과입니다.

Remote Presence는 다른 사용자의 커서, 선택과 작업 미리보기입니다.

Camera와 Selection은 내가 어디를 보고 무엇을 선택했는지를 나타냅니다.

Confirmed Document
        +
Pending Operations
        +
Local Gesture Preview
        |
        v
Local Document View

Remote Presence
        |
        v
Separate Collaboration Overlay

다른 사람의 커서가 움직였다고 문서의 저장 버전이 증가하지 않습니다.

내가 확대했다고 동료의 화면도 확대되지 않습니다. 다른 사용자를 따라가는 기능이 필요하다면 별도의 명시적인 모드로 제공합니다.

RDBMS에서 제외할 데이터

확정된 노드와 편집 연산은 관계형 DB에 저장할 수 있습니다. 실시간 서비스라는 이유만으로 모든 데이터를 DB 밖으로 옮길 필요는 없습니다.

다만 이미지와 폰트 원본, 큰 결과물은 객체 저장소에 두고 DB에는 자산 식별자와 메타데이터를 저장합니다.

커서 위치와 작업 중 미리보기는 짧게 유지되는 존재 정보입니다. 서버 메모리에서 제한된 기간 보관하고 만료시킬 수 있습니다.

GPU 텍스처 핸들, 화면 좌표, 선택 핸들과 공간 인덱스도 업무 원본이 아닙니다. 브라우저의 현재 실행 환경에 맞춰 다시 만듭니다.

미확정 작업은 복구가 필요하므로 로컬 저널에 기록할 수 있습니다. 이 기록은 서버에 저장된 결과와 구분합니다.

API Signatures

먼저 문서 접근 권한을 확인하고 편집 세션을 만듭니다.

POST /api/documents/d_1/sessions

그다음 현재 페이지의 Snapshot을 가져옵니다.

Snapshot은 특정 순서까지 확정된 노드 상태를 묶은 결과입니다.

GET /api/documents/d_1/pages/p_2/snapshot

반환해야 하는 정보는 다음과 같습니다.

Snapshot

snapshotId = snap_52
schemaVersion = 3
pageId = p_2
pageSequence = 810

nodes = current page nodes
assetRefs = referenced assets
dependencies = required component definitions

`pageSequence`는 해당 페이지의 확정 편집 배치 순서입니다. 클라이언트의 렌더링 프레임 번호가 아닙니다.

재연결에서는 같은 페이지의 810번 이후 변경을 요청합니다.

다른 페이지의 순서 번호와 비교하지 않습니다. 페이지별 구독에서 일부 문서 이벤트를 받지 않았다는 이유로 무조건 누락이라고 판단하지 않도록 순서의 범위를 정하는 것입니다.

편집 명령에는 의도와 기준 버전을 포함합니다

사용자가 드래그를 완료하면 다음과 같은 필드를 가진 명령을 보냅니다.

아래는 전송 계약의 필드를 설명하는 예시입니다.

Commit Command

documentId = d_1
pageId = p_2
operationId = op_107
gestureId = drag_19

type = setGeometry
nodeId = node_8

expectedGeometryRevision = 12
expectedPlacementRevision = 4
expectedParentWorldVersion = parent_world_31

localMatrix = [1, 0, 0, 1, 220, 80]
width = 240
height = 64

`operationId`는 같은 작업을 재전송할 때 사용하는 식별자입니다.

`gestureId`는 하나의 사용자 동작과 미리보기를 연결합니다.

`expectedParentWorldVersion`은 드래그를 계산할 때 기준으로 삼은 부모와 조상 변환이 아직 유효한지 확인하기 위한 값입니다. 클라이언트의 현재 시각으로 만든 timestamp가 아니라 서버와 문서 모델이 정의하는 버전입니다.

부모가 바뀌거나 회전했는데 노드 자신의 버전만 확인하면, 오래된 좌표계에서 계산한 값을 그대로 저장할 수 있기 때문입니다.

저장 확인과 변경 이벤트를 연결합니다

성공 응답에는 작업 식별자와 확정된 변경을 포함합니다.

Committed

operationId = op_107
gestureId = drag_19
pageId = p_2
pageSequence = 811

nodeId = node_8
geometryRevision = 13
canonicalGeometry = accepted geometry

서버가 스냅이나 업무 규칙에 따라 값을 조정할 수 있으므로, 성공 응답을 받으면 서버가 확정한 값을 사용합니다.

같은 작업의 결과가 응답과 변경 스트림 양쪽에서 도착해도 한 번만 반영합니다.

응답을 받지 못했다면 작업 결과를 다시 조회할 수 있어야 합니다.

GET /api/documents/d_1/operations/op_107

결과를 모르는 상태에서 새로운 작업 ID를 만들어 같은 이동을 반복하지 않습니다.

High-Level Design

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2026 실리콘밸리_생존자 · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture