RELATIONS Developers Blog

RELATIONS株式会社の開発ブログです。

GraphQL code generatorで、スキーマからTypeScriptの型情報を自動で手に入れよう!

f:id:suzukalight:20200622144515p:plain

RELATIONSエンジニアの久原です。

Wistant、このたびめでたくフルリニューアルできました! 今回は特にフロントエンドの改修が中心で、UI改善のほか、技術スタックの大幅刷新も行っております。

その一つとして、コードベースをTypeScriptに変更し、型情報が利用できるようになりました。一方バックエンドはGraphQLを利用しており、こちらにもスキーマ情報があります。

そこで今回、スキーマ情報を TypeScript の型に自動変換することで、開発者体験を向上させていく取り組みを行いました。これを実現するツールである GraphQL code generator とあわせてご紹介したいと思います。

GraphQL code generator とは

f:id:suzukalight:20200622144426p:plain
GraphQL code generator

GraphQL Code Generator | GraphQL Code Generator

GraphQL code generator は、シンプルなCLIを使用して、GraphQLスキーマから型情報などのコードを生成することができるツールです。

今回はGraphQLスキーマからTypeScriptの型情報を取得する目的で利用しています。

特徴

  • CLIコマンド1つで、簡単に型情報を生成できる
  • 状況に合わせた、様々なカスタマイズも可能
  • Java, Kotlin, C# などへの出力も対応

どういう人なら使えるか?

TypeScriptでフロントエンド開発を行っている環境であれば利用可能です。型安全な開発者体験が得られます。さらに apollo-client と親和性が高いため、apollo-clientを使っている場合は強く推奨できます。

具体例

f:id:suzukalight:20200622144501p:plain
サンプル:TODOリスト

解説のための具体例として、下記の機能を考えます;

  • マネージャが、各メンバーに対するTODOリストを持っている
  • TODOリストを、指定した filters, orders, paging で Fetch する API について考える

セットアップ

インストール

バックエンドにGraphQLを使っているプロジェクトであれば、下記の要領でcodegenを追加できます;

yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-document-nodes @graphql-codegen/typescript-graphql-files-modules @graphql-codegen/typescript-operations @graphql-codegen/fragment-matcher
{
  "scripts": {
    "codegen": "graphql-codegen --config codegen.yml"
  }
}

設定ファイルを作成

パッケージ設定が完了したら、下記のような設定ファイルをプロジェクトルートに置きます;

# codegen.yml

overwrite: true
schema: 'http://localhost:3000/graphql'
documents: 'src/**/*.graphql'
generates:
  src/generated/introspection-result.ts:
    plugins:
      - 'fragment-matcher'
  src/generated/graphql.ts:
    plugins:
      - 'typescript-document-nodes'
      - 'typescript-operations'
      - 'typescript'
  src/generated/modules.d.ts:
    plugins:
      - typescript-graphql-files-modules

設定している事項のなかで、重要なポイントとしては、下記の3点です;

  • schema の置き場として GraphQL サーバを指定している
  • すべての .graphql ファイルを型生成の起点とする
  • 生成物として typescript の .ts ファイルを期待している

GraphQLスキーマから型情報を自動生成する

クエリのスキーマを定義

「引数としてマネージャとメンバーのIDを渡すと、allActions サブクエリを通してTODOリストが取得できる」という getManagement クエリを考えます。ページネーションはRelay Cursor Connections形式です;

type Query {
  getManagement(managerId: ID!, memberId: ID!): Management
}

type Management {
  manager: User
  member: User

  allActions(
    filters: MemberActionFilter, 
    orders: [MemberActionOrder],
    paging: MemberActionPaging
  ): ActionConnection
}

type ActionConnection {
  edges: [ActionConnectionEdge]
  pageInfo: PageInfo
}

type ActionConnectionEdge {
  node: Action
  cursor: String
}

type PageInfo {
  totalCount: Int
  hasNextPage: Boolean
  endCursor: String
}

type Action {
  id: ID!
  status: ActionStatuses
  title: String
    # ...
}

enum MemberActionOrderKey {
  id
  createdAt
  updatedAt
}

enum OrderByEnum {
  ASC
  DESC
}

input MemberActionFilter {
  date: DateRangeInput
  isDone: Boolean
}

input MemberActionOrder {
  field: MemberActionOrderKey
  orderBy: OrderByEnum
}

input MemberActionPaging {
  cursor: String
  page: Int
  limit: Int
  field: MemberActionOrderKey
}

.graphql ファイルを作成(TODOリスト取得クエリ)

TODOリストを取得するためのGraphQLクエリを作成し、graphql ファイルとして保存します。前節の設定ファイルにおいて、クエリを .graphql ファイルとして保存することで、codegen の対象になるようにしましたので、このファイルはCLI経由で自動的に型処理されます;

# GetMemberActions.graphql

query GetMemberActions(
  $managerId: ID!
  $memberId: ID!
  $actionFilters: MemberActionFilter
  $actionOrders: [MemberActionOrder]
  $actionPaging: MemberActionPaging
) {
  management: getManagement(managerId: $managerId, memberId: $memberId) {
    member {
      id
      displayName
    }

    allActions(filters: $actionFilters, orders: $actionOrders, paging: $actionPaging) {
      edges {
        node {
          ...__Action
        }
        cursor
      }
      pageInfo {
        totalCount
        hasNextPage
      }
    }
  }
}

型情報を自動生成

では型情報を生成してみましょう。CLIを実行するnpm scriptを使います;

yarn codegen

たったこれだけです!

実行結果として、3つのファイルが生成されますが、このうち実際に利用するのは generated/graphql.ts です。

型情報を使ってコーディングする

自動生成された型情報をimportして利用する

import {
  GetMemberActionsQuery,
  GetMemberActionsQueryVariables,
  GetMemberActions,
  MemberActionOrderKey,
  OrderByEnum,
} from '../../../../../../generated/graphql';

generated/graphql.ts に、すべての型情報が生成されています。具体的にはクエリ型、引数型、enum型の各種型情報が自動生成されます。これらを import することで、コードに型情報を持ち込むことができます。

引数の型+enumの型が利用できる

const variables: GetMemberActionsQueryVariables = {
  memberId,
  managerId,
  actionFilters: { isDone },
  actionPaging: {
    page,
    limit,
    field: MemberActionOrderKey.Id,
  },
  actionOrders: [
    {
      field: MemberActionOrderKey.Id,
      orderBy: OrderByEnum.Desc,
    },
  ],
};

クエリの引数である variables に対して型情報が付与できます。型情報を頼ることで、必要な引数がサジェストされ、不要な引数は型エラーになることから、DXが大幅に向上します!

f:id:suzukalight:20200622144504p:plain

f:id:suzukalight:20200622144508p:plain

もうひとつ嬉しい点は、GraphQLで定義したenumを、型として利用できる点です。誤ったオプションを指定してしまう可能性がさらに減少します;

f:id:suzukalight:20200622144512p:plain

誤った引数指定に起因するサーバエラーも減らせそうですね。

クエリの戻り値にも型がついている

apollo-client の useQuery は、型情報もセットで指定することができます;

const { data, loading, error, refetch } = useQuery<
  GetMemberActionsQuery,
  GetMemberActionsQueryVariables
>(GetMemberActions, { variables });

useQueryのジェネリクスでクエリの戻り値型を指定できることから、クエリの実行結果 data にも型が付与されます。以下のスクリーンショットから、戻り値にも型がついていることがわかります;

f:id:suzukalight:20200622144515p:plain

f:id:suzukalight:20200622144518p:plain

開発者体験が大幅に向上することは明白ですね。

フロントエンド・バックエンドで型情報を同期できる

codegenで得られる戻り値型情報は、そのままバックエンドのエンティティ型情報になっていることが多いと思われます。つまり実質的に、フロントエンド・バックエンドで同じ型情報を使っていることになります。

もしスキーマに変更ができてしまった場合(ないほうが嬉しいですが…)、その変更は型エラーという結果でフロントエンドに伝わってきます。コンポーネントやロジックの修正が必要になるということが一瞬でわかるため、デグレが起きてしまう期間を極めて小さくできます。

f:id:suzukalight:20200622144522p:plain

presenterコンポーネントも、スキーマ提供の型情報(今回だと Action など)を使って組んでいれば、同様にエラー検知できますので、デグレもだいぶ怖くなくなります!

メリット・デメリット

メリット

  • 型情報を使うことで、開発者体験が向上し、高速に開発ができる
  • 型情報に守られることで、バグを作りづらくなる
  • 型を同期することで、デグレが起きづらくなる

デメリット

  • 型ファイルが肥大化していく
    • とはいえgraphqlファイルで記述しているぶんだけなので、必要な肥大化ではある
  • 毎度 Diff が出るため、GitHub上でレビューするときに毎度 Viewed をつけるのが少し手間
    • コミットはするけど、レビュー対象から外す。みたいなことができると嬉しい

まとめ

GraphQLのスキーマ情報を TypeScript の型に自動変換することで、フロントエンドに型情報を提供し、開発者体験を向上させていった取り組みをご紹介しました。

型情報によって、より高速に、より安全に開発ができるようになりました。こういった体験は単に開発者のためだけでなく、プロダクトの堅牢性を向上させ、バグ検出率を下げる効果が期待できるものでもあるため、開発者だけでなくユーザの体験も良くなるものと感じます。

今回の件で、体験の向上をヒシヒシと感じましたので、バックエンドにも型情報をつけていきたくなりました。やっていくぞ!

Reduxを完全に廃止し、Apollo Clientに乗り換えた話

こんにちは、フロントエンドエンジニアの池田です。 前回のWistantのフロントエンド技術スタックを刷新した話に引き続き、 今回はWistant上でReduxを廃止した背景と、刷新の経緯について話してければと思います。

Redux廃止の背景

Reduxはもう必須ではない

前提として、前の記事で書いた内容を再び述べようと思います。

Global Stateの管理はモダンなWeb開発において、重要なトピックとなってます。 Redux は長らく React を使う上でのGlobal Stateのデファクトスタンダードになってましたが、 それに対する様々なアプローチが可能になった(Apollo Client や React Hooks の登場)今は、最近はその傾向が薄れて来ました。

全ページのUI改修

同じく、前の記事で述べたのですが、全ページのUIを改修するタスクに伴い、一からフロントエンドの基盤を作り直すことで、今まで積み残されていた技術的負債や開発者経験(DX)の改善も同時に行っていくことが決まりました。その技術選定を改める際、Reduxを廃止することを決定しました。

Reduxを使いながら感じた問題点

開発者経験(DX)の低さ

単純に実装量が多いことが開発者経験を妨げていました。 例えば、以前の技術スタックでは新しいエンティティーのAPIを追加する場合、以下のプロセスが必要でした。

1. reducerを作る(action types、action creator含む)
2. entityのnormalizeのためのコードを書く
3. GraphQLのquery/mutationを書く
4. APIを呼び出す関数を書く
5. 4を呼び出すredux-sagaのコードを書く
6. selectorを書く

新しいentityではない場合でも、3〜6の過程が必要になり、UI/UX以外での実装コストが高く感じました。 また、APIの改修が必要になった時も、ファイルが色んな場所に分散されていたため、修正箇所がわかりづらい場合が多々ありました。

バンドルサイズの増加

アプリケーションの規模が大きくなるに連れ(エンティティーの増加)、reducerの数も増加しました。問題はバンドルした時の root.jsのサイズも共に上がり(他の問題も重なったりして、parsedで3 MB以上に及ぶ大きさを誇ってました)、アプリケーションのパフォーマンス(主にファーストペイント)に影響している点でした。

f:id:so99ynoodles:20200610144021p:plain
様々なreducerとselector、entitiesが存在していた

ユーザーが最初にアクセスするページはログイン画面またはホーム画面で、その時必要なreducerのみダウンロードすれば良いのですが、すべてのreducercombineReducersでまとめてるため、それができてない状態でした。

コードスプリッティング すれば良い話では?と言われればその通りですが、最初からそれができるアーキテクチャーにしておらず、改修に多くのコストが掛かる恐れがあったため、着手できてませんでした。

他の技術との噛合

v5.0を開発する際に、 Apollo ClientRelay など、GraphQL のリクエストと状態管理を楽にしてくれるライブラリーを使用することを決めていたので、Reduxを引き続き使用する必要性を感じませんでした。

ReduxをどうReplaceしたか

実装量が多いかつコードが分散されていることが歪みとしてあったので、それらをコンパクトにまとめることにしました。 バンドルサイズは、reducer を廃止し、コードスプリッティングを徹底することで解決できました。(parsedで1 MB以下になりました)

APIリクエストとデータをApollo Clientに任せる

Redux 廃止前は、fetch してきたデータをストアに保存し、normalize まで独自で管理してましたが、 Apollo Client ではデータのキャッシュを自動で管理してくれるので、そこらへんを考えなくても良くなりました。 また、APIリクエストは実際それを使用するページやコンポーネントにまとめることで、どこを修正すれば良いのかをわかりやすくしました。

Reduxを使ってたときは、APIリクエストで返ってきたデータを Store に入れて、Selector を使ってUIに適用する形でしたが、Apollo Client はデータドリブンなUIに特化してるので、APIリクエストで返ってきたデータをそのままUIに適用する実装にしてます。

しかし、どうしてもキャッシュされたデータが必要な場合があります。(例:ユーザー検索など) そういう時は readFragment などのメソッドを使用すればOKです。

Redux

// selector
const userSelectorFactory = id: string => createSelector(
  allUsersSelector,
  allUsers => allUsers.find(user => user.id === id)
);

// usage
return connect(createStructuredSelector({
  user: (_, props) => userSelectorFactory(props.userId)
}))(UserComponent);

Apollo Client

const __typename = 'User';
const UserFragment = gql`
  fragment __User on User {
    id
    displayName
    avatarUri
    status
    roles
  }
`;

// util
const getUserFromCache = (
  userId: string,
  fragment: DocumentNode = UserFragment,
): User | undefined => {
  return client.readFragment({
    id: `${__typename}:${userId}`,
    fragment,
  });
};

// usage
const user = getUserFromCache(userId);

注意する点としては、readFragment とか readQuery は、必ずそのデータや Query が呼ばれていることを前提としているため、使い場所を選ぶということですね。

Conext APIとReact HooksでGlobal Stateを管理する

Global State は、ユーザーの情報をアプリ上で管理するために必要でした。 その代案としては MobXApollo Client のローカルステートなどが挙げられますが、今回は Context APIReact Hooksを使うことにしました。

選定理由は別途のライブラリーが要らず、学習コストがかからないためです。

Redux

// reduxのconnectを使う(v7.1.0以前)
return connect(createStructuredSelector({
  me: meSelector
}))(Component);

Apollo Client

// useContextを使う
const { me } = useContext(MeContext);

// or custom hooksを使う
const me = useMe();

useContext を使えば、どのコンポーネントでも簡単に Global State にアクセスできるので、便利ですね。

Redux も v7.1.0でhooksに対応するようになり、useSelector などで同様の書き方ができますが、Wistantではv5.0.5を3年前から使っていたので、そのような実装はできてませんでした。

気になったのは、アプリケーションのGlobal StateContext を分解しているため、Provider が無限に重なっていくこととですね……。

const App = () => {
  return (
    <ThemeProvider>
        <NowProvider>
          <HideProvider>
            <ModalProvider>
              <SideSheetProvider>
                <PulldownProvider>
                  <EditProvider>
                    <AppRouter />
                  </EditProvider>
                </PulldownProvider>
              </SideSheetProvider>
            </ModalProvider>
          </HideProvider>
        </NowProvider>
    </ThemeProvider>
  );
};

ネストされた Provider をまとめる方法もいくつかあったのですが、そこまでクリティカルな問題でもないので、一旦このままにしております。

Redux-Formについて

Redux を使うならRedux-Form も一緒に採用される傾向がありました。 Wistantでも Redux-Form を使ってた時代がありますが、Redux 廃止の6ヶ月以上前に Formik に移行しました。

Redux および Redux-Form の作者であるDan Abramov氏やコミュニティーの結論は、フォームの状態管理を Global State で管理すべきではないということです。レポジトリREADME.md にもその旨のメッセージが書いており、React Final Form などを使うことを推奨しています。

最後に

Redux は現在もグローバルな状態管理をするために使えるツールですが、Context API の登場後、React の基本APIで同じような実装が可能になってるので、今後自分が採用することはないと思います。最近話題のFacebook製の状態管理ライブラリー Recoil などは今度試してみたいですね。廃止後に登場したため候補には入らなかったので。

Wistantのフロントエンド技術スタックを刷新した話

こんにちは、フロントエンドエンジニアの池田です。 弊社のサービスであるWistantのv5.0が4月にリリースされました。 簡単にその経緯と技術選定理由を共有させて頂きます。

技術刷新に至った経緯

v5.0の情報アーキテクチャ(レイアウト)変更

新しいデザイナーさんにJOINして頂き、プロダクトのUI・UXの大胆な改善を行うことになりました。 具体的には、プロダクトのコンセプト・ターゲットの変更に伴って、既存のすべてのページにおいてデザインの変更を行うことになりました。

当たり前ですが、全ページの改修は簡単にできることではありません。 モーダルなどを含め約100のページが存在しており、それらをすべて改修・チェックする必要があります。

あれこれ考えるうちに、「一からフロントエンドを作り直そう」という提案が出ました。 アプリケーション内のすべてのページ改修が必要になるなら、いっそフロントエンドの構造から作り直すことで、 今まで積み残されていた技術的負債や、開発者経験(DX)の改善も同時に行っていくのが効率的ではないかという意見です。

技術的負債と開発者経験(DX)

RELATIONSの開発チームでは新機能・バグ対応・保守を6:2:2の割合で開発することをルールとしております。 しかし、Wistantは開発が始まってから2年以上の時間が経っていることや、 最近の開発スケジュールでは新機能開発に注力してきたことなどもあり、技術的負債が多く溜まってきました。

ライブラリーのバージョンが古かったり、実装におけるDXが低下してしまったり、などはまだ些細な問題ですが、 アプリケーションのパフォーマンスに直接影響する部分については、顧客への提供価値を毀損してしまうことから、大きな問題として捉える必要がありました。

これらを解決するために、構造上の問題のある現システムを捨て、 新しいアーキテクチャーを一から構築し直すタイミングとしては、最適だと言えると思います。 なにより、技術的負債を返済しつつ、DX・パフォーマンスの向上のために新しい技術を使えることは、 エンジニアチームとしてとても胸が熱くなることだったので、開発チーム全員が意気投合してた覚えがあります。

主な技術的変更

v5.0以前の技術スタックに関しては、この記事をお読みください。

Reduxの廃止

Global Stateの管理はモダンなWeb開発において、重要なトピックとなってます。 Redux は長らく React を使う上でのGlobal Stateのデファクトスタンダードになってましたが、 それに対する様々なアプローチが可能になった(Apollo ClientReact Hooks の登場)今は、最近はその傾向が薄れて来ました。

また、Wistantではreducerが大きくなるに連れ、バンドルサイズが大きくなったり、 actionやreducerの場所が分かりづらくなったり、実装量も増えてしまったり、などといったところに問題を感じていました。

今回は、必要最低限のGlobal Stateは ReactContext APIHooks に任せることにして、 サーバーから返ってくるデータの管理は Apollo Client に任せるという技術選択をしました。 結果としてDX及びパフォーマンスの二兎を同時に得ることができるような改善ができたと思います。

デザインシステムの構築

デザインシステム の構築は前からチーム内で多く話されてきたトピックです。 現行のWistantのUIは、Stylus を使ってcssを書き、classNameでスタイルを当てる実装にしてました。 しかし、コーディングの指針や、デザインの原則などが統一されてない箇所も多く、似たようなコンポーネントが色んな場所に作られるなどの弊害が出てきました。

今回の全ページのUI改修にあたって、デザインシステムを構築し、一貫性のあるUIの提供と、DX向上を図れるようになりました。 特に、全ページにデザインシステムを適用したことで、前のバージョンでは不可能に近かった、ダークモードを簡単に実装し、提供することが出来たことも大きいメリットになったと思います。

TypeScriptの導入

時代の流れに便乗して TypeScript を導入しました。 型がついていることの便利さは前々から技術共有会などでも話されてきた内容でした。

今回、TypeScript によってコードの多くに型をつけることが出来たことによって、コードベースに対する信頼性が上がったのと、VSCodeで型ヒントが出るようになったことで、実装面でのDXが格段に向上しました。 GraphQL のスキーマをベースに、TypeScript の型を生成してくれる GraphQL Code Generator の使用感もとても良く、かつ非常に便利でした。

最後に

その他にも、ビルド周りを create-react-app に変更したり、開発環境への自動デプロイができるようにしたり、 moment.js というライブラリーを date-fns に変えたり…、とても勉強になることが多かった2ヶ月間でした。 次からは、今回の詳しい内容を詳しく話していければなと思います。

SlackでWEBアプリライクな社内システムを開発しました

f:id:yoshhiide:20191206171947p:plain

Slack Advent Calendar 2019 の10日目の記事です。

こんにちは。鍋山です。
ようやくSlackに入門して2ヶ月が過ぎました。気後れしていたSlackにもやや慣れてきたところです。 先月開催されたSlack Tiny Spec Tokyoにも参加したことでモチベーションが上がり、アドベントカレンダーに参加させていただこうと思いました。

続きを読む

SlackのHome tabがBeta版で使えるようになっていたのでSlack BoltでTODOアプリを作ってみました

f:id:yoshhiide:20191111193224p:plain

こんにちは。最近RELATIONSにジョインしました鍋山です。
RELATIONSではSlackを連絡手段として使っていて、エンジニアや大多数を占める非エンジニア全員で活発にやりとりしてます。
さて、僕自身がSlackに入門して1ヶ月ほど経ちましたので、どんなことができるのかを試してみる目的でとりあえずTODOアプリを作ってみました。

今回使用した機能はHome tabというBeta版(2019/11/11時点)でリリースされた、Slack上で表示できるWEBアプリライクな機能です。
実際に便利なものだと思っているのですが、Beta版なので今後どうなるかはわかりません。

続きを読む

Slackのリアクションに反応するアプリをGASで作ってみた話

こんにちは。RELATIONS株式会社の大川です。
最近はSUMMER SONICの3日目が楽しみで仕方がないです。

普段はWistantの開発・運用をしていますが、 今回はGoogle Apps Script・Slack・スプレッドシートを連携させたアプリ開発の話をしていきます。

きっかけは、OJT中の新入社員に声をかけられたことでした。
(OJTの様子はこちらからご覧いただけます。)
彼は社内にバリュー(行動規範)を浸透させていくために、Slack上の「バリューを体現する行動を称賛する投稿」を増やし、バリュー自体を意識してもらう回数を増やそうとしていました。

弊社ではすでに「(各バリューに対応するカスタム絵文字)(メンション)称賛メッセージ」の形式で、バリューを体現した行動を称賛できるSlackアプリが存在していました。

もともとあるバリューを体現する行動を称賛した時に反応するSlackアプリ
もともとあるバリューを体現する行動を称賛した時に反応するSlackアプリ

ですが、これだけだとメッセージを作成することが面倒だったり、他の人が称賛した内容と同じメッセージを送ることに対してハードルがちょっと高いというフィードバックがありました。
そこで、もっと簡単にSlackメッセージにリアクション(絵文字)をつけるだけで投稿としてシェアされるようになると良いのでは、という話になり、開発が始まりました。
ちなみに「リアクションに反応するアプリ」というアイデアは以前に見かけていたランサーズ株式会社さんのAdvent Calenderの記事を参考にしていました。
(参考: Slackで感謝を送り合うツールを3日で実装、導入した話 - Qiita

つくったもの

Slack上で、バリューに対応する絵文字のリアクションがされると、バリューを発揮した行動が特定のチャンネルに投稿されるようにしました。

上の投稿にリアクションがされたことで、新しいメッセージが投稿された
上の投稿にリアクションがされたことで、新しいメッセージが投稿された

アプリの流れは以下の通りです。
Slack Events APIでリアクションの追加イベントをサブスクライブしておき、
イベントデータがGASに渡ってきたらユーザー情報やメッセージ内容を取得して、
スプレッドシートに記録&Slackにメッセージを送信するようになっています。

アプリ概要図
アプリ概要図

SlackアプリのインフラをGoogle Apps Scriptにする

自分が開発するのであればAWS Lambdaを使うのですが、
プログラミング未経験の新入社員にも実際に手を動かしてもらおうということで、 動作環境には Google Apps Script(以下、GAS)を採用しました。

結果として、ペアプロしつつ自分も半分くらい実装することになったのですが、 以下の点でGASを利用してよかったなと感じています。

  • 特別な開発環境構築が不要なので入門のハードルが低い
  • ドキュメントが充実してるのでスプレッドシート連携も簡単
  • 運用コストがかからない
  • 自分がGASを使ったことなかったのでノウハウが溜まった

開発のTips

GAS

Webアプリとして実装するのも doPost / doGet関数を用意するだけなので簡単でした。
ただ、公開されているWebアプリに変更を反映させるためには毎回デプロイが必要なので、デバッグは工夫が必要でした。
関数の実行はデプロイしなくても試せるので、1つの関数でやることを小さくしてテスト用関数を用意しながらデバッグを進めていきました。

POSTリクエストに反応するdoPostメソッドは、別関数を呼び出すだけのシンプルな実装になっています。

// POSTリクエストの処理
function doPost(e) {
  handleDoPost(e);
}

handleDoPost関数も、POSTリクエストで渡ってきたイベントデータの抽出や スプレッドシートとの連携など色々な処理が多いですが、ほぼ別の関数に切り出してその呼び出しを行うようにしています。

// POSTリクエストの処理の中身
function handleDoPost(e) {
  // (...略)
  if(checkReaction(reaction)){
    // (...略)
    // イベントデータを抽出したり、スプレッドシートに記録したり、メッセージを構築したり
    postMessageToSlack(message)
  }
}

以下はSlackにメッセージを送信するpostMessageToSlack関数と、 Slackからのリアクション内容をチェックするcheckReaction関数です。

// Slackへのメッセージ送信
function postMessageToSlack(message) {
  var data = {
    'text': message,
    link_names:1
  };
  var options = {
    'method' : 'post',
    'contentType': 'application/json',
    'payload' : JSON.stringify(data),
    muteHttpExceptions: true
  };
  UrlFetchApp.fetch(incomingWebhookUrl, options);
}

// リアクションの絵文字がバリュー関連のものか確認する
function checkReaction(stamp){
  return stamp === 'value_team' || stamp === 'value_shinka' || stamp === 'value_sekai' 
}

Slack API

Slackの「リアクションが追加された」というイベントに反応するためEvents APIを利用しました。

SlackにはWeb API / Events API / RTM APIといくつか種類がありますが、 どれを利用するべきかフローチャートが用意されていたので参考にしました。
(参考: 必要な Slack API はどれ? - Slack アプリの作成のためのヒント | Slack

Events APIは全パブリックチャンネルでのリアクション追加に反応します。 しかしSlack Botを用意することでBotが参加しているチャンネルのイベントのみをサブスクライブできるようになります。
(参考: Slack API: Receive events in specific channels only? - Stack Overflow

今回はどのチャンネルでも使えるように特にBotは用意していません。 Slackのメンバー数にもよりますが、弊社ではまだGASの実行回数リミットを過ぎてしまって実行できなくなる…ということも発生していないです。

また、POSTリクエストで渡ってくるイベントデータだけでは、スプレッドシートに記録する情報が不足していたので、 ユーザー情報やメッセージ内容を取得するために別途OAuth認証を行いWeb APIを利用しましたが、ライブラリを利用して簡単にOAuth認証に対応できました。
(参考: gsuitedevs/apps-script-oauth2: An OAuth2 library for Google Apps Script.

今後について

GASを利用してSlackアプリを開発するのは初めてでしたが、すごく簡単に実装できてよかったです。
ただし、今運用されているシステムにはいくつか課題を抱えています。

監視をちゃんとしてない

stackdriverでのログ出力はできるので、異常検知するような仕組みも用意しなきゃいけないなと考えています。

リトライ処理に対応できない

SlackのAPIは3秒以内にレスポンスを返さないとリトライ処理を行うので、 今回構築したシステムも3秒以内にレスポンスを返せないとリトライ処理の分だけ動作してしまいます。

これに対応するためには、POSTリクエストヘッダーの値を見てリトライかを判断する必要があるのですが、 GASではリクエストヘッダーにアクセスできずにリトライ処理をさばけませんでした。

こちらはそもそもリトライ処理を生まない仕組みに改善する必要があり、 ざっと調べてみた感じだとGAS上でジョブキューを構築するような方法があるそうです。(未検証)

手軽に始められたGASでのSlackアプリ開発ですが、 Events APIのサブスクライブ、OAuth認証後のWeb API呼び出し、スプレッドシート連携など 処理が複雑になってきたのでリトライ処理を生まない仕組みにするついでにAWS Lambdaに移行しようかとも考え中です。

まとめ

今回はGASを利用してSlack APIとスプレッドシートを連携させるアプリ開発について紹介しました。
初めてのGASでしたがノウハウが結構たまったので、 社内勉強会など開いて他のメンバーもGASを使って生産性を向上できるようにしていけたらいいなと考えています。

Webエンジニアがスマホアプリをリリースするまでに学んだ32のコト(後編)

こんにちは。RELATIONS株式会社の久原です。

最近はAtomicDesignを生かしたデザインシステムの構築を試しています。こちらはまた別の記事で改めてご紹介できればと思っています。できればアプリとWebの共通システムにしてみたい(難しそう)。

さて、前回の記事では、Webのフロントエンドエンジニアである私が、「自分のフロントエンドスキルセットを活かしつつ、最速でスマホアプリをリリースするためには、どうすればよいか?」を試行錯誤した結果について、その前半部分をご紹介しました。

今回の記事は、残りの後半部分になります。

カテゴリ 学習したところ
開発環境 React Native・Expo
ナビゲーション React Navigation・スタックベースナビゲーション
ネイティブUI NativeBase・レイアウト手法
メディア表現とプリロード ロード方法・CSSとの差の埋め方
ネイティブ機能 プッシュ通知・アイコン・スプラッシュ
デバッグ storybook・react-native-debugger
ビルドとリリース expo-cli・expo client
フロントエンド以外の技術 Firebase

後編では、「ネイティブ機能」「デバッグ」「ビルドとリリース」について、加えて「フロントエンド以外の技術」について学習したところについて、ご紹介したいと思います。

各種ネイティブ機能の利用と設定 (5)

Webエンジニアには利用が難しいネイティブ機能、その中でもプッシュ通知はアプリ開発におけるコア機能と言えます。こういったネイティブ絡みの機能の活用は、React Native(以下、RN)でアプリを作るための大きなモチベーションですね。

そんなプッシュ通知機能ですが、Expoはライブラリを提供するだけでなく、なんとプッシュ通知用のサーバまで提供してくれます!ですのでExpoの領域内で、プッシュ通知の生成から配信までを一貫して構築可能になっています。素晴らしいですね!

exports.sendNotification = functions.firestore
  .document('users/{userId}/messages/{messageId}')
  .onCreate(async () => {
    const { params: { userId, messageId } = {} } = context || {};

    const userRef = db.collection('users').doc(userId);
    const userSnapshot = await userRef.get().catch(() =>({}));
    const user = userSnapshot.data();
    const { deviceToken } = user || {};

    const message = {
      to: deviceToken,
      sound: 'default',
      body: '新しいメッセージを受信しました!',
      data: { type: 'messageCreated', userId, messageId },
    };

    const chunks = expo.chunkPushNotifications([message]);
    const tickets = [];

    chunks.forEach(async chunk => {
      const ticketChunk = await expo
        .sendPushNotificationsAsync(chunk)
        .catch(error => console.error(error));
      tickets.push(...ticketChunk);
    });

    return tickets;
  });

詳細な例が、弊社大川の記事に載っていますので、ぜひご覧ください!

アプリのアイコン(AppIcon)やスプラッシュスクリーンなどの設定も、Expoで完結できます。app.jsonというファイルに設定を記述することによって、環境の差を自動で埋めてくれますので、開発者としてはこれらの画像の用意と、簡単な記述を行うだけでOKです。

{
  "expo": {
    "orientation": "portrait",
    "primaryColor": "#665d8c",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "cover",
      "backgroundColor": "#9a96dd"
    },
    ...
  }
}

その他のネイティブ機能やネイティブ設定などについても、その多くがExpoのライブラリによってサポートされます。実際にこれらの実装においてXcodeやAndroid Studioを開く必要はなく、すべてJSで完結することができました。本当にありがたい話です。

デバッグ (4)

Webエンジニアとしてはお馴染みのStorybookで、Webと同様のUIカタログが作れます。表示はシミュレータ上で、操作はWebブラウザから、それぞれ行うことができます。

import React from 'react';
import { storiesOf } from '@storybook/react-native';
import styled from 'styled-components/native';

import TextButton from '..';
import CenterView from '../../CenterView';

storiesOf('atoms/TextButton', module)
  .addDecorator(getStory => <CenterView>{getStory()}</CenterView>)
  .add('with text', () => <TextButton>Hello Button!</TextButton>)
  .add('variant=primary', () => <TextButton variant="primary">Hello Button!</TextButton>)
  .add('variant=primary + roundMd', () => (
    <TextButton variant="primary roundMd">Hello Button!</TextButton>
  ))
</QuizButton>);

f:id:mkubara:20190327105716p:plain
Storybook for React Native

注意点として、執筆時点ではExpoとStorybookの食い合わせがやや悪いため、storyの更新にreact-native-storybook-loaderが必要になりました。近日v5で解決されるかもしれません。

余談ですが、v4からスタンドアロンアプリ形式でstorybook配布が可能になりました。ビルドパイプラインを分けて、storybook用の配信チャネルを別途設けておくと、デザインチェックが捗る気がします。ぜひ試してみたいですね。

デバッグは、WebエンジニアはChromeの検証タブを頻用していると思います。react-native-debuggerをインストールすると、この検証タブをRNアプリに接続できるようになりますのでオススメです。UIのインスペクタ表示もReduxのトレースも可能です。

f:id:mkubara:20190327105840p:plain
react-native-debugger

もうひとつ、RN組み込みのデバッグメニューもあり、こちらはシミュレータ上からCmd+Dで表示できます。こちらの固有機能としては、ライブリロードのON/OFFなどがあります。(Cmd+Rで手動更新できるので、ロジック調整時などに使用しています)。

このように、デバッグについてはWeb開発とほぼ同様の手法で可能です。心強いですね!

ビルドとリリース (4)

  • スタンドアロンビルド: expo build
  • テスター向け配信: TestFlight/Expo Client
  • パブリッシュ/コードアップデート: expo publish --channel
  • アプリリリース

Webの場合と大きく異なる部分です。ですが多くのフローをExpoがサポートしてくれます。

スタンドアロンのアプリをビルドする操作は、expo-cliを使ってbuildコマンドを叩くだけです。オプション無指定であれば、iOS/Androidそれぞれのバイナリを一括生成してくれます。

$ expo publish

配布については、各プラットフォームの流儀に則って進めていく必要があります。

テスター向けにアプリを配信する場合、Webであれば自前テストサーバにデプロイすればOKですが、スマホアプリの場合は審査が入ります。

iOSの場合はTestFlightを使い、審査を受けてからテスターへ公開します。Androidの場合は公開用のExpoアカウントを作成し、ExpoClient経由でテスターへ公開する形が最短です(審査不要)が、よりしっかりとやる場合は、GooglePlayのテスト配信機能を使うことになるでしょう。

本番リリースにおいても、審査を通して、公開するというプロセスが必要になります。Webのリリースと比べて、どうしても面倒に感じてしまうところですね…。

他方、コードのアップデートについては、JSバンドルの再配布のみで気軽に行なえます。CLIからpublishを行えば、Expoサーバ経由で更新が自動配信され、設定した更新ポリシーに基づいて自動更新できます(自動更新・ダイアログで確認して更新など)。

デバッグ用/テスト用/公開用など、チャネルを分けてパブリッシュも可能です。テストビルドやstorybookビルドなどの配布は、個別にチャネルを分けて配信することができます。

$ expo publish --release-channel stg

以上のように、アプリの公開に関してはExpoの力を借りることはできないのですが、アプリのビルドやアップデートに関しては、Expoの力で簡単に行うことができるようになっています。

フロントエンド以外の技術 (4)

f:id:mkubara:20190327110149p:plain
https://firebase.google.com/?hl=ja より引用)

いわゆるバックエンド側はFirebaseで固めました。mBaaSを使うことで、バックエンド部分を専門のエンジニアに実装を依頼する形ではなく、自分自身で、データストア(Firestore)・バッチ処理(Cloud Functions)・認証(Authentication)まで構築することに成功しました。しかもこれらの実装記述はJSだけで可能です。昨今JSがどんどん汎用性を持っていくことに驚きと楽しさを感じます。

データベースにはCloud Firestoreを使用しています。NoSQL系であり、情報を正規化せずに保存することが多いため、慣れが必要かもしれません。しかしながら上手くデータモデリングすると、「ReadキャッシュがJSONで保存されている」ような環境が実現できます。

CRUDは専用のAPIを通して行います。単にRESTのような使い方もできますが、リアルタイムデータベースですので、データ更新のリッスンができ、更新内容のプッシュを受けることができます。リッスンはエンティティだけではなくクエリで行うことも可能で、たとえばクエリで画面全体のデータを表現できていれば、結果的にオートリフレッシュのような機能を高速に実装することも可能になります。

db.collection("cities").where("state", "==", "CA")
  .onSnapshot(function(querySnapshot) {
    var cities = [];
    querySnapshot.forEach(function(doc) {
      cities.push(doc.data().name);
    });
    console.log("Current cities in CA: ", cities.join(", "));
  });

プッシュ通知のバックエンドは、Cloud Functionsを利用します。Firestoreの更新トリガと連携させることができ、そこからExpoサーバと接続してプッシュする形式です。下記が簡易的な例になります。詳細は弊社大川の記事をぜひご覧ください!

exports.sendNotification = functions.firestore
  .document('users/{userId}/messages/{messageId}')
  .onCreate(async () => {
    const { params: { userId, messageId } = {} } = context || {};

    const userRef = db.collection('users').doc(userId);
    const userSnapshot = await userRef.get().catch(() =>({}));
    const user = userSnapshot.data();
    const { deviceToken } = user || {};

    const message = {
      to: deviceToken,
      sound: 'default',
      body: '新しいメッセージを受信しました!',
      data: { type: 'messageCreated', userId, messageId },
    };

    const chunks = expo.chunkPushNotifications([message]);
    const tickets = [];

    chunks.forEach(async chunk => {
      const ticketChunk = await expo
        .sendPushNotificationsAsync(chunk)
        .catch(error => console.error(error));
      tickets.push(...ticketChunk);
    });

    return tickets;
  });

アプリ用の管理画面(Web)も作成したのですが、CRUDのロジックはアプリと全く同じ知識が使用できたため、アプリと同じようにFirestoreを叩くだけで実装できました。このためかなりの処理が共通化できています。

管理画面のデプロイについては、Hostingが提供するCLIのコマンド一つで完了できます。実装以外にかかるはずだった手間がほぼゼロになり、そのぶん開発に集中できる環境を得ることができました。非常に大きなメリットです。

$ firebase deploy

このようにFirebaseには、フロントと同じJS記述でバックエンド開発が可能になる環境が整っていますので、フロントエンドエンジニアとしてはガンガン採用したいサービスです。ちなみにほかの選択肢としてAWS Amplifyなどもあります。こちらも試してみたいですね。

結果

開発の結果ですが、社内のドッグフーディング版をリリースするまでに、素振り期間を加えても2ヶ月程度で完成させることができました。

チーム内でアプリの開発実績がない中、このスピード感でリリースすることができたことについて、プロダクトオーナー側からは、品質・速度感ともに高い評価を受けました!

まとめ

Webエンジニアがスマホアプリをリリースするまでに学んだことについて、前編・後編として共有させていただきました。

全編を通してまず感じたのは、JSスキルのカバレッジの高さです。ブラウザの世界を超え、バックエンドからアプリ開発まで、非常に潰しの効くスキルになってきたなと感じます。

もうひとつは、Web開発スキルのポータビリティが向上したことです。RNによって環境間におけるスキルセットの垣根が低くなったことは、とても喜ばしいことだと感じています。

これらによりエンジニアは、適応すべきコンテキストを減らせることになりますから、より本質的価値の開発に素早く注力でき、ビジネス側の期待にも応えやすくなることでしょう。

もちろんRN万能!というわけではないと思います。速さを求めるアプリや、個別のカスタマイズが多いアプリ、デバイスのエッジな機能を使うようなアプリなどでは、RNよりもネイティブで開発したほうが良い場面も出てくると思います。

要は使い所の問題なだけで、今回のように仮説検証のような速度感を求められるフェーズにおいては、最適解のひとつだなと感じました。

個人的には、スマホアプリの開発ができるようになったことで、Webエンジニアの域を超えて、表現の幅を大きく増やせたことに満足感を得ています。自分の提供可能なスキルセットに「スマホアプリ」を加えられたことは、より多くの環境で自分がコミットできることになるため、さらに多くの面白い案件に関われることになりそうで楽しみです。

上記のように、弊社ではWebアプリ、スマホアプリ、ほか様々な環境でチャレンジできる案件が揃いつつあります。また今回のように技術選択もエンジニアの裁量で行うことがほとんどです。そんな現場にご興味を持っていただけたなら、ぜひこちらからお気軽にオフィス訪問などお申込みください!

www.wantedly.com