gRPC-Web를 이용한 브라우저-백엔드 통합
Grace Collins
Solutions Engineer · Leapcell

소개
웹 개발의 진화하는 환경에서 클라이언트와 서버 간의 고성능, 실시간 및 효율적인 통신에 대한 수요가intensified되었습니다. 전통적인 RESTful API가 오랫동안 백본 역할을 해왔지만, 직렬화 오버헤드 및 강력한 유형 안전성 부족과 관련된 제한 사항은 현대적이고 복잡한 애플리케이션에서 점점 더 분명해지고 있습니다. Google에서 개발한 강력하고 오픈 소스인 범용 RPC 프레임워크인 gRPC를 소개합니다. gRPC는 프로토콜 버퍼를 사용하여 효율적인 직렬화를 수행하고 HTTP/2를 사용하여 다중화 및 스트리밍을 수행하여 성능과 개발자 경험을 크게 향상시킵니다. 그러나 웹 브라우저가 gRPC 서비스와 직접 통신하는 것은 HTTP/1.1(브라우저에서 보편적으로 사용됨)과 gRPC에 필요한 HTTP/2 간의 아키텍처 차이로 인해 역사적으로 어려움이 있었습니다. 이 정확한 과제가 브라우저 기반 애플리케이션이 gRPC의 성능을 직접 활용할 수 있도록 하는 중요한 기술인 gRPC-Web를 탄생시켰습니다. 이 글에서는 gRPC-Web의 메커니즘, 이점 및 실용적인 구현을 탐구하며 브라우저 프런트엔드와 gRPC 백엔드 서비스 간의 격차를 원활하게 해소하는 방법을 보여줍니다.
핵심 개념 이해
gRPC-Web의 세부 사항을 자세히 살펴보기 전에, 그것이 활용하는 기본 기술을 이해하는 것이 필수적입니다.
- gRPC (gRPC 원격 프로시저 호출): 어떤 환경에서도 실행될 수 있는 현대적인 오픈 소스 고성능 RPC 프레임워크입니다. 클라이언트 및 서버 애플리케이션이 투명하게 통신할 수 있도록 하여 연결된 시스템을 더 쉽게 구축할 수 있습니다. gRPC는 서비스 정의, 원격으로 호출할 수 있는 메서드와 해당 매개변수 및 반환 유형을 지정하는 아이디어를 기반으로 합니다. 프로토콜 버퍼를 인터페이스 정의 언어(IDL) 및 메시지 교환 형식으로 사용합니다.
- 프로토콜 버퍼 (Protobuf): 구조화된 데이터를 직렬화하기 위한 언어 독립적이고 플랫폼 독립적인 확장 가능한 메커니즘입니다.
.proto
파일에서 데이터 구조를 정의하면 Protobuf 컴파일러는 다양한 언어(Go, Java, Python, C++, C#, JavaScript 등)에서 직렬화 및 역직렬화를 위한 코드를 생성하여 강력한 유형 안전성을 보장합니다. - HTTP/2: HTTP 네트워크 프로토콜의 두 번째 주요 버전입니다. HTTP/1.1에 비해 주요 개선 사항으로는 바이너리 프레이밍, 다중화(단일 연결을 통해 여러 요청/응답 전송), 서버 푸시 및 헤더 압축이 있으며, 이 모든 것이 상당한 성능 향상에 기여합니다. gRPC는 주로 효율적인 통신을 위해 HTTP/2에 의존합니다.
- gRPC-Web: 웹 브라우저가 gRPC 서비스와 상호 작용할 수 있도록 하는 프록시 기반 솔루션입니다. 브라우저는 주로 HTTP/1.1을 통해 통신하며 gRPC가 중요하게 사용하는 트레일러 및 임의 데이터 프레임 액세스와 같은 HTTP/2 기능에 대한 기본 지원이 부족합니다. gRPC-Web는 클라이언트 측(JavaScript)에 작은 코드 계층을 추가하여 작동하며, 종종 브라우저에서 gRPC 백엔드가 이해하는 HTTP/2 요청으로 HTTP/1.1 요청을 변환하는 프록시(Envoy 또는 사용자 지정 gRPC-Web 프록시와 같은)가 필요합니다.
gRPC-Web 통신 원칙
gRPC-Web의 핵심 원칙은 변환입니다. HTTP/1.1에 의해 제한된 브라우저는 gRPC가 필요로 하는 HTTP/2 프로토콜을 직접 처리할 수 없습니다. gRPC-Web는 gRPC 요청을 HTTP/1.1와 호환되는 형식(일반적으로 특정 Content-Type
을 가진 POST 요청)으로 마샬링하는 클라이언트 측 라이브러리를 도입합니다. 이 요청은 프록시 서버로 전송되며, 이 서버는 이러한 요청을 gRPC 백엔드를 위한 HTTP/2로 변환합니다. gRPC 백엔드로부터의 응답은 역 경로를 따릅니다. HTTP/2 응답이 프록시로 전송되고 HTTP/1.1로 변환되어 브라우저로 전송되며, 마지막으로 gRPC-Web 클라이언트 라이브러리에 의해 언마샬링됩니다.
이 과정에는 다음이 포함됩니다.
- 프로토콜 버퍼 정의:
.proto
파일에서 서비스 메서드 및 메시지 구조를 정의합니다. - 코드 생성:
protoc
(프로토콜 버퍼 컴파일러)과protoc-gen-grpc-web
플러그인을 사용하여 클라이언트 측 JavaScript 코드(또는 TypeScript 정의)와 서버 측 gRPC 아티팩트를 생성합니다. - 클라이언트 측 gRPC-Web 라이브러리: 브라우저 친화적인 HTTP/1.1 형식으로 gRPC 요청을 구성하는 데 생성된 코드를 활용하는 JavaScript 라이브러리입니다.
- gRPC-Web 프록시: 브라우저와 gRPC 백엔드 사이에 위치하는 중개 서버(Envoy 또는 전용 gRPC-Web 프록시와 같은)입니다. 브라우저에서 gRPC 백엔드로의 프로토콜 변환(HTTP/1.1에서 HTTP/2로) 및 그 반대도 처리합니다.
예제를 포함한 구현 단계
간단한 "Greeter" 서비스 예제를 통해 이를 설명해 보겠습니다.
1. Protobuf 서비스 정의
greet.proto
파일을 만듭니다.
syntax = "proto3"; option go_package = "./;greet"; // For Go backend package greet; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); rpc SayHellosStream (HelloRequest) returns (stream HelloReply); // Example of server streaming } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
2. 서버 측 코드 생성 (Go 예제)
일반적으로 protoc
을 사용하여 gRPC 서버에 대한 Go 코드를 생성합니다.
protoc --go_out=. --go-grpc_out=. greet.proto
이것은 greet.pb.go
및 greet_grpc.pb.go
를 생성합니다.
3. gRPC 서버 구현 (Go 예제)
package main import ( "context" "fmt" "log" "net" "time" "google.golang.org/grpc" pb "your_module_path/greet" // Replace with your actual module path ) type server struct { pb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, } func (s *server) SayHellosStream(in *pb.HelloRequest, stream pb.Greeter_SayHellosStreamServer) error { log.Printf("Received streaming request for: %v", in.GetName()) for i := 0; i < 5; i++ { resp := &pb.HelloReply{Message: fmt.Sprintf("Streaming Hello %s, message %d", in.GetName(), i+1)} if err := stream.Send(resp); err != nil { return err } time.Sleep(500 * time.Millisecond) } return nil } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } s = grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) log.Printf("server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
4. gRPC-Web 클라이언트 측 코드 생성 (JavaScript 예제)
grpc-web
및 protoc-gen-grpc-web
을 설치해야 합니다.
npm install grpc-web go install github.com/grpc/grpc-web/protoc-gen-grpc-web@latest
그런 다음 브라우저용 JavaScript 코드를 생성합니다.
protoc -I=. greet.proto --js_out=import_style=commonjs,binary:. --grpc-web_out=import_style=commonjs,mode=grpcwebtext:.
--js_out=import_style=commonjs,binary:.
: 메시지 클래스를 생성합니다.--grpc-web_out=import_style=commonjs,mode=grpcwebtext:.
: gRPC-Web용 서비스 클라이언트 및 스텁 코드를 생성합니다.grpcwebtext
는 텍스트 형식으로 base64로 인코딩된 Protobuf를 메시지로 보내 프록시가 디버깅하기 쉽게 만드는 일반적인 모드입니다.grpcweb
모드는 원시 이진을 보냅니다.
이 명령은 greet_pb.js
및 GreetServiceClientPb.js
(gRPC-Web 클라이언트용)를 생성합니다.
5. gRPC-Web 프록시 구성 (Envoy 예제)
Envoy는 gRPC-Web 프록시에 인기 있는 선택입니다. 다음은 Envoy의 기본 envoy.yaml
구성입니다.
admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } # Port for browser traffic filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http codec_type: AUTO route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: grpc_backend # This is crucial for gRPC-Web grpc_web: {} cors: allow_origin_string_match: - prefix: "*" allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web max_age: "1728000" expose_headers: custom-header-1,grpc-status,grpc-message http_filters: - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router clusters: - name: grpc_backend connect_timeout: 0.25s type: LOGICAL_DNS # Or STATIC, if backend IP is fixed lb_policy: ROUND_ROBIN # The actual gRPC backend runs on port 50051 load_assignment: cluster_name: grpc_backend endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 # IP of your gRPC backend port_value: 50051 # Enable HTTP/2 for gRPC backend communication http2_protocol_options: {}
Envoy 시작: envoy -c envoy.yaml
6. 브라우저 클라이언트 생성 (JavaScript/HTML 예제)
<!DOCTYPE html> <html> <head> <title>gRPC-Web Greeter</title> <script src="https://unpkg.com/google-protobuf@3.12.2/google-protobuf.js"></script> <script src="https://unpkg.com/grpc-web@1.3.1/grpc-web.js"></script> </head> <body> <h1>gRPC-Web Greeter</h1> <input type="text" id="nameInput" placeholder="Enter your name" value="World"/> <button onclick="sayHello()">Say Hello</button> <button onclick="sayHelloStream()">Say Hello Stream</button> <div id="response"></div> <script type="module"> import {GreeterClient} from './GreetServiceClientPb.js'; import {HelloRequest, HelloReply} from './greet_pb.js'; const client = new GreeterClient('http://localhost:8080', null, null); // Envoy proxy URL window.sayHello = () => { const name = document.getElementById('nameInput').value; const request = new HelloRequest(); request.setName(name); client.sayHello(request, {}, (err, response) => { const responseDiv = document.getElementById('response'); if (err) { console.error(err); responseDiv.innerText = `Error: ${err.message}`; return; } responseDiv.innerText = `Greeting: ${response.getMessage()}`; }); }; window.sayHelloStream = () => { const name = document.getElementById('nameInput').value; const request = new HelloRequest(); request.setName(name); const stream = client.sayHellosStream(request, {}); const responseDiv = document.getElementById('response'); responseDiv.innerText = 'Streaming greetings...\n'; stream.on('data', (response) => { responseDiv.innerText += `\nStreaming Greeting: ${response.getMessage()}`; }); stream.on('end', () => { responseDiv.innerText += '\nStreaming finished.'; }); stream.on('error', (err) => { console.error(err); if (err.code === 2) { // UNKNOWN error code for stream reset responseDiv.innerText += '\nError: Stream stopped unexpectedly. Check console for details.'; } else { responseDiv.innerText += `\nError: ${err.message}`; } }); }; </script> </body> </html>
greet_pb.js
, GreetServiceClientPb.js
, 및 이 index.html
파일을 디렉토리에 배치하고 간단한 웹 서버(예: python3 -m http.server 8000
)로 제공하십시오.
이제 브라우저에서 http://localhost:8000
을 열고 버튼을 클릭하면 브라우저가 포트 8080의 Envoy로 HTTP/1.1 요청을 보냅니다. 그런 다음 Envoy는 이를 HTTP/2로 변환하고 포트 50051의 gRPC 서버로 전달합니다. 응답은 동일한 방식으로 다시 전송됩니다.
애플리케이션 시나리오
gRPC-Web는 다양한 시나리오에서 특히 유용합니다.
- 단일 페이지 애플리케이션 (SPA): React, Angular 또는 Vue와 같은 프레임워크로 구축된 데이터 집약적인 SPA의 경우 gRPC-Web는 REST에 비해 뛰어난 성능과 유형 안전성을 제공하여 상용구 코드를 줄이고 유지 관리성을 향상시킵니다.
- 마이크로서비스 아키텍처: 백엔드가 gRPC 마이크로서비스로 구성된 경우 gRPC-Web는 프런트엔드부터 서비스 메쉬까지 일관된 통신 패러다임을 제공합니다.
- 실시간 대시보드 및 분석: HTTP/2 및 gRPC-Web 지원을 활용하는 서버 스트리밍은 간단한 스트림 요구 사항에 대한 폴링 또는 복잡한 WebSocket 구현의 오버헤드 없이 대시보드에 실시간 업데이트를 푸시하는 데 탁월합니다.
- 내부 도구: gRPC 백엔드와 많이 상호 작용하는 내부 개발자 도구 또는 관리 패널의 경우 gRPC-Web는 개발 속도를 크게 높이고 코드 품질을 향상시킬 수 있습니다.
결론
gRPC-Web는 현대 웹 브라우저가 고성능 gRPC 백엔드 서비스와 직접 통신할 수 있도록 하는 문제를 독창적으로 해결합니다. 프록시를 활용하여 HTTP/1.1과 HTTP/2 간의 격차를 해소함으로써 프로토콜 버퍼의 강력한 유형 지정과 gRPC의 효율성, 스트리밍 기능 및 성능의 이점을 브라우저 기반 애플리케이션에 직접 제공합니다. 이를 통해 개발자는 더 강력하고 성능이 뛰어나며 유지 관리 가능한 웹 애플리케이션을 구축할 수 있으며, gRPC 패러다임으로 클라이언트-서버 통신 스택을 진정으로 통합할 수 있습니다.