lamechang-dev

Webフロントエンドエンジニア lamechangのブログ。

2023年の振り返り

明けましておめでとうございます!

TL;DR

  • 転職して楽しく働いてます
  • 関東に帰りました
  • 今年は引き続き技術と英語を頑張ります

転職

2023年4月に現職である株式会社Yoiiにフロントエンドエンジニアとして転職しました!入社して1年弱経ちますが楽しく働いています。 転職の動機は

  • アーリーなスタートアップで0 -> 1のフロントエンド開発(環境構築からディレクトリ構成・設計、状態管理戦略あたりを全て考える)がしたい

  • 今後のキャリアを社内公用語が英語の環境で積みたく、それを満たす会社に行きたい

というところでしたが、今年についてはこれらの欲求を全て満たすことができたので楽しく働けました。

また、現在社内のフロントエンド専任のエンジニアが1人という環境なのもあり、「ほぼ全て自分が責任を持って決めなきゃいけない」という環境がより自分を成長させてくれたかな、と振り返ると思います。社内のコンポーネントライブラリのレポジトリを0ベースから構築できたことも良い経験になりました。

これはカルチャー的なところになりますが、弊社は現在週3出社がルールとなってますが、ブロッカーとなる予定がない限りはみんなで外でランチに行くことが多く個人的にはこれが楽しくて出社が楽しみになってるところがグッドです。

関東に戻った

2023年8月に1年間住んでいた石川県から関東に戻りました。

家庭の事情が理由の100%ですので、特に詳細は語りませんが、関東に帰ってから会いたかった友人に会えるようになったことや、会社にも出社できるようになったので戻ってきてよかったなと思うことが多いです。

今年の抱負

引き続き当分は社内で1人のフロントエンドエンジニアとして働くことが想定されるので、自発的に最新技術のキャッチアップを行うことや、社内ドキュメントの整備なども意識的にコミットすることでチーム全体のフロントエンド周りに貢献できたらなぁと考えています。

また、バックエンド開発に関してもそろそろ業務でコミットしたい、とも最近感じてます。もう2年以上は業務で触っていないのですが、最近UdemyなどでGolangでのバックエンド開発の勘所は学習したので、チャンスがあり次第こちらも貢献したいです。

また組織体制の話になりますが現在PdMが不在です。自分としてはプロダクトのリファクリングPJを通してドメイン知識についてはかなり詳しくなったのもあり、自分が要件定義の上流部分にコミットすることが増えるのでPdMのような仕事も担当すると思います。この辺りの業務内容は、NRIで仕事をしていた際に英語で要件定義を行いドキュメントを書く経験はある程度積んだので、積極的に取り組んでいこうかなと思います(というか去年の末からこの辺りはすでに手を出し始めました。笑)

あとAtCoderも引き続き頑張ります。

長期的なキャリアでいうと、当分は社内公用語が英語で多国籍な会社でのキャリアを積んでいこうかなぁと考えているので、英語力についても今年の1年でもっと改善できたらなと考えています。英会話スクールに通うかオンライン英会話を始めるかなど、選択肢が多すぎてどれを始めるか悩んでいるところ…

仕事でも英語を使っているので少しずつは改善していくとは思いますが、意識的に勉強することの必要性を最近感じることが多いです。また、日本人同士での会話で日本語を使ってしまう癖があるので、なるべくEnglish-Only Policyを自分に課していく必要がありますね🥺

【ChatGPT】【GraphQL】ChatGPT-4に教えてもらいながらポートフォリオにGraphQLを爆速導入してページ追加してみた【20分で完了】

はじめに

この記事では、ChatGPT-4を利用した開発がどれだけの生産性向上に寄与するのか、というのをざっくり試したものになります。 今回は、あまり自分が詳しくないGraphQLで試してみました。

やってみたこと

試したことは以下の通りです。前提として私は業務上でのGraphQLの利用経験は一度もありませんし、そもそもGraphQLを個人開発などでも利用したことがありません。 また、一応事前にPlaygroundを通してGraphQLのクエリの書き方などは簡単に学びました(その際も疑問点はほぼ全てChatGPT-4に聞いた・・)


最終的に出来上がったページが以下です。ちなみにコンテンツ以外の部分は元々存在していたコンポーネントを再利用しています。 なお実装に要した時間は20分ほどです。


追加したページ
追加したページ

https://lamechang-dev.vercel.app/playground/pokemon_graphql

ざっくり振り返り

仮に1から自分が取り組むのであれば様々ググりながら進めていくとして、見積もりに1時間半-2時間ほどを設定すると思います。

が、今回 20分ほど で完了してしまいました。はっきり言って強力すぎますね。。。 しかもChatGPT-4って回答に周辺ライブラリの利用背景とかも完結に説明してくれていて非常に分かりやすいんですよね。開発体験がとても良かったです。とんでもないものが誕生してしまった気がします。

【英語】私の英語学習のこれまでとこれからを晒す

TL;DR

私は自分に甘く、興味が湧かないものについては必然性がないと努力をし続けることができない愚かな人間なのです…

これまで

〜高校卒業

大学受験をしているので英語は予備校を通ってちゃんと(?)勉強をしていました。参考になるか謎ですが、河合塾の最後の模試では英語の偏差値で65前後だったと記憶しています。

なお、学習時間は読む書くにほぼ全て突っ込んでました。英語自体は特別得意だったり好きという訳ではなかったですが、嫌いではなかった、といった感じです。

大学在学中

大学1年次に入学と同時にTOEICを受けたのですが、確か500点くらいでした。大学の講義の中にも英語関連のものはありましたが、当時はモンスターハンターがとても楽しかったので事前学習や予習・復習などは特にしませんでした。そのまま大学3年次まで過ごします。

大学4年次に、英語を集中的に勉強したいと思うきっかけが生まれ、フィリピン留学に4ヶ月ほど行きました。ちなみにそれまで海外経験はほぼゼロで、物心ついてから以降の初海外でした。特に行きのフライトは信じられないくらい揺れて本当に死ぬのかと思った

留学先から事前に課された勉強スケジュールを日本でこなしてからフィリピンに行ったので、現地に着く直前に受けたTOEIC800点ほどを取ることができ、なんだかんだで大学入学直後から300点ほどスコアを上げることができました。読む書くは受験勉強の資産もあってかキャッチアップしやすく、留学期間は苦手だった聞く話すにかなりフォーカスできたことが振り返ると良かったです。

留学を通して日常会話レベルなら聞く話すはできるようになり、また現地で受けたTOEIC900点ほどを取れました🥶TOEICのスコアに満足してからは、フィリピン人講師との授業はフリートークをメインにしたり適当にご飯食べに遊びに行ったりTOEFL対策などをしたりしていました。なお結局学士で卒業 => 就職を選んだので結局TOEFLは受けてません。

フィリピン留学は私にとって本当に大切な思い出です🌝🌝

英語力が上がったことももちろん理由の1つですが何より現地での生活は楽しかったし、ちょっとクレイジー社会人の先輩がたくさんおり、そういった方々から聞けた話から学生ながら価値観を大きく広げることができました。

まぁ簡単にいうと「人生って大体どうにかなるんだなぁ」ってことを理解できたと思います🌝なおこの留学をきっかけに海外旅行にドップリハマるようになります。

社会人

SIer在籍時

留学経験をよく話して就職活動をしたので、1社目のSIerでは運良く英語を使う部署に配属されました。開発パートナーはインド人のメンバーで構成され、業務の半分弱ほどを英語で2年半ほど行いました。

正直新卒入社時点で英語力は日常会話程度であったので入社直後は結構苦労しました。が、インドメンバーが日本人特有のブロークンイングリッシュに理解があるメンバーだったので、業務で絶望するほど困ったことはなかったです。

2年半の業務期間中、英語を集中的にインプットすることは一度もなく業務をしながら「あれ、こういう時ってどう表現すればいいんだろう?」という場面に出くわすたびにググる => 使うを繰り返してちょっとずつ話せる内容のバリエーションを増やした感じかと思います。こういったプロセスのお陰か、システム周りの細かい議論が業務特性上多かったのでその辺りについては苦手意識はなくすことができたのかな、と思います。

Webエンジニア転向以降

聞く話すでは一切使わなくなってしまいました。また、読む書くについても頻度が落ちたことが影響してスピード・理解度ともに下落傾向にあることを感じています。英語で書かれた技術系のリファレンスをたまに読みますが毎回衰えを感じてちょっと凹みます。。。

ただ、映画を英語音声で見るときに「あれ、今の単語なんて意味だ?」みたいなことを都度ググったり聴き直ししたりはしたので、完全に使わなくなった訳ではないかもです。

すいません、「ゆーてサボってた訳じゃなくて映画を通して活きた英語学習をしてたで?」みたいなことを突然書いてしまいました。本当はこの期間も本腰を入れて英語学習をする余暇の時間はいくらでもありましたし、正直勉強をあまりしてこなかったことを後悔してます。。

これから

と、ダラダラとこれまでの英語学習の経緯を話してきたんですが結局今回のエントリーで何を伝えたかったというと私は自分に甘いので、あまり興味が湧かないものについては強烈な必然性がないと努力をし続けることができない人間であり、これまでも必然性が生まれる環境に身を置くことでどうにかして何かを習得してきたということです。

ちなみに英語学習はぶっちゃけ好きではないです。が、私は1社目やフィリピンでの経験から多国籍な組織・文化での活動や就労にずっと興味があり、それはWebエンジニアに転向してからも変わりません。

実は海外就職にもずっと興味はありましたが、運悪くコロナが大流行したり結婚を機に価値観が変わったりと、そう言った理由で少し興味が薄れていました。

ただ、日本にいながらも多国籍 & 英語環境で自分が好きな領域でのアプリケーション開発に没頭できるような組織は実はあるんじゃないかとフワッと最近思っていたのですが、最近運良くこれに該当するスタートアップさんからお誘いいただき、入社することになりました。

今回の記事は転職エントリーだとは思ってないので細かい転職理由は触れませんが、運良くまた英語環境でのエンジニアの業務ができることを非常に楽しみにしています。

また、改めて自分のスタイルは わざと自分を追い込んで、背水の陣の状態でどうにか頑張って成長するというところにあるな、と思ったので今後も英語学習はそう言ったスタイルで継続していこうと思います🤢

補足

ちなみに そもそもなぜ英語を学習するのかですが、これは以下のような理由になります。上から優先度が高いです🙏🏻

  • エンジニアにおける技術や情報のインプット・アウトプットに関して、 読む書く聞く話すの4技能全てにおいて英語でストレスなく行えるようにしたい
  • 年齢も性別も国籍も人種も関係ないような多様性・透明性のある組織での仕事に興味があるので、そう言った組織で採用されてるであろう英語を自由に扱えるようになりたい
  • 妻が家の近くでアメリカ人の英語の先生やフランス人のエンジニアの友達を気づいたら作ったりしており私も会って話す機会があるので、ちゃんと意思疎通できるようになりたい
  • 2024年公開予定の ストレンジャー・シングス 未知の世界 シーズン5 を英語音声・英語字幕で楽しみたい♥️

自然言語人工言語も頑張っていきましょう🤢

【React】【Recoil】ポートフォリオ作成でRecoilを安全かつ綺麗に使おうとしたのでその構成を紹介する

はじめに

昨日、ポートフォリオサイトをvercelでデプロイ・公開しました。

lamechang-dev.vercel.app

レポジトリも公開しています。

github.com

公開の背景としては、以下のようなところが理由になります。

  • 技術的な検証ができるなんでもできるレポジトリが欲しかった
  • せっかくだから公開できるようなものにしたかった

といったところになります🤢

また今回、実装に入る前にアーキテクチャを図に起こしつつで着手しながら進めております。 その時の図を元にどう言った意図があって今の構成にしているかについて、今回はglobalStateにフォーカスして整理してみたいと思います。

レポジトリでのRecoilの運用ルール

  • 1つの単位(ドメイン・UIの状態など)に対して index.tsactions.tsselector.tsをそれぞれ用意する
  • recoilで定義したstatesetStateそのものを直接露出させず、write hookactions.tsread hookselector.tsの中に定義してそれらのカスタムフックのみを利用できるようにする(できる限りライブラリの知識・ルールを隠蔽する)
  • state の定義自体は index.tsの中に集約する
  • stateのkeyは必ず一元管理
  • ドメイン関連のglobalStateは domain/ の下に、 ui関連のglobalStateは ui/ の下に置いていく

実際のコードを以下のサンプルコードで明示していきます。今回実装したものの中で movie と言うドメインがあるので、そちらの構成を見ていきます。

サンプルコード

src/context/model/movies/index.ts

import { atom, selector } from "recoil";
import { GLOBAL_STATE_KEYS } from "src/context/constants";
import { MovieList, Movie } from "../../../domain/movies/model";
import { stateSelectedGenreIds } from "../genres";

export const stateMyFavoriteMovieList = atom<MovieList>({
  key: GLOBAL_STATE_KEYS.DOMAIN.MOVIE.MY_FAVORITE_MOVIE_LIST,
  default: [],
});

export const stateSelectedMovie = atom<Movie | undefined>({
  key: GLOBAL_STATE_KEYS.DOMAIN.MOVIE.SELECTED_MOVIE,
  default: undefined,
});

export const stateSelectedMovieList = selector<MovieList>({
  key: GLOBAL_STATE_KEYS.DOMAIN.MOVIE.SEELCTED_MOVIE_LIST,
  get: ({ get }) => {
    return get(stateMyFavoriteMovieList).filter((movie) => {
      return get(stateSelectedGenreIds).some((id) => {
        return movie.genres?.map((genre) => genre.id).includes(id);
      });
    });
  },
});

ここには純粋な atomselectorの箱だけを置いておくイメージです。特筆する点は特にないとは思います。

src/context/model/movies/selector.ts

import {
  stateMyFavoriteMovieList,
  stateSelectedMovie,
  stateSelectedMovieList,
} from ".";
import { useGlobalValue } from "../../hooks";

export const useStateMyFavoriteMovieList = () => {
  return useGlobalValue(stateMyFavoriteMovieList);
};

export const useStateSelectedMovie = () => {
  return useGlobalValue(stateSelectedMovie);
};

export const useStateSelectedMovieList = () => {
  return useGlobalValue(stateSelectedMovieList);
};

こちらが、globalStateの read hookになっており、単純にatom・selectorを参照し返すものになります。各々のhookは先ほどの index.ts で定義したstateと1対1対応となっており、これらのhooksを view層usecase層 で呼ぶイメージです。

実際の参照する側の記述では

const myFavoriteMovieList = useStateMyFavoriteMovieList();

と言った具合に、recoilの仕様などを意識せずに直感的にhookを通してglobalStateを参照できるようになっています。

ちなみに、useGlobalValue と言うものが登場してますが、こちらは useRecoilValue を単純に代入した関数になっています。recoilに対する直接的な依存を避けるために、間に挟んでいるイメージです。以下のようなhookを用意して利用しています。

■ src/context/hooks/index.ts

import {
  useRecoilCallback,
  useRecoilState,
  useRecoilValue,
  useRecoilValueLoadable,
  useSetRecoilState,
} from "recoil";

export const useGlobalValue = useRecoilValue;

export const useSetGlobalState = useSetRecoilState;

export const useGlobalState = useRecoilState;

export const useGlobalValueLoadable = useRecoilValueLoadable;

export const useGlobalCallback = useRecoilCallback;

src/context/model/movies/actions.ts

import { useRecoilCallback } from "recoil";
import { Movie, MovieList } from "src/domain/movies/model";
import { stateMyFavoriteMovieList, stateSelectedMovie } from ".";

export const useStateMyFavoriteMovieListActions = () => {
  const setStateMyFavoriteMovieList = useRecoilCallback(
    ({ set }) =>
      (movieList: MovieList) => {
        set(stateMyFavoriteMovieList, () => movieList);
      }
  );

  return { setStateMyFavoriteMovieList };
};

export const useStateSelectedMovieActions = () => {
  const setStateSelectedMovie = useRecoilCallback(
    ({ set }) =>
      (movie: Movie) => {
        set(stateSelectedMovie, () => movie);
      }
  );

  const resetStateSelectedMovie = useRecoilCallback(({ set }) => () => {
    set(stateSelectedMovie, () => undefined);
  });

  return { setStateSelectedMovie, resetStateSelectedMovie };
};

こちらが actions.ts の定義になります。いわゆる write hook の置き場所になっており、RecoilのAPIを直接露出させずに更新処理を行えることを狙っています。 また、globalStateに対するwrite処理をこのようにカスタムフックに集約することで、発生しうるwrite処理が網羅的にここで確認できるようになることも大きなメリットであり、テストを書く組織ではテストカバレッジの向上に貢献すると思います。

こういったAPIの隠蔽は、今後Recoil以外のよりナイスな状態管理ライブラリが誕生した際に、そのリプレースの影響範囲を限定できますし、アプリケーションに対するリスクをグッと抑えられるのではないかなと思います。

実際の呼び出し側のコードサンプルも置いておきます。

const { resetStateSelectedMovie } =
    useStateSelectedMovieActions();

const handleClickCloseIconButton = useCallback(() => {
    resetStateSelectedMovie();
  }, [resetStateSelectedMovie]);

その他補足

ドメイン関連のglobalStateは domain/ の下に、 ui関連のglobalStateは ui/ の下に置いていくと言う規約についてですが、これは単純にグルーピングできるものはしていこう、と言う発想からこのようにしました👼🏻が、今後ポートフォリオの機能を増やしていくうちにこの構成が扱いづらいとなったら他の構成に変えていくかもしれませんし、そもそも現段階で domainui の2つのグルーピングが最適解だとは全然思えてないので、作っていきながら調節したいです。

あと、このレポジトリは前述した通り色々なことを実験していくために用意したもので、例えば上述した Recoil以外のよりナイスな状態管理ライブラリが誕生したというシチュエーションも実際に試運転できたらなぁと思ったりしてます。 つまり、この後 Jotai に状態管理を載せ替えてみる、とかも試してみたいなぁなんて思ってたりしますね。Jotaiの方がRecoilより優れている、と言いたい訳ではないです。

jotai.org

【ESlint】フロントエンドアーキテクチャ上で決めた依存ルールをESlintで制約として表現する

この記事は Wano Group Advent Calendar 2022の23日目の記事となります。

はじめに

改めてですが、私は音楽ディストリビューションサービス「TuneCore Japan」のフロントエンドエンジニアを担当しております。みなさん、フロントエンド開発楽しんでますか。私は相変わらず楽しくやっております🥹

さて、プロダクトTuneCore Japanは クリーンアーキテクチャ を参考にしたアーキテクチャを採用しております(ここについての詳細は割愛)。新規機能の実装などはその構成によせ、また適宜既存箇所の修正をする場合はボーイスカウトルールに乗っ取ってなるべく新しい構成に寄せられるように頑張っております。

各レイヤーの責務・依存関係をチームで擦り合わせた上で実装を進めることができており、バグ発生時の影響範囲の特定がしやすくなったことや、レイヤーごとの責務が明確になったことからコード品質が改善し、スムーズに実装に取り組めるようになったと思います。 

そして、当たり前のことですが実際の実装フェーズにおいては、レビュー担当・実装者は「実装がアーキテクチャに沿っているか」がレビュー観点に含まれることになります。もちろん、各メンバーがアーキテクチャを理解した上で日々の実装に取り組んでいるので大きく構成から外れるようなことはないですが、もちろん人間ですのでミスはします。例えば、APIの型定義をそのままアプリケーション固有のロジック・定義が存在する層で参照してしまうことなど、例を挙げればキリがありません。

ということで、どうにかしてそういった問題を自動検知できないか、と調べていた時にimportプラグインに定義されている no-restricted-paths を見つけました。今回は、その紹介をしようと思います。

import/no-restricted-pathsの設定

no-restricted-paths の実際の記述例を以下に紹介します。

"import/no-restricted-paths": [
      "warn", // or error
      {
        zones: [
          {
            from: "./src/domain/**/*",
            target: "./src/components/!(organisms)/**/*",
            message:
              "The organisms components can only depend on the domain layer",
          },
          {
            from: "./node_modules/openapi",
            target: "./src/components/**/*",
            message:
              "Components layer can not depend on OpenAPI type definition ",
          },
        ],
      },
    ],

まず1つ目の例について。例えば、Atomic Designを採用しているプロダクトにおいては、しばしばcomponents層においてドメインに依存する箇所を制限させたい、というようなルールを取り入れているかもしれません。そのような場合、上記のように記述することで、 organisms以外のコンポーネントにおいて src/domain/配下からのimportを行った場合にアラートを出すことができます。

そして2つ目の例は、node_modules下にレポジトリの依存パッケージとして存在するOpenAPI上の型定義の参照を、components層で許さないことを表現したものになります。 このように、複数個のルールを定義することが可能であり、チームで取り決めた構成に沿った依存ルールを柔軟に表現できることがわかるのではないでしょうか?

(補足)no-restricted-importsについて

no-restricted-imports は上で紹介したルールと似たような名前ですが、こちらは文字通り指定したモジュールのimportを禁止することが可能です。実際のFEプロジェクトでのユースケースとしては、サイズの大きいライブラリ(例えばlodash)の静的なimportを禁止することや、プロダクトとして非推奨となったライブラリの利用を制限させることなどができそうです。こちらもチーム内で取り決めたルールを表現する手段としては非常に有効だと思います。

"@typescript-eslint/no-restricted-imports": [
      "warn",
      {
        paths: [
          {
            name: "lodash",
           "message": "lodash is deprecated, please use 'just'",
          },
        ],
      },
    ],

最後に

チームで取り決めたルール・運用をESlint上に表現し自動検知できるようにすることで、開発をする上での実装者・レビュワーの工数・負担を軽減させることは間違いありません。弊社プロダクト TuneCore Japan もお陰様でフロントエンド開発に携わってくれるメンバーが少しずつ増えてきており、こういった改善策の恩恵をより受けることができているのではないかな、と思います。

また、TuneCore Japan では 一緒に働くメンバーを募集しています。

エンジニアだけでなく、Director等の各種ポジションをオープンしているので興味がわいた方はぜひご覧ください!

www.tunecore.co.jp

【React】ちゃんと知って使うReact.memo

この記事は Wano Group Advent Calendar 2022の10日目の記事となります。

はじめに

改めてですが、私は音楽ディストリビューションサービス「TuneCore Japan」のフロントエンドエンジニアを担当しております。そしてこの2022年も1年通してReactをゴリゴリ書き、コードレビューも何度もしてきました。TailWind CSSだけをひたすら書いてた時期もちょっとありましたが

そんな中、自分の中のもやもやとして上がっていたのがReact.memoによるパフォーマンス改善をやるべきコンポーネントとやるべきでないコンポーネントの明確な定義ができていないというところでした。この点にフォーカスして今回は React.memoをちゃんと知り、正しく使えるようにするために自分が調べたこと を整理したいと思います。

React.memoに関する公式での説明

以下、Facebookが提供している公式Docから引用します。

React.memo は高階コンポーネントです。

もしあるコンポーネントが同じ props を与えられたときに同じ結果をレンダーするなら、結果を記憶してパフォーマンスを向上させるためにそれを React.memo でラップすることができます。つまり、React はコンポーネントのレンダーをスキップし、最後のレンダー結果を再利用します。

React.memo は props の変更のみをチェックします。React.memo でラップしているあなたのコンポーネントがその実装内で useState、useReducer や useContext フックを使っている場合、state やコンテクストの変化に応じた再レンダーは発生します。

デフォルトでは props オブジェクト内の複雑なオブジェクトは浅い比較のみが行われます。比較を制御したい場合は 2 番目の引数でカスタム比較関数を指定できます。

これはパフォーマンス最適化のためだけの方法です。バグを引き起こす可能性があるため、レンダーを「抑止する」ために使用しないでください。

自分なりの言葉で上記の説明を補足すると

  • React.memoは、propsオブジェクト内の値に対して、 Object.isを通した値の比較を適用して、仮にtrutyだった場合はレンダーフェーズにおける仮想DOMの差異チェックをスキップする

  • React.memo でラップしているコンポーネント内部のstateの変更に対しては、従来通り再レンダーが走る

といった感じでしょうか。ここで注意したいことは、React.memoコンポーネントをラップした場合、追加で Object.is によるprops同士の浅い比較がレンダリングごとの計算処理として追加されていることになります。

なので、仮に各レンダーフェーズにおいて毎回Propsの変更が行われるコンポーネントがあった場合は、React.memoでラップしても毎回再レンダリングされた上、追加でObject.is によるprops同士の浅い比較が行われるため結局総合的な計算量は増え、パフォーマンスに悪影響を及ぼすことになります。このことからも、慎重に利用するべきライブラリであることは間違いありません。

ではどういった時にReact.memoを使えば良いのか

上記のような仮説を自分なりに持って調査をしている時に出会った記事が

Use React.memo() wisely

になります。記事の第二章「2. When to use React.memo」が今回の疑問の解決に役立ちました。

詳細は記事を参照していただきたいですが、基本的な方針は 同じPropsで再レンダリングされる頻度が高い場合に React.memo を適用する、というところです。

筆者が記事の中で紹介している コンポーネント MovieMovieViewsRealtime のケースが理解しやすいと思います。

MovieViewsRealtimeコンポーネントはリアルタイムに情報が変わる頻度が高い箇所とstaticな情報をもつ箇所(titlereleaseDate)にDOM要素が分かれており

staticな情報をもつ箇所をMovieコンポーネントとして切り出して React.memoでラップすることで無駄な再レンダリングを防ぐ、と言うアプローチです。

このように、親コンポーネントの再レンダリングにつられて不必要な再レンダリングが子コンポーネントで発生するようなケースは多くの現場でのReactを通したUI開発でも肌感でよく起きている印象なので、このような場面においてはReact.memoが有効だと思いました。

また、筆者が「3. When to avoid React.memo」において指摘しているように、React.memoが悪影響を及ぼすパターンも多くあります。個人的にありがちだと思ったのが、コールバックのメモ化を怠った結果、常にReact.memoによるObject.is の値がfalseを返し意図しない再レンダリングが毎回行われてしまっていたと言うケースです。

さいごに

Reactの文脈においては、レンダリングの回数が減ったことが、そのままパフォーマンス向上につながったと考えられる傾向にあると予想しますが、コンポーネントによっては、React.memoによるpropsの比較が悪影響を及ぼし結果としてパフォーマンスに問題を起こすことは、今回の調査によって注意しなければならない、と改めて思いました。

Reactの公式サイトにも書かれていますが、React.memoに上記のようなリスクがあることは明らかなので、やはりレンダリング頻度や計算時間などを確認していくことは間違いなく必要になってくると思います。自分もProfilingを運用上で適切に活用して、より高品質なUI開発ができるように気をつけて行きたいところです。

【アルゴリズム】【動的計画法】ナップザック問題のアルゴリズム実装

動的計画法とは

アルゴリズム分類の1つ。与えられた問題全体を一連の部分問題として分解した上で、それぞれの部分問題に対する解をメモ化しながら、小さな部分問題から大きな部分問題へと順に計算 => 解を求めていく手法のことを指します。動的計画法を適用できるようなケースは非常に多く、解法パタンの中には特殊なテクニックを要するものも存在します。

動的計画法を用いることで解を用いることができる問題の代表例は以下の通りです。
- ナップザック問題
- スケジューリング問題
- 発電計画問題

今回は、動的計画法の理解をする上で典型パターンといてよく用いられるナップザック問題のアルゴリズム実装をします。

ナップザック問題とは

ナップサックの中にいくつかの品物を詰め込み入れた品物の総価値を最大にするという問題を指します。より一般化して、以下のような命題を考えます。


 N個の品物があり, i(=0,1,...,N-1)番目の品物の重さは weight_i,価値は value_iで与えられます。 この N個の品物から,重さの総和が Wを超えないように,いくつか選びます。 選んだ品物の価値の総和として考えられる最大値を求めてください(ただし, W weight_iは0以上の整数とします).


動的計画法における部分問題の考え方は、以下のことを意識しながら部分問題を構成することで、部分問題同士の遷移を考察し、数式に落とし込むことで解法を導くことができます。

部分問題の作り方の基本パターンは
N個の対象物{0,1,...,N-1}に関する問題に対して,最初のi個の対象物{0,1,...,i-1}に関する問題を部分問題として扱い、一般化する
ということになります。

なので、この基本パターンを上記の命題にも適用してみるとしましょう。

部分問題の適用

以下のような部分問題を考えることができます。

ans[i] = 最初のi個の品物{0,1,...,i-1}までの中から重さがWを超えないように選んだときの,価値の総和の最大値

こうした場合の部分問題同士の遷移を考えてみます。

ans[i] から ans[i+1] に遷移する場合、 i番目の品物を選択するか否かの2通りが考えられます。しかし命題をちゃんと確認してみると重さの総和が Wを超えないようにという条件がありますが、こちらの条件を満たすか否かの判断が上記の部分問題だけでは分かりません。よって、部分問題の ans[i] を決定づける要素に、重さwを付け加えてやることにします。

ans[i][w] = 最初のi個の品物{0,1,...,i-1}までの中から重さがwを超えないように選んだときの,価値の総和の最大値

こうすることで、選択肢のグループを決定づける変数にwが加わりました。この状態で、グループごとの遷移を表現できるか考えてみます。以下において、dp[i][w](w=0,1,...,W)の値が求まっている状態を仮定した上で、dp[i+1][w](w=0,1,...,W)を求めていくことを考えてみます。

i番目の品物を選ぶとき

選んだ後の価値の総和ans[i+1][w]に対し、選ぶ前の価値の総和は ans[i][w - weight[i]]となります。 また、その選ぶ前の状態の価値の総和から value[i+1]が加わることを考えると、

ans[i+1][w]ans[i][w - weight[i]] + value[i] と表現することができることがわかります。よって、ans[i+1][w]ans[i][w - weight[i]] + value[i] の間の最大値を求めればいいことがわかります。

ただし、w - weight[i]>=0 の場合のみ適用できることに注意します。

i番目の品物を選ばないとき

選ばなかった後の価値の総和をans[i+1][w]に対し、選ばなかった前は重さが変わるはずがないので価値の総和はans[i][w]です。よって、ans[i+1][w]ans[i][w] の間の最大値を求めてあげればいいことがわかります。

具体例で遷移を確認

ここで、この遷移をより具体的に考えてみます。このような状況を考えてみましょう。品物が4個で、(weight,value)={(2,3),(1,2),(3,6),(2,1)} 実際のans[i][w]を以下のマス目状の図に埋めていきます。

先ほど紹介した遷移を利用していきこの表を左上から埋めていくことを考えた時に、赤マスで表現されるans[3][3] の値の算出は以下のように行われます。

  1. 2番目の品物を選んだ場合は、ans[2][3 - weight[2]] + value[2] = ans[2][2] + 2 = 3 + 2 = 5

  2. 2番目の品物を選ばない場合は、ans[2][3] = 3

と計算されます。

仮に、それぞれのマスがまず0で初期化されていると前提を置くとなると、この2パターンによって算出される値の最大値がans[3][3]の最終解になっていることがイメージできるのではないでしょうか。

また、アルゴリズムの計算量自体は、それぞれのグループにおける計算量が O(1)であり、グループの個数は  NWだけ存在しますので、 全体で O(NW)で表現されます。

C++ でのアルゴリズム実装

以下のようになります。

#include <iostream>
#include <stdio.h>
#include <vector>
using namespace std;

template<class T> void chooseMax(T& a, T b) {
  if (a < b) {
    a = b;
  }
}

int main() {

  int N; long long W;
  cin >> N >> W;
  vector<long long> weight(N), value(N);
  for (int i = 0; i < N; i++)
  {
    cin >> weight[i] >> value[i];
  }

  // アンサーテーブル定義
  vector<vector<long long> > ans(N + 1, vector<long long>(W + 1, 0));

  for (int i = 0; i < N; i ++) {
    for (int w = 0; w <= W; w ++) {
      if (w - weight[i] >= 0) {
        chooseMax(ans[i + 1][w], ans[i][w - weight[i]] + value[i]);
      }

      chooseMax(ans[i + 1][w], ans[i][w]);
    }
  }

  cout << ans[N][W] << endl;
}