Gaudiy Tech Blog

Gaudiyの技術、開発組織、カルチャーについてお伝えするブログです

Gaudiyのプロダクト戦略とGraphQLの活用

f:id:gaudiy:20211020114906p:plain

こんにちは!Gaudiyのエンジニアの永井(@sho0910K)です。

Gaudiyでは昨年からGraphQLを導入し、スキーマ駆動開発に取り組んできました。導入からまもなく1年を迎えようとしていますが、良かったところや、改めて浮き彫りになった課題や気づきがあったので、今回はGraphQLの取り組みについてまとめてみたいと思います。

GraphQLの導入を検討しているチームや、実際に活用しているチームのご参考になれば嬉しいです。

1. GraphQL導入の背景

導入の背景については「技術スタックと選定における思想」というブログでも簡単に触れましたが、もともとGaudiyでは、フロントエンドとバックエンドでAPIインターフェイスの認識齟齬が発生することから手戻りが起こり、開発効率を下げているという課題がありました。

またフロントエンドでは、事業ドメインの複雑化によって展開するコミュニティごとに提供する機能や扱うデータが異なったり、扱うデータの増加によってアプリケーションとして管理するデータが複雑化したり、といった状況でした。

具体的には、GraphQL導入以前は、状態管理をRedux、データベースとしてFirestoreを利用していましたが、バックエンドのマイクロサービス群と掛け合わせたクライアントサイドジョインも多くなり、クライアントからのリクエスト数の増加や扱うグローバルステートが増えたことで、コードが複雑化して可読性も低くなっていました。

f:id:strawberry4062:20211019015250p:plain

結果として、状態管理やコンポーネントの繋がりが見えにくくなり、意図しないところで再レンダリングが発生するなど、プロダクトのユーザー体験として思わしくない課題を抱えていました。

2. GraphQLをプロダクトでどのように活用しているのか

Gaudiyが提供しているコミュニティアプリは、コミュニティごとに提供する機能や扱うデータに差があります。

この理由としては、Gaudiyが提供するコミュニティの主軸はIPコンテンツになりますが、IPごとにファンのユーザー層も様々です。そのため、提供する機能の出し方に関しても、そのファン層が利用しやすい形態として提供するなど、IPごとの考慮も必要になってきます

この差分をBFF(backend for frontend)であるGraphQLサーバーを使って、できるだけ複雑性を回避したフロントエンドで扱いやすいデータとして返却しています。

f:id:strawberry4062:20211019021320p:plain

例えば、コミュニティ内でNFTアイテムを提供するサービスに関しても、コミュニティ内のストアを通して手に入れられるものや、イベントに参加することで手に入れられるものなど、獲得方法はコミュニティによって様々です。この差異をBFFであるGraphQLサーバーにてマージ処理を挟むことで、できるだけフロントエンドでロジックを挟まずにデータ展開できるようにしています。

Gaudiyのバックエンドは、各コミュニティで必要な機能をマイクロサービスとして構成しており、各サービスのレスポンスをコミュニティのユーザーデータと組み合わせて返却しています。機能単位にマイクロサービス化している意図としては、作成時は特定のコミュニティに対して特化した機能だったとしても、ユーザー体験の改善や機能拡張をしながら他のコミュニティでも展開できるようにするためです。こうすることで、サービス単体として継続的な機能開発やデプロイを実行しやすくしています。

これらのマイクロサービスで構成された機能群を、BFFであるGraphQLサーバーを通すことで複雑性を回避し、フロントエンドではできるだけユーザー体験の最適化にフォーカスできるようにしています。

3. GraphQLを実践する上での工夫

では次に、実際にGraphQLを実践する上での工夫についていくつかご紹介します。

3-1. クライアントもサーバーもApolloを利用

まずGraphQLをどのように導入しているかというと、クライアントとサーバー共にApolloを採用しており、Apollo Serverは、BFF(backend for frontend)として活用しています

Apollo採用の前には、クライアントはgraphql-requestを使い、バックエンドはGo言語でGraphQLサーバーを立てていました。しかし、Gaudiyでは創業時からフロントエンドにNext.jsを採用しており、TypeScriptを書けるエンジニアが大半を占めていたため、このGraphQL構成だとGo言語に慣れていないために実装スピードが遅くなってしまう、という懸念がありました。

これを解消するために、フロントエンドのエンジニアでもスムーズに開発できるTypeScriptを使える構成として、Apollo Client、Apollo Serverを利用しています。Apolloに関しては、Apollo Studioなどのエコシステムが揃っていることもあり、BFF含めてフロントエンドのエンジニアの開発範囲として扱えています。

3-2. Apollo Datasourceを使ったFirestore設計とテスト

BFFからFirestoreを参照するにあたっては、DataSourceを使って利用できるようにしています。これによって利用する際には、Firestoreのコレクションパスを指定するだけで利用できるようにしています。

DataSourceとして渡しているため、BFFのIntegrationテスト時には、テスト用のFirestoreインスタンスに差し替えて実行できるなど、テストもしやすくなっています。

一部分となりますが、参考にソースコードは以下のような形で利用しています。

  • FirestoreをDatasourceとして作成
export class FirestoreDatasource extends DataSource {
  public db: firestore.Firestore;
  private _tenantId: string | undefined;


  constructor(firestore: firestore.Firestore) {
    super();
    this.db = firestore;
  }

  // apollo server のcontextを受け取る
  initialize(config: DataSourceConfig<Context>): void {
    this._tenantId = config.context.tenantId;
  }

  private collection(collectionPath: string) {
    return this.db.collection(withMultiTenancy(this._tenantId, collectionPath));
  }

  public get = async <T>(collectionPath: string, docId: string) => {
    const snap = await this.collection(collectionPath).doc(docId).get();
    if (!snap.exists) return undefined;
    return getDocData<T>(snap);
  };

  public set = async <T>(collectionPath: string, docId: string, data: T) => {
    await this.collection(collectionPath).doc(docId).set(data);
  };
};
  • QueryResolverでの利用するときは、Context経由で利用
const Query: QueryResolvers = {

  async user(_, { userId }, { dataSources }) {

    return withApolloError(async () => {

      const user = await dataSources.firestore.get<User>(userColPath(), userId);

      if (!user) throw new Error(`no user userId: ${userId}`);

      return userToGQL(user);

    });

  },

};

3-3. ドメイン駆動によるスキーマ設計

Gaudiyでは、ユーザーストーリーマップや画面のプロトタイプが見えてきたら、実装に着手する際に、今回はどのようなデータが必要かをDDD(ドメイン駆動設計)を取り入れて設計しています。

具体的には、デザイナーやドメインエキスパートを含めてDDDを実施し、ドメインモデルを設計したタイミングでスキーマ設計を行っています。

この開発フローにすることで、モデリングに必要なデータとUI視点で必要なデータを分けて考慮することができ、認識齟齬をなくす効果があります。またスキーマ設計時にUI視点で必要なデータについて議論することで、ドメインモデルにおける考慮漏れを見つけ出せるといった副次的な効果も得られています。

khalilstemmler.com

3-4. GraphQLのナレッジをチームで貯める

GraphQLは、実践するにあたって学ぶべきものが多いアーキテクチャだと思っています。

その導入に伴い、開発効率をあげられそうな機能を学習しつつ、それをチームメンバーが利用できるように、サンプル実装を加えたドキュメントを用意したり、勉強会を開いたりしました。

f:id:strawberry4062:20211019023040p:plain

また、GraphQLの知見をメンバーに布教するにあたっては、以下の技術書や資料がとてもわかりやすくて、今も活用させていただいています!

booth.pm

speakerdeck.com

zenn.dev

speakerdeck.com

4. 今後の課題

4-1. レガシーコード改善と並行したGraphQLを使ったアーキテクチャの浸透

冒頭でも述べましたが、もともとGaudiyのフロントエンドのアーキテクチャとしてはRedux + Firestoreを採用しており、今もレガシーコードとして残っています。

早々にレガシーコードをGraphQLを使った形に改善していきたい気持ちはありますが、置き換え自体がユーザーに対するアウトカムには紐付かないと考えているため、事業を進める開発に関しては率先してGraphQLを採用しつつ、その合間を見てレガシーコードのGraphQL化を進めています。そのため、レガシーコードが使われている機能に関してはユーザー体験が悪いものも存在してしまっています。

4-2. マイクロサービスとの接続・運用を簡単にしたい

Gaudiyではコミュニティアプリ以外にもGraphQLを使っているサービスが存在しています。 中には似たような処理を実装するコードやスキーマ定義がそれぞれで管理されてしまっていたり、部分的に二重管理になってしまっているという課題があります。

また、BFFとしてGraphQLサーバーを立てているため、マイクロサービスとの接続は基本的にはバックエンドAPIをラップするためだけに行っている場合もあり、記述するコードも少なからず存在するため、積もり積もればもったいないとも感じています。

解決策としては、Apollo FederationGraphQL Meshなどがあげられると思いますが、プロダクトの今後の戦略やリソースと相談しつつ、最適な形で導入していきたいと考えています。

5. さいごに

本記事では、弊社のGraphQLに関する取り組みをご紹介しました。 レガシーコードのGraphQL化や複数GraphQLサービスの運用方式など課題もありますが、フロントエンドとバックエンドでコラボレーションをしながら、コミュニティでファンの方に最高の体験を届けるような開発を進める上で、Gaudiyでは欠かせない技術になってきています。

GaudiyではがっつりGraphQLを取り入れて運用しているので、もしこの記事を読んで、少しでもGraphQLの実運用の話を聞いてみたい!Gaudiyのコミュニティ開発をもっと聞きたい!などご興味を持っていただけたら、ぜひお話しましょう!

meety.net

Gaudiyの技術選定については、こちらの記事をご参考ください。

techblog.gaudiy.com

オープン勉強会も毎週水曜に開催しています。気軽に遊びにいらしてください!

www.notion.so