🚀

SHEとGraphQL:プランニングからリリースまで

2023/03/08に公開

初めまして、CTOの村下瑛 (@akirakiron)です ✨

この投稿はSHEの最初のテックブログ記事です。僕は発信に対して非常に後ろ向きな人間なのですが、重い腰を上げて筆を取りました。

僕は、SHEをテックカンパニーにしたいと考えています。エンジニアリングが事業成長と分かち難く結びつき、エンジニアが事業価値の源泉を担う — そんな組織を作りたいのですが、残念なことに現実はまだまだ厳しい状態です。技術負債が山積し、事業構想についても勝ち筋が見えておりません。この難局を打破するためには、事業における技術のあり方を一緒に考えていただける方を、今我々は切実に求めています。

そんなわけで、このブログでは将来のチームメイトであるエンジニアの方々に向けて、我々が日々直面している意思決定をありのままに発信していきたいと思っています。ありのままの現状を公開するのはやや恥ずかしいですが、我々が求めているのは不完全さを受け入れて『より良い状態』を一緒に作ってくれる方なので、あえて発信していくことにします。もし、『私ならなんとかできるかも』と思った方がいらっしゃいましたら、ぜひ一度お会いしましょう。  末尾のリンクから是非是非お声がけいただけると嬉しいです✨

GraphQLについて

さて、最初のテーマとして、 『GraphQL』 を選びました。SHEでは2年前に試験導入をして以来、全社的にGraphQLを導入しています。我々にとって、GraphQLは単なるプロトコルを超え、我々の目指す開発のあり方を体現する思想的なバックボーンにもなっています。我々の意思決定を紹介するという観点では、最初に紹介するのにこれ以上ない題材だと思ったのです。

まず、SHEとしてプロトコルに求める思想について述べた後、実際の開発現場での活用方法について紹介したいと思います。思ったより長くなってしまいましたが、是非お付き合いいただけると嬉しいです!

選定における思想

それではまず、選定において大事にしている思想について語っていきましょう。

リーンさ — 無駄をなくしていく

リーンな開発は、SHEで最も大事にしている考え方の一つです。リーンというのは、『無駄がない』ということです。なんだか息苦しいように感じられる方も多いかもしれませんが、無駄をなくすことは創造性の源であると僕は思っています。それは、エンジニアやデザイナーの努力を100%価値に変えようと努力することだからです。

無駄という観点で日々の開発を眺めてみると、開発の現場にはいろいろな無駄があることに気づきます。実際のところユーザの価値になっていない活動が、結構あるのです。

例えば、開発におけるコミュニケーションの中には、価値に結びついていないものもあります。仕様が明確に定義されていれば回避できた仕様確認やコードリーディングの時間、考慮できてなかったエッジケースに対応するための急遽招集される会議、などです。また、コーディングにおいても多くの無駄が発生し得ます。クライアントコード生成や、フロントエンド側の型定義など、多くのボイラープレートの時間。API実装を待つ時間、UI側の仕様変更のために費やされるAPIの実装変更のコスト、などです。

これらの無駄をなくしていくことが、生産性の向上、最速で価値を届けることにつながっていきます。そして、プロトコルには無駄をなくし、生産性を底上げする力があるのです。

では、どういった基準でプロトコルを選定すれば恩恵を受けられるのでしょうか?我々が大事にしている思想をいくつかご紹介します。

思想①:インターフェースに仕様を語らせる

一つ目の思想は、プロトコルはインターフェースとして多くの情報を簡潔に伝えるべきだと言うことです。UIの実装中にAPIの実装を読みたいと思う人は少ないはずですし、APIの実装中にUIの挙動を詳細に確認したい人も多くはありません。実装者は仕様について必要十分な情報を最速で得たいだけなのです。インターフェースがパワフルであれば、このような仕様確認の無駄を大部分削減できます。

GraphQLを含む、スキーマ駆動のプロトコルの魅力は、それが一つの《言語》として設計されていることです。例えば、下記のAPI定義を見てください。

mutation {
  submitEntry(input: SubmitEntryInput!): SubmitEntryResult!
}

union SubmitEntryResult = InvalidRequest | AlreadyApplied | SubmitEntrySuccess

コードや説明がなくとも、それが応募のAPIであること、応募の重複は許さないことなどが理解できますね。インターフェースとは、第一に言語として記述されるべきなのです。

思想②:クライアントとサーバーを疎結合にする

二つ目は、プロトコルがクライアントとサーバを疎結合にするべきだと言うことです。

疎結合というのは、UIを修正するのにAPI側を気にしなくて良いということです。サーバとクライアントは物理的に別れているのだから境界は自明だと思うかもしれませんが、実際のところ、API・クライアントが密結合になっている例はいくつもあるのです。

例えば、予約の一覧・詳細APIがあるとします。今までは詳細ページにのみ表示していた情報を、一覧ページに追加したいとします。もし予約一覧APIを修正しなければならないとしたら、サーバ・クライアントは密に結合していることになります。逆に、一覧画面で表示していない情報を取得していて気付いてない場合、それもまた一つの不必要な依存になります。

GraphQLの魅力は、クライアントとサーバーの関心が分離可能なことです。GraphQLでは、取得可能なデータ全体を一つの大きなグラフとして表し、何を取得するべきかをクライアント自身が決定することができます。そのため、サーバは常に提供可能な情報を提供し、常にクライアントが必要十分な情報を取得することで疎結合を実現できます。

例えば、先に挙げた予約の一覧・詳細APIをGraphQLで書いてみましょう。サーバ側ではGraphQLのスキーマには、ドメインが保有する全情報を解放することができます。つまり、予約一覧に対する問い合わせというユースケースレベルの仕様にフォーカスでき、何がどこで表示されるかについてはサーバ側は検討する必要がないのです。

query {
  reservations: [Reservation!]!
  reservation(id: ID!): Reservation!
}

type Reservation {
  location: Location!
  startsAt: DateTime!
}

type Location {
  name: String!
	zipcode: String!
  address: String!
}

一方で、フロント側はフロント側で、独立に開発を行うことができます。例えば、予約一覧画面は、独自に予約一覧に何が表示されるかを自分で定義することができます。アンダーフェッチング・オーバーフェッチングについて、ここでは考慮する必要がないのです。

gql`
query GetReservations {
  reservations {
    startsAt
    location { 
      name # ここで何を取得するかは、ReservationListコンポーネントの責務で決定できる。
    }
  }
}
`;

const ReservationList = () => {
  const [data, ] = useGetReservationsQuery();
  // ...
}

思想③:APIをマージできる

3つ目は、APIをマージできることです。

頻繁なデータ統合が発生するケースでは、連携用のエンドポイントを逐一書いていくことが開発のボトルネックとなる場合があります。SHEでは『学ぶ』と『働く』が循環するキャリアプラットフォームとして、複数のサービスのデータをマージして表示したいケースが多々あります。例えば会員さんのポートフォリオでは、学習状況だけでなく、お仕事の情報、コミュニティ貢献など様々な情報を一元化したくなります。プロトコルレベルでこのようなデータ統合をサポートできれば、大きく開発を加速することができます。

GraphQLの魅力はAPI全体を一つの大きなグラフ構造として表すことです。本質的に組み合わせが容易なように設計されており、Schema Merging等の新しいテクノロジーの登場によって、Graph QLでのデータ連携を加速させています。

例えば、ユーザと投稿一覧を別々のサービスが管理していたとして、投稿一覧をユーザ情報とともに取得したいとしましょう。Schema Mergingを使えば、スキーマに下記のようなディレクティブを添えるだけで、このような統合を実現できます。

Users schema

type User {
  id: ID!
  username: String!
  email: String!
}
 
type Query {
  users(ids: [ID!]!): [User]! @merge(keyField: "id") @canonical
}

Posts schema

type Post {
  id: ID!
  message: String!
  author: User
}
 
type User {
  id: ID!
  posts: [Post]
}
 
type Query {
  post(id: ID!): Post
  users(ids: [ID!]!): [User]! @merge(keyField: "id")
}

簡単ですね・・

思想④:静的型付けができる。

最後は静的型付ができるということです。

これは同意いただける方も多いのではないでしょうか。APIのインターフェースを、静的に型として表せるということは、静的解析を通してクライアントコードを初め多くのものを自動で生成できるということです。

実際にGraphQLでは、graphql-codegenのコード生成のエコシステムが充実しており、クライアントコードやAPIの実装雛形など、多くのものを自動生成することができます。実装の誤りを、ユニットテストや簡単なタイプチェックによって検出できることも魅力です。

このように、静的型付と、リーンは非常に相性が良く、プロトコルに型システムが内蔵されていることをSHEでは非常に重要視しています。GraphQLはOpenAPIと比べて簡潔に型を表現でき、自動化のエコシステムも充実しているので、SHEでは重宝しています。

SHEにおけるGraphQL活用

さて、ここからは実際にSHEでどのようにGraphQLを活用しているかを紹介していきます。

リリース・プランニング

SHEではリリース・プランニングに時間をかけます。1.5ヶ月に2w程度は時間をとるようにしており、アウトプットには、

  • 実装順にグループ分けされた実装機能(ストーリー)のリスト
  • ドメインモデリングとワイヤーフレーム作成
  • 大まかなリリーススケジュール
  • GraphQLのスキーマ定義

までを含みます。そう、我々はGraphQLのスキーマまでをプランニングの中で決定するのです。

これではまるでウォーターフォールじゃないかと、懸念を感じる方もいらっしゃるでしょう。表層だけ捉えれば、指摘はもっともだと思います。しかし、詳細なプランニングはアジャイルの思想と矛盾せず、相互に補完し合うものだと考えています。アジャイルの思想の教えとは、できるだけ早期に不確実性を削減せよということだからです。

不確実性を正しく評価できていないが故に、大きな出戻りを強いられることが、現実には多々あります。想定と違うAPI、DBに対応項目がないUI項目、些細だと思ったが重大な軌道修正が必要なエッジケース、、これらは全てプランニングをきちんと行うことで、ある程度予防することができます。

想像してみてください。主要なドメインモデルをチーム全員が共有していて、その定義がGraphQLのスキーマとして齟齬なくドキュメントされている状態を。簡潔にハッピーケースとエッジケースがまとめられており、必要に応じてワイヤーフレームに戻ってユースケースを確認ができる、そのようなチームで認識の不一致による出戻りが発生するでしょうか?PdMとエンジニアで仕様やデータモデルについて認識がずれて大幅な軌道修正が必要になったり、APIの挙動が噛み合わないことがあるでしょうか。おそらくないでしょう。

このように、チームの認識を深く(コマンドやデータモデルのレベルまで)合わせることによって、その後の開発を非常にスムーズに進めることができます。また、このような深い認識を持っていれば、**変化に対応しやすくなります。**工数が足りなくなった時でも、データモデルについての認識を合わせていれば、チームは同じデータフローを再現するもっと簡潔なUIについて議論できるのです。

実装

実装時にもGraphQLの恩恵を活かして開発を行っています。

プランニングでスキーマが決定されていることで、スキーマからフロント・バックエンドを独立に開発を進めることができます。 特にフロントエンドでは、Storybookとモックデータの作成を通して、バックエンド実装を待たずに開発をガシガシ進めることができるのが大きいです。

例えば、会員の検索結果ページの挙動を再現したい場合、以下のように書くことができます。

// Factoryを使ってモックデータを作成する。
// モックデータはインテグレーションテストでも利用するのでエクスポートしておく(後述)
export const userProfiles = UserProfileFactory.buildList(100);
const meta: ComponentMeta<typeof Page> = {
  component: Page,
  render: (args, context) => (
    <ApolloProvider client={context.parameters.client}>
      <Page />
    </ApolloProvider>
  ),
  parameters: {
    client: mockClient({
      resolvers: {
        Query: {
          searchResultProfiles: paginationMock(() => userProfiles), // 対応するクエリーにモックレスポンスを返す
          searchCount: () => ({ count: userProfiles.length }),
        },
      },
    }),
    // ...
  },
};

DesignOps

また、SHEではいわゆるDesignOps、デザイナーと開発を跨いだオペレーションの効率化にも力を入れています。特に力を入れているのが、Storybook + Chromaticによる、ページ単位でのUXレビューです。ワイヤーフレームだけでは、なかなか細かいUXについての認識合わせは行いづらいですよね。かといって、検証環境への反映を待っては時間がかかってしまう。この問題を、SHEでは開発中のページのUI・UXをChromatic上でデザイナーがレビューできる仕組みにより解決しています。

ステップとしては以下の通りです。

  • エンジニアはGraphQLをモックして、ページの必要な挙動をStorybookに追加する
  • 開発中のバージョンに対して、専用のStorybookが作成し、ホスティングされる(Chromatic)
  • PR作成時点で、レビュー依頼がデザイナーに飛ぶ(Chromatic)
  • デザイナーはStorybookに直接コメントをする(Chromatic)
  • デザイナーのコメントを確認し、開発者が修正

これは体感としても、UXのレビュー速度に大きく寄与した実感があります。

GraphQLを活用する利点の一つとして、テストにおける静的型の恩恵が利用しやすいことが挙げられます。Fragment Colocationを利用すれば、コンポーネントごとに必要十分な型を低コストで定義することができます。したがって、ページで使われている全クエリをモックするというようなことが、低コストで行えるようになるのです。

品質保証

品質保証は、重要な生産性の指標であると考えています。リーンな開発を追求する我々にとって、リードタイム(コミットから本番反映までにかかる時間)を一つのベンチマークとし、これを24時間以内にしたいと考えています。その中で中心的な割合を占めるのが、品質保証のコストなのです。

ここで言及する品質保証のコストとは、リリース担当者が「この変更は正しく動いているし何も壊していない」と判断するまでにかかる時間のことを指します。我々は、Kent C. Dodds氏の提唱する、The Testing Trophyを参考に、品質保証のポートフォリオを組み、最小のコストで最大の問題を検知できる基盤を整備しています。

特に、GraphQL導入のメリットとして挙げられるのが、インテグレーション・テストの記述の容易さでしょう。SHEでは開発時に作ったStorybookをインポートして、テストを記述していくことで記述コストを削減してメンテナンスが容易な仕組みを作っています。

例えば、先の検索画面のテストは下記のようになります。

const { userProfiles, ...Stories } = storyExports;
const { Default, NotFound } = composeStories(Stories);

describe("pages/search", () => {
  beforeEach(() => {
    mockRouter({ query: {}, prefetch: async () => {} });
  });

  test("should display search results", async () => {
    render(<Default />);

    await waitFor(() => {
      expect(screen.getByText("検索結果")).toBeInTheDocument();
    });

    const cardLinks = await screen.findAllByTestId(profileCardLinkTestId);

    expect(cardLinks).toHaveLength(PAGE_SIZE);

    for (let i = 0; i < PAGE_SIZE; i++) {
      const link = within(cardLinks[i]);
      expect(link.getByTestId("profile-card-nickname")).toHaveTextContent(userProfiles[i].nickname as string);
      expect(link.getByTestId("profile-card-introduction")).toHaveTextContent(userProfiles[i].introduction as string);
    }
  });

  test("should display message if result is not found", async () => {
    render(<NotFound />);

    await waitFor(() => {
      expect(screen.getByText("検索結果")).toBeInTheDocument();
    });

    expect(
      screen.getByText("検索条件にあてはまるシーメイトさんがいませんでした。別の条件で検索してみてください。")
    ).toBeInTheDocument();
  });
});

最後に

以上、駆け足ではありましたが、SHEがGraphQL導入を決めた考え方、そして、現在の活用方法までを手短に紹介させていただきました。

改めて、プロトコルとは無駄をなくし開発者を支援する強力なツールだと我々は信じています。GraphQLは我々の描く理想に対して、現状ではベストなソリューションを与えてくれており、今後も投資していく予定です。

そして、かっこいいことばかり言いましたが、我々の理想とするリーンな開発組織にはまだまだ程遠く、なんとかしなければいけないことが本当にたくさんあります。例えば、、、

  • Schema Mergingの構想がPoCから半年塩漬けになっており、本番運用ができていない。実際のデータ連携はRESTやgRPCなど、その都度、アドホックに実行されている。
  • レガシーなコードベースではE2Eが品質保証の中心を占めており、ローカルでは永遠に終わらないくらいの時間がかかるため、動作確認が遅くなっている。フロントエンドのインテグレーションテストへの移行を進めているが、品質面での課題も多い。
  • 業界『非』標準の独自認証基盤を使っているため、サービスを跨いだ認証の品質保証が大変かつサービスごとに独自実装されている。

などなど、、

完璧でないことを受け入れつつ、一緒に良い開発とは何かを考え続けてくださる方を、今切実に我々は求めています。もし、このブログを読んで興味を持ってくださった方、ぜひ一度お話ししましょう。下記リンクからご応募ください・・・!

開発募集一覧
https://herp.careers/v1/sheinc/requisition-groups/883e8918-022f-4e6b-a6bf-2a01a640cdb6
PdM募集一覧
https://herp.careers/v1/sheinc/requisition-groups/e726ca6e-2cc7-4651-b797-0c42d3b5e367
カルチャーデック
https://sheinc.notion.site/Culture-Deck-for-Engineers-1f7f59b10d1c4638abc739bbae6d2db8
カジュアル面談
https://youtrust.jp/users/akirakiron

SHE Tech Blog

Discussion