Atrae Tech Blog

People Tech Companyの株式会社アトラエのテックブログです。

ビジネス版マッチングアプリ"Yenta"のWebフロントの構成について

f:id:muttsu_623:20211116124618j:plain

こんにちは、エンジニアの @muttsu_623 です。

前回のブログ で分離キーボードを購入した話をしましたが、今回はPixel6 Proを買いました。

購入した初日ですが、さっそく「消しゴムマジック」という、写真内のとある対象物を消したりして遊びました。

所感としては消えてくれるものの、やっぱり多少違和感は残るなーという感じはありますね。

Pixel 3a を使っていたので Pixel 6 Pro へは超躍進なので、これからもどんどん使い倒していきたいなと思っております💪

今回の構成について

さて、前置きはこれくらいにして、今回は Yenta という弊社のビジネス版マッチングアプリでWeb版の構成について記事を書こうと思います。

なぜWeb版をつくることになったのかは、先輩の @ysk_aono の記事をご覧ください。

atraetech.hatenablog.com

YentaのWeb版は Next.js (React) + Typescript で作られています。

特徴的な構成としては以下かなと思っています。

  • Next.js依存層と非Next.js依存層の分離
  • MVVM
  • DI

それぞれ具体的にどういうことか説明します。

Next.js依存の部分と非Next.js依存の部分の分離

勝手な考えにはなりますが、モバイル技術に比べてWebフロントの技術は移り変わりが激しいなと思っています😓。

それを考慮し、今後React.jsを利用したNext.js以外のフレームワークが出てくる可能性も十分にあるかなと思いました。(もちろん、React.js以外のUIライブラリも同様ですが…)

そこで、Next.js依存の部分と非Next.js依存の部分を分離して、他のフレームワークに乗り換えやすい状態にしたいと思いました。

※正直そんなに依存している部分が多かったわけではないので、やってもやらなくてもいいかなとは思いますが、他の理由としては、どこが何に依存しているからそれを分離するという構成を教育するという観点も含まれています。

今回のプロジェクトではUIに関するディレクトリは以下になります。

./
  /src
    /page <- Next.js 依存層
      /hoge.tsx
      /fuga.tsx
    /presentation <- 非Next.js 依存層
      /components
      /state-managements
      /view-models

ここで疑問が生じるのが、

ディレクトリ構成は分かったけど、実際何を hoge.tsx に書いて何を /components 以下に書いているの?

ということですよね。

具体的には、hoge.tsx は以下のようにしています。

hoge.tsx

const getHoge: (router: NextRouter) => string | undefined = (router) => {
    ...
    return hoge;
}

const HogePage: NextPage = () => {
    const router = useRouter();
    const goToHome = useCallback(() => router.push('/'), [router]);
    const hoge: string | undefined = getHoge(router);

    return <HogeContainer hoge={hoge} goToHome={goToHome} />
}

export default HogePage;

hoge-container.tsx

interface Props {
    hoge: string | undefined;
    goToHome: () => void;
}

const HogeContainer: React.FunctionComponent<Props> = ({ hoge }) => {
    ...
    return <Something />;
}

上記記載から明らかかと思いますが、hoge.tsx には Next.js に依存したものを記述し、それらをNext.jsに依存していない形に変換し、hoge-container.tsx に Props として渡しています。

これにより、 Next.js 以外のフレームワークに乗り換えを行う場合、UI層に関しては /pages 配下のみ該当のフレームワークに対応させ、/presentation 配下はそのフレームワークに対応させる必要がない状態になっているかと思います。

改めて、ここまでやる必要があるかと言われたら怪しいのですが、、、教育的な観点も含めてあえてこの構成にしております。

MVVM

おそらくWebフロントの構成ではあまり MVVM は用いられていないかと思いますが、あえて MVVM という構成を今回Webフロントにも取り入れてみました。

MVVMという構成にした理由は モバイルアプリエンジニア ⇄ Webフロントエンジニア 間のコンバートをしやすくしたい というものです。

Yentaのモバイルアプリでは、MVVM+ Repository (iOSのみ + Flux) パターンで両方とも作られています。この構成をWebフロントにも適応することで、モバイルアプリを書いている人がWebフロントを書きやすくなり、Webフロントを書いている人もモバイルアプリを書きやすくなったかなと思います。

実際、僕と ysk_aono はWebフロントの習熟度がすごく高い状態ではなかったものの、レイアウト構築以外はほとんどモバイルアプリ開発と変わらなかったため、スムーズにWebフロントの開発にも慣れることができるようになりました。

実際どのように書いているかは以下の通りです。

hoge-container-view-model.ts

interface HogeContainerViewModel {
  isLoading: boolean;
  fuga: Fuga | null | undefined;
  onClickButton: () => void;
}

interface Input {
  hoge: string | undefined;
  goToHome: () => void;
}

const useHogeContainerViewModel: (input: Input) => HogeContainerViewModel = ({ hoge, goToHome }) => {
  const [isLoading, setIsLoading] = useState(false);
  const [fuga, setFuga] = useState<Fuga | null>();
  const onClickButton: () => void = useCallback(() => goToHome(), [goToHome]);

  ...

  return {
    isLoading,
    fuga
  } 
}

hoge-container.tsx

interface Props {
    hoge: string | undefined;
    goToHome: () => void;
}

const HogeContainer: React.FunctionComponent<Props> = ({ hoge, goToHome }) => {
  const { isLoading, fuga, onClickButton } = useHogeContainerViewModel({ hoge, goToHome });
    return <Something isLoading={isLoading} fuga={fuga} onClickButton={onClickButton} />;
}

基本的に、Viewに関するロジックはViewModelに集約し、Viewはレイアウトの構築とViewModelから受け取ったプロパティやメソッドをレイアウトに対して渡すことだけを責務とするようにしています。

これにより、 *.tsx でのレイアウトの書き方やReactのお作法させ理解すれば、モバイルアプリエンジニアがWebフロントをすぐにかける状態になったかなと思います。

ネガティブな点は(ViewModelを導入したからではないですが)、ViewModelにロジックを集約させると、責務が多いページではViewModelが肥大化してどこになんの処理が書かれているかわからないという状態になりました。

これを解消する手段として、Reducerカスタムhooks により、特性のオブジェクトに関する処理は別ファイルとして分け、ViewModelはそれらを呼び出してViewに渡すという方法を取りました。

実際にこれにより、ViewModel内の記述は減りましたが、Reducerカスタムhooks を理解しなければいけない状態になりましたが、逆に理解すればそれなりに責務の多いページであってもすっきりと書くことができ、他の人にも理解しやすいコードになってので、現在はそのように運用しています。

DI

最後にDIに関してです。

DIとはなんぞや?という方は、ぜひこの機会に勉強してみてください。 プログラミングを行う上で大切な考え方が詰まっていると思います。(偏見)

モバイルアプリの開発でも(iOS, Android共に)DIを利用し、いつでもテストコードを導入できる状態にしていたため、Webフロントでも(すぐにテストは入れないものの)いつでもテストコードを導入できるようにしよう と考えました。

そこで、 InversifyJS というライブラリを導入し、 Repository, Daoというデータアクセスレイヤーを Interface と 実装で分けて DIコンテナを利用して Injection する方法を取りました。

参考にしたものは下記の通りです。

Dependency injection in React using InversifyJS

Dependency injection in React using InversifyJS. Now with React Hooks

Dependency Injection with React using InversifyJS Inversion of Control Container

感想

実際上記のような構成にしてみてどうだったかという部分が論点になるかと思いますが、Yentaのチームとしては良かったなと思います。

この構成にしたことによって、iOSAndroidのエンジニアがReactのお作法さえ身につければ、すぐWebフロントを書くことができるようになりました。

また、この構成によってViewとViewに関するロジックが分断されたので、いわゆるレイアウトを組み人とロジックを書く人で分けることも容易になり、コンフリクトなども起きにくい状態になりました。

もし、こちらの記事を読んでいただいてご興味いただけたら、ぜひ試してその結果をシェアしていただけると嬉しいです。