easy going tech

エンジニアリング、プログラミング学習のアウトプット

JavaScriptでString.prototype.reverseを作る

はじめに

こちらの記事ではJavaScriptにおけるStringオブジェクトを拡張してreverseメソッドを追加する過程をまとめています。 JavaScript以外の言語では文字列クラス(オブジェクト)のビルトインメソッドとしてreverseが備わっているものもありますが、それをJavaScriptにも実装してみようという、ちょっとした遊び心から生まれた記事です。

尚、JavaScriptではビルトインオブジェクトのprototypeを改変することはプロトタイプ汚染と呼ばれ(モンキーパッチと呼ばれることもある)、あまり好まれません。

今回はあくまでも学習の一環で実装していますのでご容赦ください。

概要

JavaScriptではStringオブジェクトにreverseメソッドが存在しないため、以下を実行するとエラーとなります。

'hoge!'.reverse()
// => Uncaught TypeError: "hoge".reverse is not a function

そして今回は以下の結果が得られるようにプログラムを作ります。

'hoge!'.reverse()
// => !egoh

ただしここで1つ制約を設けます。 Array.prototype.reverseは使用してはいけないという制約です。

そもそもこの記事を書こうと思ったきっかけはこちらの記事を読んで、「JavaScriptで同じ事をするには🤔」と考えたのがはじまりです。reverseを使わないという制約がないと大変簡単な実装となってしまうため、この制約を設けています。

Array.prototype.reverseを使用した場合

'hoge!'.split('').reverse().join('')
// => !egoh

これで終了です😅

そもそもprototypeって?

prototypeについてまとめるにはかなりの根気がいるためここでは簡単にまとめます。 詳しく学びたい方は下記を参考にしていただければと思います。

オブジェクトのプロトタイプ - ウェブ開発を学ぶ | MDN

さて、今回の実装に必要最低限な説明のため、Stringオブジェクトと文字列を使ってprototypeの中身を確かめてみます。 まずはお使いのブラウザでdeveloper toolを開きコンソールにString.prototypeと打ってみてください。

そうすると以下のような結果が得られるはずです。

この結果からprototypeの中身には文字列に使用できるメソッドが格納されていることがわかります。(slice、repeatなど) つまりprototypeとはそのオブジェクトで使用できるメソッドやプロパティが格納されたオブジェクトであると言えます。

次にdeveloper toolのコンソールに'hoge'.prototypeと打ってみましょう。 これはundefinedが返ってきます。ここで、先の「prototypeとはそのオブジェクトで使用できるメソッドやプロパティが格納されたオブジェクト」という説明が正しい場合、String.prototypeと同様の結果が得られないのはおかしいと思いませんか?なぜなら'hoge'という文字列に対してもsplicerepeatメソッドは使用できるため、矛盾が生じています。

ではなぜ、文字列がStringオブジェクトのメソッドを使用できるのかというと、JavaScriptではプリミティブな値に対してプロパティアクセスを行った場合、ラッパーオブジェクトへ自動変換されるという仕組みがあるためです。

ラッパーオブジェクト · JavaScript Primer #jsprimer

つまり、文字列へプロパティアクセスがあった場合はStringオブジェクトのprototypeが参照されているということになります。

この仕組みから、String.prototypeにreverseメソッドを追加してあげることで'hoge!'.reverse()が実現できることがわかります。具体的には下記のようにします。

String.prototype.reverse = function () {
  // ここに処理を実装
}

実装

まずはいきなりString.prototypeに実装するのではなく、「受け取った文字列を反転して返す関数(Array.prototype.reverseは使用禁止)」を作ります。 尚、筆者は普段TypeScriptを使用しているためTypeScriptのコードも記載します。但し、TypeScriptの場合はString.prototype.reverse =とした時点でコンパイルエラーが発生するため、関数の実装のみとして、Stringオブジェクトの拡張は割愛します。

Javascript

function reverse(string) {
  const chars = [...string]
  const reversedChars = []

  for (let i = 0; i < string.length; i += 1) {
    reversedChars.push(chars.pop())
  }

  return reversedChars.join('')
}

TypeScript

function reverse(string: string) {
  const chars = [...string]
  const reversedChars: string[] = []

  for (let i = 0; i < string.length; i += 1) {
    const char = chars.pop()
    if (char) reversedChars.push(char)
  }

  return reversedChars.join('')
}

実装のポイントは以下です。

  • const chars = [...string]として受け取った文字列を1文字づつ配列に展開(...スプレッド演算子本当に便利)
  • chars.pop()で後ろから順番に要素を取得して新しい配列に追加

Array.prototype.popについては配列の一番後ろの要素から要素を1つ削除することに注目されがちですが、実は返り値がその削除された要素になります。

console.log([1,2,3,4,5].pop())
// => 5

この特性を利用して、ループ内で配列の後ろから一個づつ要素を取得して新しい配列に追加しています。 TypeScriptの場合はpopの返り値は対象となる配列の要素の型とundefinedのユニオン型になるため(今回の場合はstring | undefined)、if (char)とすることでpush時のコンパイルエラーを防いでいます。

そして注意点としては以下です。

  • for文の条件式をi < chars.lengthとせずにstring.lengthとする

ループ内でcharsに対してpopを呼び出している(charsの要素数が減少していく)ため、i < chars.lengthとしてしまうと期待通りに繰り返しが行われません。

次にこれをString.prototype.reverseに代入していきます。

String.prototype.reverse = function () {
  const chars = [...this]
  const reversedChars = []

  for (let i = 0; i < this.length; i += 1) {
    reversedChars.push(chars.pop())
  }

  return reversedChars.join('')
}

先に実装した関数との変更点は以下です。

  • 引数を削除
  • 関数内で使用していた引数stringを全てthisに変更

このthisは具体的にはレシーバーとなる文字列を指します。 prototypeメソッド内でthisを呼び出した場合はレシーバーとなるオブジェクトが渡ってくるためです。

以上で実装が完了です。

最後のコードをdeveloper tool内で実行した後にお好きな文字列に対してreverse()を呼び出してあげましょう。

期待通りの結果が得られることでしょう。

まとめ

ひょんなことから取り組んだ問題ではありましたが、付随して以下の様々な事を学べました。

  • Array.prototype.popの挙動
  • prototype
  • this
  • ラッパーオブジェクト

Webアプリケーション開発には直接関係のない部分ですが、JavaScriptの機構を知っておくことは必要なことかと思いますので、今後も折りを見て学習していきたいと思います。

eslintとprettierを連携して開発効率を上げる

はじめに

フロントエンド開発では必須のeslint、prettierですが皆さんはうまく連携できてますか? 今回はeslintとprettierの連携方法とファイル保存時に自動でフォーマッタが走るようにVSCodeを設定する方法についてまとめます。

prettierとeslintを連携させる理由

この2つのルールに関する設定項目が一部被っているため、どちらか一方のルールに統一することが目的です。 (クォーテーションを""''か設定するquotesや文末の;の有無を設定するsemiが該当。)

今回はeslint-config-prettierというnpmを使用してルールが被っている場合はprettierのルールを優先してeslintのルールをオフにするという設定をしていきます。

事前準備

VSCodeで下記拡張機能をインストールしておきましょう。

eslintのインストール

今回はeslintの細かい設定を省略するためにcreate-next-appを使用します。 ターミナルでcreate-next-appを実行すると対話形式で設定項目を問われます。

ESLintを使用するか聞かれるのでYesと回答しましょう。 その他の項目についてはプロジェクトに合わせて適宜回答します。

プロジェクトの作成が完了すると.eslintrc.jsonファイルが自動で作られるので各種pluginのインストールなど.eslintrc.jsonファイルの編集を行います。今回は特にeslintの設定をせずに進めるため、eslintの設定について気になる方は下記を参考にしていただければと思います。

https://umatsui.hatenablog.com/entry/2023/07/10/102109

さて、eslintのインストールはこれにて完了です。 create-next-appによって自動生成された.eslintrc.jsonファイルの内容を載せておきます。

.eslintrc.json

{
  "extends": "next/core-web-vitals"
}

prettierのインストール

下記コマンドでprettierをインストールします。

% yarn add -D prettier

package.jsonファイルにprettierが追加されていることを確認しましょう。

"devDependencies": {
  "@typescript-eslint/eslint-plugin": "^5.61.0",
  "@typescript-eslint/parser": "^5.61.0",
  "eslint-plugin-react": "^7.32.2",
  "prettier": "^3.0.0"
}

Next.jsを使用しているのでprettier以外のパッケージも記載されていますが、今回は特に気にしなくて大丈夫です。

eslint-config-prettierのインストール

次にeslintとprettierを組み合わせて使うためのeslint-config-prettierをインストールします。

% yarn add -D eslint-config-prettier

eslintとprettierの連携

次にeslintとprettierの連携を行います。 とはいっても.eslintrc.jsonファイルのextendsにprettierを追加するだけです。

.eslintrc.json

{
  "extends": ["next/core-web-vitals", "prettier"]
}

連携するときの注意点

これまでの方法で連携を行った場合でも.eslintrc.jsonファイルのrulesにsemiquotesを追加してしまうとそちらが優先されてしまうので気をつけましょう。 prettierで設定されているルールは下記で確認ができます。

https://prettier.io/docs/en/options.html

VSCodeの設定

最後にファイル保存時にコードが自動でフォーマットされる設定を行います。

VSCodeの設定手順は以下です。

  1. 右下の設定アイコンから設定ページへ遷移

  2. 画面左上のタブから「ワークスペース」を選択

  3. 画面右上の「設定」アイコンからsettings.jsonファイルを開く

  4. 下記設定を行う

{
  // ファイル保存時にeslintの指摘箇所が自動修正される
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  // VSCodeのデフォルトのフォーマッターをprettierに指定
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  // ファイル保存時に自動でprettierが実行される
  "editor.formatOnSave": true
}

設定が効いているか試してみる

実際に自動修正がされるか試してみましょう。 まずはeslintの自動修正を確認します。確認のために一度、.eslintrc.jsonに下記を追加します。

.eslintrc.json

{
  "extends": ["next/core-web-vitals", "prettier"],
  "rules": { "quotes": "error" }
}

そして_app.tsxファイルで""''に変えてみましょう。 すると次のようなエラーになるはずです。

この状態でCommand + sでファイルを保存してみてください。 クォーテーションがダブルクォーテーションに修正されてエラーが消えたのではないでしょうか?

次にprettierの自動修正を試してみましょう。.eslintrc.jsonのrulesを削除して、今度は_app.texファイルで;を消してみましょう。

eslintのruleを追加していないのでエラーは出ていませんが、この状態でCommand + sでファイルを保存してみてください。 自動で;が補完されたのではないでしょうか。

prettierのルール設定

prettierのルールを別途設定したい場合は設定ファイルを作成することで変更が可能です。 例えばデフォルトの設定ではsemiがtrue、singleQuotesがfalseで指定されています。これらを変更したい場合はルートディレクトリにprettier.config.jsを用意して下記のように記述することで変更ができます。

module.exports = {
  singleQuote: true,
  semi: false,
}

この状態でいずれかのファイルを保存してみてください。 全てのセミコロンが取り除かれ、""''に変わったのではないでしょうか? コマンド一つで一気に修正できてしまうのは爽快ですよね。

その他のルールについても下記で確認ができます。

https://prettier.io/docs/en/options.html

おわりに

はじめに書きましたがeslint、prettierはフロント開発には必須です。個人開発をしているけどeslint、prettierをまだ使用していない方やこれから個人開発をする方の参考になれば幸いです。

eslintの設定ファイルに関するまとめ

はじめに

eslintの設定ファイルってかなりややこしいと思いませんか?
項目が多いので今までそれぞれが何を意味しているのかあまり深く考えずに使用してきました。
そんな設定ファイルについて簡単にまとめてみます。

ファイル形式

eslintの設定ファイルは.yml.json.js.cjsのいずれかを指定できます。 個人的にはコメントアウトが行えて馴染みのある.jsが好きです。

公式ドキュメントはこちら

v 9.0.0以降について

Warning We are transitioning to a new config system in ESLint v9.0.0. The config system shared on this page is currently the default but will be deprecated in v9.0.0. You can opt-in to the new config system by following the instructions in the documentation.

ドキュメントにも記載がありますがファイル形式はeslint.config.jsへ移行中でv 9.0.0からはeslint.config.jsを使用する予定とのこと。現在の最新版は8.4.4なので次のメジャーアップデートからはeslint.config.jsを使用することになりそうです。

eslint.config.jsの場合の記述方法についても既にドキュメントが用意されていました。

https://eslint.org/docs/latest/use/configure/configuration-files-new

sourceTypeがmoduleのプロジェクトではこちらが使用できるようですが、exportを使えるようになるのはいいですよね。

設定項目

それぞれの設定項目についてまとめます。

env

global変数を事前に読み込むための項目です。

https://eslint.org/docs/latest/use/configure/language-options#specifying-environments

下記の場合はブラウザで定義されている変数(documentなど)やES2021で用意されている変数をeslintが事前に読み込んでくれます。

"env": {
  "browser": true,
  "es2021": true,
 }

これだけではなんのこっちゃという感じなので例を挙げます。

後述するrurlesで"no-undef": "error"(未定義の変数をいきなり呼び出すとエラーになる)を指定している場合にenvのbrowserをtrueに指定していないとdocumentを呼び出した際にerrorが発生します。

eslintのエラー

指定できる項目は多いですがbrowser、esXXXX、jestなどが使用頻度として高いのではないでしょうか。

parser

parserは下記のいずれかを使用しているプロジェクトの場合に設定が必要になります。

それぞれに指定する値が決まっていて下記で確認ができます。

https://eslint.org/docs/latest/use/configure/parser

parserOptions

https://eslint.org/docs/latest/use/configure/language-options#specifying-parser-options

構文解析時のオプションを設定できます。 主に使用するものはecmaVersionsourceTypeです。

"parserOptions": {
  "ecmaVersion": "latest",
  "sourceType": "module"
}

ecmaVersion

eslintがES5がまでの構文しかサポートしていないため、ES5以降の構文を使用している場合にはecmaVersionの指定が必要です。基本的にlatestまたはenvで指定した同じバージョンを指定しておけば良い。

sourceType

import/export文を使用している場合はsourceTypeにmoduleを指定する必要がある。

plugins

rulesを拡張するための設定項目。 eslintの拡張パッケージをインストールした場合はこちらに追加していく。

例えばeslint-plugin-reactをインストールして使用する場合は下記のように記載する。

"plugins": ["react"]

eslintのpluginは基本eslint-plugin-xxxx命名されているのでpluginsへ追加する際はeslint-pluginの省略が可能。

extendsとの関係

後述するextendsに追加した項目から暗黙的にpluginsが追加されることもある。 eslint-plugin-reactはまさにその例でextendsにplugin:react/recommendedを追加した場合にpluginsへreactの追加は不要となる。

※npmのインストールは必要

"plugins": [],
"extends": ["plugin:react/recommended"] 

extendsと併せてpluginsへの追加が必要か不要かはpluginによって変わるため、pluginのドキュメントを確認する必要がある。 また、こういった背景からpluginsとextendsは混同しがちなので、それぞれの役割を下記にまとめます。

項目 意味
plugins rulesで使用できるruleを拡張するための設定。
extends eslintrcファイル自体を拡張するための設定。

extends

eslintrcファイル自体を拡張するための設定。 ここで追加された項目からeslintrcの各項目のデフォルト値が暗黙的に追加される。

例えばplugin:react/recommendedを追加した場合はparcerOptionsのecmaFeaturesが暗黙的に{ "jsx": true }になる。

"parserOptions": {
// 暗黙的に追加
//  "ecmaFeatures": { "jsx": true }
},
extends: ["plugin:react/recommended"]

extendsへの追加によって追加される項目については各pluginのドキュメントを確認する必要がある。

rules

eslintにwatchさせる項目を設定できる。 各ルールについては公式ドキュメントで確認ができます。

https://eslint.org/docs/latest/rules/

pluginで追加されるルールについては各pluginのドキュメントで確認ができます。

設定方法についても公式に詳細が記載されているので説明は割愛します。

さいごに

理解をややこしくしている原因は主に以下2点だと思いました。

  • pluginsとextendsの違いがわかりづらい
  • extendsの挙動(暗黙的に他の設定項目が追加されたりされなかったりする点)

また、pluginの数が多くてドキュメントを追いきれないのも理由としてあげられそうです。 概要だけ押さえてあとは都度、ドキュメントを確認するのが良いのではないかと思いました。

createContextを使用したグローバルステート管理の注意点⚠️

はじめに

7月3日にTasting Noteというサービスをリリースしまして、こちらのフロントエンドをReactで開発をしたのですが、グローバルステートの管理方法に問題があったので、その問題点と改善方法についてまとめたいと思います。

ステートは参照系と更新系で分けて管理しよう

まずは変更前のコードを記載します。

RequestingContext.ts

import { Dispatch, SetStateAction, createContext } from 'react'

const RequestingContext = createContext<{
  requesting: boolean
  setRequesting: Dispatch<SetStateAction<boolean>>
}>({
  requesting: false,
  setRequesting: () => {}
})

export default RequestingContext

RequestingProvider.tsx

import { FC, useMemo, useState, ReactNode } from 'react'

import { RequestingContext } from '../contexts'

const RequestingProvider: FC<{ children: ReactNode }> = ({ children }) => {
  const [requesting, setRequesting] = useState(false)

  const requestingState = useMemo(() => ({ requesting, setRequesting }), [requesting])

  return <RequestingContext.Provider value={requestingState}>{children}</RequestingContext.Provider>
}

export default RequestingProvider

RequestingProviderはAPIリクエスト中の状態管理をしており、APIリクエストを行うロジックの中でsetRequestingを呼び出すことで、リクエスト中の画面にLoadingコンポーネントを表示するといった使い方をしています。

問題点

requestingが更新された場合にsetRequestingしか呼び出していないコンポーネントも再レンダリングされてしまうことです。 例えば複数コンポーネントAPI通信を行う処理があり、その処理の中でsetRequestingを使用したいとします。setRequestingのみ必要なので、const { setRequesting } = useContext(RequestingContext)といった具合にsetRequestingのみを取り出しますが、この場合、requestingが更新されたタイミングでrequestingを呼び出していないにも関わらず、setRequestingを呼び出しているすべてのコンポーネントが再レンダリングされてしまいます。

理由は同じコンテキストでrequestingとsetRequestingを管理しているからです。

Reactコンポーネントが再レンダリングされる条件は主に以下です。

前述の例ではrequesting(ステート)が更新されたため、同じコンテキストで管理しているsetRequestingを呼び出しているコンポーネントも再レンダリングの対象になったということです。

不要な再レンダリングを起こすとサービスの規模が大きくなった場合にパフォーマンスへ大きな影響を及ぼすため、可能な限り避けるべきです。

解決方法

requesting、setRequestingを別のコンテキストで管理することで解決します。 先に変更後のコードを記載します。

RequestingContext.ts

import { createContext } from 'react'

const RequestingContext = createContext<boolean>(false)

export default RequestingContext

RequestingDispatchContext.ts

import { Dispatch, SetStateAction, createContext } from 'react'

const RequestingDispatchContext = createContext<Dispatch<SetStateAction<boolean>>>(() => {
  throw Error('No default value!')
})

export default RequestingDispatchContext

RequestingProvider.tsx

import { FC, useState, ReactNode } from 'react'

import { RequestingContext, RequestingDispatchContext } from '../contexts'

const RequestingProvider: FC<{ children: ReactNode }> = ({ children }) => {
  const [requesting, setRequesting] = useState(false)

  return (
    <RequestingContext.Provider value={requesting}>
      <RequestingDispatchContext.Provider value={setRequesting}>
        {children}
      </RequestingDispatchContext.Provider>
    </RequestingContext.Provider>
  )
}

export default RequestingProvider

requestingをRequestingContext、setRequestingをRequestingDispatchContextで管理するように変更しています。 こうすることでsetRequestingしか呼び出していないコンポーネントの不要な再レンダリングを防げます。

補足

例えばsetRequestingを呼びだす関数をRequestingProvider内で定義して、その関数をステートとして管理するケースを考えます。

RequestingDispatchContext.ts

import { Dispatch, SetStateAction, createContext } from 'react'

const RequestingDispatchContext = createContext<(function: () => void) => void>((() => {}) => {
  throw Error('No default value!')
})

export default RequestingDispatchContext

RequestingProvider.tsx

import { FC, useState, ReactNode } from 'react'

import { RequestingContext, RequestingDispatchContext } from '../contexts'

const RequestingProvider: FC<{ children: ReactNode }> = ({ children }) => {
  const [requesting, setRequesting] = useState(false)

  const startRequesting = (function: () => void) => {
    setRequesting(true)
    try {
      function()
    } catch {
      throw Error('Something went wrong.')
    } finally {
      setRequesting(false)
    }
  }

  return (
    <RequestingContext.Provider value={requesting}>
      <RequestingDispatchContext.Provider value={startRequesting}>
        {children}
      </RequestingDispatchContext.Provider>
    </RequestingContext.Provider>
  )
}

export default RequestingProvider

上記コードではrequestingが更新されるタイミングでstartRequestingが再生成されてしまうため、コンテキストを分けているにも関わらず、requestingが更新されるとstartRequestingを使用しているコンポーネントも再レンダリングの対象になってしまいます。

この問題の解決方法はuseCallbackを使用してstartRequestingの再生成を防ぐことです。

RequestingProvider.tsx

import { FC, useState, useCallback, ReactNode } from 'react'

import { RequestingContext, RequestingDispatchContext } from '../contexts'

const RequestingProvider: FC<{ children: ReactNode }> = ({ children }) => {
  const [requesting, setRequesting] = useState(false)

  const startRequesting = useCallback((function: () => void) => {
    setRequesting(true)
    try {
      function()
    } catch {
      throw Error('Something went wrong.')
    } finally {
      setRequesting(false)
    }
  }, [])

  return (
    <RequestingContext.Provider value={requesting}>
      <RequestingDispatchContext.Provider value={startRequesting}>
        {children}
      </RequestingDispatchContext.Provider>
    </RequestingContext.Provider>
  )
}

export default RequestingProvider

また、RequestingDispatchContextで管理する関数が複数になった場合はRequestingDispatchProviderへ渡すvalueがオブジェクトになるかと思いますが、この場合も対象のオブジェクトをuseMemoを使用して再生成を防がないと同様のことが発生します。

最後に

修正を行なった際のPRを掲載しておきます。

https://github.com/yuma-matsui/tasting_note_front/pull/388

自作サービスの規模であれば大きな問題にはなりませんが、これから仕事をする上でパフォーマンスを意識することは重要だと思い修正を行いました。 createContexを使用したステート管理は気をつけるべきことが多いため、状態管理ライブラリを使用することで、こういったことを意識せずにグローバルステートを使用するのが一番だと思います。 それをいってしまってはこの記事の立つ瀬がないのですが...。

React 〜制御・非制御コンポーネント〜

自作サービスの開発でフォームを使用したのですが、自分が実装したコンポーネントが制御か非制御かを全く理解せずに開発をしていたので、少しまとめてみようと思います。

一応公式でも説明されていますが内容が古く、コーディング例もクラスコンポーネントで書かれているので正直分かりづらいです。

非制御コンポーネント – React

制御 vs 非制御コンポーネント

この2つはReactにおいてフォームを扱うコンポーネントの分類。

制御コンポーネント

useState、onChangeを使用してフォームの入力値をstateで管理する。

const ControlledForm: FC = () => {
  const [inputValue, setInputValue] = useState<string>('');

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => setInputValue(e.target.value);

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log(inputValue);
    setInputValue = '';
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" value={inputValue} onChange={handleChange} />
      <input type="submit" value="確定" />
    </form>
  );
};

メリット

  • 入力に応じたバリデーションを設定するなど、リアクティブな実装が可能

デメリット

  • 入力値が変更される度に再レンダリングが発生するのでパフォーマンスが落ちる
  • 記述が多くなりがち

Input、Formのそれぞれ、引数のeに型を定義しないといけないのが少し手間で直感的に分かりづらいと思いました。 ですが、Reactに入門するとまずはこの方法でフォームの状態管理を説明しているものが多いと思います。

非制御コンポーネント

フォームの入力値をDOMで操作する。 onSubmit関数に型ComponentProps<"form">["onSubmit"]を指定することで引数eの型定義が不要になり、また、eからフォームの入力値へアクセス可能。

const UncontrolledForm: FC = () => {
  const handleSubmit: ComponentProps<"form">["onSubmit"] = (e) => {
    e.preventDefault();
    // 入力された値を取得
    console.log(e.currentTarget.text.value);
    e.currentTarget.reset()
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="text" />
      <input type="submit" value="確定" />
    </form>
  );
}

メリット

  • 記述量がグッと減る
  • レンダリングが発生しないので動きが軽くなる

デメリット

  • リアクティブなバリデーションが行えない

submitされないと入力値の参照ができないため、submitの前にバリデーションをかけることや、バリデーションにかかった場合にボタンを非活性にするといった処理が行えないのが少々痛い。

react-hook-form

https://react-hook-form.com/

フォームのバリデーションを簡単に実装できるnpm。 非制御コンポーネントが用いられているがリアクティブにバリデーションを行うことも可能。

制御・非制御コンポーネントかを意識せずに使用できるのでおすすめ。

まとめ

Reactの学習が不十分だったため、サービス開発中は制御コンポーネント、非制御コンポーネントという言葉すら知りませんでした...。 そしてreact-hook-formを使用していたので全く意識せずとも実装ができてしまっていました。

とりあえず動くものを作れるようになったので次はちゃんとReactを書けるようになりたい。 基礎概念の理解に努めようと思います。

テイスティングの記録が行えるサービス「Tasting Note」をリリースしました

はじめに

2023年7月3日に自作サービス Tasting Note をリリースしました!

Tasting Note Logo

サービスリポジトリ

https://github.com/yuma-matsui/tasting_note

この記事ではサービスの概要、開発を通して得た経験についてまとめています。

対象読者

  • 未経験からプログラマーへの転職を目指している方
  • 自作サービスの開発に取り組んでいる方
  • J.S.A.ソムリエ呼称資格認定試験 2次試験(テイスティング)対策をしている方

自己紹介

@yuma_matzui です。 昨年、2022年2月からFJOROD BOOT CAMP(フィヨルドブートキャンプ)(以下FBC)でWebプログラマーになるための学習をしています。

Tasting Noteのリリースをもって卒業となり、現在は就職活動中です。

Tasting Noteの紹介

Tasting Noteとは

ワインテイスティングの記録が行えるサービスです。 試験で使用されるマークシートを模した回答フォームを用意しており、 本番さながらのコメントシートでテイスティングの記録が行えます。

Tasting Sheet

こんな人に使って欲しい

  • ソムリエ呼称資格試験・2次試験の対策をしている方
  • ワインが好きな方
  • ブラインドテイスティングをされる方

試験対策のために記録をつけるもよし。
趣味で飲んだワインの記録をつけるもよし。
ワインバーで目の前のワインを評価するもよし。

右手にワイングラス、左手にスマートフォンをもって気軽にテイスティングの記録をしていただけたら嬉しいです。

使い方

登録をしないで使用

  • 「登録しないではじめる」を押下 instruction1

  • シート名、テイスティング時間、ワインの色を入力 instruction2

  • 外観、香り、味わい、まとめの順に回答を入力する(前へ戻ることも可能) instruction3

instruction4

instruction5

instruction6

  • 回答を確認
    instruction7

登録をして使用

  • 「サインアップ」を押下 instruction8

  • メールアドレス、パスワードを入力してアカウントの作成 instruction9

  • テイスティングをはじめる」を押下 instruction10

  • 「登録をしないで使用」同様のフローでテイスティングを記録

テイスティング記録後にワインの登録が行えます

  • 記録したテイスティングの個別ページ内「ワインの登録」を押下 instrocution7

  • ワインの情報を登録(ワイン名、ぶどう品種、生産国、ラベルの写真など) instrocution8

このサービスが解決できること

  • 記録を行う際に必要な紙、ペン、タイマーを準備する手間が省ける
  • 記録した紙を持ち運ぶ手間が省ける
  • 振り返りが簡単に行える

筆者がソムリエ資格を取得した際は自宅で紙とペン、スマートフォンのタイマーを使用して模擬試験を行いました。 本番も紙とペンを使用するのでそれが正しい姿ではありますが、以下のデメリットがありました。

  • コメントシートを毎回印刷しないといけない
  • 外出先で復習をしたい場合に記録した紙を持ち運ぶ必要がある

Tasting Noteではワインとグラスさえあれば手軽に記録が行えて、持ち運びにも困りません。 また、振り返りの際も絞り込み検索を使用することで目的の記録まであっという間に辿り着けます。

技術スタック

  • フロントエンド
    • React 18.2.0
    • TypeScript 4.9.3
  • バックエンド
  • インフラ
    • Terraform 1.4.6
    • AWS
      • S3(Web Hosting)
      • Cloudfront
      • Route53
      • ACM
      • S3(画像保存)
      • ECR
      • ECS
      • ALB
      • RDS
  • CI/CD
  • 開発環境
    • Docker 23.0.5
    • docker-compose 2.17.3

開発中にやって良かったこと

もくもく自作サービス開発会

FBCではチャットツールにDiscordを使用しており、様々なチャンネルが存在しているのですが、「もくもく自作サービス開発会」というチャンネルを自ら立ち上げて、自作サービス開発のプラクティスに取り組む他のFBC生と一緒に開発を行いました。 作業中は特に会話はありませんが、インターバル中に作業内容や詰まったポイントを共有することで、サービス開発に対するモチベーションを高く維持できました。

もくもく自作サービス開発会 - Speaker Deck

こちらについてはFBC内のイベントにて登壇、発表も行いました!

開発後半は中々実施をすることができず、一人での開発がほとんどになってしまいましたが、今振り返るとやはり仲間と時間を共有していた時の方が開発スピード、モチベーション共に高かったように思います。

また、就職に向けた情報・意見交換ができたのがとても良かったです!

Issueをできるだけ小さくした

カンバン

https://github.com/users/yuma-matsui/projects/11

Issueを立てる際は細かいTaskに切り分けることを意識しました。 例えば「ログインページを実装する」というIssueがあった場合は以下のように切り分けます。

Taskを細分化することのメリット・デメリットは以下です。

メリット

  • Issueをcloseする機会が多くなりモチベーションが上がる
  • 作業時間が見積りやすくなる
  • 詰まった場合でも自分が何に詰まっているのか特定しやすい
  • 次にやることが明確で迷わない
  • PRが小さくなりやすいのでレビューの負担を減らすことができる

デメリット

  • Issueを立てるのに時間がかかる
  • ぱっと見のTask数が膨大なので先が思いやられる

自作サービス開発はとにかく長い旅路です。 地図は少しでも細かい方がゴールまでのイメージが湧きやすく、モチベーションを高く維持できるのでおすすめです!

また、今回は基本セルフレビューでしたが、PRを作った際には必ず変更を一つ一つ確認していたので、Taskを小さく分けることで変更があまり多くならず、レビュー時の負担が小さくなったのも良かったと思います。

インフラ構築

今回のサービス開発で一番楽しかったことがインフラ構築です。 Tasting NoteのインフラはAWS環境をTerraformで構築しています。

AWSは料金が割高なので、自作サービスには敬遠されがちですが、前から興味があり、また、IaCにも興味があったのでこの機会に頑張ってみようと採用しました。

インフラ構成図

infrastructure map

インフラをコードで管理するメリット

  • 再利用が可能になる
  • 高速に構築・変更が行える
  • コードレビューを行うことでミスなく構築・変更が行える

実装をしてみて一番強く感じたメリットはやはり構築の速さです。

今回筆者はステージング環境をマネジメントコンソールから作成し、本番環境をTerraformで構築するという手順を取りました。 いきなりTerraformで本番環境を構築するのはハードルが高いと感じたためです。 その過程でマネジメントコンソールからの構築よりもTerraformでの構築の方が圧倒的にスピードが速いことを体感しました。

コストの観点から本番環境構築後にステージング環境を手作業で削除しましたが、これにも少なからず時間がかかり、また、漏れなく不要なリソースを削除することに多少なりともストレスを感じました。 Terraformでリソースを管理していれば不要になったリソースの削除もコマンド1つで済むので、こういったストレスからも解放されます。

また、ステージング環境を再度構築したいと思った場合は本番環境を再利用することであっという間に環境が構築できてしまいます。

インフラ構築において最も大事だと思うこと

インフラ構築で最も大事なことは設計ではないでしょうか。

インフラをコード化する作業はテストコードを書くことに似ています。 インフラ構成をドキュメントとして残す作業なので、設計が固まっていないと手が動きません。

この点はテスト駆動開発をする際にある程度の設計が固まっていないとテストが書けない点と似ていると感じました。

また、Terraformではリソースの依存関係があり、これを把握していないとリソースが構築できません。 設計ができている状態であればそれぞれのリソースの依存関係も把握できている状態なので詰まることなく実装を進められるはずです。

FBC生の自作サービスリポジトリを見る

自分と似通った技術スタックを採用しているサービスのリポジトリを確認することで、実装のイメージをもつことができたり、便利なnpmを発見できたりと、とても有益な情報を得ることができました。

仕事の際はオープンソースや他の人のコードを読む機会が多いと聞くので、人のコードを読むことに少しでもなれることができたのは良かったと思います。

開発中に苦労したこと

フロントエンドのビルド時間がとにかく遅い

create-react-appはビルドにとても時間がかかるので開発中のストレスが大きかったです。 また、今回は開発環境にDockerを使用しており、バックエンドとの連携を行う場合はdocker-composeで複数コンテナを起動する必要があったため、CPUへの負荷がさらに掛かり最悪でした...。 サーバー起動後にコードを変更した場合も、変更が反映されるまでの時間がとにかく遅く、開発体験はあまりよくなかったように思います。

開発後半にViteの存在を知りましたが移行するにはハードルが高く、今回は諦めてしまいました。

後述の「開発中にやっておけばよかったと思うこと」でも述べておりますが、技術調査を事前に行なっていれば防げた問題なので技術選定時はその時に流行っているものを一通り確認するなど、調査を怠らないようにしたいです。

コンポーネント単体テストの数がものすごかった...

今回の開発はまずは動くものをつくって最後にテストを書く、テストラストで進めました。 バックエンドはそれで全く問題なかったのですが、フロントエンドはコンポーネント、カスタムフックの量がとても多く、テストを書くことにとても根気が必要でした。

テストを書き始めた時は楽しかったのですが、何日も同じ作業となると段々と辛くなり... モチベーション管理の観点からも1つのコンポーネントを実装する毎にテストも書いておけば良かったと反省しました。

開発中にやっておけば良かったと思うこと

技術ブログの更新

自作サービス開発では開発環境の構築や本番環境へのデプロイ、新しく使用するnpmやgemなど、何かを学びながら即実装の連続でした。 こういった新しく触れる技術については開発を一時中断してでも技術ブログにまとめておけば良かったと後悔しています。

理由は実装を行ったその時が一番その技術に詳しくなっている状態だと思うからです。

とは言え全くできなかったわけではなく、開発初期にまとめたものもいくつかあります。

アウトプットまで行って初めて知識という自分の血肉になるということをFBCでの学習を通して学んだので、今後はマメにブログや記事にまとめていきたいと思います。

念入りな技術調査

技術調査が不十分であったという話です。

create-react-app(以下CRA)について、ビルド時間がとても遅いと前述しましたが、CRAの開発者も将来的に非推奨になる可能性を含め、CRAの今後について下記にて触れています。

Replace Create React App recommendation with Vite by t3dotgg · Pull Request #5487 · reactjs/react.dev

普段からこういった技術的なトレンドにアンテナを貼っておけば、開発初期の段階でViteを導入することを検討できましたし、そもそもNext.jsを使ってしまおうという選択もできました。

もちろん調査に時間を使い過ぎるのもよくないと分かっていますが、今回の課題は調査の時間を十分に確保しなかったことより、最新技術の情報収集に対するアンテナが低かったことだと考えています。

情報収集力もプログラマーにに求められる重要なスキルの1つだと思うので、公式サイトのブログを購読したり、発信力のあるプログラマーTwitterでフォローするなど、常に最新の情報を取得できる環境に身を置くようにします。

サービスの改善点

認証周り

Tasting Noteは認証にFirebase Authenticationを採用しており、メールアドレスとパスワードでの認証を行なっているのですが、これをGoogle認証に切り替えたいと考えています。

このサービスのコンセプトが手軽にテイスティングを記録できることであり、登録の手間を極力減らしたいと考えたためです。

そもそも何故、Googleログインを採用していないのかというと、iOS16.1Safari16.1など、主要なブラウザ、OSでサードパーティクッキーがブロックされる仕様になっており、セッション管理が行えず、ログイン状態が保持できないというバグが発生したためです。

https://github.com/firebase/firebase-js-sdk/issues/6636

サードパーティークッキーのブロックについて簡単にまとめると、同一ドメインでないサイトからのクッキーを全てブロックするというものです。

解決方法についてはこちらの公式ドキュメントに明示されているのですが、サービスの仕様上、いずれも採用ができず、やむを得ずメールアドレスとパスワードでの認証を採用しました。

唯一、解決策として採用できそうなのが、アプリケーションサーバーにプロキシを設定して、特定のパスにリクエストが来たらFirebaseの認証ドメインへリクエストを転送させるという方法でした。 これであればサードパーティークッキーと判断されず、ブロックを免れることができます。 (公式ドキュメントの緩和策3に当たる。)

ですが、Tasting NoteはSPAを採用しており、S3のWebHostingを利用して動かしているため、プロキシの設定が行えませんでした。

また、バックエンドはECSを採用しており、ECSの前段にサービスのサブドメインを割り当てたALBを配置しているので、Railsが動いているTask内にNginxコンテナを用意して、特定のパスへのリクエストが来たらFirebaseの認証ドメインへリクエストを転送させるという試みもしましたが、これもうまくいきませんでした...。

開発に費やせる時間も限られていたため、今回はメールアドレスとパスワードでの認証という消極的な選択肢をとってしまいましたが、これを改善したいと考えています。

CRAではなく、Next.jsを採用していた場合はNextが動くサーバーにプロキシの設定を行えば簡単に解決ができるので、Next.jsへの移行を検討しています。

Next.jsの導入

CRAからNext.jsへの移行をしたいと考えています。 コードの量がそこそこあるので根気が入りますが、前述のCRAと認証の課題を踏まえ、また、最近とても流行っているということも鑑みて学習後のアウトプットとして挑戦したいと思っています。

今すぐにとはいきませんが、就職活動が落ち着き、Next.jsの基礎学習が済んだら始めてみようと思います。

さいごに

これまでサポートをしてくださった@komagataさん、 @machidaさん、FBCメンターの方々、切磋琢磨してくれたFBC生徒の皆さん、本当にありがとうございました。

不思議と途中で諦めそうになることはなく、必ずやり切るという強い思いでここまできましたが、その思いを持ち続けることができたのはひとえに心強いメンターの皆様と同じ強い思いを持つ生徒の皆さんの存在があったからだと言えます。

FBCで皆さんから学ばせていただいた集大成として自分のキャリアを活かしたサービスをリリースできたことは大きな自信になりました。

この自信をもって一人前のWEBプログラマーになれるように引き続き切磋琢磨していきたいと思います。

大変長くなりましたが最後までお付き合いいただきありがとうございました。