PartyKit 완벽 가이드 — 실시간 멀티플레이어·WebSocket·상태 동기화·AI

PartyKit 완벽 가이드 — 실시간 멀티플레이어·WebSocket·상태 동기화·AI

이 글의 핵심

PartyKit은 Cloudflare 기반의 실시간 협업·멀티플레이어용 백엔드 플랫폼입니다. Party.Server와 Room·Connection 모델, 브로드캐스트·Storage 영속화, Hibernation 스케일링, PartyKit AI와 y-partykit(Yjs)까지 한 번에 정리합니다.

이 글의 핵심

PartyKit은 실시간 멀티플레이어·협업 애플리케이션을 위해 설계된 백엔드 플랫폼입니다. WebSocket 연결, 방(Room) 단위 격리, 엣지에 가까운 실행, 그리고 Cloudflare Durable Objects에 기대는 상태ful 모델을 개발자 친화적인 Party.Server API로 노출합니다.

이 글에서는 다음을 다룹니다.

  • PartyKit의 위치와 핵심 개념(Party, Room, Worker)
  • Party 인스턴스와 Connection(클라이언트별 WebSocket)의 역할
  • 상태 동기화 패턴(브로드캐스트, 태그, 커스텀 메시지 프로토콜)
  • Persistence(Room.storage)와 Hibernation의 트레이드오프
  • PartyKit AI(partykit-ai, Vectorize, RAG)
  • Yjsy-partykit으로 협업 에디터를 구성하는 실전 흐름

대상 독자: WebSocket 경험이 있고, Cloudflare Workers나 엣지 런타임에 대한 기초 개념(요청 단위 격리, Durable Object)을 읽어본 적이 있는 백엔드·풀스택 개발자를 가정합니다.


1. PartyKit이 해결하는 문제

멀티플레이어 게임, 공동 편집, 라이브 대시보드, 실시간 채팅은 모두 동시 접속자 간 상태 일치가 필요합니다. 전통적으로는 다음과 같은 부담이 따릅니다.

  • 연결 수·지역별 지연·재연결 처리
  • 방(Room) 또는 매치 단위로 프로세스/스레드를 나누는 설계
  • 배포·스케일 아웃 시 세션 유실과 마이그레이션

PartyKit은 이런 문제를 “Party 단위의 프로그래밍 가능한 프리미티브”로 추상화합니다. 각 Party는 URL 경로의 이름(:name)방 ID(:id)로 식별되며, 해당 방에 연결된 모든 클라이언트는 같은 Party.Server 인스턴스(논리적 Room)와 통신합니다.

공식 블로그에서 강조하듯, 새 API는 Room을 생성자로 주입하는 클래스 기반 Party.Server 모델을 표준으로 삼습니다. 레거시 객체 리터럴 스타일(PartyKitServer)도 문서화되어 있으나, 신규 프로젝트는 클래스 + implements Party.Server를 권장합니다.


2. 아키텍처 개요: 엣지 Worker와 Room

요청이 들어오면 먼저 사용자에 가까운 엣지 Worker가 처리할 수 있습니다. PartyKit은 여기서 onBeforeRequest, onBeforeConnect 같은 훅을 제공해, Room Storage에 닿기 전에 인증·레이트 리밋·차단을 수행할 수 있습니다. 실제 상태ful 로직은 특정 Party URL에 매핑된 Room에서 실행됩니다.

이 구조 덕분에 “전역 HTTP API”와 “방 안의 실시간 로직”을 분리할 수 있습니다. 예를 들어 JWT 검증은 엣지에서 하고, 통과한 연결만 Room의 onConnect로 넘기는 패턴이 자연스럽습니다.


3. 핵심 개념 정리

3-1. Party.Server

Party.Server는 TypeScript 클래스로, 생성자에서 Party.Room을 주입받습니다. 라이프사이클 메서드는 대표적으로 다음과 같습니다.

메서드역할
onStart서버 기동 또는 히버네이션에서 깨어난 직후, 첫 onConnect/onRequest 전에 비동기 초기화
onConnect새 WebSocket 연결 시
onMessage특정 Connection에서 메시지 수신 시
onClose / onError연결 종료·오류 시
onRequest해당 Room URL로의 HTTP 요청 처리
onAlarm스케줄된 알람 실행

또한 options.hibernate로 히버네이션 여부를 설정할 수 있습니다(기본값 false).

3-2. Party.Room

Room은 “이 방”의 식별자·연결 목록·Storage·환경 변수·다른 Party로의 참조(context.parties) 등을 제공합니다.

  • room.id: URL에 나오는 방 ID (예: /parties/:name/:id:id)
  • room.broadcast(msg, excludeIds?): 연결 전체에 메시지 전파(발신자 제외 브로드캐스트에 유용)
  • room.getConnections(tag?): 현재 연결 나열, 태그 필터 지원
  • room.storage: 방 단위 영속 KV(아래 Persistence 절 참고)

3-3. PartyKit Worker(정적 메서드)

클래스의 static 메서드로 프로젝트 전역 동작을 정의합니다.

  • onFetch: Party URL이 아닌 HTTP에 응답(경량 REST 등)
  • onSocket: Party가 아닌 WebSocket
  • onCron: partykit.jsoncrons 스케줄 실행

Room Storage에 접근할 수 없는 위치에서 실행되므로, 헬스 체크·공개 메타데이터 API 같은 용도에 맞습니다.


4. Party와 Connection

문서와 커뮤니티에서 말하는 “Party”는 한 방(Room)에 대응하는 서버 인스턴스와 그 상태 전체를 가리키는 경우가 많습니다. 타입 이름은 Party.Room이지만, 개념적으로는 “멀티플레이어 세션 하나”로 이해하면 됩니다.

4-1. Connection이란

Party.Connection은 클라이언트 하나의 WebSocket을 감싼 객체입니다.

  • connection.id: 연결 고유 ID (클라이언트가 PartySocket에서 id를 지정 가능)
  • connection.send(data): 해당 클라이언트에게만 전송
  • connection.setState / connection.state: 연결 수명 동안만 유지되는 소량 메타데이터(예: 닉네임). Storage와 달리 영속되지 않습니다.

4-2. 연결 태그와 타겟팅

getConnectionTags(connection, ctx)를 구현하면, 연결마다 태그 배열을 붙일 수 있습니다. 예를 들어 국가 코드 GB로 태그한 뒤 room.getConnections("GB")로 필터링해 지역별 메시지를 보낼 수 있습니다. 단순 브로드캐스트보다 역할·팀·지역 등으로 나눌 때 유용합니다.

4-3. 브로드캐스트 패턴

가장 흔한 동기화는 한 클라이언트의 입력을 검증한 뒤 나머지에 전파하는 것입니다.

import type * as Party from "partykit/server";

export default class Server implements Party.Server {
  constructor(readonly room: Party.Room) {}

  async onMessage(message: string | ArrayBuffer, sender: Party.Connection) {
    if (typeof message !== "string") return;

    // 예: JSON 프로토콜 { type, payload }
    let parsed: { type: string; payload: unknown };
    try {
      parsed = JSON.parse(message);
    } catch {
      return;
    }

    if (parsed.type === "move") {
      // 발신자 제외 브로드캐스트
      this.room.broadcast(JSON.stringify({ type: "move", payload: parsed.payload }), [
        sender.id,
      ]);
    }
  }
}

왜 이런 형태인가: Room은 단일 스레드에 가까운 실행 모델에서 동작하므로, 서버가 권위(source of truth)가 되기 쉽습니다. 클라이언트가 임의로 다른 플레이어 상태를 덮어쓰지 못하게 하려면, 위와 같이 타입별 검증필요 시 서버 측 시뮬레이션을 넣는 것이 안전합니다.


5. 상태 동기화 전략

PartyKit은 “동기화 알고리즘”을 강제하지 않습니다. 대신 메시지 전달방 단일 작성자(싱글 라이터) 환경을 제공합니다. 선택지는 크게 세 가지입니다.

5-1. 커스텀 프로토콜 + 브로드캐스트

위 예시처럼 JSON 메시지로 이벤트 스트림을 주고받습니다. 구현은 단순하지만, 충돌 해결·오프라인·역행(rewind)은 직접 설계해야 합니다. 게임의 락스텝 입력, 단순 화이트보드 스트로크 등에 적합합니다.

5-2. CRDT(Yjs)로 자동 병합

공동 편집·피그마 스타일 캔버스처럼 동시 편집 병합이 필요하면 Yjs 같은 CRDT가 사실상 표준에 가깝습니다. PartyKit 생태계에서는 y-partykit이 Yjs 문서를 Room에 연결하는 프로바이더를 제공합니다. 서버에서는 onConnect 시 Yjs 룸에 클라이언트를 붙이는 패턴이 문서화되어 있습니다.

5-3. 운영형 패턴: 스냅샷 + 델타

게임 서버에서는 주기적 스냅샷과 그 사이 델타를 섞어 대역폭을 줄입니다. PartyKit에서도 Storage에 스냅샷을 저장하고, 재접속 시 전송하는 방식을 취할 수 있습니다. 이때 히버네이션과의 상호작용(아래)을 반드시 고려합니다.


6. Persistence: Room.storage

room.storage는 방마다 붙는 비동기 KV 저장소입니다.

  • : 문자열, 최대 2,048바이트
  • : structured clone 가능 타입, 최대 128KiB per entry

문서에 따르면, 재배포·크래시·최대 수명 도달 등으로 Room이 재시작될 수 있으므로, 복구 가능해야 하는 상태는 Storage에 둡니다. 반대로 순간적인 게임 틱만 메모리에 두고, 주기적으로 체크포인트를 저장하는 전략이 일반적입니다.

async onStart() {
  const checkpoint = await this.room.storage.get<{ tick: number }>("checkpoint");
  // … 메모리 상태 복원
}

async saveCheckpoint(state: { tick: number }) {
  await this.room.storage.put("checkpoint", state);
}

대용량 문서는 한 키에 몰지 말고 샤딩(여러 키로 분할)하는 것이 권장됩니다. 키 개수 자체에 실질적 상한이 거의 없다는 설명이 가이드에 있습니다.


7. Hibernation(히버네이션)

options.hibernate = true이면, 활동이 없을 때 Room 인스턴스를 메모리에서 해제할 수 있습니다. 공식 가이드에 따르면, 히버네이션 비활성 시 방당 대략 100개 연결 제한이 언급되고, 활성수만 개 규모로 확장 가능하다고 설명됩니다(제품 한계는 변경될 수 있으므로 최신 문서를 확인하십시오).

7-1. 동작 직관

메시지가 없을 때 서버 객체를 잠재우되, WebSocket 연결은 유지되는 이미지에 가깝습니다. 다시 메시지가 오면 새 Server 인스턴스가 만들어지고 onStart가 다시 호출됩니다. 그래서 생성자/onConnect에만 의존한 비영속 상태는 사라질 수 있습니다.

7-2. 설계 시 유의사항

  • 영속이 필요한 것은 Storage로
  • 히버네이션과 맞지 않는 인메모리만의 연결 맵에 의존하지 않기
  • Yjs 등은 전용 호환 레이어를 사용해 최신 권장 패턴 따르기

히버네이션은 비용과 연결 수를 맞바꾸는 기능이므로, 프로토타입 단계에서는 끄고, 트래픽 패턴을 본 뒤 켜는 것도 합리적입니다.


8. PartyKit AI와 Vectorize

PartyKit AI는 Cloudflare의 AI 스택을 PartyKit 프로젝트에 엮기 위한 레이어입니다. partykit-ai 패키지의 Ai 클래스는 @cloudflare/ai와 동일한 사용 경험을 목표로 합니다.

8-1. Room에서 모델 호출

문서 예시에 따르면, 생성자에서 다음과 같이 초기화할 수 있습니다.

import { Ai } from "partykit-ai";
import type * as Party from "partykit/server";

export default class Server implements Party.Server {
  ai: Ai;

  constructor(public room: Party.Room) {
    this.ai = new Ai(room.context.ai);
  }

  // onMessage 등에서 ai.run(...)으로 텍스트 생성·분류 등 수행
}

onFetch / onSocket / onCron처럼 Room 밖에서도 lobby.ai로 동일 패턴을 쓸 수 있어, 비실시간 배치 작업실시간 Room을 한 리포지토리에 둘 수 있습니다.

8-2. 스트리밍 응답 예시

문서의 onFetch 예시는 Llama 계열 모델에 stream: true를 주고 SSE로 반환합니다. 챗 UI와 궁합이 좋습니다.

8-3. Vectorize로 RAG

room.context.vectorize.<이름>으로 인덱스에 접근합니다. partykit.jsonvectorize 설정을 추가하고, insert / query시맨틱 검색을 구현합니다. 메타데이터 필터(filter)로 테넌트·카테고리를 좁힐 수 있어, 협업 문서 안에서의 근거 검색 같은 응용이 가능합니다.

8-4. OpenAI·외부 API와의 결합

PartyKit AI는 호스팅 모델에 강점이 있습니다. 반면 GPT-4 등은 환경 변수의 API 키fetch 호출해 Room 메시지 흐름과 합치는 패턴이 흔합니다. 이때 비밀은 서버(Room) 측에만 두고, 클라이언트에는 절대 노출하지 않습니다.


9. 실전: Yjs 협업 에디터 구축 흐름

여기서는 개념적 단계만 정리합니다. 실제 패키지 버전·import 경로는 프로젝트에 맞게 y-partykit 문서를 따르십시오.

9-1. 구성 요소

  • 클라이언트: Y.Doc, 에디터 바인딩(예: Monaco, ProseMirror, TipTap 등)
  • 네트워크: YPartyKitProvider로 동일 방 ID의 Yjs 문서를 동기화
  • 서버: y-partykit이 제공하는 onConnect 헬퍼로 Yjs 연결을 Room에 장착

9-2. 왜 Yjs인가

문서 동시 편집은 문자 단위 락으로는 부족합니다. Yjs는 CRDT로 로컬 편집을 즉시 반영하고, 나중에 병합합니다. PartyKit은 그 전송 계층을 안정적으로 제공합니다.

9-3. Persistence와 협업

y-partykit은 스냅샷/히스토리 모드 등 문서에 설명된 영속 옵션을 지원합니다. 장시간 세션을 위해 Storage와의 연계를 설계하면, Room 재시작 후에도 문서를 복구할 수 있습니다.

9-4. Awareness(존재 정보)

많은 프로바이더가 커서 위치·사용자 정보를 위한 awareness 채널을 지원합니다. 협업 UX를 완성하려면 문서 동기화와 별도로 awareness 이벤트를 디버깅할 준비가 필요합니다.


10. 보안: onBeforeConnect와 인증

Room에 들어오기 전에 연결을 막으려면 static onBeforeConnect를 사용합니다. 이 훅은 엣지에서 실행되며 Storage에 접근하지 못합니다. 대신 JWT 검증, 쿼리 토큰 확인, IP 제한 등을 적용하기 좋습니다.

반면 방 안에서만 아는 데이터(예: 게임 세션 레코드)는 Room의 onConnect에서 재검증하는 방어가 필요할 수 있습니다. “엣지에서 한 번, Room에서 한 번”의 이중 검증은 운영 시스템에서 흔한 패턴입니다.


11. 로컬 개발과 배포 개요

일반적인 흐름은 다음과 같습니다.

  1. partykit CLI로 프로젝트 생성 또는 기존 앱에 추가
  2. partykit.json(또는 신규 설정 파일)에 Party 이름·호스트·크론 등 정의
  3. partykit dev로 로컬 WebSocket 호스트(문서에 따라 기본 포트가 안내됨) 실행
  4. 프런트엔드에서 PartySocket 또는 Yjs 프로바이더가 해당 호스트로 연결
  5. 프로덕션에 배포 후, 클라이언트의 연결 URL을 환경별로 분기

프런트엔드 프레임워크(Next.js 등)와의 통합 튜토리얼은 공식 문서의 “Add PartyKit to …” 시리즈를 참고하는 것이 가장 안전합니다.


12. 트러블슈팅 체크리스트

  • 메시지가 가끔 사라진다: 히버네이션 켜짐 + 인메모리 캐시만 사용 중인지 확인합니다.
  • 스토리지에 큰 객체를 넣었다가 실패: 128KiB 제한, 키 길이 제한을 점검하고 샤딩합니다.
  • 브로드캐스트가 중복된다: broadcastexclude 배열에 발신자 id가 들어갔는지 확인합니다.
  • Yjs가 어긋난다: 프로바이더·서버 버전 불일치, 잘못된 문서 ID, 동일 사용자 다중 탭 등을 의심합니다.

13. 정리

PartyKit은 WebSocket 멀티플레이어 백엔드를 “방 단위 서버 클래스”로 표현하게 해 주는 플랫폼입니다. Party.Room으로 연결·브로드캐스트·Storage를 한데 묶고, Party.Connection으로 개별 클라이언트를 제어하며, Hibernation으로 스케일과 비용을 조절합니다. 그 위에 Yjs를 얹으면 협업 에디터를, PartyKit AI를 얹으면 실시간 RAG·챗을 같은 인프라 스택에서 확장할 수 있습니다.

다음 단계로는 공식 문서의 Persisting state, Hibernation, y-partykit API를 읽고, 작은 방 하나로 부하 테스트(연결 수·메시지 빈도)를 해보는 것을 권장합니다.


참고