これは、CoinbaseのスタッフエンジニアであるErnie Turnerによるゲスト投稿です。CoinbaseはアプリケーションにRelayを徹底的に採用しており、Relayチームの強力な同盟国です。昨年、彼らはRelay VSCode拡張機能の共同開発に貢献しました。Ernieは、この内部エンジニアリングブログ投稿を共有することに同意しました。
サービス中断中の顧客への最適なエクスペリエンスの提供方法
理想的には、Coinbaseのサービスに障害が発生することはなく、GraphQLスキーマのすべてのフィールドは常に正しく解決されます。これは現実的ではないため、Coinbaseアプリケーションはダウンタイムに耐え、顧客への影響を最小限に抑える必要があります。単一のサービスのダウンタイムが、ユーザーによるアプリ全体の使用または操作を妨げるべきではありません。ただし、アプリケーションが期待どおりに動作していない場合に、ユーザーに問題を伝えることも重要です。リトライボタン付きでダウンタイムを伝えるエラーメッセージを表示することは、ユーザーを欠落したコンテンツや操作できないUIで混乱させるよりも良いエクスペリエンスです。
この投稿では、Relayアプリケーションにおける欠落データの処理に関する一般的なパターンとベストプラクティスについて説明します。
画面アーキテクチャとエラーバウンダリ
GraphQLクエリにおけるサービスのダウンタイムと障害の処理について説明する前に、まず広範な画面アーキテクチャと、React Error Boundariesを正しく使用することでより良いユーザーエクスペリエンスの作成にどのように役立つのかについて説明します。
人生のほとんどのことと同様に、Error Boundariesは適度に使用する必要があります。Coinbase Retailアプリの一般的な画面を見てみましょう。
上記の画面のどのセクションでも、レンダリングに必要なデータの取得に失敗する可能性がありますが、これらの障害への対処方法が、ユーザーがアプリで体験するエクスペリエンスを決定します。たとえば、単一の画面レベルのErrorBoundaryのみを使用すると、エラーの重大度にかかわらず、エラーが発生するとアプリが使用できなくなります。対照的に、各コンポーネントを独自のErrorBoundaryでラップすると、同様に悪いエクスペリエンスにつながる可能性があります。最後に、エラーのあるコンポーネントを完全に省略することは、他の2つのオプションと同じくらい悪いです。万能なアプローチはありません。そこで、それぞれを分解し、なぜそれらが悪いユーザーエクスペリエンスにつながるのかを説明します。
フルスクリーンエラー
上記のUIは、サービスに障害が発生し、この画面のコンポーネントをレンダリングするために必要なデータを取得できなかった場合に表示されるCoinbaseのフルスクリーンエラーフォールバックです。特定の状況では、これは実際に良いユーザーエクスペリエンスを生み出します。ユーザーに何が起こったのかの詳細な情報を提供できない場合がありますが、ほとんどの場合、技術的な原因を提供することは不可能であり、ユーザーのエクスペリエンスを改善するわけでもありません。しかし、何かが正しく動作していないことを伝え、アプリを再び動作させるための明確なリトライボタンを提供しています。
これがユーザーに表示される理由は、資産価格履歴グラフやウォッチリストの状態など、重要なものではないものをロードできないためである場合、画面全体を停止するべきではありません。ビットコインの現在の価格を非表示にし、ビットコインがウォッチリストにあるかどうかを伝えることができないだけで、ユーザーが取引することを妨げるのは、ネガティブなユーザーエクスペリエンスです。
このUIのもう1つの欠点は、ユーザーからすべてのアプリナビゲーションを隠すことです。フルスクリーンエラーを表示する正当な理由がある場合でも、その過程でアプリの残りの部分を隠す必要はありません。ユーザーは依然として別の画面に移動できるはずです。実際には、「フルアプリエラー」ではなく「フルスクリーンエラー」のみを表示する必要があります。
至るところにエラーメッセージ
上記のUIは、多くの点で、さらに悪いです。これは、前のエクスペリエンスの反対側であり、フルスクリーンエラーを表示する方が望ましいでしょう。価格履歴グラフのエラーメッセージは理にかなっていますが、ユーザーがビットコインの価格を見たり、取引ボタンを見つけたりできない場合は、最初のスクリーンショット(ただしナビゲーション付き)を表示する必要があります。この画面の中心的な目標と目的が失われているためです。
この画像は、ErrorBoundariesが過度に普及している様子も示しています。時間範囲セレクターを含む価格履歴グラフ全体には、時間範囲ごとに1つではなく、単一のエラーメッセージのみが必要です。
空のフォールバック
上記のUIは、前の例と同じくらい悪いです。この場合、ErrorBoundariesは空のコンテンツにフォールバックします。特定のUI要素では、これは理にかなっています。ウォッチリストの横にある共有ボタンが欠落していることは、このUIにとって重要ではないため、省略しても問題ありません。しかし、ビットコインの現在の価格、価格履歴グラフ、取引ボタンを非表示にすると、UIが使用できなくなり、やや誤解を招く可能性があります。毎日アプリを使用しないユーザーでも、何かがおかしいことに気付くでしょう。また、ユーザーに障害を再試行するオプションも提供していません。ユーザーは、回復方法のない空のコンテンツのみが表示されます。
代わりにユーザーに何を表示するべきか
次の2つのスクリーンショットは、ユーザーにとってより良いエクスペリエンスの例を示しています。最初のスクリーンショットは、ビットコインの現在の価格を取得できない場合、またはユーザーが取引を許可されているかどうかを判断できない場合にユーザーに表示する必要があるものです。2番目のスクリーンショットは、ビットコインの価格の現在の変化または価格履歴を取得できなかった場合に、ユーザーにとってより良いエクスペリエンスになります。
これらすべてから、画面上のUIセクションを分類する必要性が示唆されます。ユーザーエクスペリエンスにとって重要なもの、ユーザーが表示することを期待するUI、エクスペリエンスにとってオプションのサポートコンテンツなどです。
クリティカルなUI、期待されるUI、オプションのUI
アプリケーション画面のすべてのUI要素が同じであるわけではありません。UIの一部は画面の中心的な情報やインタラクションを定義し、その他はユーザーにとってより多くの情報やヘルプを提供する可能性があります。Coinbaseのアプリケーション設計では、UI要素を「クリティカル」、「期待される」、「オプション」の3つのカテゴリに分類します。
クリティカルなUI要素
画面の重要な情報やユーザーとのインタラクションを定義する部分です。これらの要素がUIにない場合、画面は意味をなさなくなり、これらの要素が欠落している場合、ユーザーは混乱したり、怒ったりする可能性があります。アプリが期待どおりに動作しなかった理由が明確ではないためです。
これらのクリティカルなUI要素を表示するために必要なデータを読み込めなかったとします。その場合、問題を説明する(可能な場合)フルスクリーンエラーメッセージと、欠落しているデータの再要求を簡単に試みることができるリトライボタンをユーザーに表示する必要があります。
クリティカルなUI要素が欠落しているアプリケーションとユーザーがやり取りすると、混乱や怒り、そして何が起こっているのかを完全に知らなくてもユーザーがトランザクションを完了できる場合、資金の損失さえ引き起こす可能性があります。
クリティカルなUI要素の例
- Coinbaseアプリのホーム画面のユーザーの現在のポートフォリオ残高
- 注文プレビュー画面の資産価格、支払い方法、購入総額
- Earn画面のユーザーの生涯収益と資産ごとの収益
期待されるUI要素
期待されるUI要素は、画面の中心的な目的を果たさない可能性がありますが、ほとんどのユーザーが存在することを期待する画面の一部です。期待されるUI要素が画面にない場合、ユーザーは何かが間違っていると思う可能性がありますが、これにより画面の中心的なアクションを実行できなくなるわけではありません。
期待されるUI要素を表示するために必要なデータを読み込めなかった場合、ユーザーに、欠落しているUI要素があることを伝えるコンポーネントローカルのエラーメッセージを表示する必要があります。これらのエラーメッセージには、ユーザーが欠落しているデータの再要求をできるようにする再試行ボタンも付ける必要があります。ローカライズされたエラーは、ユーザーに見過ごされたり、操作されなかったりする可能性が高くなりますが、画面の主要な目的には必須ではないため、ある程度許容できます。
期待されるUI要素が欠落しているアプリケーションとユーザーが対話できるようにすることは許容できるかもしれませんが、何が起こっているのかについて混乱を招く可能性があります。エラーメッセージを伴わずにこれらのUI要素を完全に省略すると、より悪いエクスペリエンスになります。
期待されるUI要素の例
- 資産購入画面(購入数量を入力する画面)における資産の現在の価格
- 資産詳細画面の価格履歴グラフ
- Coinbase Card画面の最近のトランザクションリスト
オプションのUI要素
オプションのUI要素とは、画面の主要な目的を純粋にサポートする画面の一部です。一部のユーザーはこれらの要素の欠落に気付くかもしれませんが、他のユーザーはそれらが存在するはずであること自体に全く気づかないかもしれません。どちらの場合でも、ユーザーは画面上の主要な目標を達成することが妨げられることはありません。
これらのオプションのUI要素を表示するために必要なデータを読み込めなかった場合、UIから完全にそれらを省略する必要があります。ただし、これには次のリスクが伴います。
A. ユーザーは何も欠落していることを知らない可能性がある B. ユーザーがフルスクリーン更新を行わない限り、このUIのデータの再要求方法がない。
開発者はこれらの欠点を考慮し、ネガティブなユーザーエクスペリエンスを引き起こさないようにする必要があります。代わりに、これらのエラーはログに記録されるべきであり、ユーザーエクスペリエンスが理想的でない場合にプロダクトエンジニアに通知されるようにする必要があります。
オプションのUI要素の例
- 資産詳細画面のオファーカード
- 取引画面の資産カテゴリセクション(Coinbaseの新着情報、トップムーバーなど)
- ホーム画面のニュースフィード
上記の画像に戻り、UIのセクションをこれらのカテゴリに分類しましょう。
要素分類の制限
上記の例では、2つの重要なコンポーネント、2つの期待されるコンポーネント、1つのオプションのコンポーネントを持つ画面があります。アプリのほとんどの画面には、少数の重要なUIコンポーネントしか含まれていません。一部の画面では、UI全体が1つの重要なコンポーネントで構成されている場合があります。
期待される要素についても同様です。5つの個別の期待されるUI要素で構成される画面があると、上記のスナップショットのように、「再試行」ボタンがアプリ全体に散らばることになります。1つの画面上の期待される要素と再試行ボタンの数は、可能であれば1つか2つに制限してください。
プルダウン更新
上記のすべてのシナリオにおいて、モバイルアプリのユーザーは、画面上の失敗したリクエストを再試行するためにプルダウン更新を行うことができます。Relayアプリケーションでは、通常、これはフルスクリーンレベルのクエリを再試行することを意味します。画面にデータの欠落によってエラーメッセージまたは非表示コンポーネントがある場合、プルダウン更新を使用すると、常にこれらのエラー状態を修正しようとします。
この分類は主観的なものであり、上記の例はあくまで一つの意見であり、デザイナーやPMは画面の劣化方法について異なる意見を持つ可能性があります。アプリケーションUIを設計する際には、クロスファンクショナルな連携が重要です。チームは、エンジニア、デザイナー、プロダクトマネージャーに相談して、アプリ全体のシームレスでブランドに合った画面を確保する必要があります。
プロダクトマネージャーおよびデザイナーとの連携
Relayがどのように役立つのか
画面をセクションに分類したら、次のステップとして、適切なErrorBoundariesをアプリに追加し、分類に応じてコンポーネントのGraphQLフラグメントを構成します。Relayはここで役立ちます。Relayアプリでの作業経験に基づいて、GraphQLクエリからのデータの欠落に対処する方法に関するいくつかのベストプラクティスを作成しました。
Coinbaseの目標は、Relayチームが推奨するように、null許容スキーマを使用することです。主な理由は、サービスの中断とクエリデータの欠落に対処する方法に関する決定をクライアントエンジニアの手中に置くことです。null許容スキーマがない場合、欠落データの処理方法に関する決定はサーバー側で行われ(null値を最も近いnull許容親にバブルアップすることで)、クライアントコードはこの決定を変更する手段がありません。
この決定は、Relayの@required
ディレクティブの存在によって支えられています。これにより、クライアントエンジニアは、ランタイム時に欠落データの処理方法をRelayに指示するディレクティブを使用して、クエリとフラグメントに注釈を付けることができます。これにより、エンジニアがそうでなければ記述する必要がある定型コードが削減されます。表面上、このディレクティブは非常にシンプルに見えます。非常に単純な3つのオプションしかありません。しかし、さまざまなユースケースでこのディレクティブを使用しようとすると、どのオプションを選択するか、そもそもディレクティブを使用するかどうかという決定が常に明らかではないことがわかります。
@required
の局所性
@required
ディレクティブの優れた機能の1つは、使用したフラグメントのみに影響を与えることです。同じフィールドをクエリする他のフラグメントの動作を変更することは決してありません。これにより、コンポーネントのスコープ外のものを考慮せずに、ディレクティブを追加または削除できます。これは、同じクエリからデータを取得する場合でも、異なるコンポーネントが異なるカテゴリに分類される可能性があるため重要です。同じクエリのフラグメント内のフィールドに異なる@required
引数を付けることができることは、理想的なユーザーエクスペリエンスを構築するために重要です。
action: LOG
とaction: NONE
の使用
LOG
とNONE
のアクションはどちらもランタイム動作は同じですが、LOG
は選択したロギングメカニズムにメッセージを送信し、nullとして返されたフィールドへの完全なパスをログに記録します。@required
ディレクティブが必要なほとんどのユースケースでは、NONE
よりもLOG
を使用する必要があります。NONE
が優先されるのは、一部のユーザーに対してフィールドがnullになることが予想される場合のみです。
action: LOG
を使用することで作成されたログエントリは、それ自体では操作可能ではない可能性がありますが、将来のエラーのブレッドクラムとして役立つ信号になる可能性があります。エラーの履歴を確認し、特定のフィールドが予期せずnullであったことを確認することで、ユーザーがワークフローで遭遇する可能性のある将来のエラーを追跡するのに役立ちます。
@required(action:LOG/NONE)
を使用する場合
LOG/NONE
アクションは、コンポーネント内のオプションのUIを表示するために必要なフィールドでのみ使用してください。アプリケーションを設計する際に、これが表示される2つの異なるユースケースがあります。
- コンポーネントはオプションのUIであり、フィールドまたはフィールドセットがnullの場合、まったくレンダリングされるべきではありません。
- コンポーネントの一部はオプションのUIであり、オブジェクトタイプのフィールドに依存しており、そのオブジェクトはその子フィールドの1つ以上がないと意味がありません。
これらのユースケースの両方を包含するフラグメントを見てみましょう。
fragment MyFragment on Asset {
id
name @required(action: LOG)
slug @required(action: LOG)
color
supply {
total @required(action: LOG)
circulating @required(action: LOG)
}
}
このフラグメントでは、nameフィールドまたはslugフィールドを取得しない場合、フラグメント全体が無効であると述べています。これらのフィールドがサーバーからnullとして返された場合、このコンポーネントをまったくレンダリングできません。このフラグメントは、@required(action: LOG/NONE)
ディレクティブを使用してオブジェクトタイプのフィールド全体を無効にする方法も示しています。このフラグメントは、supply.total
フィールドまたはsupply.circulating
フィールドのいずれも存在しない場合、supplyオブジェクト全体自体が無効であり、nullである必要があると述べています。このnull可能性は、コンポーネントのUIのオプションの部分を非表示にするために使用されます。
では、コンポーネントがこのクエリの結果をどのように処理するかを見てみましょう。
const asset = useFragment(
graphql`
fragment MyFragment on Asset {
id
name @required(action: LOG)
slug @required(action: LOG)
color
supply {
total @required(action: LOG)
circulating @required(action: LOG)
}
}
`,
assetRef,
);
if (asset === null) {
return null;
}
return (
<>
<Title color={asset.color}>{asset.name}</Title>
<Subtitle>{asset.slug}</Subtitle>
{asset.supply && (
<SupplyStats total={asset.supply.total} circulating={asset.supply.circulating} />
)}
</>
);
@required
ディレクティブは、そうでなければ記述する必要がある複雑なnullチェックを削除するため、ここで非常に役立ちます。asset.name
フィールドとasset.slug
フィールドの両方がnullかどうかを確認する代わりに、フラグメント全体がnull化されたかどうかを確認し、レンダリングを防止するだけで済みます。SupplyStatsコンポーネントをレンダリングするかどうかを確認する場合も同様です。親フィールドがnullかどうかを確認するだけで、2つのサブフィールドがnullではないことがわかります。
@required(action:THROW)
を使用する場合
@required(action: THROW)
の使用はより簡単です。このアクションは、期待されるUIコンポーネントまたは重要なUIコンポーネントをレンダリングするために必要なフィールドで使用してください。これらのフィールドがサーバーからnullとして返された場合、コンポーネントは最も近いErrorBoundaryにエラーをスローし、ユーザーにエラーメッセージが表示されます。
ErrorBoundaryがツリーの上部にどれだけあるかは、エラーが発生した場合にUIのどの部分を削除したいかによって異なります。たとえば、資産価格履歴グラフの代わりにユーザーにエラーを表示する場合、時系列ボタンをそのまま表示しておくのは意味がなく、そのUI全体も消える必要があります。しかし、そのために行全体を削除する必要もありません。
ErrorBoundaryには、ユーザーが失敗したクエリを再試行して、後続の試行でデータを取得できるメカニズムを備えるようにしてください。ユーザーが復旧できるように、エラーメッセージと実行可能な要素を常にペアにする必要があります。画面のリロードにプルツーリフレッシュを使用できる(または知っている)とユーザーが仮定すべきではありません。
配列内のフィールドに対する@required(action: THROW)の使用に関する注記
配列フィールドとその配列のフィールドの両方を選択するコンポーネントでTHROW
アクションを使用することは、ほとんどの場合避けるべきです。やってはいけない例を示します。
function Component({ assetPriceRef }) {
const { quotes } = useFragment(
graphql`
fragment ComponentFragment on AssetPriceData {
quotes {
timestamp
price @required(action: THROW)
}
}
`,
assetPriceRef,
);
}
このコンポーネントは、quotes
配列と、その配列内の各アイテムのtimestamp
およびprice
フィールドの両方を選択します。クォートが返ってこなかった場合にユーザーにエラーを表示したい場合は、quotes
フィールドにTHROW
を置くことは許容されます。しかし、price
フィールドにTHROW
を置くことは、その配列内の単一のpriceフィールドがnullの場合でもユーザーにエラーを表示することになります。それはおそらく私たちが望む動作ではありません。過去1日の24個のクォートのうち23個が正しく返ってきた場合、おそらく持っている結果を表示し、空の値を省略するだけで済みます。
代わりに、action: LOG/NONE
を使用し、すべてのアイテムではなく、配列内の単一のアイテムだけを無効にするようにします。必要に応じて、配列からnull値をフィルタリングすることもできます。
function Component({ assetPriceRef }) {
const { quotes } = useFragment(
graphql`
fragment ComponentFragment on AssetPriceData {
quotes {
timestamp
price @required(action: LOG)
}
}
`,
assetPriceRef,
);
const validQuotes = quotes.filter(removeNull);
}
フィールドに@requiredを使用しない場合
この質問に対する役に立たない答えは「フィールドが必要ない場合は@required
を使用しない」です。その答えは、特にフラグメントに12個以上のフィールドがある場合、何が必須で何が必須でないかの決定を単純化しすぎます。しかし、フィールドを必須としてマークするかどうかを決定するために、いくつかのベストプラクティスに従うことができます。繰り返しますが、これらの決定にはPMとデザイナーと協力することが重要です。
@required
ディレクティブを省略する場合と、LOG/NONE
アクションで使用する場合の微妙な違いもあります。主な違いは、そのフィールドによってレンダリングされるUIがオプションUIの場合、@required
ディレクティブを省略する必要があることです。
アプリケーションのいくつかのコンポーネントは、異なる分類のUIを組み合わせたものをレンダリングできます。たとえば、単一のコンポーネントが、資産の現在の価格と、ある期間にわたってユーザーの何パーセントが資産を買ったり売ったりしたかの両方の表示を担当する場合があります。つまり、コンポーネントはクリティカルUI(資産価格)とオプションUI(売買統計)の両方を混合しています。
フィールドがオプションのコンテンツのレンダリングに使用され、ユーザーに混乱を与えることなくUIから完全に省略できる場合(オプションUIの定義であることを思い出してください)、そのフィールドに@required
ディレクティブを使用しないでください。代わりに、フィールドがnullの場合にUIを省略するようにコードにチェックを追加してください。
function SomeComponent({ queryRef }) {
const { asset } = useFragment(
graphql`
asset {
latestQuote @required(action: THROW)
buyPercent
}`,
queryRef,
);
return (
<div>
<div>Price: {asset.latestQuote}</div>
{asset.buyPercent !== null && (
<>
<div>Buy Percent: {asset.buyPercent}</div>
<div>Sell Percent: {1 - asset.buyPercent}</div>
</>
)}
</div>
);
}
この例では、buyPercent
フィールドに@required(action: LOG/NONE)
を使用することは正しくありません。これは、私たちが望む動作ではない、つまりフラグメント全体が無効になるためです。
@required
ディレクティブを省略する場合の別のあまり一般的ではないユースケースは、安全なフォールバック値を提供できる場合です。フィールドにフォールバック/デフォルト値を提供することは、間違った方法で行うと非常に危険です。デフォルト値にフォールバックする可能性のあるケースはいくつかありますが、一般的には非常にまれであり、避けるべきです。ただし、安全なフォールバック値を提供できる場合は、そのフィールドに@required
を追加する代わりに、フォールバック値を使用する必要があります。
フォールバック値を提供する場合のいくつかのガイドライン
- 数値フィールド(数値または数値を表す文字列)のフォールバック値は使用しないでください。
- 欠損値の代わりに0を使用すると、常にユーザーにとってより多くの混乱が生じます。Coinbaseは金融会社であり、ユーザーに正確な値を表示できない場合は、まったく表示すべきではありません。ユーザーの口座残高が$0.00であることを表示するのは、エラーメッセージを表示するよりも明らかに悪いです。これは明らかなユースケースですが、資産の価格変動率、CoinbaseカードのAPY%、またはCoinbase Earnでユーザーが得られる金額などでも、実際の値がない場合は0を表示すべきではありません。
- ブールフィールドのフォールバック値は注意して使用する必要があります。
- ブールフィールドのフォールバックの最初の選択肢は、通常、フィールドをfalseに設定することです。ブールフィールドが何を表しているかによっては、falseにフォールバックすると、ユーザーにエラーを表示するよりも悪い顧客体験につながる可能性があります。
isEligibleForOffer
のようなフィールドに対してfalseにフォールバックするのはおそらく許容されますが、これはおそらくオプションのコンテンツを表示しているためです。hasCoinbaseOneSubscription
のようなフィールドに対してfalseにフォールバックすることは許容されません。Coinbase Oneの加入者であるユーザーにとって、コンテンツは予想されるものであり、ユーザーはそのUIがアプリに表示されないことに混乱します。
- 配列フィールドの空の配列へのフォールバックは注意して使用する必要があります。
- ユーザーにCoinbaseカードのトランザクションリストを表示する場合、空の配列にフォールバックするのは悪いアイデアですが、ユーザーに最近追加された資産のリストを表示する場合は、空の配列にフォールバックしてUIの表示を省略するのはおそらく問題ありません。コンポーネントはすでに空の配列のケースを処理する必要があるからです。
- 文字列フィールドは通常、nullをそのまま処理する必要があります。
- 場合によっては、nullとして返される文字列フィールドに対して空の文字列にフォールバックしたい場合がありますが、通常、フィールドをnullのままにする場合と同じコードパスが作成されます。スキーマ内のほとんどの文字列フィールドは空であることが想定されていないため、空の文字列にフォールバックすると、ユーザーが実際のコンテンツの代わりに空の文字列が表示されるというネガティブなユーザーエクスペリエンスが発生する可能性があります。
function SomeComponent({ queryRef }) {
const asset = useFragment(
graphql`
fragment MyFragment on Asset {
canTrade @required(action: THROW)
hasOfferToStake
}
`,
assetRef,
);
const showStakeOffer = asset.hasOfferToStake ?? false;
return (
<div>
{asset.canTrade && <Button>Trade</Button>}
{showStakeOffer && <Button>Stake your currency</Button>}
</div>
);
}
サマリー
このドキュメントから何かを学んだとすれば、それはダウンタイムとサービスの中断を処理する方法について多くの検討が必要であるということです。エラー状態の処理は、世界レベルのアプリケーションを構築する上で重要な部分です。新しい機能のスコープを決定する際には、設計チームとPMチームがあなたのチームと同じページにいることを確認してください。データが不足している場合にユーザーに何を表示するかについてアドバイスが得られない場合は、これらの決定についてチームとして合意に達するように働きかけてください。
Relayは、アプリケーションの障害に対処する上で強力なツールとなる可能性があります。障害に対処する方法を決定する際のRelayのきめ細やかな機能は、これまで慣れていたよりも多くの作業を伴う可能性があります。しかし、この追加の努力は長期的に報われ、アプリケーションでの顧客エクスペリエンスの向上に大きく貢献します。