Next.js ConnectRPC 프록시 패턴
Source: Dev.to

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}
))}
);
} 