1P by GN⁺ 7시간전 | ★ favorite | 댓글 1개
  • Cap'n WebTypeScript로 구현된 신규 RPC 프로토콜로, 웹 환경에 최적화되어 다양한 자바스크립트 런타임에서 동작함
  • 스키마나 번거로운 보일러플레이트 없이, JSON 기반 직렬화인간이 읽을 수 있는 데이터 포맷을 제공함
  • 객체-권한 기반 모델을 통해 양방향 호출, 함수·객체 레퍼런스 전달, 약속(promise) 파이프라이닝, 보안 패턴 구현이 가능함
  • WebSocket, HTTP, postMessage 등 다양한 네트워크 환경을 지원하며, 10kB 이하의 경량 오픈소스임
  • GraphQL과 유사한 waterfall 문제 해결 뿐만 아니라, 일반 자바스크립트 API와 같은 자연스러운 RPC 모델링을 가능하게 해줌

Cap'n Web이란 무엇인가

  • Cap'n Web은 Cloudflare에서 개발된 TypeScript 기반 오픈소스 RPC(protocol) 시스템
  • Cap'n Proto에서 영감을 받았으나, 별도의 스키마 정의 없이 동작하고, JSON을 활용한 인간 친화적 직렬화 방식 채택
  • TypeScript와 통합되어 자동 완성, 타입 체크 등 개발자 경험을 향상시키며, 런타임 타입 검증은 별도(type guard 등)로 처리 가능
  • HTTP, WebSocket, postMessage 등 네트워크 프로토콜을 지원하며, 주요 브라우저·Cloudflare Workers·Node.js 등에서 동작
  • 종속성 없는 경량 구조로 minify + gzip 시 10kB 미만으로 제공됨

Cap'n Web의 객체-권한 기반 모델(OCap)

  • 객체-권한(object-capability) 기반 모델을 채택하여, 기존 RPC 시스템보다 더 다양한 표현 가능
    • 양방향 호출: 클라이언트와 서버가 서로 함수를 호출 가능
    • 함수·객체 레퍼런스 전달: 함수나 객체를 RPC로 넘기면, 상대방은 스텁을 받아 호출 시 원위치에서 실행함
    • Promise Pipelining: 여러 RPC를 체인으로 연결할 때 한 번의 네트워크 왕복으로 처리
    • 보안 패턴: 권한 부여 및 세션 관리 등 보안적 제어를 자연스럽게 구현 가능

기본적인 사용법

  • 클라이언트 예시

    import { newWebSocketRpcSession } from "capnweb"
    let api = newWebSocketRpcSession("wss://example.com/api"">wss://example.com/api";)
    let result = await api.hello("World")
    console.log(result)
    
  • 서버 예시(Cloudflare Worker 기반)

    import { RpcTarget, newWorkersRpcResponse } from "capnweb"
    class MyApiServer extends RpcTarget {
      hello(name) {
        return `Hello, ${name}!`
      }
    }
    export default {
      fetch(request, env, ctx) {
        let url = new URL(request.url)
        if (url.pathname === "/api") {
          return newWorkersRpcResponse(request, new MyApiServer())
        }
        return new Response("Not found", {status: 404})
      }
    }
    
  • API에 메소드 추가, 클라이언트의 콜백 함수 전달, TypeScript 인터페이스 정의 및 적용이 간단하게 가능함

RPC란 무엇이고, Cap'n Web에서의 특징

  • RPC(Remote Procedure Call) 는 네트워크 상의 두 프로그램이 마치 함수 호출처럼 통신할 수 있도록 해주는 개념임
  • 전통적인 HTTP/REST 프로토콜과 달리, RPC는 함수 호출 추상화로 개발자의 사고방식과 일치하는 코드 작성 가능
  • Cap'n Web은 async/await, Promise, Exception 지원 등 최신 자바스크립트의 흐름과 잘 맞음
  • RPC의 역사적 논란(동기적 호출, 네트워크 오류)과 달리, 현대 JS 환경에서는 더욱 안전하고 효율적 사용이 가능함

Cap'n Web의 활용 시나리오

  • 두 자바스크립트 애플리케이션 간 네트워크 통신이 필요한 모든 환경에서 활약
    • 클라이언트-서버, 마이크로서비스 간 호출 등
    • 특히 실시간 협업 웹앱, 복잡한 보안 경계를 넘는 상호작용에 적합함
  • 실험적 단계로, 최신 기술 수용에 열려있는 개발자에게 더욱 유익함

다양한 기능

HTTP 배치 모드

  • 지속적인 연결이 필요하지 않을 때 HTTP 배치(batch) 모드로 여러 RPC 호출을 한 번에 묶어서 처리 가능

    import { newHttpBatchRpcSession } from "capnweb"
    let batch = newHttpBatchRpcSession("https://example.com/api";)
    let result = await batch.hello("World")
    console.log(result)
    
  • 하나의 배치 내에서 여러 호출을 동시에 실행, 결과를 병렬로 받을 수 있음

    let promise1 = batch.hello("Alice")
    let promise2 = batch.hello("Bob")
    let [result1, result2] = await Promise.all([promise1, promise2])
    

Promise Pipelining(체인 호출)

  • 이전 호출의 결과를 기다리지 않고, 결과를 바로 다음 호출의 인자로 사용하는 방식 지원

  • 예시) getMyName()의 결과 Promise를 바로 hello()에 넘겨 한 번의 네트워크 왕복으로 처리

    let namePromise = batch.getMyName()
    let result = await batch.hello(namePromise)
    
  • Cap'n Web의 Promise는 프록시(proxy) 객체로 동작하여, 추가 메소드 호출 시 지연 없이 체인 처리 가능

    let sessionPromise = batch.authenticate(apiKey)
    let name = await sessionPromise.whoami()
    

보안: 인증과 객체-권한

  • 인증(authenticate) 메소드를 통해 성공시 권한(세션) 객체 할당, 이후 추가 인증 단계 없이 기능 호출 가능
  • 기존 RPC와 달리 세션 객체를 위조할 수 없고, 인증 없이 권한이 필요한 메소드에 접근이 불가함
  • WebSocket의 구조적 한계를 자연스럽게 극복, 인증 로직의 일관성 보장
  • TypeScript로 API 인터페이스 선언 시, 클라이언트~서버에 자동으로 적용할 수 있으며, 자동완성 및 타입 안정성 확보

GraphQL과의 비교 및 Cap'n Web의 차별점

  • GraphQL은 REST의 waterfall(다단계 호출) 문제를 완화하지만, 새로운 언어·스키마·툴체인 도입이 필요함

  • Cap'n Web은 자바스크립트 코드만으로 waterfall 문제를 해결하며,

    • 약속 파이프라이닝/객체 레퍼런스 지원으로, 중첩 호출이나 복합 트랜잭션 로직을 자연스럽게 모델링 가능
    let user = api.createUser({ name: "Alice" })
    let friendRequest = await user.sendFriendRequest("Bob")
    
  • GraphQL의 복잡성, 학습·관리 비용 없이, 자바스크립트 API와 유사하게 활용 가능

배열 연산(array.map 등)과 최적화

  • Cap'n Web에서는 배열의 각 원소에 대해 네트워크 추가 왕복 없이 map 연산 가능

  • map 콜백 함수를 클라이언트에서 한 번 실행해 연산 내용을 기록(record-replay), 서버로 전송하여 서버 측에서 일괄 처리

    let friendsWithPhotos = friendsPromise.map(friend => {
      return {friend, photo: api.getUserPhoto(friend.id)}
    })
    let results = await friendsWithPhotos
    
  • 제한된 도메인 특화 언어(DSL)를 통해, 자바스크립트 함수처럼 표현하되, 실제로는 Cap'n Web 프로토콜을 활용해 다중 호출을 최적화 처리함

내부 프로토콜 구조 및 통신 흐름

  • JSON + 특별 전처리를 통한 구조화 데이터 전송, 배열·날짜 등 특별 타입 지원
  • 대칭적 프로토콜로 클라이언트·서버 구분 없는 양방향 통신 가능
  • 각 파티(예시: Alice와 Bob)는 export/import 테이블을 관리하며, 객체·함수 레퍼런스를 ID로 구분
  • push/pull 메시지 및 Promise ID 할당 등을 통해, 한 번의 라운드 트립에 다수 호출 반영 가능

현황 및 적용 사례

  • Cap'n Web은 아직 실험적 오픈소스로, Cloudflare Wrangler의 remote bindings 등 실제 서비스에 활용 중

  • 추가적인 블로그 포스팅과 다양한 프론트엔드 실험 예정

  • MIT 라이선스로 공개, 누구나 자유롭게 적용 가능

  • GitHub 저장소 바로가기

Hacker News 의견
  • 두 가지가 궁금함

    1. RPC 의미론이 업데이트되는 앱 배포를 어떻게 하면 좋을지에 대한 방법이 궁금함. 즉, 클라이언트와 서버가 동일한 버전의 RPC를 사용한다는 걸 어떻게 보장할 수 있을지 질문임. 프로토콜 버퍼(grpc/avro 등)는 이 문제를 직접적으로 해결하려고 함
    2. 불안정한 네트워크 연결은 어떻게 다루는게 좋을지 궁금함. export/import 테이블이 상태를 가진 웹소켓 연결에 직접 묶여있어서 연결이 끊어지면 상태를 잃게 될 거라고 생각함. 이론적으로는 클라이언트/서버가 상태를 캐싱하고 재연결 시 복원하는 것도 가능하겠지만, 테이블에 클로저가 포함될 수 있어 직렬화가 어렵고 메모리 이슈가 발생할 수 있다고 생각함. 팀에서 어떻게 고민했는지 궁금함
      정말 혁신적인 작업이라고 생각함
      1. 기존 호출자를 깨뜨리지 않고 JavaScript API를 업데이트하는 것과 비슷하게 보면 좋음. 로컬 함수 호출에서 지켜야 할 호환성 규칙과 같은 기본만 따르면, 새 메서드/옵셔널 인자 등은 추가해도 됨
      2. 연결이 끊어지면 다시 연결해서 객체를 처음부터 재구축해야 함. 실제 React 앱에선, 최상위 컴포넌트에 메인 RPC 스텁을 인자로 전달함. 이 컴포넌트가 하위 객체를 여러 개 생성해 자식들에게 내려보냄. 연결이 끊기면, 새 스텁을 만들어서 최상위 컴포넌트에 다시 전달함. 그러면 다른 state change처럼 리렌더링이 발생하고, 모든 자식들이 필요한 하위 객체를 다시 fetch함
        만약 콜백이 있는 구독(subscription) 객체가 있다면, 시작 시점에 ‘마지막으로 본 메시지’를 지정할 수 있게 API를 설계해야 함. 그래야 바로 이어서 데이터를 받을 수 있어 중간에 누락되지 않음
        블로그 포스트로 이런 디자인 패턴 시리즈를 한번 정리해야 할 것 같음
  • 배열 문제를 어떻게 해결했는지에 관한 섹션이 정말 흥미롭고 동시에 약간은 무서움 블로그 링크
    .map()의 경우, 서버에 JavaScript 코드를 직접 보내는 건 아니지만, "코드" 같은 뭔가를 보내는데, 이건 한정된 도메인 전용 언어(DSL)를 사용함. 클라이언트 쪽에서 콜백을 placeholder 값을 넣고 한번 실행해보고, 그 동작을 record-replay 방식으로 추적해서 서버로 instruction set을 보냄. 서버에선 그 instruction을 받아서 배열의 각 멤버별로 실행하게 됨.
    즉 개발자는 그냥 js 메서드만 썼는데, 실제로는 이걸 좁은 DSL로 변환하는 트릭이 적용됨. 콜백은 동기적으로만 동작해야 하며, await은 불가능. 대신 promise pipelining만 허용해서, 그 과정을 전부 잡아내 서버로 넘기고, 서버에서는 필요할 때마다 재실행하게 됨

    • C#에는 이런 문제를 처리하는 expression tree가 있음. Entity Framework가 람다식을 받아 SQL 쿼리로 변환할 때 그걸 활용함. 즉, 코드를 실행하지 않고 스캔하거나 변환하면서 사용 가능함
      예를 들어, db.People.Where(p => p.Name == "Joe")는 Where가 실제 predicate 함수를 받는 게 아니라, expression을 받으므로 전달받은 코드를 스캔해서 Name 필드가 "Joe"와 일치하는지 확인하고 SQL WHERE 절로 변환함
      JavaScript는 이런 메커니즘이 없으니 placeholder 값을 넣어 어떻게 동작하는지 하나하나 기록해서 흉내내는 방식임

    • 최근 Tanstack DB의 쿼리 DSL을 만들 때도 이 record-replay 트릭을 사용했음 가이드 링크. where/select/join 콜백에 RefProxy 객체를 넘기고, 그 객체에 어떤 prop/연산이 발생하는지 추적함.
      js에서는 일반 연산자(==, > 등)를 직접 가로챌 수 없으니, eq/gt/not 등 trace 가능한 작은 함수들을 만들어서 콜백에서 한 번만 실행해서 연결된 표현식을 잡아내고 IR로 만듦
      신기하게도 js spread 연산자도 추적에 성공함
      Kenton, 혹시 이 개념을 capnweb에도 fake operator(eq, gt, in 등)로 추가해 원격 트레이싱 기능을 넣어줄 수 있는지 궁금함

    • 조건문은 금지된 것 같은데(마치 react 훅 규칙처럼) 그런 제약을 어떻게 구현하는지 궁금함

  • 이 프로젝트가 흥미로움
    ML 컴파일러 라이브러리(TensorFlow 1, JAX jit, PyTorch compile 등)와 흡사한 측면이 있음. 트레이싱 방식으로 operation graph를 만들고, 이걸 compile하거나 VM에 맞게 변환해서 실행함
    현재는 동적 언어를 프론트엔드 삼아 새로운 DSL을 정의하지 않고, 기존 스크립트 언어에 AST 변환을 숨겨둠
    ML에서는 GPU/linalg 커널 실행을 늦춰서 커널을 합치지만, Cap'n Web 같은 RPC에서는 네트워크 요청을 미뤄서 여러 network call을 합칠 수 있음
    결국 instruction/data plane을 분리하는 것이 핵심이며, 아주 작은 scale의 단일 CPU도 distributed system 구조(명령/데이터 캐시 분리)를 가지고 있음
    Cap'n Web에선 RPC graph 자체가 instruction 역할을 함
    이런 패턴이 정말 흥미로운데, 스택 구조(compiler 위에 interpreter, interpreter 위에 compiler...)가 무한 반복되는 느낌도 있음. Lispy 코드 is data, data is code 패턴을 또다른 버전으로 보는 기분임. 뭔가 근본적으로 더 깊은 스토리가 있을 것 같음

    • 정말 동의함—이걸 보편적 추상으로 보는 관점이 멋짐
      동적 언어가 이제는 새로운 DSL의 프론트엔드가 되고, 대신 새로운 문법을 지정하지 않고 스크립트에 AST 생성을 녹여넣음
      TypeScript가 여기서 게임체인저가 된다고 생각함. JavaScript 의 런타임 유연성(Proxy를 clever하게 쓰는 Cap'n Web 처럼)과 타입 안정성을 동시에 잡을 수 있어서임
      요즘 ORM 쪽에서 이 개념에 빠져 있음. 대부분 ORM은 직렬적이고 eager 방식이라서 쿼리 실행 직전에만 조작할 수 있음
      진짜로 composable ORM은 compiler처럼 동작해야 한다고 생각함: TypeScript로 SQL 위에서 완전히 타입 안정적인 DSL을 정의해서 쿼리 AST를 만든 뒤, 마지막에만 SQL로 컴파일하면 됨
      내가 개발 중인 Typegres도 이 아이디어 그대로임. 이런 패턴이 흥미롭다면 참고하면 좋겠음
  • RPC 라이브러리의 핵심 문제는 round-trip이 어디서 어떻게 발생하는지 숨기려 한다는 점임
    Cap'n Web의 array .map()만 봐도 실제로 어디서 network round-trip이 발생하는지 알기 어려움.
    이게 ‘기능’이 아니라 오히려 ‘버그’라고 생각함—코드를 보면 동작을 바로 파악할 수 있어야 하는데, 이걸 가리는 건 바람직하지 않음
    참고 링크

    • round-trip은 await 쓸 때 발생함
      promise pipelining은 여러 statement를 await 없이 쭉 설정할 수 있기 때문에 중간에 추가 network 왕복이 없음. 마지막에 await 한 번 하면 그게 전부임
  • gRPC랑 웹을 써봤다면 Protobuf를 웹에 적용시키는 일이 얼마나 고통스러운지 알 수 있음
    Cap'n Web의 단순함이 정말 좋음 capnproto 문서
    Cap'n Web은 Cap'n Proto와 달리 스키마가 아예 없음. 불필요한 boilerplate가 거의 없으므로, Cloudflare Workers의 자바스크립트 네이티브 RPC 느낌이 강함
    github 참고

  • kentonv의 새로운 라이브러리를 발견해서 바로 달려옴
    GitHub에 올라온 코드를 보니 의외로 규모가 매우 작아서 놀람. 이게 전부 맞는지 궁금함
    이론적으로는 서버 사이드 쪽을 다른 언어로 포팅하는 것도 어렵지 않을 것 같은데, 나는 Elixir 서버와 JS/TS 프런트엔드에서 활용해보고 싶어짐
    LLM에게 이런 언어 포팅 시키는 것도 재밌을 듯. 혹시 이 레포에 LLM 기반 코드가 들어갔는지 궁금함. 몇 달 전 kentonv가 AI가 만든(사람이 검수한) POC를 만들었다는 이야기를 본 적 있음

    • 테스트 일부는 LLM이 생성한 것을 사용했으나, 라이브러리 본체는 전혀 아님
      현재 시점에서 LLM이 이 라이브러리를 만들기는 어려웠을 것 같음. 내부 구조가 매우 정교하게 맞물린 퍼즐처럼 설계됨
      실제 코드보다 설계 고민에 시간이 더 들었음
      well-known spec을 novel하게 구현하는 workers-oauth-provider 라이브러리와는 완전히 다름
      코드 구조가 동적 언어, 예를 들어 Python에는 이식이 쉬울 수 있지만, 정적 타입 언어로는 어렵다고 생각함. 임의 객체 타입에 의존하는 부분이 많음
  • OCapN과 비슷한 점과 중요한 차이점도 있음 참고
    둘 다 capability transfer와 promise pipelining, schemaless 모델을 지원함
    Cap'n Web은 OCapN의 sturdyref(복원 가능한 URI) 같은 out-of-band capability가 없음. 이것 때문에 API key 인증이 필요하다고 추정함. sturdyref는 일종의 추측이 불가능한 토큰으로, 소유하고 있으면 해당 엔드포인트 접근 권한이 생김
    또한 Cap'n Web에는 Alice가 Bob을 Carol에게 소개하는 3자 핸드오프 기능이 없음. 이건 분산 앱엔 필수인데, 그래서 Cap'n Web은 전통적인 SaaS 스타일의 client-server 용도+ocap 특징만 살린 서비스에 더 가까움

    • 3PH 지원을 나중에 추가하고 싶긴 하지만, 이번 초기 릴리즈는 브라우저<->웹 서버 통신에 초점이 맞춰져 있다는 점이 더 우선 순위였음
      SturdyRef는 플랫폼마다 복원 방식이 달라서 RPC 프로토콜 레벨보다는 각 플랫폼에 맞게 구현해야 하는 게 맞다고 봄
      예를 들어 Cloudflare Workers에서는 곧 Durable Object storage에서 capability 영속화가 가능해질 예정인데, 구현 방식이 워커 플랫폼에 특화되어 있음
      Sandstorm도 persistent capability가 있지만 내부 서비스에 한정됨
      그래서 Cap’n Proto에서 persistent capability 개념을 아예 뺐고, web 표준에서 그나마 비슷한 개념은 OAuth임
      OAuth refresh token 기반 sturdyref 정의도 상상할 수 있겠지만, 특정 플랫폼에서 쓸 수 있는 구조는 아님
  • 빠르게 살펴본 바로는, 이 시스템은 import/export 테이블이나 객체 상태를 서버 쪽에 stateful하게 저장하는 것을 요구(혹은 장려)하는 것처럼 보임
    전통적인 RPC에선 모든 호출이 최상위로 들어가고, 각 호출에 key 등을 전달하므로 여러 서버에 요청이 분산되어도 문제 없는데, Cap’n Web은 그렇지 않음
    테이블을 직렬화해 DB에 저장해서 같은 방식으로 서버 분산이 가능한지, 아니면 서버 affinity나 Durable Objects 같은 구조를 반드시 요구하는지 궁금함

    • 상태는 오직 하나의 RPC 세션에서만 유지됨
      WebSocket을 사용하면, WebSocket 연결이 유지되는 동안 상태도 살아있음
      HTTP batch 전송을 사용할 경우, 세션은 하나의 HTTP 요청 전체로 한정되고, 그 안에서 모든 호출이 한 번에 처리됨
      따라서 여러 HTTP 요청/연결에 걸쳐 상태를 유지할 필요가 Cap’n Web에는 없음
      단, 세션이 중간에 끊겨 capability를 모두 잃게 되는 구조라면 그게 문제되는 디자인은 피해야 함. 언제든지 연결 재설정 후 capability를 복원할 수 있도록 해야 함

    • 문서를 읽어보니 websocket으로 affinity를 맞추는 구조로 보임
      http batching은 모든 요청을 한 번에 보내고 응답을 기다리는 방식임
      이런 방식은 로드밸런싱이 까다로워짐. 채팅 클라이언트가 많으면 특정 서버에 연결이 몰리는 구조가 될 수 있음. 그러면 해당 서버가 과부하될 우려가 있음
      서버 스케일 인/아웃도 번거로워짐. 장기 연결 유지와 동시에 여러 요청이 동시 처리 중일 때는 관리가 매우 어려움
      또 한 가지, 클라이언트가 항상 응답을 안 받고 push 이벤트만 계속 보내면 서버는 해당 응답을 계속 메모리에 들고 있어야 하므로, DDOS 공격이 쉽게 가능하다고 생각함

    • Cap'n Proto 문서를 예전에 읽어본 바로, 서버와 클라이언트가 peer stub을 주고받을 수 있음
      서버 C가 클라이언트 B를 통해 A에서 생성된 stub을 전달받으면, C가 직접 A를 호출할 수도 있음

  • RPC is often accused of committing many of the fallacies of distributed computing.

But this reputation is outdated. When RPC was first invented some 40 years ago, async programming barely existed. We did not have Promises, much less async and await. 이 부분을 보고 헷갈렸음. RPC의 핵심 전제가 특정 언어나 동시성 모델에 강하게 의존한다면, 이게 어떻게 프로토콜일 수 있는지 궁금함

  • "RPC"는 원래 원격 호출이 내부 함수 호출과 구분이 불가능하게 보이게 만드는 프로그래밍 패러다임임
    실제로 이를 위해 wire protocol, 클라이언트/서버 라이브러리 등이 필요함
    최근엔 인식이 많이 바뀌어 REST endpoint처럼 함수 시그니처를 가지는 구조가 주류임
    Future, Optional 등 프로그래밍 언어 피처가 생기면서, "이 동작은 지연될 수 있음"이나 "실패할 수 있음"같은 특성을 명확하게 구분할 수 있음
    과거 RPC에서는 이런 속성이 모두 숨겨져 있었음

  • 어떤 의미인지 궁금함. 비동기 프로그래밍은 다양한 언어에 존재함. JavaScript, C++, Python, Rust, C# 등 거의 모두 사용한 경험이 있음
    요지는, 초기 RPC 시스템은 네트워크 요청이 진행되는 동안 호출 스레드를 블록시키는 구조였고, 이건 정말 나쁜 설계였기에 지금은 비동기가 당연해진 것임

  • Cap'n Web이 Cloudflare 제품에만 얽혀있지 않고 따로도 존재해서 매우 기대됨
    문서의 이 부분을 읽고 궁금한 것이 있음

    as of this writing, the feature set is not exactly the same between the two. We aim to fix this over time, by adding missing features to both sides until they match. 두 쪽이 feature parity에 도달하면 이후에도 계속 싱크를 맞춰줄 의향이 있는지, 아니면 Cap'n Web이 결국 cloudflare workers에 뒤처지는 구도가 될지, 그 간격도 궁금함

    • 두 제품은, 공통적으로 의미있는 기능만큼은 거의 계속 동기화할 계획임
      오히려 Cap'n Web이 worker RPC보다 더 앞설 수 있다고 생각함(실제로 파이프라인 기능이 이미 앞서 있음)
      Cap'n Web 구조가 훨씬 단순해서 새로운 기능 실험도 Cap'n Web에서 먼저 하게 될 것임