Use Cases (업데이트: 2026. 6. 3.)

Claude Code로 gRPC 개발하기: Protobuf, 스트리밍, 운영 점검

Claude Code로 실행 가능한 gRPC 서비스를 만들고 기한, 인증, 관측, 실패 사례를 정리합니다.

Claude Code로 gRPC 개발하기: Protobuf, 스트리밍, 운영 점검

서버보다 계약을 먼저 고정한다

gRPC는 원격 프로시저 호출 방식입니다. 클라이언트는 다른 서비스의 메서드를 로컬 함수처럼 호출하지만, 실제 처리는 네트워크 너머에서 일어납니다. Protocol Buffers는 그 호출의 계약을 정의합니다. 서비스 이름, 메서드, 요청, 응답, 필드 번호가 .proto 파일에 들어갑니다.

Claude Code는 서버와 클라이언트를 빠르게 작성할 수 있지만, 계약이 흐리면 생성 코드와 테스트도 함께 흔들립니다. 작업할 때는 공식 문서를 기준으로 두세요. 기본 개념은 gRPC IntroductionCore concepts, Node 구현은 gRPC Node basics, 기한은 Deadlines, 오류는 Status Codes, 인증은 Authentication, 관측은 OpenTelemetry Metrics, 스키마 변경은 Protocol Buffers proto3 guide를 확인합니다.

함께 보면 좋은 글은 운영용 API 개발, 마이크로서비스 설계, API 버전 전략, 테스트 전략입니다.

구체적인 사용 사례

gRPC는 “빠르다”는 이유만으로 선택하면 안 됩니다. 타입이 있는 계약, 명확한 실패 상태, 여러 언어에서 같은 인터페이스가 필요한 경우에 특히 잘 맞습니다.

사용 사례gRPC가 맞는 이유Claude Code에 맡길 일
주문, 재고, 결제 같은 내부 서비스팀 사이의 계약 변경을 추적하기 쉽다.proto, 서버, 클라이언트, 상태 코드 표 작성
대량 내보내기서버 스트리밍으로 큰 응답을 나눠 보낸다returns (stream Item) 설계와 취소 처리
Go, Node, Python이 섞인 조직한 스키마에서 여러 언어 구현을 맞춘다생성 명령, 폴더 구조, CI 확인 정리
내부 도구와 AI 에이전트자유 텍스트보다 명시적 메서드가 안전하다예제 클라이언트, 인증 메타데이터, 기한, 로그 추가

Masa의 실무 메모로는, 모든 조회를 하나의 Search 메서드에 넣으면 처음에는 편하지만 곧 유지보수가 어려워집니다. 읽기, 생성, 스트리밍을 나눠 두고 Claude Code가 계약 변경부터 검토하게 하는 편이 안전했습니다.

바로 실행할 수 있는 Node gRPC 예제

이 예제는 @grpc/proto-loader.proto를 실행 시점에 읽습니다. 첫 검증에서는 protoc 없이도 동작합니다.

mkdir claude-grpc-demo
cd claude-grpc-demo
npm init -y
npm install @grpc/grpc-js @grpc/proto-loader
mkdir proto

package.json:

{
  "type": "commonjs",
  "scripts": {
    "server": "node server.js",
    "client": "node client.js"
  },
  "dependencies": {
    "@grpc/grpc-js": "latest",
    "@grpc/proto-loader": "latest"
  }
}

proto/task.proto:

syntax = "proto3";

package tasks.v1;

service TaskService {
  rpc CreateTask(CreateTaskRequest) returns (Task);
  rpc GetTask(GetTaskRequest) returns (Task);
  rpc ListTasks(ListTasksRequest) returns (stream Task);
}

message Task {
  string id = 1;
  string title = 2;
  string status = 3;
  int64 created_at_unix = 4;
}

message CreateTaskRequest {
  string title = 1;
}

message GetTaskRequest {
  string id = 1;
}

message ListTasksRequest {
  int32 limit = 1;
}

server.js:

const path = require("node:path");
const { randomUUID } = require("node:crypto");
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");

const PROTO_PATH = path.join(__dirname, "proto", "task.proto");
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: false,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});
const taskProto = grpc.loadPackageDefinition(packageDefinition).tasks.v1;
const token = process.env.DEMO_TOKEN || "dev-token";
const tasks = new Map();

function grpcError(code, message) {
  const error = new Error(message);
  error.code = code;
  return error;
}

function assertAuthenticated(call) {
  const value = call.metadata.get("authorization")[0];
  if (value !== `Bearer ${token}`) {
    throw grpcError(grpc.status.UNAUTHENTICATED, "UNAUTHENTICATED");
  }
}

function createTask(call, callback) {
  try {
    assertAuthenticated(call);
    const title = String(call.request.title || "").trim();
    if (!title) {
      return callback(grpcError(grpc.status.INVALID_ARGUMENT, "INVALID_ARGUMENT: title"));
    }

    const task = {
      id: randomUUID(),
      title,
      status: "OPEN",
      createdAtUnix: String(Math.floor(Date.now() / 1000)),
    };
    tasks.set(task.id, task);
    callback(null, task);
  } catch (error) {
    callback(error);
  }
}

function getTask(call, callback) {
  try {
    assertAuthenticated(call);
    const task = tasks.get(call.request.id);
    if (!task) {
      return callback(grpcError(grpc.status.NOT_FOUND, "NOT_FOUND: task"));
    }
    callback(null, task);
  } catch (error) {
    callback(error);
  }
}

function listTasks(call) {
  try {
    assertAuthenticated(call);
    const limit = Math.min(Math.max(Number(call.request.limit) || 10, 1), 100);
    for (const task of Array.from(tasks.values()).slice(0, limit)) {
      call.write(task);
    }
    call.end();
  } catch (error) {
    call.destroy(error);
  }
}

const server = new grpc.Server();
server.addService(taskProto.TaskService.service, { createTask, getTask, listTasks });
server.bindAsync("127.0.0.1:50051", grpc.ServerCredentials.createInsecure(), (error, port) => {
  if (error) throw error;
  console.log(`TaskService listening on ${port}`);
});

client.js:

const path = require("node:path");
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");

const PROTO_PATH = path.join(__dirname, "proto", "task.proto");
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: false,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});
const taskProto = grpc.loadPackageDefinition(packageDefinition).tasks.v1;
const client = new taskProto.TaskService("127.0.0.1:50051", grpc.credentials.createInsecure());
const metadata = new grpc.Metadata();
metadata.set("authorization", `Bearer ${process.env.DEMO_TOKEN || "dev-token"}`);

function deadline(ms) {
  return new Date(Date.now() + ms);
}

function createTask(title) {
  return new Promise((resolve, reject) => {
    client.createTask({ title }, metadata, { deadline: deadline(1000) }, (error, task) => {
      if (error) return reject(error);
      resolve(task);
    });
  });
}

function getTask(id) {
  return new Promise((resolve, reject) => {
    client.getTask({ id }, metadata, { deadline: deadline(1000) }, (error, task) => {
      if (error) return reject(error);
      resolve(task);
    });
  });
}

function listTasks(limit) {
  return new Promise((resolve, reject) => {
    const rows = [];
    const stream = client.listTasks({ limit }, metadata, { deadline: deadline(1000) });
    stream.on("data", (task) => rows.push(task));
    stream.on("error", reject);
    stream.on("end", () => resolve(rows));
  });
}

async function main() {
  const created = await createTask("Claude Code gRPC");
  const fetched = await getTask(created.id);
  const rows = await listTasks(10);
  console.log(JSON.stringify({ created, fetched, streamed: rows.length }, null, 2));
  client.close();
}

main().catch((error) => {
  console.error(error.code, error.details || error.message);
  client.close();
  process.exitCode = 1;
});

첫 번째 터미널:

npm run server

두 번째 터미널:

npm run client

스키마 변경, 기한, 보안

Protocol Buffers에서 가장 위험한 실수는 필드 번호 재사용입니다. 공개된 번호를 바꾸거나 다른 의미로 다시 쓰면 오래된 데이터가 잘못 해석될 수 있습니다. 삭제한 번호와 이름은 reserved로 막아 둡니다.

message Task {
  string id = 1;
  string title = 2;
  string status = 3;
  int64 created_at_unix = 4;
  optional string assignee_email = 5;
  reserved 6, 7;
  reserved "owner_email";
}

기한도 필수입니다. gRPC 공식 문서는 클라이언트가 기한을 정하지 않으면 사실상 계속 기다릴 수 있다고 설명합니다. 예제의 1초는 로컬 확인용이며, 운영에서는 지연 시간, 처리 시간, 재시도 정책을 보고 정해야 합니다.

스트리밍은 내보내기와 진행 상황 전달에 유용하지만, 모든 데이터를 메모리에 모은 뒤 쓰면 장점이 사라집니다. 취소, 연결 끊김, 부분 실패를 처리하세요. 인증에서는 로컬 예제의 createInsecure()를 운영에 쓰면 안 됩니다. 서버는 grpc.ServerCredentials.createSsl(...), 클라이언트는 grpc.credentials.createSsl(rootCert)로 바꾸고, 토큰은 TLS 위의 메타데이터로 보내야 합니다.

관측성은 메서드, 상태, 시간, 취소, 기한 초과, 백엔드 불가 상태를 볼 수 있어야 합니다. OpenTelemetry gRPC 지표는 좋은 출발점입니다. 오류는 전부 UNKNOWN으로 만들지 말고 INVALID_ARGUMENT, NOT_FOUND, UNAUTHENTICATED, DEADLINE_EXCEEDED, UNAVAILABLE, RESOURCE_EXHAUSTED를 구분합니다. 생성 계열 호출은 멱등 키가 없으면 자동 재시도하지 않는 편이 안전합니다.

Claude Code 프롬프트

gRPC TaskService를 구현해 주세요.
조건:
- 먼저 proto/task.proto를 만든다
- Node.js, @grpc/grpc-js, @grpc/proto-loader를 사용한다
- CreateTask, GetTask, ListTasks 서버 스트리밍을 구현한다
- 모든 클라이언트 RPC에 deadline을 넣는다
- authorization metadata를 검증한다
- 운영에서는 createInsecure를 TLS로 바꿔야 한다고 문서화한다
- npm run client를 실행하고 출력을 보고한다
수정 가능 파일:
- proto/task.proto
- server.js
- client.js
- package.json

리뷰 요청은 이렇게 좁힙니다.

이 gRPC 구현을 findings first로 리뷰해 주세요.
필드 번호 재사용, schema evolution, deadline, cancellation,
status code, streaming 메모리 사용, TLS/auth, observability를 확인하고
심각도순으로 파일 위치와 함께 지적해 주세요.

수익 연결과 검증 결과

혼자 배우는 경우에는 무료 Claude Code 치트시트로 기본 명령과 검토 습관을 잡고, 이 예제를 작은 연습 서비스로 바꿔 보세요. 팀에서 .proto 운영, CI 게이트, 리뷰 프롬프트, TLS 전환, 관측성을 함께 설계해야 한다면 Claude Code 교육 및 상담이 현실적인 다음 단계입니다. 반복 가능한 자료는 ClaudeCodeLab 제품에서 확인할 수 있습니다.

이번 갱신에서 예제는 Node v24.14.1과 npm 11.11.0의 임시 디렉터리에서 실제로 실행했습니다. npm install, node server.js, node client.js가 통과했고, 클라이언트 출력에서 created, fetched, streamed: 1을 확인했습니다. 이 환경에는 go, protoc, Go용 Protobuf 플러그인이 없어서, 검증 가능한 Node 동적 로딩 방식으로 예제를 제한했습니다.

#Claude Code #gRPC #Protocol Buffers #microservices #backend
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.