단항, 서버, 클라이언트 및 양방향 스트리밍 RPC
네 가지 gRPC 호출 유형을 구현하고 각 상호 작용에 적합한 패턴을 선택합니다.
단항, 서버, 클라이언트 및 양방향 스트리밍 RPC은(는) CoddyKit의 무료 Node.js Backend Development Bootcamp 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Node.js Backend Development Bootcamp 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Node.js Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Four Ways to Call an RPC
gRPC defines four kinds of method in your .proto file, and each one maps to a different streaming shape:
- Unary — one request, one response (like a normal function call).
- Server streaming — one request, a stream of responses.
- Client streaming — a stream of requests, one response.
- Bidirectional streaming — both sides stream independently.
The stream keyword in the service definition is what decides the shape. In this lesson you'll implement all four in Node.js with @grpc/grpc-js and learn when to reach for each.
syntax = "proto3";
package chat;
service ChatService {
// unary
rpc GetUser (UserRequest) returns (User);
// server streaming
rpc ListMessages (RoomRequest) returns (stream Message);
// client streaming
rpc UploadLogs (stream LogLine) returns (UploadSummary);
// bidirectional streaming
rpc Chat (stream Message) returns (stream Message);
}Loading the Proto in Node.js
Before you implement any handler, you load the .proto definition at runtime with @grpc/proto-loader and turn it into a gRPC service object with @grpc/grpc-js.
The loaded packageDefinition mirrors your proto packages and services. You attach handlers to the service with server.addService() — the names you provide must match the rpc names exactly.
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const pkgDef = protoLoader.loadSync('chat.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const proto = grpc.loadPackageDefinition(pkgDef).chat;
const server = new grpc.Server();
// handlers get attached here
server.addService(proto.ChatService.service, { /* ... */ });Unary RPC — One In, One Out
A unary handler receives (call, callback). The request payload is on call.request, and you reply by invoking the Node-style callback(error, response).
- Pass
nullas the first argument for success. - Pass a
{ code, details }error object to signal failure.
Unary is the right choice for classic request/response work: fetching a record, validating input, performing a single mutation.
function getUser(call, callback) {
const { id } = call.request;
if (!id) {
return callback({
code: grpc.status.INVALID_ARGUMENT,
details: 'id is required',
});
}
const user = { id, name: 'Ada Lovelace' };
callback(null, user);
}
server.addService(proto.ChatService.service, { GetUser: getUser });Server Streaming — One In, Many Out
A server-streaming handler receives only (call) — there is no callback. You push each response with call.write(message), then signal completion with call.end().
This pattern shines when the server produces a sequence the client can consume incrementally: paginated results, log tails, progress events, or large result sets you don't want to buffer in memory.
function listMessages(call) {
const { roomId } = call.request;
const messages = loadMessagesForRoom(roomId); // array
for (const msg of messages) {
call.write({ id: msg.id, text: msg.text });
}
call.end(); // closes the stream to the client
}
server.addService(proto.ChatService.service, {
ListMessages: listMessages,
});Backpressure in Server Streaming
call.write() returns a boolean. When it returns false, the internal buffer is full and you should wait for the 'drain' event before writing more. Ignoring this on large streams can balloon memory usage.
The robust pattern wraps writes in a promise that resolves on drain, so an async loop naturally pauses when the consumer is slow.
function writeAsync(call, msg) {
return new Promise((resolve) => {
if (call.write(msg)) resolve();
else call.once('drain', resolve);
});
}
async function streamBigResult(call) {
for (let i = 0; i < 100000; i++) {
await writeAsync(call, { id: i, text: 'row ' + i });
}
call.end();
}Client Streaming — Many In, One Out
A client-streaming handler receives (call, callback). You listen for incoming items with call.on('data', ...), do final work on call.on('end', ...), and send the single response via the callback.
Use it when the client feeds many items but you only need one aggregate result: bulk uploads, batched metrics ingestion, or computing a summary/average over a stream.
function uploadLogs(call, callback) {
let count = 0;
let bytes = 0;
call.on('data', (logLine) => {
count += 1;
bytes += Buffer.byteLength(logLine.text || '');
});
call.on('end', () => {
callback(null, { received: count, totalBytes: bytes });
});
call.on('error', (err) => console.error('client stream error', err));
}Bidirectional Streaming — Many In, Many Out
A bidirectional handler receives only (call) and treats it as a duplex stream: read incoming items with call.on('data', ...) and emit responses with call.write(...) at any time. Both directions are fully independent.
This is ideal for chat, multiplayer state sync, or live request/response negotiation where either side may speak first or out of lockstep.
function chat(call) {
call.on('data', (msg) => {
// echo every message back to the sender, augmented
call.write({ id: msg.id, text: 'echo: ' + msg.text });
});
call.on('end', () => call.end());
call.on('error', (err) => console.error('chat error', err));
}
server.addService(proto.ChatService.service, { Chat: chat });Calling From a gRPC Client
The client API mirrors the server shapes. Unary returns via a callback; server streaming returns a readable stream you iterate with 'data'; client streaming gives you a writable stream you write() to and then end(); bidirectional gives you both at once.
The single rule: the stream keyword on each side of the proto determines whether you get a callback or a stream object.
const client = new proto.ChatService(
'localhost:50051',
grpc.credentials.createInsecure(),
);
// unary
client.GetUser({ id: '1' }, (err, user) => console.log(user));
// server streaming
const stream = client.ListMessages({ roomId: 'general' });
stream.on('data', (m) => console.log(m.text));
stream.on('end', () => console.log('done'));Mental Model: Pick by Cardinality
Choosing the right RPC type is almost always a question of cardinality on each side:
- One request, one response → unary.
- One request, results arrive over time → server streaming.
- Many inputs collapsed into one result → client streaming.
- Continuous, independent two-way flow → bidirectional.
Don't reach for streaming just because data is large — pagination over unary calls is often simpler. Reach for streaming when the data is open-ended in time or genuinely incremental.
A Runnable Cardinality Picker
Here's a tiny, framework-free helper that encodes the decision rule above. It takes whether each side streams and returns the gRPC method type — useful as a sanity check when designing a service.
This is plain JavaScript with no gRPC dependency, so an online judge can run it directly.
function rpcType(clientStreams, serverStreams) {
if (!clientStreams && !serverStreams) return 'unary';
if (!clientStreams && serverStreams) return 'server-streaming';
if (clientStreams && !serverStreams) return 'client-streaming';
return 'bidirectional';
}
console.log(rpcType(false, false)); // unary
console.log(rpcType(false, true)); // server-streaming
console.log(rpcType(true, false)); // client-streaming
console.log(rpcType(true, true)); // bidirectionalErrors, Deadlines, and Cleanup
Across all four types, a few production habits matter:
- Always attach
call.on('error', ...)on streaming handlers — an unhandled stream error can crash the process. - Respect deadlines: check
call.cancelledin long loops and stop writing if the client gave up. - For client/bidi streams, do final work in
'end', not after the first'data'. - Send domain errors with
grpc.statuscodes (e.g.NOT_FOUND,INVALID_ARGUMENT) rather than throwing raw exceptions.
function listMessages(call) {
let i = 0;
const timer = setInterval(() => {
if (call.cancelled) { clearInterval(timer); return; }
if (i >= 10) { clearInterval(timer); return call.end(); }
call.write({ id: i, text: 'tick ' + i++ });
}, 100);
call.on('error', () => clearInterval(timer));
}Quick Check
Test your understanding of choosing the right RPC type.
Recap
You now know all four gRPC call shapes and how to implement them in Node.js with @grpc/grpc-js:
- Unary —
(call, callback); readcall.request, reply once. - Server streaming —
(call);call.write()repeatedly, thencall.end(), minding backpressure via'drain'. - Client streaming —
(call, callback); aggregate on'data', respond once on'end'. - Bidirectional —
(call); read and write independently for live, two-way flows.
Choose by cardinality and whether the data is open-ended in time, always handle stream errors, and respect client deadlines with call.cancelled.
자주 묻는 질문
“단항, 서버, 클라이언트 및 양방향 스트리밍 RPC” 강의는 무료인가요?
네 — “단항, 서버, 클라이언트 및 양방향 스트리밍 RPC” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Node.js Backend Development Bootcamp 강의 전체를 잠금 해제할 수 있습니다. Node.js Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
“단항, 서버, 클라이언트 및 양방향 스트리밍 RPC”에서 뭘 배우나요?
네 가지 gRPC 호출 유형을 구현하고 각 상호 작용에 적합한 패턴을 선택합니다. 브라우저에서 직접 실행하는 실습 코드로 Node.js Backend Development Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Node.js Backend Development Bootcamp을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Node.js Backend Development Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“단항, 서버, 클라이언트 및 양방향 스트리밍 RPC” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Node.js Backend Development Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Node.js Backend Development Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- Protobuf IDL로 서비스 및 메시지 정의하기
- 단항, 서버, 클라이언트 및 양방향 스트리밍 RPC
- 인터셉터, 데드라인 및 메타데이터
- Proto 진화와 하위 호환성