Upgrade to Pro — share decks privately, control downloads, hide ads and more …

コードで確認する、AWS Amplify API (GraphQL) Subscriptions によるリアルタイム通知のパターン #JAWSDAYS2021 #jawsdays2021_C

コードで確認する、AWS Amplify API (GraphQL) Subscriptions によるリアルタイム通知のパターン #JAWSDAYS2021 #jawsdays2021_C

JAWS DAYS 2021
Track C 13:00-13:40
https://jawsdays2021.jaws-ug.jp/timetable/track-c-1300/

セッション内容
Amplify の API (GraphQL) では、 AppSync をベースとした Subscriptions 機能があります。 しかし、複数の機能・設定に関わる機能であるため、関連する記述がドキュメント上のあちこちにある状態です。
そのため、まとまった理解を得るためにやや手間がかかってしまいます。

このセッションでは AWS Amplify の API (GraphQL) Subscriptions に焦点を当てて話をしていきます。
具体的なコードを使い、シンプルな例からドキュメントに明記されていない使い方までを確認していくことで、 Subscriptions によるリアルタイム通知のパターンの理解を、一緒に深めていきたいと思います。

Kihara, Takuya

March 20, 2021
Tweet

More Decks by Kihara, Takuya

Other Decks in Technology

Transcript

  1. コードで確認する、
    AWS Amplify API (GraphQL) Subscriptions による
    リアルタイム通知のパターン
    木原 卓也 / @tacck
    Track C / 13:00-13:40

    View Slide

  2. 自己紹介
    名前
    木原卓也 / @tacck
    所属
    株式会社ノースディテール
    コミュニティ
    ゆるWeb勉強会@札幌 (主催)
    Amplify Japan User Group (運営メンバー)
    好きなAWSサービス
    Amazon Route 53
    AWS Amplify
    好きなフィギュアスケートの技
    スプレッド・イーグル

    View Slide

  3. 話すこと
    AWS Amplify の API (GraphQL)
    特に Subscription 機能とそれに関連するところ
    実例として Vue.js (2.x系) によるコードの一部
    サンプルコード:
    https://github.com/tacck/sample-amplify-subscriptions
    上級者・開発者向けのトラックのため、
    はしょる部分が多いところはご容赦ください。
    わかりにくい部分は、ぜひ Twitter で指摘いただければ、
    ブログやどこかの JAWS-UG 含む勉強会で補足してい
    きたいです。
    発表できる場所あれば、どこへでも!(オンライン)

    View Slide

  4. 話さないこと
    AWS Amplify の詳細な機能
    AWS Amplify の CLI の詳細な使い方
    AWS Amplify を実際にサービスで使った場合の話
    その他のサービス
    Vue.js の実装方法
    上級者・開発者向けのトラックのため、
    はしょる部分が多いところはご容赦ください。
    わかりにくい部分は、ぜひ Twitter で指摘いただければ、
    ブログやどこかの JAWS-UG 含む勉強会で補足してい
    きたいです。
    発表できる場所あれば、どこへでも!(オンライン)

    View Slide

  5. 人間は、
    Webシステムで
    手軽にリアルタイム通知をしたがる
    生き物

    View Slide

  6. リアルタイムな通知、”手軽に” 作れる?
    チャット的な仕組みをサイトに組み込んで、
    ユーザーからの意見をすぐに受け取れるようにしたい。
    多数のクライアントに、更新情報を一斉に通知したい。
    ユーザーの利用状況を
    リアルタイムにダッシュボードで見たい。
    Etc…

    View Slide

  7. リアルタイムな通知、実装大変ですよね
    WebSocket で作ることになる場合が多い
    • EC2やECSなどの常時稼働インスタンス + サーバー機能実装
    • API Gateway の WebSocket でサーバー機能実装
    • AWS AppSync
    • AWS Amplify

    View Slide

  8. 常時稼働インスタンス + サーバー機能実装
    EC2 や ECS などを使って、インスタンス・コンテナを常時稼働。
    WebSocket の制御・管理機能を、アプリケーションサーバー内に実装。
    • 大規模で利用率が常に高いのであれば、この選択。
    • 多くのユースケースでは、過剰なインフラコストになりがち。
    • インスタンス・ミドルウェア・コンテナのメンテナンスコストが必要。
    • WebSocket クライアントの制御・管理機能をサーバー機能として実装必要。
    本来作りたいサービスの機能「以外」の手間が大きい。

    View Slide

  9. API Gateway の WebSocket でサーバー機能実装
    API Gateway + Lambda を使うことで、 Serverless WebSocket 環境を構築可能。
    WebSocket の制御・管理機能の一部を、 API Gateway が担う。
    • 大規模で利用率が常に高いのであれば、この選択。
    • 多くのユースケースでは、過剰なインフラコストになりがち。
    • インスタンス・ミドルウェア・コンテナのメンテナンスコストが必要。
    • WebSocket クライアントの制御・管理機能をサーバー機能として実装必要。
    インフラ管理コストは低減できるが、
    WebSocket クライアントの制御・管理機能の実装コストは引き続き残る。

    View Slide

  10. AWS AppSync
    GraphQL のフルマネージドサービス。
    GraphQL の規格として Subscription 機能が存在。
    • GraphQL を使い慣れていないと、導入にコストがかかる。
    • リゾルバを VTL で定義できるが、 VTL 自体の学習コストがかかる。
    AppSync / GraphQL の導入経験がないプロジェクトでは、
    “手軽に” とはいかない。

    View Slide

  11. そこで AWS Amplify
    API (GraphQL) ライブラリによって、
    AppSync を “手軽に” 利用可能。
    基本的な機能 (Query, Mutation, Subscription) を利用するための
    標準的な GraphQL を自動生成してくれる。
    開発者は JavaScript で呼び出すだけ。

    View Slide

  12. AWS Amplify で手軽に始められる
    AWS Amplify の API (GraphQL) 機能を使い
    Subscriptions を利用することで
    “手軽に” リアルタイムな通知を実装しよう!

    View Slide

  13. AWS Amplify の Subscriptions のパターン
    1. Basic / @model ディレクティブのみ
    2. Filtering / @model + type Subscription 定義
    3. Auth + Filtering / @model + @auth + type Subscription 定義
    4. Multi-Auth + Filtering / 複数の認証先を利用して通知範囲を制御
    5. Single Publisher / Lambda を利用した Subscription 制御

    View Slide

  14. 1.Basic / @model ディレクティブ

    View Slide

  15. 1.Basic / @model ディレクティブ
    もっとも基本的なパターン。
    API (GraphQL) 機能を有効にしたら、すぐに使用可能。
    書き込み内容 (Mutations) を、通知として受け取る。
    例えば、誰でも(匿名で)書き込めるチャットサービスで、
    誰かが書き込んだメッセージをすぐに画面に反映させる場合。
    1.Basic

    View Slide

  16. $ amplify add api
    Initializing new Amplify CLI version...
    Done initializing new version.
    Scanning for plugins...
    Plugin scan successful
    ? Please select from one of the below mentioned services: GraphQL
    ? Provide API name: sampleamplifysubscri
    ? Choose the default authorization type for the API API key
    ? Enter a description for the API key:
    ? After how many days from now the API key should expire (1-365): 365
    ? Do you want to configure advanced settings for the GraphQL API No, I am done.
    ? Do you have an annotated GraphQL schema? No
    ? Choose a schema template: Single object with fields (e.g., “Todo” with ID,
    name, description)

    1.Basic
    CLI

    View Slide

  17. type OpenChat @model {
    id: ID!
    message: String!
    }
    schema.graphql
    export const onCreateOpenChat = /* GraphQL */ `
    subscription OnCreateOpenChat {
    onCreateOpenChat {
    id
    message
    createdAt
    updatedAt
    }
    }
    `;
    src/graphql/subscriptions.js
    export const createOpenChat = /* GraphQL */ `
    mutation CreateOpenChat(
    $input: CreateOpenChatInput!
    $condition: ModelOpenChatConditionInput
    ) {
    createOpenChat(input: $input, condition:
    $condition) {
    id
    message
    createdAt
    updatedAt
    }
    }
    `;
    src/graphql/mutations.js
    1.Basic

    View Slide

  18. this.onCreateOpenChatSubscription =
    API.graphql(
    graphqlOperation(onCreateOpenChat),
    ).subscribe({
    next: ({ provider, value }) => {
    console.log({ provider, value })
    this.subscriptionMessages.push(value.data.onCreateOpenChat)
    },
    })
    1.Basic
    src/views/OpenChat.vue

    View Slide

  19. sendMessage: async function() {
    const message = await API.graphql(
    graphqlOperation(createOpenChat, {
    input: { message: this.inputMessage },
    }),
    )
    console.log(message)
    this.messages.push(message.data.createOpenChat)
    this.inputMessage = ''
    },
    1.Basic
    src/views/OpenChat.vue

    View Slide

  20. this.onCreateOpenChatSubscription =
    API.graphql(
    graphqlOperation(onCreateOpenChat),
    ).subscribe({
    next: ({ provider, value }) => {
    console.log({ provider, value })
    this.subscriptionMessages.push(value.data.onCreateOpenChat)
    },
    })
    1.Basic
    src/views/OpenChat.vue

    View Slide

  21. Subscription 登録
    1.Basic
    API.graphql(graphqlOperation(onCreateOpenChat))

    View Slide

  22. Mutation 実行
    1.Basic
    API.graphql(graphqlOperation(createOpenChat,{…})

    View Slide

  23. リアルタイムに通知
    subscribe({next: ({ provider, value }) => {…}})
    1.Basic

    View Slide

  24. 2.Filtering / @model + type Subscription 定義

    View Slide

  25. Filtering / @model + type Subscription 定義
    基本的なパターン その2。
    Basic の場合、すべての書き込み (Mutations) を、通知として受け取る。
    そこで、引数として「どの情報を通知として受け取るか」を
    指定することで、必要なものだけを受信できるようにする。
    例えば、誰でも(匿名で)書き込めるチャットサービスで、
    複数のチャットルームがある場合。
    必要なのは、自分の開いているルームの最新メッセージのみ。
    2.Filtering

    View Slide

  26. type RoomChat @model {
    id: ID!
    roomName: String!
    message: String!
    }
    type Subscription {
    onCreateRoomChatByRoomName(roomName: String!): RoomChat
    @aws_subscribe(mutations: ["createRoomChat"])
    }
    schema.graphql
    2.Filtering

    View Slide

  27. this.onCreateMultiRoomChatSubscriptions[roomName] =
    API.graphql(
    graphqlOperation(onCreateRoomChatByRoomName,
    { roomName: roomName }),
    ).subscribe({
    next: ({ provider, value }) => {
    console.log({ provider, value })
    this.subscriptionMessages[
    value.data.onCreateRoomChatByRoomName.roomName
    ].push(value.data.onCreateRoomChatByRoomName)
    },
    })
    src/views/MultiRoomOpenChat.vue
    2.Filtering

    View Slide

  28. sendMessage: async function() {
    const message = await API.graphql(
    graphqlOperation(createRoomChat, {
    input: { message: this.inputMessage, roomName: this.roomName },
    }),
    )
    console.log(message)
    this.messages[this.roomName].push(message.data.createRoomChat)
    this.inputMessage = ''
    },
    src/views/MultiRoomOpenChat.vue
    2.Filtering

    View Slide

  29. this.onCreateMultiRoomChatSubscriptions[roomName] =
    API.graphql(
    graphqlOperation(onCreateRoomChatByRoomName,
    { roomName: roomName }),
    ).subscribe({
    next: ({ provider, value }) => {
    console.log({ provider, value })
    this.subscriptionMessages[
    value.data.onCreateRoomChatByRoomName.roomName
    ].push(value.data.onCreateRoomChatByRoomName)
    },
    })
    src/views/MultiRoomOpenChat.vue
    2.Filtering

    View Slide

  30. Subscription 登録
    API.graphql(graphqlOperation(onCreateRoomChatByRoomName,
    { roomName: ‘Room1’ }))
    API.graphql(graphqlOperation(onCreateOpenChat,
    { roomName: ‘Room2’ }))
    2.Filtering

    View Slide

  31. Mutation 実行 API.graphql(graphqlOperation(createRoomChat,
    { input: { message: this.inputMessage, roomName:
    ‘Room1’}})
    2.Filtering

    View Slide

  32. Room1 のみ受信可能
    2.Filtering
    subscribe({next: ({ provider, value }) => {…}})

    View Slide

  33. 3.Auth + Filtering
    / @model + @auth + type Subscription 定義

    View Slide

  34. 3.Auth + Filtering
    / @model + @auth + type Subscription 定義
    少し応用的なパターン。
    特定の認証方法で認証されたユーザーのみが書き込みを行ない、
    同様に認証されたユーザーが通知を受け取る。
    例えば、会員制のサイト内でのメンバー間のチャットや、
    メンバーとオペレーターのメッセージのやりとり。
    3.Auth + Filtering

    View Slide

  35. type CloseRoomChat
    @model
    @auth(rules: [{ allow: owner, provider: userPools }]) {
    id: ID!
    roomName: String!
    message: String!
    }
    type Subscription {
    onCreateCloseRoomChatByRoomName(roomName: String!): CloseRoomChat
    @aws_subscribe(mutations: ["createCloseRoomChat"])
    }
    schema.graphql
    3.Auth + Filtering

    View Slide

  36. this.onCreateMultiRoomChatSubscriptions[roomName] =
    API.graphql(
    graphqlOperation(onCreateCloseRoomChatByRoomName, {
    roomName: roomName,
    }),
    ).subscribe({
    next: ({ provider, value }) => {
    console.log({ provider, value })
    this.subscriptionMessages[
    value.data.onCreateCloseRoomChatByRoomName.roomName
    ].push(value.data.onCreateCloseRoomChatByRoomName)
    },
    })
    src/views/CloseChat.vue
    3.Auth + Filtering

    View Slide

  37. sendMessage: async function() {
    const message = await API.graphql(
    graphqlOperation(createCloseRoomChat, {
    input: { message: this.inputMessage, roomName: this.roomName },
    }),
    )
    src/views/CloseChat.vue
    3.Auth + Filtering

    View Slide

  38. this.onCreateMultiRoomChatSubscriptions[roomName] =
    API.graphql(
    graphqlOperation(onCreateCloseRoomChatByRoomName, {
    roomName: roomName,
    }),
    ).subscribe({
    next: ({ provider, value }) => {
    console.log({ provider, value })
    this.subscriptionMessages[
    value.data.onCreateCloseRoomChatByRoomName.roomName
    ].push(value.data.onCreateCloseRoomChatByRoomName)
    },
    })
    src/views/CloseChat.vue
    3.Auth + Filtering

    View Slide

  39. 認証済みユーザーのみSubscription 登録可能 API.graphql(graphqlOperation(
    onCreateCloseRoomChatByRoomName,
    { roomName: ‘Room1’ }
    ))
    3.Auth + Filtering

    View Slide

  40. 認証済みユーザーで Mutation 実行
    API.graphql(graphqlOperation(
    createCloseRoomChat,
    { input:
    { message: this.inputMessage,
    roomName: ‘Room1’}
    }
    )
    3.Auth + Filtering

    View Slide

  41. 認証済みユーザーのみ受信可能
    subscribe({next: ({ provider, value }) => {…}})
    3.Auth + Filtering

    View Slide

  42. 4.Multi-Auth + Filtering
    / 複数の認証を利用して通知範囲を制御

    View Slide

  43. 4.Multi-Auth + Filtering
    / 複数の認証を利用して通知範囲を制御
    応用的なパターン。
    特定の認証方法で認証されたユーザーのみが書き込みを行ない、
    別の認証方法で認証されたユーザーが通知を受け取る。
    つまり、機能と権限の調整を行なうことができる。
    例えば、書き込みは認証済みユーザーのみ可能だが、
    読み込みや通知の受信は誰でもできるような掲示板サイト。
    4.Multi-Auth + Filtering

    View Slide

  44. type CloseRoomChat
    @model
    @auth(
    rules: [
    { allow: owner, provider: userPools }
    { allow: public, provider: apiKey, operations: [read] }
    ]
    ) {
    id: ID!
    roomName: String!
    message: String!
    }
    type Subscription {
    onCreateCloseRoomChatByRoomName(roomName: String!): CloseRoomChat
    @aws_subscribe(mutations: ["createCloseRoomChat"])
    @aws_api_key
    }
    schema.graphql
    4.Multi-Auth + Filtering

    View Slide

  45. this.onCreateMultiRoomChatSubscriptions[roomName] =
    API.graphql({
    query: onCreateCloseRoomChatByRoomName,
    variables: {
    roomName: roomName,
    },
    authMode: GRAPHQL_AUTH_MODE.API_KEY,
    }).subscribe({
    next: ({ provider, value }) => {
    console.log({ provider, value })
    this.subscriptionMessages[
    value.data.onCreateCloseRoomChatByRoomName.roomName
    ].push(value.data.onCreateCloseRoomChatByRoomName)
    },
    })
    src/views/CloseChat.vue
    4.Multi-Auth + Filtering

    View Slide

  46. sendMessage: async function() {
    const message = await API.graphql(
    graphqlOperation(createCloseRoomChat, {
    input: { message: this.inputMessage, roomName: this.roomName },
    }),
    )
    src/views/CloseChat.vue
    4.Multi-Auth + Filtering

    View Slide

  47. this.onCreateMultiRoomChatSubscriptions[roomName] =
    API.graphql({
    query: onCreateCloseRoomChatByRoomName,
    variables: {
    roomName: roomName,
    },
    authMode: GRAPHQL_AUTH_MODE.API_KEY,
    }).subscribe({
    next: ({ provider, value }) => {
    console.log({ provider, value })
    this.subscriptionMessages[
    value.data.onCreateCloseRoomChatByRoomName.roomName
    ].push(value.data.onCreateCloseRoomChatByRoomName)
    },
    })
    src/views/CloseChat.vue
    4.Multi-Auth + Filtering

    View Slide

  48. API_KEY で Subscription 登録
    4.Multi-Auth + Filtering
    API.graphql({ query: …, variables: {…},
    authMode: GRAPHQL_AUTH_MODE.API_KEY,
    })

    View Slide

  49. 認証済みユーザーで Mutation 実行
    4.Multi-Auth + Filtering
    API.graphql(graphqlOperation(
    createCloseRoomChat,
    { input:
    { message: this.inputMessage,
    roomName: ‘Room1’
    }
    }
    )

    View Slide

  50. 全ユーザーが受信可能
    4.Multi-Auth + Filtering
    subscribe({ next: ({ provider, value }) => {…}})

    View Slide

  51. 5.Single Publisher
    / Lambda を利用した Subscription 制御

    View Slide

  52. 4までの課題
    1つの Mutation に対して、必ず 1つの通知が発生する。
    10ユーザーが同時に書き込みをしたら?
    AppSync は 10の通知 を 10ユーザーに(ほぼ)「同時」に送信
    各ユーザーは 10の通知 を(ほぼ)「同時」に受信
    つまり、ユーザー数が増えると、 AppSync が同時に送信すべき通知が
    指数関数で増加する。
    ユーザーの受信する通知は線形とはいえ、
    マシン・ブラウザへの負荷に直結する。
    5.Single Publisher

    View Slide

  53. 5.Single Publisher
    / Lambda を利用した Subscription 制御
    Multi-Auth のさらに応用したなパターン。
    通知を行なわない Mutation と、通知を行なう Mutation を用意し、
    通知を行なう方のみユーザー認証を必要とする作りとする。
    通知用の Mutation には、 @function (Lambda) を利用する。
    例えば、多数の人が同時に利用・書き込みをし、
    特定のユーザーが結果を全体へ定期的に通知する。
    今回利用してもらった、デモのアンケートシステムがこれ。
    5.Single Publisher

    View Slide

  54. type ResponseLateRoomChat
    @model(subscriptions: null)
    @auth(
    rules: [
    { allow: owner, provider: userPools }
    { allow: public, provider: apiKey, operations: [read] }
    ]
    ) {
    id: ID!
    roomName: String!
    message: String!
    createdAt: AWSDateTime
    }
    schema.graphql
    5.Single Publisher

    View Slide

  55. type Mutation {
    postResponses(input: PostResponsesInput): PostResponsesOutput
    @function(name: ”XXXXXXXXXX-${env}")
    }
    type Subscription {
    onPostResponses(roomName: String!): PostResponsesOutput
    @aws_subscribe(mutations: ["postResponses"])
    @aws_api_key
    }
    schema.graphql
    5.Single Publisher

    View Slide

  56. this.onPostResponses[roomName] = API.graphql({
    query: onPostResponses,
    variables: {
    roomName: roomName,
    },
    authMode: GRAPHQL_AUTH_MODE.API_KEY,
    }).subscribe({
    next: ({ provider, value }) => {
    console.log('onPostResponses', { provider, value })
    value.data.onPostResponses.items.map(item => {
    this.subscriptionMessages[item.roomName].push(item)
    })
    },
    })
    src/views/CloseChat.vue
    5.Single Publisher

    View Slide

  57. API.graphql(
    graphqlOperation(createResponseLateRoomChat, {
    input: { message: this.inputMessage, roomName: this.roomName },
    }),
    )
    src/views/ResponseLateRoomChatClient.vue
    5.Single Publisher

    View Slide

  58. this.interval = setInterval(async () => {
    const getStartTime = new Date(Date.now() - this.intervalTime)
    const postedMessages = await API.graphql({
    query: listResponseLateRoomChats,
    variables: {
    filter: { createdAt: { ge: getStartTime.toISOString() } },
    },
    authMode: GRAPHQL_AUTH_MODE.API_KEY,
    })
    console.log('interval listResponseLateRoomChats', postedMessages)
    if (postedMessages.data.listResponseLateRoomChats.items.length > 0) {
    this.rooms.forEach(roomName => {
    const messages = postedMessages.data.listResponseLateRoomChats.items.filter(
    item => item.roomName === roomName,
    )
    if (messages && messages.length > 0) {
    this.postResponsesToAll(roomName, messages)
    }
    })
    }
    }, this.intervalTime)
    src/views/ResponseLateRoomChatClient.vue
    5.Single Publisher

    View Slide

  59. postResponsesToAll: async function(roomName, messages) {
    const message = await API.graphql(
    graphqlOperation(postResponses, {
    input: {
    roomName: roomName,
    items: messages,
    },
    }),
    )
    console.log('postResponsesToAll', message)
    },
    src/views/ResponseLateRoomChatClient.vue
    5.Single Publisher

    View Slide

  60. this.onPostResponses[roomName] = API.graphql({
    query: onPostResponses,
    variables: {
    roomName: roomName,
    },
    authMode: GRAPHQL_AUTH_MODE.API_KEY,
    }).subscribe({
    next: ({ provider, value }) => {
    console.log('onPostResponses', { provider, value })
    value.data.onPostResponses.items.map(item => {
    this.subscriptionMessages[item.roomName].push(item)
    })
    },
    })
    src/views/ResponseLateRoomChatClient.vue
    5.Single Publisher

    View Slide

  61. @function の方を Subscription 登録
    5.Single Publisher
    API.graphql({
    query: onPostResponses,
    variables: {…},
    authMode: GRAPHQL_AUTH_MODE.API_KEY,
    })

    View Slide

  62. 登録用 Mutation 実行
    API.graphql(
    graphqlOperation(createResponseLateRoomChat, {
    input: { message: this.inputMessage,
    roomName: this.roomName },
    }),
    )
    5.Single Publisher

    View Slide

  63. 通知用 データを Query で取得
    API.graphql({
    query: listResponseLateRoomChats,
    variables: {…},
    authMode: GRAPHQL_AUTH_MODE.API_KEY,
    })
    5.Single Publisher

    View Slide

  64. 通知用 Mutation 実行
    API.graphql(
    graphqlOperation(postResponses, {
    input: { roomName: roomName, items: messages }
    })
    )
    5.Single Publisher

    View Slide

  65. コントロールされた通知を受信 subscribe({ next: ({ provider, value }) => {…}})
    5.Single Publisher

    View Slide

  66. まとめ

    View Slide

  67. AWS Amplify の Subscriptions のパターン
    1. Basic / @model ディレクティブのみ
    2. Filtering / @model + type Subscription 定義
    3. Auth + Filtering / @model + @auth + type Subscription 定義
    4. Multi-Auth + Filtering / 複数の認証先を利用して通知範囲を制御
    5. Single Publisher / Lambda を利用した Subscription 制御

    View Slide

  68. もっと AWS Amplify を知りたくなったら

    View Slide

  69. View Slide

  70. 発表者 タイトル
    木村宏明氏 (株式会社スカラパートナーズ) バックエンドエンジニアがAmplifyで中規模案件に挑戦した話
    青木 光平氏 (O: 株式会社 CTO) Amplify によるWeb&ネイティブアプリ開発とチーム運営
    ララタン!!肉屋のCTO 谷 耕平 氏(株式会社ビータス
    CTO)
    toCサービスをAWS Amplify使って構築、運用しているお話
    南島 康一 氏/向 宇 氏 (株式会社マーケットエンター
    プライズ Tech Lead/Front-end Engineer)
    レガシーなWebシステムにAmplifyを生やしてみた話
    松井 英俊 氏 (株式会社スタートアップテクノロジー) Amplifyで作る配信サイト/ビデオチャットWebアプリ

    View Slide

  71. View Slide