Michi's Tech Blog

一人前のWebエンジニアを目指して

ブログ更新終了のお知らせ

こんにちは!
スマレジ テックファームのMichiです!

タイトルの通り、この投稿をもちまして、当ブログの更新を終了することになりました。

理由は、私が今年いっぱいで株式会社スマレジを退職するためです。このブログは、一応会社の名前を使用して書いていたものですので、退職に伴いブログも更新終了する運びとなりました。

とはいいつつ、このような発信の場は持ち続けていたいので、今後別のプラットフォームでブログは継続する予定です。次はNext.jsとmicroCMSを使って、一からブログを自作してみたいなと考えています。

スマレジではエンジニア未経験から採用していただき、1年8カ月お世話になりました。この間、様々な経験を積ませていただき、会社へは本当に感謝しています。

今後は、フリーランスのWebエンジニアとして活動予定です。

最後にはなりますが、このブログを開設してから約1年、記事を読んでいただいた読者の皆さま、本当にありがとうございました。

Michi

Fastify × Zod × SwaggerでOpenAPIの仕様書を自動生成する

こんにちは!
スマレジ テックファームのMichiです!

今回はFastifyでREST APIを開発するときに、自分でYAMLファイルを定義せずとも、自動でOpenAPI形式の仕様書を生成する方法を解説します。

技術スタック

今回、主に使う技術は次の3つです。

  • Fastify ... 高速なレスポンスを誇るNode.jsのWebフレームワーク
  • Zod ... TypeScriptのスキーマ・バリデーション定義のライブラリ。今回はFastifyの拡張ライブラリであるfastify-zodと合わせて使用。
  • Swagger ... API開発の支援ツールで、OpenAPI形式の仕様書を出力できる。生成された仕様書から、実際にリクエストを送ってテストすることもでき、Postmanような使い方が可能。

今回使用した環境・バージョンは以下の通りです。

node: v18.16.1
typescript: 5.3.3
fastify: 4.24.3
zod: 3.22.4
fastify-zod": 1.4.0
fastify/swagger: 8.12.1
fastify/swagger-ui: 2.0.1
@types/node: 20.10.4
ts-node-dev: 2.0.0

実装

プロジェクの作成

以下のコマンドで必要なパッケージのインストールとtsconfig.jsonファイルの作成を行います。

npm init -y
npm i @fastify/swagger @fastify/swagger-ui fastify fastify-zod zod
npm i -D @types/node ts-node-dev typescript
npx tsc --init

package.jsonscriptsのセクションを更新します。

{
  "name": "fastify-sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "tsnd --respawn --transpile-only --exit-child ./src/bootstrap.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@fastify/swagger": "^8.12.1",
    "@fastify/swagger-ui": "^2.0.1",
    "fastify": "^4.24.3",
    "fastify-zod": "^1.4.0",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/node": "^20.10.4",
    "ts-node-dev": "^2.0.0",
    "typescript": "^5.3.3"
  }
}

tsndts-node-devエイリアスです。よって、tsnd --respawn --transpile-only --exit-child ./src/bootstrap.ts./src/bootstrap.tsを実行するコマンドになります。(このファイルは後ほど作成)

エンドポイントの作成

先にエンドポイントの実装から行います。

今回は簡単なメモアプリのAPIを作成することを想定します。./src/app/noteというディレクトリを切り、その中で次のファイルをそれぞれ作成します。

schema.ts

メモ取得と作成のスキーマ定義です。

import { z } from "zod";

export const noteSchema = z.object({
  title: z.string().min(1).max(20).describe("件名"),
  content: z.string().min(1).max(100).describe("内容"),
  status: z.enum(["todo", "doing", "done", "cancel"]).describe("ステータス"),
});

export const postResultSchema = z.object({
  isSuccess: z.boolean(),
  message: z.string(),
});

export const searchNoteQuerySchema = noteSchema
  .omit({ content: true })
  .partial();
export const noteListSchema = z.array(noteSchema);

export type GetNoteRequest = z.infer<typeof searchNoteQuerySchema>;
export type GetNoteResponse = z.infer<typeof noteListSchema>;

export type CreateNoeRequest = z.infer<typeof noteSchema>;
export type CreateNoteResponse = z.infer<typeof postResultSchema>;

export const NoteSchemas = {
  searchNoteQuerySchema,
  noteListSchema,
  noteSchema,
  postResultSchema,
};

controller.ts

GETとPOSTの処理を実装します。

今回はDBを作っていないので、dataという配列の中にメモデータを格納します。

import { FastifyRequest, FastifyReply } from "fastify";
import { GetNoteRequest, CreateNoeRequest, GetNoteResponse } from "./schema";

const data: GetNoteResponse = [];

export const getNoteHandler = async (
  request: FastifyRequest<{ Querystring: GetNoteRequest }>,
  reply: FastifyReply
) => {
  const { title, status } = request.query;
  const note = data.filter((d) => {
    if (title && status) {
      return d.title === title && d.status === status;
    } else if (title) {
      return d.title === title;
    } else if (status) {
      return d.status === status;
    } else {
      return true;
    }
  });

  reply.code(200).send(note);
};

export const createNoteHandler = async (
  request: FastifyRequest<{ Body: CreateNoeRequest }>,
  reply: FastifyReply
) => {
  data.push(request.body);
  reply
    .code(200)
    .send({ isSuccess: true, message: "メモの登録が完了しました。" });
};

route.ts

ルーティングを登録します。

今はまだ、Fastifyの中でZodを使えないので型エラーが発生します。これは後ほど解消します。

import { FastifyInstance } from "fastify";
import { getNoteHandler, createNoteHandler } from "./controller";

export const createNoteRoutes = async (server: FastifyInstance) => {
  server.zod.get(
    "/note",
    {
      operationId: "getNote",
      querystring: "searchNoteQuerySchema",
      response: {
        200: {
          description: "メモ取得成功",
          key: "noteListSchema",
        },
      },
      tags: ["Note"],
    },
    getNoteHandler
  );

  server.zod.post(
    "/note",
    {
      operationId: "createNote",
      body: "noteSchema",
      response: {
        200: {
          description: "メモ作成成功",
          key: "postResultSchema",
        },
      },
      tags: ["Note"],
    },
    createNoteHandler
  );
};

index.ts

export用にindex.tsファイルを作成します。

export * from './route';
export * from './schema';

基盤の作成

先ほどでnoteのエンドポイントは完成しましたが、Fastify側の基盤の作成をしていないのでまだ動きません。この項で、基盤の作成、及びZod・Swaggerとの連携をやっていきます。

.src/app/patch.ts

API全体に適用するスキーマを登録します。今回は先ほど作成したNoteSchemasのみになります。

import { NoteSchemas } from "./note";

export const patchSchemas = {
  ...NoteSchemas,
};

.src/app/register.ts

API全体に適用するルーティングを登録します。今回は先ほど作成したcreateNoteRoutesのみになります。

import { FastifyInstance } from "fastify";
import { createNoteRoutes } from "./note";

export const registerRoutes = async (server: FastifyInstance) => {
  await server.register(createNoteRoutes);
};

.src/app/index.ts

export用にindex.tsファイルを作成します。

export * from './patch';
export * from './register';

./src/bootsstrap.ts

npm run devしたときに最初に起動するファイルです。

ここで、先ほど登録したスキーマとルーティングをFastifyInstanceに紐づけます。

また、Fastifyの中でZodのスキーマ定義を使用するには、FastifyInstanceの型をFastifyZodで拡張してやる必要があります(7~11行目)。これで、src/app/note/route.tsの型エラーが解消されます。

import Fastify, { FastifyInstance } from "fastify";
import { FastifyZod, buildJsonSchemas, register } from "fastify-zod";
import { patchSchemas, registerRoutes } from "./app";

export const server: FastifyInstance = Fastify({ logger: true });

declare module "fastify" {
  interface FastifyInstance {
    readonly zod: FastifyZod<typeof patchSchemas>;
  }
}

const swaggerSetting = {
  swaggerOptions: {
    openapi: {
      info: {
        title: "API docs",
        version: "1.0.0",
      },
    },
  },
  swaggerUiOptions: {
    routePrefix: "/api-docs",
    staticCSP: true,
  },
};

const bootstrap = async () => {
  await register(server, {
    jsonSchemas: buildJsonSchemas(patchSchemas),
    ...swaggerSetting,
  });

  await registerRoutes(server);

  try {
    await server.listen({
      port: 8080,
    });
    console.log(`Server listening on localhost: 8080`);
  } catch (err) {
    server.log.error(err);
    process.exit(1);
  }
};

bootstrap();

完成形

最終的にはこのようなディレクトリ構成になります。

.
├── node_modules
├── src
│    ├── app
│    │    ├── note
│    │    │    ├── controller.ts
│    │    │    ├── index.ts
│    │    │    ├── route.ts
│    │    │    └── schema.ts
│    │    ├── index.ts
│    │    ├── patch.ts
│    │    └── register.ts
│    └── bootstrap.ts
├── package.lock.json
├── package.json
└── tscofig.json

確認

npm run devでサーバーを立ち上げます。

localhost:8080/api-docsをブラウザで開くと、Swaggerの画面が表示されます。

ここから、各エンドポイントのインターフェースの確認や、実際にリクエストを送信してテストすることが可能です。

TypeScriptのthisについての忘備録

こんにちは!
スマレジ テックファームのMichiです!

TypeScriptのthisは不思議な動きをすることで有名です。自分も定期的に調べては忘れてしまうので、今回は忘備録として残しておきます。

thisとは?

まず前提として、thisとは何かについて説明しておきます。

thisとは自分自身を表すオブジェクトです。次のコードを見てください。

class User {
  name: string;
  #age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.#age = age;
  }

  public isAdult() {
    return this.#age >= 20;
  }
}

const mike = new User("Mike", 26);
console.log(mike.isAdult()); // 結果:true

上記のコードはconsole.logmike.isAdult()の実行結果を出力しています。

isAdult()メソッドの返り値はreturn this.#age >= 20;です。このとき、thisは呼び出し元のオブジェクトになります。

具体的には、今回の例ではmike.isAdult()という形でメソッドを読んでいるので、thismikeになります。mike#age = 26というプロパティを持つUser型のインスタンスなので、return this.#age >= 20;の結果はtrueとなります。

よって、mike.isAdult()の実行結果もtrueとなるわけです。

呼び出し方によってthisの値が変わる?

これだけならば簡単なのですが、そうはいかないのがTypeScriptのthisがややこしいところです。

次のコードを見てください。

class User {
  name: string;
  #age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.#age = age;
  }

  public isAdult() {
    return this.#age >= 20;
  }
}

const mike = new User("Mike", 26);
const isAdult = mike.isAdult;

console.log(isAdult()); 
// ランタイムエラー: Cannot read private member from an object whose class did not declare it

先ほどとは違い、mike.isAdultをいったんisAdultに代入してから呼び出しています。

いったん代入しただけで、処理は変えていないので結果は変わらないと思えそうですが、実際はランタイムエラーが発生していまいます。

実は、メソッド記法(オブジェクト.メソッド名()の形)を使わずに関数が呼び出された場合、その中でのthisの値はundefinedになります。*1なぜなら、今回isAdult()はグローバルスコープから呼び出されたので、参照するオブジェクトが存在しないからです。

thisの値がundefinedである以上、 return this.#age >= 20;の結果は当然エラーです。よって、上記のコードを実行しても、ランタイムエラーとなるわけです。

このことからわかるように、thisは関数の呼び出し方によって参照が変わる」という少し厄介な性質を持っています。今回のようなエラーを避けるために、thisを使うオブジェクトのメソッドは原則として、メソッド記法で呼んだ方がよいでしょう。

アロー関数はthisを持たない

ここまで書いてきて、一つ例外があります。それはアロー関数を使用した場合です。

次のコードを見てください。

class User {
  name: string;
  #age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.#age = age;
  }

  public isAdult = () => {
    return this.#age >= 20;
  };
}

const mike = new User("Mike", 26);
const isAdult = mike.isAdult;

console.log(isAdult()); // 結果:true

isAdultの定義にアロー関数を使用した以外は、先ほどのコードと同じです。よって、実行結果も同じくランタイムエラーとなりそうな気がします。

ところが、このコードは正しく動作します。これは、「アロー関数は自分自身のthisを持たない」という特殊な性質によるものです。

自分自身のthisを持たないならば、アロー関数はどこからthisの参照を引っ張ってきているのでしょうか?

この場合、アロー関数はthisを外側のスコープから引き継ぎます。上記の例において、isAdultUserクラスのスコープに属しています。よって、このthisは、Userから生成されたインスタンス(オブジェクト)となり、ランタイムエラーとはならないわけです。

アロー関数は通常の関数と異なり、呼び出し方によってthisの参照が変化しません。アロー関数を定義した段階で、thisが何であるかは決まっているのです。

このことからも、普段から通常の関数よりもアロー関数を使うほうがよいといえます。

参考

プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで (Software Design plus) | 鈴木 僚太 |本 | 通販 | Amazon

*1:JavaScriptのstrictモードがOFFの場合は、thisはグローバルオブジェクトとなります。TypeScriptでは、原則すべてstrictモードで実行されるので、undefinedになると覚えておいて問題ありません。

React Hook Formでコンポーネントを分割する方法

こんにちは!
スマレジ テックファームのMichiです!

今回はタイトルの通り、React Hook Formでコンポーネントを分割する方法をご紹介します。

解説

サンプル

サンプルとして、 メールアドレス、ユーザー名、パスワードを入力するシンプルな登録フォームを用意しました。それぞれのフィールドには、React Hook Form で必須入力のバリデーションをかけています。

SignUp.tsx

import { useForm } from "react-hook-form";

type FormData = {
  email: string;
  username: string;
  password: string;
};

export const SignUp = () => {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm<FormData>();
  const onSubmit = (data: FormData) => console.log(data);

  return (
    <>
      <h1>登録フォーム</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div className="field">
          <label htmlFor={"email"}>メールアドレス</label>
          <input id="email" {...register("email", { required: true })} />
          {errors.email && (
            <span className="error">このフィールドは必須です</span>
          )}
        </div>

        <div className="field">
          <label htmlFor={"username"}>ユーザー名</label>
          <input id="username" {...register("username", { required: true })} />
          {errors.username && (
            <span className="error">このフィールドは必須です</span>
          )}
        </div>

        <div className="field">
          <label htmlFor={"password"}>パスワード</label>
          <input
            id="password"
            {...register("password", { required: true })}
            type="password"
          />
          {errors.password && (
            <span className="error">このフィールドは必須です</span>
          )}
        </div>

        <button className="button" type="submit">
          登録
        </button>
      </form>
    </>
  );
};

これら3つのフィールドは同じ作りなので、別コンポーネントに切り出して抽象化したいです。

抽象化

まず、SignUp.tsxを次のように修正します。

SignUp.tsx

import { FormProvider, useForm } from "react-hook-form";
import { InputField } from "./InputField";

export type FormData = {
  email: string;
  username: string;
  password: string;
};

export const SignUp = () => {
  const methods = useForm<FormData>();
  const onSubmit = (data: FormData) => console.log(data);

  return (
    <>
      <h1>登録フォーム</h1>
      <FormProvider {...methods}>
        <form onSubmit={methods.handleSubmit(onSubmit)}>
          <InputField name="email" labelText="メールアドレス" />
          <InputField name="username" labelText="ユーザー名" />
          <InputField name="password" labelText="パスワード" type="password" />
          <button className="button" type="submit">
            登録
          </button>
        </form>
      </FormProvider>
    </>
  );
};

InputFieldは各フィールドを抽象化したコンポーネントで、次に説明します。

ここでのポイントは、FormProviderでフォーム全体をラップすることです。propsには、useFormから取り出したmethodsを丸ごと渡します。

こうすることで、各InputFieldへ個別にpropsを渡さなくても、それぞれのコンポーネントmethodsを利用できるようになります。

次に、InputField.tsxの実装を確認します。

InputField.tsx

import { useFormContext } from "react-hook-form";

type InputFieldProps = {
  name: string;
  labelText: string;
} & JSX.IntrinsicElements["input"];

export const InputField = ({ name, labelText, ...others }: InputFieldProps) => {
  const {
    register,
    formState: { errors }
  } = useFormContext();

  return (
    <div className="field">
      <label htmlFor={name}>{labelText}</label>
      <input id={name} {...register(name, { required: true })} {...others} />
      {errors[name] && <span className="error">このフィールドは必須です</span>}
    </div>
  );
};

先ほど、親コンポーネントからFormProvidermethodsを受け取っているので、 組み込み関数のuseFormContextを呼び出すだけで、methodsの各プロパティが利用できます。

あとは、<label><input>要素に適用していくだけです。

propsのothersには、JSX.IntrinsicElements["input"]各属性が入るので、親から属性値を渡すことで<input>フォームをカスタマイズできます。

おわりに

React Hook Formでコンポーネントを分割する方法をご紹介しました。

React Hook Formは便利なライブラリですが、学習コストがなかなか高いので、しっかり使いこなすには勉強が必要だなと思いました。

1年間ブログを継続して良かったこと・悪かったこと

こんにちは!
スマレジ テックファームのMichiです!

2022年11月にこのブログを開設し、それ以来、1週も欠かすことなく更新してきました。そして、今月末でブログを始めてからちょうど丸1年となります。

そこで、今回の記事では「1年間ブログを継続して良かったこと・悪かったこと」を振り返ってみたいと思います。

良かったこと

周りからの評価が上がる

ブログを書く1番のメリットはこれだと思います。

自分はSESをやっていて、案件面談のスキルシートにこのブログのリンクをいつも載せています。

そうすることで、この人は「技術に興味がある人」「自己研鑽に積極的な人」なんだなという印象を持ってもらうことができます。

実際に、今年の7月からお世話になっている今の現場では、ReactもTypeScriptも未経験の状態でしたが、このブログに自主学習で学んだ内容を載せていたおかげで、契約をゲットすることができました。

インフラ系の職種であれば、「資格を取る」という手法もありますが、アプリケーションエンジニアの場合だと資格でアピールするというのは難しい面もあります。

そのときに、代わりにブログを見せることで、自分のエンジニアとしてのレベル感や、学習意欲をアピールすることができます。

意外とアウトプットしている人は少ないので、これだけでも他候補と差別化することができると感じています。

アウトプットすることで記憶に定着する

これもブログをやって良かったなと思う部分の、かなり大きなウエイトを占める点です。

新しい技術や知識を学んだとき、本や動画でインプットするだけでは、記憶としてなかなか定着しません。ブログのような場所でアプトプットすることで、真に記憶に定着するということはよく言われています。

とくに、ネットに公開する以上、「間違ったことは書けない」というプレッシャーから、公式ドキュメントなどを読んで深堀りする機会が増えたのは、自分にとって非常に良かったと思っています。

アウトプットの方法はブログに限らず、「LTをする」「人に教える」「何か作ってみる」などの方法があります。

これらの中でブログの良いところは、「ひとりで気軽に始められる」ところです。

「LTをする」「人に教える」といった方法は、当然ひとりではできません。また、「何か作ってみる」というのも、人によっては結構心理的ハードルが高かったりするのではないでしょうか。

その点、とりあえず何か書いて公開すれば形になるブログは、割ととっつきやすいのではないかなと思います。自身のブログサイトを持っていなくても、QiitaやZennもありますからね。

文章力が上達する

これは、ブログをはじめたときにはとくに意識していなかったのですが、副産物として「文章力が上達したな」と感じています。

やはり、ネットに公開する以上、「変な文章は書けない」というプレッシャーがあります。また、せっかく技術的には正しいことを書いても、「伝わらなければ意味がない」という思いもあり、「わかりやすい文書を書くにはどうすればよいのか?」と考える機会が増えました。

(先日のブログでも、文章術の本を読んだ感想を書いたりしました。)

また、文章を書くスピードについても、格段に上がったと感じています。

ブログをはじめたころは、一つの記事を仕上げるのに丸1日かけていました。今では、ガッツリとした技術記事でも半日、このような振り返り系の記事であれば、2~3時間程度で書くことができます。

このスキルは、リモートワークでテキストコミュニケーションするときなどに、活かされているなと感じます。

悪かったこと

時間を消費する

先にも書きましたが、記事を書くのにはとにかく時間がかかります。

慣れてきた今でも、ちゃんとした技術記事を書こうと思ったら、調べる時間を含めて半日は確保しなければなりません。 (というより、記事を書いている時間よりも、調べている時間の方が圧倒的に長いです。コーディングと同じですね。)

とくに自分は毎週更新してるので、忙しくて時間が取れない週などはキツかったです。

2、3回くらい「今週は投稿やめとくか~」と思った週もありましたが、なんとか1年間続けることができました。

儲からない

ぶっちゃけると儲かりません(笑)

弊社では、「ブログ手当」というものがあり、会社の名前を使ってブログを書くと、1万円/月もらえます。

ただし、1万円もらうためには毎週更新しなけばなりません。1記事平均4時間で書けるとしても、4週分で16時間必要です。

これを時給計算すると、「625円/時」になります。

お金が欲しいなら、副業やアルバイトするほうがよっぽど効率的です。

また、一般の人は当然ブログ手当なんてものはありませんし、広告を付けてアフィリエイト化するとしても、ほとんどのテックブログはサーバー代でマイナスだと思います。

ただし、ブログを書くことによって、よって良い案件が取れたり、転職先が見つかるということは大いにあるので、長い目で見ると、どこかで回収できるとは思います。

さいごに

今回は、「1年間ブログを継続して良かったこと・悪かったこと」というテーマで書きましたが、総評すると、「ブログをやっていて非常に良かった」と思います。

とくに、自分のような経験の浅いエンジニアは、「実務経験」という指標では、シニアエンジニアに絶対に勝てません。

その分、「成長性」や「積極性」といったもので差別化していく必要がありますが、ブログでアウトプットすることは、その指標を補完する意味で非常に有益だったと感じています。

さて、業務連絡ですが、2023年11月からこのブログは隔週更新となります。

この1年間アウトプットし続けることで、自分も力がついてきたこともあり、来月以降は少し他のことに時間を使いたいという事情からです。

読者の皆さまにおかれましては、今後ともよろしくお願いいたします。

【書評】『「文章術のベストセラー100冊」のポイントを1冊にまとめてみた。』を読んでみた

こんにちは! スマレジ テックファームのMichiです!

『「文章術のベストセラー100冊」のポイントを1冊にまとめてみた。』という本を読んでみました。今回の記事はその書評になります。

本の概要

本書は、現役ライターの筆者2名が名著100冊を読み込み、文章のプロが持つ共通のノウハウを洗い出し、ランキング化したのものです。

  • 1~7位 「すべての人に身に着けてほしい7つの基本ルール」
  • 8~20位 「ワンランク上の文章を書くための13のポイント」
  • 21~40位 「気を付けるとさらに文章がよくなる20の秘訣」

といった具合で、本書では合計40個の「わかりやすい文章を書くコツ」を紹介しています。

「文章術のベストセラー100冊」のポイントを1冊にまとめてみた。 | 藤吉 豊, 小川 真理子 |本 | 通販 | Amazon

自分もこうしてブログを書いており、またリモートワークでテキストコミュニケーションをする機会も多いので、「もっとわかりやすい文章を書くにはどうすればよいのか?」と考える機会は多いです。

そういうわけで、本書を読んでみることにしました。

自分なりに気になったランキングの要約

1~40位までのすべてのランキングを紹介することはできないので、今回は自分が気になった・ためになった項目をピックアップしてお伝えします。

1位 文章はシンプルに

「シンプルに書く」とは、「なくても意味が通じる言葉を削る」ということです。

では、「なくても意味が通じる言葉」とは、どのようなものでしょうか?

  1. 接続詞 ... 「そして」「しかし」「だから」など
  2. 主語 ... 「私は」「彼が」など
  3. 指示語 ... 「その」「それは」「これは」など
  4. 形容詞 ... 「高い」「美しい」「楽しい」「嬉しい」など
  5. 副詞 ... 「とても」「非常に」「すごく」「かなり」など
  6. 意味が重複する言葉 「まず最初に → 最初に」「思いがけないハプニング → ハプニング」「余分なぜい肉 → ぜい肉」など

また、一文の長さを60文字以内にすることも、文章をシンプルにするテクニックです。60文字を超えそうな場合は、一度「。」で文書を区切り、その後に改めて続きの文章を書きます。

3位 『文章も「見た目」が大事』

紙面、誌面、画面いっぱいに文字が詰まっていると、読む気が失せてしまいます。

そこで、文章の見た目を整えるために、3つのテクニックを使います。

  • 余白
  • ひらがなと漢字の使い分け
  • リズム

「余白」とは、文字、写真、画像がない部分のことです。文章のプロの多くが「余白のない文章は読みにくい」と指摘しています。

余白を作るには、行間をあけ、「空白行」を意図的につくることが大事です。

次に、「ひらがなと漢字の使い分け」ですが、漢字が多すぎると、文章全体が固い印象になり、また字面が黒くなって、読みにくくなります。

目安としては、漢字:ひらがなの割合は3:7程度にするとよいでしょう。

最後に、「リズム」というのは、改行のタイミング、段落の区切り方、句読点の打ち方などによってつくられる文章のリズムです。特に、ブログやSNSなど、パソコンやスマホで読む文章の場合はこまめに改行するほうが読みやすくなります。

目安として、ブログやSNSでは2~3行でひとつの段落とするのがよいようです。

7位 『接続詞を「正しく」使う』

接続詞を使うときに気を付けるべきポイントは、次の4つです。

  1. なくても意味が通じる場合は削除する
  2. 順接(「だから」「それで」など)の接続詞はなくてもよい場合がある
  3. 逆説(「しかし」「だけど」「でも」など)の接続詞は、あったほうが文章が伝わりやすい
  4. 論文では、接続詞が多くなってもかまわない(論理展開を正しく読者に伝えることが目的のため)

「1位 文章はシンプルに」の項でも書いたように、なくても意味が通じる言葉は削除するほうが、文章がすっきりとします。接続詞の場合、それは順接に当たることが多いです。

11位 『主語と述語はワンセット』

主語と述語が離れていると、どの主語がどの述語に対応するのかわかりづらくなります。

悪い例

山田さんが、子どもたちが林間学校に出かけていないので、週末に天気がよければ佐藤さんに声をかけて3人で高尾山に登ろうと誘ってくれた。

良い例

子どもたちが林間学校に出かけていない。もし、週末に天気がよければ、佐藤さんに声をかけて、3人で高尾山に登ろうと 、山田さんが誘ってくれた。

良い例では、主語と述語の距離を近づけたので、両者の関係がわかりやすくなりました。日本語は語順を入れ替えても通じる言語ですが、そのせいで文章がわかりにくくなることもあります。

16位 『「わかりにくい」と思ったら修飾語を見直す』

主語・述語の関係と同じく、修飾する語とされる語は近くに置くのが基本です。

悪い例

原稿を印刷所に渡すまでの時間があまりなかったので、大急ぎで私が仕上げた原稿に編集者は目を通した。

良い例

原稿を印刷所に渡すまでの時間があまりなかったので、私が仕上げた原稿に編集者は大急ぎで目を通した。

悪い例では、「大急ぎで」が「(私が)仕上げた」にかかっているのか、「(編集者が)目を通した」にかかっているのかわかりにくくなっています。「大急ぎで」が、両方の述語にかかる可能性があるからです。

修飾する語は、修飾される語の近くに置くと、どちらに意味がかかるのかがはっきりします。

まとめ

近年読んだ本の中でも、かなりの良書だと思いました。ホワイトカラーの仕事をしている社会人は、一度は読んでおく方がよいと思うくらいにおススメです。

「わかりやすい文章を書くコツ」を紹介している本だけあって、本書の文章も非常に読みやすく、内容がすらすらと頭に入ってきました。

自分はどちらかというと、他人の書いた文章を読むのは苦手な方なのですが、この本を読むときは一切苦痛がありませんでした。文章のテクニックを知っているだけで、こんなにも変わるものなのかということを実感できた気がします。

このブログをはじめてから1年経ちますが、もっと早くに出会いたかったなという1冊でした。

【React】レンダリングの最適化について考える

こんにちは! スマレジ テックファームのMichiです!

今回は、Reactのレンダリング最適化について自分なりに調べたことをまとめていきます。

Reactのコンポーネントが再レンダリングされるとき

まずおさらいですが、Reactのコンポーネントが再レンダリングされるタイミングについて確認しておきます。

  1. stateが更新されたとき
  2. コンポーネントが再レンダリングされたとき
  3. Contextが変更されたとき

なお、このほかによく挙げられるのが「propsが更新されたとき」ですが、これは厳密に言えば間違いです。

なぜなら、props を変更するには親コンポーネントが更新される必要があるからです。逆に、親コンポーネントが更新されなければ、propsが変わることはあり得ません。

つまり、「propsが更新されたから再レンダリングされる」のではなく、「親コンポーネントが再レンダリングされたから、子コンポーネントも再レンダリングされる」が正しいです。

(ちなみに、この点については自分の過去の記事でも間違えていますね。)

レンダリングの最適化方法

さて、ここからが本題です。

レンダリングの最適化方法について、コンポーネント分割」「childrenの使用」「メモ化」という3つの視点で見ていきます。

コンポーネントを適切に分割する

悪い例

次に示すのは、簡単なTODOリストをReactで実装した例です。

■TodoList.tsx

import { useState } from "react";
import "./styles.css";

export default function TodoList() {
  const [value, setValue] = useState("");
  const [taskList, setTaskList] = useState<string[]>([]);

  const handleSubmit = () => {
    setTaskList([...taskList, value]);
    setValue("");
  };

  return (
    <div className="todo">
      <div>
        <input
          type="text"
          value={value}
          onChange={(e) => setValue(e.target.value)}
        />
        <button className="btn" onClick={handleSubmit}>
          作成
        </button>
      </div>
      <div>
        <ul>
          {taskList.map((task, i) => (
            <li key={i}>{task}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

フォームに文字を入力し、「作成」ボタンをクリックすると、リストに新規TODOが登録されるだけの単純な作りです。

この実装では、フォームに値を入力するたびに、画面全体が再レンダリングされます。なぜなら、フォームの入力値valueTodoList.tsx内でステート管理されており、valueの値が変更されるたびにコンポーネント全体が再レンダリングされるからです。

しかし、フォームから下のTODOリストは、「作成」ボタンをクリックして新規TODOを追加しないかぎりは、表示される内容は変わりません。ということは、リストについては毎回無駄なレンダリングが発生しているといえます。

改善後

無駄なレンダリングを回避するために、コンポーネントを分割してステートのリフトダウンを検討します。

■TodoList.tsx

import { useState } from "react";
import "./styles.css";
import Input from "./Input";

export default function TodoList() {
  const [taskList, setTaskList] = useState<string[]>([]);

  return (
    <div className="todo">
      <Input taskList={taskList} setTaskList={setTaskList} />
      <div>
        <ul>
          {taskList.map((task, i) => (
            <li key={i}>{task}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

■Input.tsx

import { useState } from "react";
import "./styles.css";

type InputProps = {
  taskList: string[];
  setTaskList: (taskList: string[]) => void;
};

export default function Input({ taskList, setTaskList }: InputProps) {
  const [value, setValue] = useState("");

  const handleSubmit = () => {
    setTaskList([...taskList, value]);
    setValue("");
  };

  return (
    <div className="todo">
      <div>
        <input
          type="text"
          value={value}
          onChange={(e) => setValue(e.target.value)}
        />
        <button className="btn" onClick={handleSubmit}>
          作成
        </button>
      </div>
    </div>
  );
}

コンポーネントを分割し、valueのステート管理をInput.tsxへ委譲します。今度はフォームに値を入力しても、レンダリングされるのはInput.tsxだけで、画面全体はレンダリングされません。

結果的に、コンポーネントを分割することで不要な再レンダリングを防ぐことができました。

childrenを使う

悪い例

先ほどの例に、「つかいかた」の説明を追加します。

■TodoList.tsx

import { useState } from "react";
import "./styles.css";
import Input from "./Input";

export default function TodoList() {
  const [taskList, setTaskList] = useState<string[]>([]);

  return (
    <div className="todo">
      <Input taskList={taskList} setTaskList={setTaskList} />
      <div className="how-to-use">
        <h3>つかいかた</h3>
        <p>
          上部のフォームに追加したいTODOを入力し、作成ボタンをクリックしてください
        </p>
        <p>---------------------------------------------</p>
      </div>
      <div>
        <ul>
          {taskList.map((task, i) => (
            <li key={i}>{task}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

※そもそもUIとして変ですが、解説用としてご理解ください

「つかいかた」は静的なデータなので、初回レンダリング以降データの内容が変わることはありません。しかしながら、新しいTODOを追加してtaskListの状態が更新されるたびに、コンポーネント全体がレンダリングされるので、この静的な情報の部分もレンダリングされてしまいます。

改善後

まず、「つかいかた」の部分を別コンポーネントへ分離します。

■HowToUse.tsx

export default function HowToUse() {
  return (
    <div className="how-to-use">
      <h3>つかいかた</h3>
      <p>
        上部のフォームに追加したいTODOを入力し、作成ボタンをクリックしてください
      </p>
      <p>---------------------------------------------</p>
    </div>
  );
}

分離したHowToUse.tsxは親コンポーネントからchildrenとして渡すようにします。

■App.tsx

import TodoList from "./TodoList";
import HowToUse from "./HowToUse";

export default function App() {
  return (
    <TodoList>
      <HowToUse />
    </TodoList>
  );
}

■TodoList.tsx

import { ReactNode, useState } from "react";
import "./styles.css";
import Input from "./Input";

type TodoListProps = { children: ReactNode };

export default function TodoList({ children }: TodoListProps) {
  const [taskList, setTaskList] = useState<string[]>([]);

  return (
    <div className="todo">
      <Input taskList={taskList} setTaskList={setTaskList} />
      {children}
      <div>
        <ul>
          {taskList.map((task, i) => (
            <li key={i}>{task}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

childrenはpropsの一種ですので、状態の変更の影響を受けません。(冒頭でも触れた通り、propsが更新されるのは、親コンポーネントレンダリングされたときのみ。)

よって、TodoList.tsxが何回レンダリングされようとも、children(HowToUse.tsx)がレンダリングされることはないわけです。

メモ化する

レンダリングの最適化」と聞いて、おそらくこれを一番にイメージする方が多いのではないでしょうか。

Reactにおけるメモ(データキャッシュ)化には3つのプロセスがあります。

  • memo ... コンポーネントのメモ化
  • useCallback ... 関数のメモ化
  • useMemo ... (関数以外の)値・オブジェクトのメモ化

使い方については、他にも多数解説記事が出ているので割愛します。

この方法をあえて最後に持ってきた理由は、安易にメモ化に頼るのは良くないと考えるからです。

メモ化のデメリットとしては、

  • 本来、メモ化の依存配列に入れるべき値を入れ忘れて、バグの原因となる
  • コードが冗長化して読みにくくなる
  • メモ化自体のオーバーヘッドがある

などがあります。

メモ化する前に、先に紹介したコンポーネントの分割やchildrenの利用などで、レンダリング回数を減らせないか検討することが大事だと思います。

まとめ

この記事を書いたきっかけは次のツイートでした。

この考え方には、賛否両論ありそうなので、ご意見のある方はコメントやX(Twitter)などで教えていただけると嬉しいです!