gRPC-Web을 Kotlin 백엔드에서 시도했을 때의 메모 - 4. 웹 프론트 엔드 편

About



이 테마의 연재, 웹 프론트 엔드 편입니다.

소개



죄송하지만 웹 프런트 엔드는 gRPC-Web을 사용하여 ReverseProxy를 통해 백엔드 gRPC 서비스와 상호 작용합니다.



이번 샘플에서는, Web 프런트 엔드는 이하의 구성으로 했습니다.
  • TypeScript
  • Nuxt.js 2.9.2 : SPA 모드

  • Nuxt.js의 TypeScript 지원입니다.

    또, 2.9부터 나름대로의 규모 변경이 들어있는 모양 자체, 아직 SSR에는 대응하고 있지 않는 것 같습니다. 이번에는 SPA 모드에서 검증했지만 SSR에서 사용하고 싶다면 다른 옵션을 고려해야 할 것 같습니다.

    구성



    샘플 gRPC-Web 디렉터리에 웹 프런트 엔드 프로젝트가 포함되어 있습니다.

    프로젝트 만들기



    을 참고로 Nuxt 프로젝트를 만들었습니다.

    package.json



    주요 dependencies를 게시합니다.

    web/package.json
      "dependencies": {
        ...
        "google-protobuf": "^3.10.0-rc.1",
        "grpc-web": "^1.0.6",
        "vue-property-decorator": "^8.2.2"
      },
      "devDependencies": {
        "@nuxt/typescript-build": "^0.2.6",
        ...
        "@types/google-protobuf": "^3.7.1",
        ...
      }
    

    직접 추가 한 것은
  • google-protobuf
  • grpc-web
  • vue-property-decorator

  • 세 가지입니다.

    gRPC 클라이언트 구현



    service 디렉토리를 만들고 그 아래에 gRPC 서비스 ( GreeterService )를 호출하는 클라이언트 구현을 배치했습니다.
    service
    ├── GreeterService.ts
    └── grpc ← protocで生成したファイル
        ├── Greeter_grpc_web_pb.d.ts
        ├── Greeter_grpc_web_pb.js
        ├── Greeter_pb.d.ts
        └── Greeter_pb.js
    

    개별적으로 살펴 보겠습니다.

    protoc로 파일 생성



    드디어 본제의 gRPC-Web이 등장합니다.
    공식 튜토리얼 을 참고하여 protoc 플러그인으로 protoc-gen-grpc-web 를 설치합니다.

    protoc-gen-grpc-web 설치
    $ curl -o /tmp/protoc-gen-grpc-web-1.0.6-darwin-x86_64 \
              https://github.com/grpc/grpc-web/releases/download/1.0.6/protoc-gen-grpc-web-1.0.6-darwin-x86_64
    $ sudo mv /tmp/protoc-gen-grpc-web-1.0.6-darwin-x86_64 \
              /usr/local/bin/protoc-gen-grpc-web
    $ chmod +x /usr/local/bin/protoc-gen-grpc-web
    

    이제 protoc 명령으로 --grpc-web_out 옵션을 사용할 수 있으므로 Greeter.proto에서 코드를 생성합니다.
    protoc -I=service/src/main/proto Greeter.proto \
           --js_out=import_style=commonjs:web/service/grpc \
           --grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:web/service/grpc
    
    import_style 하지만 이번에는 commonjs+dts 를 지정했습니다.

    Experimental입니다만.
  • Promise 기반 Client 함수가 제공되지 않음 (콜백 기반 Client 함수 전용)
  • typescript 를 import하려고 할 때 컴파일 오류가 발생합니다.
  • TypeScript 공부에 대한 해결책에 이르지 않고


  • GreeterService.ts 구현



    protoc에서 생성한 파일을 바탕으로 d.ts 에 대한 Client를 구현했습니다.

    web/service/GreeterService.ts
    import { GreeterPromiseClient } from './grpc/Greeter_grpc_web_pb'
    import { HelloRequest } from './grpc/Greeter_pb'
    
    export default class GreeterService {
      constructor(private readonly hostname: string) {
        this.client = new GreeterPromiseClient(hostname, null, null)
      }
    
      private readonly client: GreeterPromiseClient
    
      public async sayHello(name: string): Promise<{message: string; nameLength: number}> {
        const request = new HelloRequest()
        request.setName(name)
        const response = await this.client.sayHello(request)
        return Promise.resolve({
          message: response.getMessage(),
          nameLength: response.getNamelength()
        })
      }
    }
    

    Client를 Vue 인스턴스에 주입



    구현한 Client를 Vue 인스턴스에 주입하기 위해 플러그인을 정의합니다.

    web/plugins/greeterService.ts
    import GreeterService from '~/service/GreeterService'
    import { Context } from '@nuxt/types'
    
    declare module 'vue/types/vue' {
      interface Vue {
        readonly $greeterService: GreeterService
      }
    }
    
    export default (ctx: Context, inject: (key: string, value: any) => void) => {
      const greeterService = new GreeterService(ctx.env['GRPC_HOST'])
      inject('greeterService', greeterService)
    }
    

    정의한 플러그인을 GreeterService 로 로드합니다.

    web/nuxt.config.js
    export default {
      mode: 'spa',
      ...
      /*
       ** Plugins to load before mounting the App
       */
      plugins: [
        { src: '~plugins/greeterService.ts', ssr: false }
      ],
      env: {
        GRPC_HOST: process.env.GRPC_HOST || 'http://localhost:8080'
      }
    }
    

    Component



    플러그인이 Vue 인스턴스에 nuxt.config.js를 주입하기 때문에 화면의 Component에서 호출해 보겠습니다.

    web/pages/index.vue
    <template>
      <section class="section">
        <div class="container">
          <h1 class="title">gRPC(gRPC-Web) Sample</h1>
          <div class="field">
            <label class="label">Name</label>
            <div class="control">
              <input v-model="name" class="input" type="text" placeholder="Name" />
            </div>
          </div>
          <div class="control">
            <button class="button is-primary" @click="send">Send</button>
          </div>
    
          <div
            v-if="message && nameLength"
            class="content"
            style="margin-top: 0.75rem;"
          >
            <blockquote>{{ message }} (nameLength={{ nameLength }})</blockquote>
          </div>
        </div>
      </section>
    </template>
    
    <script lang="ts">
    import { Vue, Component } from 'vue-property-decorator'
    
    @Component({ name: 'IndexPage' })
    class IndexPage extends Vue {
      name: string = ''
    
      message: string | null = null
    
      nameLength: number | null = null
    
      async send() {
        // Vueインスタンスに注入された$greeterService
        const { message, nameLength } = await this.$greeterService.sayHello(
          this.name
        )
        this.message = message
        this.nameLength = nameLength
      }
    }
    
    export default IndexPage
    </script>
    

    좋은 웹페이지 즐겨찾기