TwitterやSlackのRedux Storeを覗く

TwitterやSlackのRedux Storeを覗く

この記事はリクルートライフスタイル Advent Calendar 2019の20日目の記事です。

HOT PEPPER Beauty cosmeの開発を担当している金井です。

今年は社内システムなどのためにReduxを使ったアプリケーションをいくつか書いたのですが、Storeの設計変更は影響範囲が大きく難しいため、Storeの設計は重要だと感じました。他のサービスではどのように設計しているのか気になり、有名なWebサービスの中からReduxを使っているサービスを探してStoreの中を調べました。この記事では、今回調べたサービスのそれぞれのRedux Storeの特徴や設計について紹介します。

ただし、個人による自由研究であり、各サービスの公式の見解ではないことはご注意ください。

Redux公式が紹介している設計方針

各サービスのRedux Storeを覗く前に、Redux公式が紹介している設計方針について確認します。

Reduxの公式WebページにはRecipesというページがあり、設計の方針のプラクティスがまとまっています。

Recipes: Index · Redux

基本構造

このRecipesには以下のような3つのdataを作るのが基本だと書いてありました。

  • Domain data: アプリケーションが表示したり変更したりするデータ
    • e.g. TODOアプリなら、todo/doing/done
  • App state: アプリケーション独自の振る舞いのためのデータ
    • e.g. データの選択状態やデータフェッチのローディング状態
  • UI state: 現在の表示方法のためのデータ
    • e.g. モーダルが開かれているかどうか

なのでStoreは以下のようなオブジェクトになります。

1
2
3
4
5
6
7
8
9
10
{
    domainData1 : {},
    domainData2 : {},
    appState1 : {},
    appState2 : {},
    ui : {
        uiState1 : {},
        uiState2 : {},
    }
}

ref: Basic Reducer Structure and State Shape · Redux

正規化

Recipesでは正規化についても言及しており、リレーショナルデータを管理する場合はデータベースのように正規化することを推奨していました。

以下は投稿が複数のコメントを持つ例です。postsはcommentsのidだけを持っています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
    posts : {
        byId : {
            "post1" : {
                id : "post1",
                author : "user1",
                body : "......",
                comments : ["comment1", "comment2"]
            },
            "post2" : {
                id : "post2",
                author : "user2",
                body : "......",
                comments : ["comment3", "comment4", "comment5"]
            }
        },
        allIds : ["post1", "post2"]
    },
    comments : {
        byId : {
            "comment1" : {
                id : "comment1",
                author : "user2",
                comment : ".....",
            },
            "comment2" : {
                id : "comment2",
                author : "user3",
                comment: "....."
            }
        },
        allIds: ["comment1", "comment2"]
    }
}

ref: Normalizing State Shape · Redux

残った疑問点

Recipesはさまざまな疑問に答えてくれますが、それでもまだ基本構造の分類方法や分割粒度など、わからないことがありました。

  • Domain data
    • REST APIのレスポンスのJSONをそのまま保持する?
    • 正規化はどうやってやる?
    • アプリのモデルオブジェクトに変換する?
  • App state & UI state
    • ページごとに作る?パーツごとに作る?

そこでいくつかの有名なサービスの中からReduxを使っているものを探してそのStoreの構造を調べてみようと思いました。

Redux Storeの調べ方

調べる対象のアプリケーションがどのReactのバージョンで実装されているかによって調べ方が異なります。方法1でStoreが見れなかった場合、方法2を試してください。

方法1

  1. Chrome DevToolsでComponentsタブを開く
  2. connectしているコンポーネントを選択する
  3. このとき、右側にlegacy contextと書いてない場合、方法2を試してください。
  4. consoleタブを開くと$rに選択したコンポーネントが束縛されているので$r.store.getState()するとstateが見られる

legacy_context

getState2

方法2

  1. Chrome DevToolsでComponentsタブを開く
  2. connectしているコンポーネントを選択する
  3. Log this component data to the consoleをクリックする
  4. consoleタブを開くと[Click to expand] <Connect(t) />と表示されているのでクリックする
  5. Hooksを開いてname: "Context"なオブジェクトを選びvalueを開く
  6. storeの上で右クリックしてStore as global variableを選択する
  7. temp1という変数にStoreが束縛されているのでtemp1.getState() するとstateが見られる

select_connected

store_as_global_variable

getState

ここからは各サービスのRedux Storeを調べた結果を紹介します。今回は3つのサービスを調べました。

  • Slack
  • Twitter
  • Airbnb

それぞれのStoreで特徴的だった構造、全体的な構造、その他の特徴についてまとめました。

SlackのStore

まずSlackを調べます。Domain dataの構造において発見がありました。

Domain dataに中間テーブルのような構造がある

チャンネルに所属するメンバーを表すDomain dataが図のような構造になっていました。

channel_member

チャンネルのプロパティとしてメンバーのIDの配列を持っても良さそうなところですが、membershipOrderedというオブジェクトにチャンネルごとのメンバーのIDの配列を持たせて中間テーブルのように使うことでチャンネルとメンバーとの関連を表現しています。データモデルとしても、チャンネルとメンバーの関係は中間テーブルを使うのが一般的な設計です。

Recipesの通り、リレーショナルモデルを管理する場合はDomain dataをデータベースと同じように取り扱うというのを実践しているのかなと思いました。

また、今回の例では、RDBのようにDomain dataを構造化したことで、再レンダリングに不要な変更をComponentがlistenしなくなっています。Slackの特性上、チャンネルのdataを扱うコンポーネントは多いと考えられるため、メンバーのjoin/leftの度に再レンダリングされるのはコストが高くなります。このような場合はshouldComponentUpdateを実装して再レンダリングを防げますが、再レンダリングの対象にならないようDomain dataを構造化するという方法もあると分かりました。

全体的な構造

stateは以下のように全てのデータがrootにフラットに置かれていました。

slack_state

その他

  • 全チャンネルと全ユーザーの情報を最初にロードする
  • メッセージは遅延ロード

TwitterのStore

続いてTwitterを調べます。Twitterというサービスの特性をうまく反映したDomain dataとApp stateだと思いました。

Domain dataそれぞれがデータフェッチのステータスを持っている

Recipesによると、データフェッチのステータスはApp stateであるとされていましたが、Twitterは各Domain dataもデータフェッチのステータスを持っていました。Domain dataを正規化しているので、データフェッチの状態はApp stateが持つよりもDomain dataが持つほうが整合性を保ちやすそうです。

正規化にNormalizrを使っていそう

2017年の記事ですが、Twitterの公式ブログでモバイル版のTwitterの技術的な紹介をしています。この中でNormalizrというライブラリを使っているという記述がありました。

ref: How we built Twitter Lite

ref: GitHub - paularmstrong/normalizr: Normalizes nested JSON according to a schema

Normalizrを使う場合はRedux storeに entities というキー以下に正規化したデータをいれるという慣習があります。現行のTwitterがNormalizrを使っているという確信はありませんが、entities以下に正規化されたデータが入っており、Normalizrにより正規化した場合に構築される構造になっているため、いまもNormalizrを使っていると推測します。

全体的な構造

Domain dataは entities キーの配下にまとめられています。他の2種類のデータはrootに置かれていました。

twitter_state

その他

  • タイムラインはDomain dataではなくApp state
    • タイムラインはフォローやブロックをすることで、同じ時間帯のタイムラインでも過去に表示されたものと同じものとはならないという特性がある
    • セッションごとに生成されるその時々のデータになるのでApp stateとして設計されているのかなと思いました
  • Domain dataはキャメルケースになっていない
    • JavaScriptの命名規則にのっとるとキャメルケースにするべきだが、おそらくNormalizrでAPIのレスポンスをそのままStoreに入れているためスネークケースになっている
    • denormalizeをしたオブジェクトもスネークケースになるが、そのままComponentに渡していた

AirbnbのStore

最後にAirbnbを調べます。これまで見てきたサービスはそのサービス特有の特性に特化したアプリケーションでしたが、Airbnbは特集ページがあったり宿の一覧ページがあったり予約をするフォームがあったりと、一般的なWebサービスでありがちな機能が実装されています。

ページごとにstateを分けている

ページごとにstateを分けていました。

  • experiencePdp
    • 体験を重視する宿用のページ
  • luxuryPdp
    • 高級だったりおしゃれだったりを重視する宿用のページ
  • userProfile
    • ユーザーのプロフィールを編集するページ

それぞれのページごとに設計も異なりました。例えばluxuryPdpは entities の下にDomain dataが格納されていますが、userProfileは api のしたにDomain dataがあります。各Domain dataの正規化のされかたも異なるようでした。

おそらく、各ページで開発しているチームが異なりそうです。いわゆるマイクロフロントエンド形式です。数百人のエンジニアで同じサービスを開発しているので、ページごとに独立してstateが影響を及ぼし合わないよう工夫しているのだと思いました。

全体的な構造

ページごとに分割されています。stateはそれぞれのページのやり方で配置されています。

airbnb_state

その他

  • headerとかfooterの単位でもstateが分けられている
    • 共通して使うパーツはstateも共通化していそうです

まとめ

各サービスのRedux Storeを眺めました。Recipesに紹介されているプラクティスと比較していくことで、それぞれのアプリケーションの特徴を見られました。

最初に挙げた疑問点は以下のように解消されました。

Domain data

  • REST APIのレスポンスのJSONをそのまま保持する?
    • => 正規化する
  • 正規化はどうやってやる?
    • => Normalizrを使う
  • アプリのモデルオブジェクトに変換する?
    • => 独自モデルは作らずNormalizrでdenormalizeしたオブジェクトを使う

App state & UI state

  • ページごとに作る?パーツごとに作る?
    • => Airbnbのようなページごとで大きく特性が変わる場合はページごとに分割する
    • => SlackやTwitterのようなアプリではパーツごとに作ることが多い

React/ReduxといったらFacebookなのですが、Chrome DevToolsで見てみたらFacebookはReduxを使っていないことがわかりました。同じくFacebookが作っているInstagramも調べたところ、おおむねRecipesの通りの設計になっており、紹介することが少なかったので省略しました。しかし、リファレンス実装として一番参考になるかもしれません。

また、この記事は以下の記事にインスパイアされて執筆しました。

Dissecting Twitter’s Redux Store - Statuscode - Medium

Reactを使っているアプリケーションの場合Chrome DevToolsのアイコンが光るので、最近はついついどうやって作っているのかなと眺めてしまいます。

良いお年を!

金井 優希

(美容事業ユニット アーキテクトグループ)

アイカツおじさん

Tags

業務で採用してみたいiOSアプリのアーキテクチャと技術

Kotlin/Nativeの導入とSwiftUIへの備え

業務で採用してみたいiOSアプリのアーキテクチャと技術

はじめに

こんにちは、初めまして、iOSアプリエンジニアのtasaiです。この記事は リクルートライフスタイル Advent Calendar 2019 19日目の記事です。私は入社したばかりなので業務で使っている技術についてではなく、業務で採用してみたいiOSアプリのアーキテクチャと技術についてご紹介します。

前提

業務でMVVMでのアプリリニューアルやVIPERでのリアーキテクチャをしてきた経験をもとに、クロスプラットフォーム技術の採用を視野に入れて検討した内容となります。

解決したい課題

モバイルアプリの開発において継続的に改善を行うようなプロダクトでは、以下のような課題があるのは珍しいことではないかと思います。

  • 長年の運用で溜まっていた技術的負債
  • ユニットテスト/UIテストコードが書きづらい責務分割
  • そもそも設計方針が決まっていなかったり、複数のアーキテクチャを抱えたりしている

Appleは毎年新技術を発表し、iOSアプリの主要な開発言語であるSwiftもどんどんバージョンアップが行われています。ビジネスサイドの要求に答えつつ技術的な改善を行っていくことは難しい面もあると思いますが、アーキテクチャや構成を整えることで改善しやすいプロダクトにしておくということが重要だと思います。

アーキテクチャ

MVVM + Layered Architecture

前述した3つの課題を解決できるアーキテクチャの1つは、MVVMとLayered Architectureの併用だと考えます。

MVVMとは?

MVVMは有名なアーキテクチャのためご存知の方も多いでしょう。簡単に説明をすると、プレゼンテーションロジックとビジネスロジックを分離するGUIアーキテクチャのひとつで、ViewとViewModelをデータバインディングすることで宣言的にViewの更新が行えるという特徴があります。

MVVMアーキテクチャ

Layered Architectureとは?

Layered Architectureとは、日本語では多層/階層化アーキテクチャと呼ばれ、アプリケーションを複数の層に分割し、それらを独立したモジュールとして疎結合にすることで、モジュールごとの開発がしやすかったり入れ替えが容易になるような特徴のあるものです。Clean ArchitectureやOnion Architectureもこれの一種です。

前述した通りMVVMはGUIアーキテクチャのため、Modelの詳細な責務分割については関知していません。そこで、Modelの責務分割としてLayered Architectureを採用します。Layered Architectureと言っても層の分割方法はプラットフォームによっても違いますし粒度によっても違うなど多種多様です。その中で、今回採用するのは以下のようなInfrastructure層、Data層、Domain層と分割するものです。

Layered Architecture

Layered Architectureの各層の役割はこのように定義します。

  • Infrastructure層

    サーバーへの通信処理やDBへのアクセス、キャッシュ機構などを定義する層

  • Data層

    データの定義とInfrastructure層の機能を使った実際のデータ取得処理を記述する層

  • Domain層

    アプリケーションの仕様を実現するためにデータ取得をData層に依頼したり、取得したデータを加工する層

採用したいアーキテクチャの全貌

MVVMのModel部分にLayered Architectureを適用してみると以下のような図になります。(厳密にはLayered Architectureとは言えないかもしれませんが、そのエッセンスを組み合わせたものとしてご認識ください)

MVVM + Layered Architecture

採用技術

Embedded Framework分割

Embedded Frameworkとは?

Embedded Frameworkとはアプリのコードをフレームワークとして分割できる機能です。メインとなるターゲット(iOS App、App Extension、Watch Appなど)とは別に、任意の区分でソースコードをフレームワークとして分割する機能です。

Embedded Frameworkのメリット

  1. App ExtensionやwatchOS向けAppなどへのコード共有
  2. 差分ビルドによるビルド時間短縮
  3. アーキテクチャの依存関係の強制とテスタブルなコードの書きやすさ

などがあります。Embedded Frameworkに分割しない場合、メインとなるターゲットごとにソースコードをコンパイルするのでバイナリサイズやビルド時間が増加してしまいますが、Embedded Frameworkに分割することで、メインとなるターゲットがソースコードを共有するようになりサイズの減少/ビルド時間の短縮されるメリットがあります。

そして注目したいのが、3のアーキテクチャの依存関係の強制についてです。妄想iOSアプリ新規開発iOSアプリを作るときのおすすめ構成でも言及されていますが、前述した通りLayered Architectureは複数の層に別れた構造をするためEmbedded Frameworkとして分割することでメリットを受けやすく、わかりやすい構成にすることができます。

Kotlin/Native

Kotlin/Nativeとは?

Kotlinで記述されたプログラムをNativeなバイナリコードにコンパイルする技術です。サポートしているプラットフォームは公式サイトを見ていただくのが良いですが、iOS/Androidをはじめとする多数のプラットフォームのサポートが行われています。昨今で話題のReactNativeやFlutterとは違い共通UIを提供していないことでアプリケーション全体の構成には関与しないという特徴を持っています。2019年12月時点ではまだbeta版となっています。

Kotlin/Nativeを導入するとどうなるか

前述したようにまだbeta版のため、他社の採用事例もあまり存在しておらず業務で採用するにはリスクがあり注意が必要となります。しかし、Kotlin/NativeはOSSで開発されており、非常に細かくリリースが繰り返されているため、正式リリースに向けて着実に進んでいると考えられます。また、今回紹介したアーキテクチャのModel(= Layered Architecture)部分をKotlin/Nativeで作ることで全体のアーキテクチャに影響を与えずに、iOS/Androidアプリのドメインロジックを共通化できることが非常に大きなメリットになるため採用してみたいと考えています。さらに、KotlinConf'19Opening Keynoteでも大きく取り上げられており、今後への期待の高まりを感じられます。

SwiftUI / Combineへの備え

SwiftUIとは?

WWDC2019でAppleから発表された新しいUIフレームワークです。宣言的にUIを記述でき、リストを表示するコードは既存のUITableViewを使うよりもかなり少ないコード量で実現できるなどの特徴があります。iOS13から利用可能です。

Combineとは?

こちらもWWDC2019で発表された非同期イベントを処理するフレームワークで、データバインディング機構も提供しています。同様にiOS13から利用可能です。

将来を見据えた備え

iOSとしてMVVMを採用したいと考えている理由として、SwiftUIとCombineへの備えがあります。前述した通りこの2つの新機能はiOS13から利用が可能です。実際のプロダクトに導入できるのはiOS13以上のサポートになった時で2,3年後からです。MVVMのView部分がSwiftUIに置きかわり、ViewModelとViewのデータバインディング機構をCombineに置き換えるといった作業が必要になると思います。置き換えることを想定して作ることでコストを限りなく抑えることが可能になるため、MVVMはSwiftUI/Combineへの備えとして採用するのが良いと考えています。

まとめ

  • アーキテクチャはMVVM + Layered Architecture
    • SwiftUI + Combineを見据えて、置き換えしやすいMVVMを採用する
    • MVVMのModel部分にLayered Architectureを採用する
  • Model(= Layered Architecture)部分をKotlin/Nativeで実装する

最終的に採用したいアーキテクチャと技術を図にすると以下のようになります。

全貌

終わりに

この記事ではクロスプラットフォーム技術の採用を視野に入れて業務で採用してみたいアーキテクチャと技術を記事にさせていただきました。紹介した内容はまだ実際に採用されているものではないことにご注意ください。組織や時代、プロダクトのフェーズによって採用するべきアーキテクチャと技術は違うと思います。アプリのアーキテクチャや採用技術に正解はないですが、将来を見据えてベストを尽くすことが必要です。最後までお読みいただきありがとうございました。

浅井 徹

(飲食事業ユニット プロダクト開発グループ)

2019年11月入社。スマートデバイスアプリの開発を担当しています。アーキテクチャや技術選定をするのが好きです。

Tags

NEXT