Next.js ConnectRPC 프록시 패턴

발행: (2026년 2월 4일 오후 11:05 GMT+9)
4 min read
원문: Dev.to

Source: Dev.to

Next.js ConnectRPC 프록시 패턴의 표지 이미지

Azad Shukor

1. 종속성

ConnectRPC v2, Next.js 15 및 TanStack Query에 필요한 패키지입니다.

package.json

{
  "name": "airborneo-grpc-proxy",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint",
    "generate": "buf generate proto"
  },
  "dependencies": {
    "@bufbuild/protobuf": "^2.2.3",
    "@connectrpc/connect": "^2.0.1",
    "@connectrpc/connect-next": "^2.0.1",
    "@connectrpc/connect-node": "^2.1.1",
    "@connectrpc/connect-query": "^2.0.1",
    "@connectrpc/connect-web": "^2.1.1",
    "@tanstack/react-query": "^5.0.0",
    "next": "15.1.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@bufbuild/buf": "^1.47.0",
    "@bufbuild/protoc-gen-es": "^2.2.3",
    "@connectrpc/protoc-gen-connect-query": "^2.0.1",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "typescript": "^5"
  }
}

2. Proto 정의 및 생성

두 개의 proto 파일을 정의합니다. GreetService는 프록시와 업스트림 백엔드 간의 엄격한 타입 일치를 보장하기 위해 ElizaService 메시지를 가져옵니다.

proto/connectrpc/eliza/v1/eliza.proto

syntax = "proto3";

package connectrpc.eliza.v1;

message SayRequest {
  string sentence = 1;
}

message SayResponse {
  string sentence = 1;
}

service ElizaService {
  rpc Say(SayRequest) returns (SayResponse) {}
}

proto/greet/v1/greet.proto

syntax = "proto3";

package greet.v1;

import "connectrpc/eliza/v1/eliza.proto";

// This service mimics the remote service but is exposed locally
service GreetService {
  rpc Say(connectrpc.eliza.v1.SayRequest) returns (connectrpc.eliza.v1.SayResponse) {}
}

buf.gen.yaml

version: v1
plugins:
  # 1. Generates Types AND Service Definitions (v2 Standard)
  - plugin: es
    path: ./node_modules/.bin/protoc-gen-es
    out: gen
    opt: target=ts

  # 2. Generates React Hooks (TanStack Query)
  - plugin: connect-query
    path: ./node_modules/.bin/protoc-gen-connect-query
    out: gen
    opt: target=ts

생성기를 실행합니다:

npm run generate

3. 프록시 유틸리티

이 유틸리티는 메서드 호출을 대상 클라이언트로 전달함으로써 서비스 정의를 동적으로 구현합니다. 이를 통해 모든 리졸버를 일일이 다시 작성하지 않고도 원격 백엔드를 로컬 API 라우트에 “마운트”할 수 있습니다.

src/utils/proxy.ts

import { type DescService } from "@bufbuild/protobuf";
import { type Client } from "@connectrpc/connect";

export function createProxy(
  service: T,
  client: Client,
) {
  const implementation: any = {};

  for (const method of service.methods) {
    const fnName = method.localName;

    implementation[fnName] = async (req: any) => {
      console.log(`[Proxy] Forwarding ${fnName} for ${service.typeName}`);

      // Call the client dynamically
      return await (client as any)[fnName](req);
    };
  }

  console.log(
    `[Proxy] Built methods for ${service.typeName}:`,
    Object.keys(implementation),
  );

  return implementation;
}

4. API 라우트 구현

우리는 데모 Eliza 서비스에 대한 백엔드 연결을 만든 뒤, 로컬 GreetService를 해당 연결을 프록시하도록 라우팅합니다.

pages/api/[[...connect]].ts

import { nextJsApiRouter } from "@connectrpc/connect-next";
import { ConnectRouter } from "@connectrpc/connect";
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-node";

// Generated Code
import { GreetService } from "@/gen/greet/v1/greet_pb";
import { ElizaService } from "@/gen/connectrpc/eliza/v1/eliza_pb";
import { createProxy } from "@/src/utils/proxy";

// 1. The Backend Connection (Target)
const elizaBackend = createClient(
  ElizaService,
  createConnectTransport({
    baseUrl: "https://demo.connectrpc.com",
    httpVersion: "1.1",
  }),
);

function routes(router: ConnectRouter) {
  // ---------------------------------------------------------
  // ROUTE 1: GreetService (Proxy -> ElizaService)
  // URL: /api/greet.v1.GreetService/Say
  // ---------------------------------------------------------
  router.service(GreetService, createProxy(GreetService, elizaBackend));
}

export const { handler, config } = nextJsApiRouter({ routes });
export default handler;

5. 클라이언트 사용

프론트엔드는 connect-web을 사용해 로컬 Next.js API와 통신합니다. (원본 기사에서는 예시 클라이언트 코드를 계속해서 제공합니다; 필요에 따라 여기 포함하십시오.)

app/page.tsx

"use client";

import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { GreetService } from "@/gen/greet/v1/greet_pb";

// Point to YOUR local API
const transport = createConnectTransport({ baseUrl: "/api" });
const client = createClient(GreetService, transport);

export default function ChatPage() {
  const [input, setInput] = useState("");
  const [history, setHistory] = useState([]);

  const mutation = useMutation({
    mutationFn: async (msg: string) => {
      // Input is typed as SayRequest (from Eliza)
      const res = await client.say({ sentence: msg });

      // Output is typed as SayResponse (from Eliza)
      return res.sentence;
    },
    onSuccess: (answer) => {
      setHistory((prev) => [...prev, `Eliza (via Proxy): ${answer}`]);
    },
  });

  return (
    
      

프록시 채팅

      
         setInput(e.target.value)}
          placeholder="Say something..."
          style={{ padding: "8px", flexGrow: 1 }}
        />
         mutation.mutate(input)}
          disabled={mutation.isPending}
        >
          {mutation.isPending ? "Sending..." : "Send"}
        
      

      
        {history.map((line, i) => (
          
            {line}
          
        ))}
      
    
  );
}
Back to Blog

관련 글

더 보기 »