<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yn2011's blog</title>
        <link>https://blog.yn2011.com</link>
        <description></description>
        <lastBuildDate>Tue, 21 Apr 2026 13:31:34 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>ja</language>
        <copyright>© 2023 yn2011</copyright>
        <atom:link href="https://blog.yn2011.com/feed" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[MD770 を使い始めて 3 年が経った]]></title>
            <link>https://blog.yn2011.com/2025-01-30-md770</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2025-01-30-md770</guid>
            <pubDate>Thu, 30 Jan 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# MD770 を使い始めて 3 年が経った

## MD770

2021 年末頃から [BAROCCO MD770](https://mistelkeyboard.com/products/d11cf7a73da49468e2a530b4cf18e76c) という分割キーボードを使用している。

MD770 にはいくつかのバリエーションがある。自分は以下のものを使用している。

- US 配列
- バックライトなし
- 赤軸

## キーのカスタマイズ

MD 770 にはキー割り当てをカスタマイズする機能がある。

自分は Mac OS 向けに以下のキーカスタマイズをしている。

### SW 1 の課題

MD 770 には DIP スイッチという機能がある。キー割り当てのプリセットが 4 種類用意されており、例えば SW 1 を ON にすると簡単に Mac OS ユーザ向けのキー割り当てに切り替えができる。

しかし、SW 1 ではファンクションキーにディスプレイ輝度を高めるなどの機能が強制的に割り当ててられてしまう。

VSCode ではファンクションキーを使えたほうが便利なので、自分は SW 1 を OFF にして Mac OS ユーザ向けの割り当てを独自に行う方針にしている。具体的には以下のカスタマイズを行っている。

- SW 3 を ON にする
- マクロプログラミングで右 alt に 左 alt と同じキーを割り当て

### SW 3 を ON にする

SW 1 を ON にすると左 Command キーと左 Option キーの位置を交換できる。これは SW 3 を ON にすることで代替できた。

### マクロプログラミングで右 alt に 左 alt と同じキーを割り当て

SW 1 を ON にすると右 alt キーに Command キーを割り当てできる。これはマクロプログラミングで代替できた。

MD 770 にはマクロプログラミングという機能があり、特定のキーに別のキーやキー入力の組み合わせを自由に割り当てることができる。

左 alt キーには Command キーが割り当て済みなので、マクロプログラミングを使って右 alt キーに左 alt キーを割り当てて、右手でも Command キーを入力できるようにしている。

## 所感

MD 770 を 3 年ほど業務やプライベートで使用してきたが、今のところ特に課題に感じる部分はない。これからも使っていきたい。
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[業務で SWR を利用して新規プロダクトを開発した]]></title>
            <link>https://blog.yn2011.com/2024-09-24-nextjs-swr</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2024-09-24-nextjs-swr</guid>
            <pubDate>Tue, 24 Sep 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# 業務で SWR を利用して新規プロダクトを開発した

2023 ~ 2024 年にかけて業務で新規プロダクトを開発した。その際の技術スタックやディレクトリ構成については、[フロントエンドのディレクトリ設計 (Next.js Pages Router)](https://blog.yn2011.com/posts/2024-08-23-nextjs-fe-directory) で紹介した。

また、ErrorBoudary と Suspense の利用については [業務で ErrorBoundary と Suspense を利用して新規プロダクトを開発した](https://blog.yn2011.com/posts/2024-08-27-nextjs-swr-suspense) という記事で紹介した。

この記事では、同じプロダクトで [SWR](https://swr.vercel.app/ja) をどのように利用したかの詳細を紹介する。

なお、この記事のサンプルコードのファイルパスは上記の記事のディレクトリ構成に対応している。

## Suspense モード

SWR には `suspense` という[オプション](https://swr.vercel.app/ja/docs/suspense)がある。このオプションを `true` にしている状態をこの記事では Suspense モードと呼ぶ。

Suspense モードでは `useSWR` フックを呼び出すと Promise を throw するので Suspense を利用してローディング中の Fallback UI を実装できる。

Suspense はデータフェッチングライブラリでの使用は推奨されておらず、フレームワークレベルでのサポートが必要とされている。例えば Next.js Pages Router で Suspense を使う場合、その Component を SSR すると Hydration エラーが発生する可能性がある。

とはいえ、以下の理由から今回は `suspense` オプションを有効にして開発した。

- 今後の React / Next.js では Suspense を活用していくことになる
- ErrorBoundary との親和性（各 Component で `if(err) throw err;` しなくて良い）

したがってほとんどの Component を SSR させないように (CSR するように) 実装した。

そもそも、今回のプロダクトでは要件とその他の都合からページの大部分は CSR が必要だったので、Suspense のためだけに CSR するわけではなく、いずれにしろ同じだったという背景もある。

以下では Suspnese モードの前提でどのように SWR を利用したかについて書いていく（とはいえ、Suspense モード特有の内容は少ない）

## Custom Hooks

SWR では `useSWR` という Hook が提供されているが、今回の開発では必ず API ごとに Custom Hooks を実装した。例えば、`/api/user` というパスに GET リクエストを行う場合は、以下のような Hook を実装する。

```ts:features/detail/hooks/useUserInfo.ts
import useSWR from "swr";

import { useCustomSWR } from "~/hooks/useCustomSWR";
import { UserInfoResponse } from "~/types/openapi.schemas";

export function useUserInfo(
  id: number,
  options?: Parameters<typeof useSWR<UserInfoResponse>>[2],
) {
  return useCustomSWR<UserInfoResponse>(`user?id=${id}`, options);
}
```

実装の実体は `useCustomSWR` という共通処理に切り出している。どの Hook もこの共通処理から API リクエストを行う。

Custom Hooks として実装する理由は 2 つある。API リクエスト時の処理を共通化することで変更容易性が高まること。また、テストコードで Hook を関数としてモックできるのでテスト容易性が高まるためだ。

その他工夫している点は以下の通り。

- OpenAPI から自動生成した API レスポンスの型を使って Hooks の返り値を定義（上記のコードの `UserInfoResponse` 型）
- SWR のオプションも引数として受け取ることで再利用性を高める（例えば、特定のコードからは `suspense` オプションを false にしてリクエストすることも可能になる）

共通処理として切り出した `useCustomSWR` は以下のように実装している。

```ts:hooks/useCustomSWR.ts
import Cookies from "js-cookie";
import useSWR from "swr";

import { APIError } from "~/lib/APIError";
import { fetcher } from "~/lib/fetcher";

export function useCustomSWR<T extends object>(
  url: string,
  options?: Parameters<typeof useSWR<T>>[2],
) {
  return useSWR<T, APIError>(
    () => {
      if (Cookies.get("AuthToken")) { // ⭐ 認証用の Token が Cookie に存在しない場合はリクエストしない
        return url;
      }

      return null;
    },
    fetcher, // ⭐ 後述
    options,
  );
}
```

## Custom Fetcher

`useCustomSWR` の `fetcher` は以下のように実装している。

```ts:lib/fetcher.ts
import { APIError } from "./APIError";

export async function fetcher(url: string, options?: RequestInit) {
  const res = await fetch(`${process.env.apiUrl}/${url}`, {
    credentials: "include",
    ...options,
  });
  const json = await res.json();

  if (!res.ok) {
    const error = new APIError(res.status, json.error.code, json.error.message);

    throw error;
  }

  return json;
}
```

`res.ok` が false（API レスポンスのステータスコードが 200 - 299 以外）だった場合は独自に実装した API エラークラスのインスタンスを生成して throw する。

API エラークラスを導入した理由は API エラーとそれ以外のエラーを区別したいのと、独自のエラーコードをクライアントサイドで扱いやすい形にするためである。

`fetch` メソッドのオプションを受け取れるようにすることで、後述の POST メソッドによるリクエストの場合にも再利用できるようにしている。

## POST リクエスト

ここまでは GET リクエストの例について見てきたので、次に POST リクエストの場合について書く。

POST メソッドによる API リクエストは、特に共通処理を切り出すことはしなかった。ただし、こちらも Custom Hooks として実装している。

例えば何らかのデータを更新する `/data/update` API に POST リクエストを行う Hook は以下のような実装になる。

```tsx:features/detail/hooks/useDataUpdate.ts
import useSWRMutation from "swr/mutation";

import { fetcher } from "~/lib/fetcher";
import { SuccessResponse } from "~/types/openapi.schemas";

export function useDataUpdate(id: number) {
  return useSWRMutation<SuccessResponse>(
    "data/update",
    async (url: string) => {
      return await fetcher(url, {
        headers: { "Content-Type": "application/json" },
        method: "POST",
        body: JSON.stringify({ id }),
      });
    },
  );
}
```

`fetcher` は GET メソッドによるリクエストと同じ実装を使用している。

## 重複排除

SWR には[重複排除(deduping)](https://swr.vercel.app/ja/docs/advanced/performance#%E9%87%8D%E8%A4%87%E6%8E%92%E9%99%A4)という機能があり、一定時間内の同一 URL に対するリクエストは自動的に 1 つにまとめられる。

これにより、複数の Component が同時に同一の URL に対してリクエストを送信する実装をしたとしても、フロントエンド・バックエンド共にパフォーマンス上の問題はなくなる。これまでは複数の Component が API のレスポンスを共有したい場合は親 Component で取得したレスポンスを State 経由で Props に渡したり、Context で共有したりしてきた。

Props で渡す場合は、以下の課題があった。

- 親 Component からレスポンスを渡される子 Component は、暗黙的に特定の API に依存していることになる（コロケーションが失われると言うらしい）
- API レスポンスに依存しない子 Component も親 Component の `setState` により re-render 対象になる（`React.memo` で回避は可能）
- 子 Component は親 Component の API リクエストが完了するまでマウントされない or ローディング表示の実装が必要

Context で共有する場合は `useContext` により API とのコロケーションは失われないが、残りの課題はなお残る。また、どちらも Suspense により子 Component のローディング表示を宣言的に実装できない。

SWR の重複排除により複数の子 Component が、単に `useSWR` を呼び出すだけで API レスポンスを取得し共有できるので積極的に活用している（そして SWR は Suspense にも対応している）

開発したプロダクトでは同一の API に対して 3 ~ 4 個の Component が初期表示時に CustomHooks 経由で `useSWR` を呼び出しているケースもある。

## 課題

全体として大きな課題はなかったが、少し気になった部分もあった。

### Suspense と ErrorBoundary が増えすぎる

`useSWR` Hook を呼び出した Component に Fallback UI とエラーハンドリングが必要な場合、必ず Suspense と ErrorBoundary をセットで実装しないといけない。上記に書いたように同一の API を 3 ~ 4 個の Component がリクエストする場合に各コンポーネントに Suspense と ErrorBoundary を実装しなければいけないのは少し煩わしさがあった。

特に、ErrorBoundary の処理が数行の場合は、単純に 1 つの Component 内で実装してしまった方が分かりやすいのでは、と感じる場合もあるかもしれない。

そもそも各 Component ごとに ErrorBoundary の実装が必要になる背景としては、認証エラーのレスポンスを Error として throw していることも関係している。未ログイン画面を描画する場合に各コンポーネントで上位の ErrorBoundary に throw するのではなく、自身を非表示にするために `<></>` をレンダリングする必要がある（ような UI だった）。したがって各 Component ごとに ErrorBoundary の実装が必要なことが多かった。そういう事情がなければ ErrorBoundary を多く実装することにはならないかもしれない。

`useSWR` Hook の呼び出しごとに、Suspense オプションを切り替えて false にできるので、実装が単純なら ErrorBoundary を使わず Component 内の実装で完結させてしまう手もある。そうではなく、多少煩わしくてもコードベース全体で ErrorBoundary は必ず使うというルールに統一するべきなのか、これは考えが分かれるかもしれない。

### Suspnese モードでも `useSWR` が返す data の型が `Data | undefined` になってしまう

Suspense モードの場合、リクエスト中はレンダリングが中断されているし、エラーの場合は ErrorBoundary が処理するので data が `undefined` になることはないが、SWR の型定義が対応していない。

今回はほとんどの API を Conditonal Fetching でリクエストしているので、Suspense モードだとしてもリクエストが実行されなければ data が `undefined` になる。この場合、`Data | undefined` は正しい型なのであまり問題にはならなかった。

例えば、リクエストを行う条件が認証用トークンを Cookie に持っているかどうかだとすると、data が `undefined` ならばリクエストされなかった＝未ログインとして扱う必要がある。その場合は未ログイン時の Component を返す実装が必要になり、そのことを型定義が示してくれるので `Data | undefined` という型はむしろ有用だった。

だが、Conditional Fetching しない場合は実際には `undefined` にならない data の型の取り扱いが面倒になる。独自の型定義で回避も可能だが、一部で Conditional Fetching も行っている場合はより煩雑になりそうだ（参考：[SWR の Suspense モードの型を調べる](https://zenn.dev/tnyo43/scraps/259073ef8271ed)）
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[業務で ErrorBoundary と Suspense を利用して新規プロダクトを開発した]]></title>
            <link>https://blog.yn2011.com/2024-08-27-nextjs-swr-suspense</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2024-08-27-nextjs-swr-suspense</guid>
            <pubDate>Tue, 27 Aug 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# 業務で ErrorBoundary と Suspense を利用して新規プロダクトを開発した

2023 ~ 2024 年にかけて業務で新規プロダクトを開発した。その際の技術スタックやディレクトリ構成については、[フロントエンドのディレクトリ設計 (Next.js Pages Router)](https://blog.yn2011.com/posts/2024-08-23-nextjs-fe-directory) で紹介した。この記事では、同プロダクトで ErrorBoundary と Suspene をどのように利用したかを紹介する。

なお、この記事のサンプルコードのファイルパスは上記の記事のディレクトリ構成に対応している。

## ErrorBoundary

API リクエストのエラーハンドリングを ErrorBoundary を利用して実装した。

具体的には以下のようなコードを実装した（コード中の `isSSR` というフラグについては Suspense の項で説明する）

```tsx:features/detail/components/Detail.tsx
export function Detail() {
  const { isSSR } = useIsSSR();

  return (
    <>
      <Header>ページタイトル</Header>
      <main className={styles.main}>
        {!isSSR && ( // ⭐ 後述
          <ErrorBoundary FallbackComponent={DetailFallbackComponent}>
            <Suspense fallback={<Loading />}>
              <Content /> {/* ⭐ API リクエスト */}
            </Suspense>
          </ErrorBoundary>
        )}
      </main>
    </>
  );
}
```

まず、今回の開発ではエラーハンドリングを以下の 2 つに分類した。

- プロダクト全体で共通の処理
- ページに固有の処理

プロダクト全体で共通のエラーハンドリングとは、例えば API レスポンスがステータスコード 500 だった場合に共通のエラー画面を用意する場合だ。共通のエラーハンドリングは、`_app.tsx` に実装した。

```tsx:pages/_app.tsx
export default function App({ Component, pageProps }: AppPropsWithLayout) {
  // ...
  return (
    // ...
    <ErrorBoundary FallbackComponent={Error}>
      <Component {...pageProps} />
    </ErrorBoundary>
  );
}
```

ページに固有のエラーハンドリングとは、そのページにだけ存在する要件に関するものだ。ページの固有のエラーハンドリングは、`pages` 直下で import する Component に実装した。

ErrorBoudanry では、API レスポンスのエラーコードを元に処理を分岐した。例えば以下のように、レスポンスごとにレンダリングする Component を分岐する。

```tsx:features/detail/components/DetailFallback.tsx
export function DetailFallback({ error }: Props) {
  if (error?.code === 102) { // 認証エラー時のエラーコードを 102 とする設計
    return <NotLogin />;
  }

  throw error;
}
```

API の設計として、エラーレスポンスにアプリケーション固有のエラーコードを含めているため、クライアントではエラーコードを元にエラー処理の分岐を実装できている。

どの条件にも当てはまらないエラーの場合は上位の ErrorBoundary に処理を委ねるため throw する。

## Suspense

今回開発したプロダクトではページの大部分を API レスポンスによって CSR するため、API リクエスト中はローディングスピナー等の Fallback UI を表示する。これは `useState` や SWR の `isLoading` を使って命令的に実装できるが、Suspense を使うことでより React らしく宣言的に実装できる。

Suspene を使った API リクエストは SWR を使って実装している。Suspense を使うため SWR の `suspense` オプションをデフォルトで true にしている。SWR についての詳細は [業務で SWR を利用して新規プロダクトを開発した](https://blog.yn2011.com/posts/2024-09-24-nextjs-swr) という記事にまとめている。

記事の冒頭のサンプルコードで `isSSR` フラグによりクライアントサイドでのみ Component をマウントしている理由は Hydration エラーの回避のためだ。この点については[以前に記事を書いた](https://blog.yn2011.com/posts/2023-10-27-swr-request-hydration-errror)ので詳細はそちらを参照。
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[フロントエンドのディレクトリ設計 (Next.js Pages Router)]]></title>
            <link>https://blog.yn2011.com/2024-08-23-nextjs-fe-directory</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2024-08-23-nextjs-fe-directory</guid>
            <pubDate>Fri, 23 Aug 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# フロントエンドのディレクトリ設計 (Next.js Pages Router)

2023 ~ 2024 年にかけて業務で新規プロダクトを開発した。その際に、フロントエンドではどのようなディレクトリ構成を採用したのかを書いていく。

## 技術スタック

### npm パッケージ

```md
"next": "^14.2.3"
"react": "^18.3.1"
"react-error-boundary": "^4.0.13"
"swr": "^2.2.5"
"typescript": "5.1.3"
"orval": "^6.31.0"
```

### その他

- Next.js は Pages Router を使用
- SSG を行い静的ホスティング
  - ログイン機能があり、ユーザごとに表示の切り替えが必要な UI がある
  - (諸事情により SSG ではあるものの) ページの大部分は API レスポンスを元に CSR する
- React の Suspense, Context, ErrorBoundary を使用

## 概要

`src` 配下のディレクトリ構成（主要なディレクトリのみを掲示）

```md
./src
├── components // ⭐
├── contexts
│   └── UserContext.tsx
├── features // ⭐
│   ├── detail
│   │   ├── components
│   │   ├── hooks
│   │   ├── tests
│   │   └── contexts
│   └── about
│       └── components
├── pages
│   ├── 404.tsx
│   ├── _app.tsx
│   ├── product
│   │   └── detail
│   │       └── index.tsx
│   └── about
│       └── index.tsx
└── types // ⭐
    ├── openapi.schemas.ts
    └── window.d.ts
```

## features ディレクトリ

```md
├── features
│   ├── detail
│   │   ├── components
│   │   ├── hooks
│   │   ├── tests
│   │   └── contexts
```

Next.js の Pages 単位で `features` 配下にディレクトリを作成した。（例: `pages/product/detail/index.tsx` には `features/detail` が対応）

各 features ディレクトリ内にはそのページでしか使用しない Component, Hooks, Context を配置した。対応するテストコードも `tests` 内に配置した。

`pages` 配下には以下のような `index.tsx` のみが配置されており、対応する `features` から Component を import する。

```tsx:pages/detail/index.tsx
import { Detail } from "~/features/detail/components/Detail"; // ⭐

export default function Page() {
  return <Detail />;
}
```

Pages Router に固有のファイルになるべく実装を書かないようにすることで今後の App Router 移行が楽になる（はず）と考えた。

## types ディレクトリ

```md
└── types
    ├── openapi.schemas.ts
    └── window.d.ts
```

[OpenAPI](https://www.openapis.org/) から TypeScript の型を自動生成している。

自動生成には [Orval](https://orval.dev/) を使用した。その他のライブラリから型を自動生成して比較してみたが、Orval が 1 番使いやすい型を生成していた印象があり採用した。

Orval は型以外にも API リクエスト用のコードも自動生成可能だが、今回は fetcher を独自に実装していたこともあり使用しなかった。Orval はデフォルトで fetch に [Axios](https://axios-http.com/) を使用したコードを生成するが、今回は Axios を使用する理由がなく、その挙動を変更できなかったのもある（もしかしたら出来るのかもしれないけど）

## components ディレクトリ

`components` ディレクトリの構成（例）

```md
./src/components
├── Modal
│   ├── Modal.module.scss
│   ├── Modal.tsx
│   └── index.ts
├── PageTopButton
│   ├── PageTopButton.module.scss
│   ├── PageTopButton.tsx
│   └── index.ts
└── SWRProvider
    ├── SWRProvider.tsx
    └── index.ts
```

`components` は `src` 直下に共通 Component 用のディレクトリを作成し、`features` 配下には各ページに固有の Component 用のディレクトリを作成した。

`components` 配下は Component ごとにディレクトリを作成した。CSS Modules を利用しているため `.scss` ファイルと実装を配下に配置した。`index.ts` では export するため以下のような実装をしている。

```ts:components/Modal/index.ts
export * from "./Modal";
```

実装の方のファイル名を `index.tsx` にしなかったのは、VSCode のファイルジャンプで扱いにくいのと、タブ名が全て `index.tsx` になると視認性が悪くなるため。

ただ、VSCode のタブ名の課題については [custom label](https://code.visualstudio.com/docs/getstarted/userinterface#_customize-tab-labels) を設定して解消できる。コンポーネントごとに `index.ts` を作成するのが少し手間でもあるので、これは個人の好みの問題かもしれない。
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Jest と MSW 2 を利用して React Component が送信する multipart/form-data 形式の Request Body をテストする]]></title>
            <link>https://blog.yn2011.com/2024-07-31-msw2-multipart-form-data-testing</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2024-07-31-msw2-multipart-form-data-testing</guid>
            <pubDate>Wed, 31 Jul 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# Jest と MSW 2 を利用して React Component が送信する multipart/form-data 形式の Request Body をテストする

## サンプルコード

[msw2-jest-sandbox](https://github.com/pokuwagata/msw2-jest-sandbox)

## 前提

```md
"typescript": "^5",
"next": "14.0.4",
"react": "^18",
"msw": "^2.0.11",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"undici": "^5.0.0"
```

## 背景

MSW 1 では モックした API に送信された `multipart/form-data` 形式の Request Body をパースできなかった。`multipart/form-data` 形式でリクエストを送信するフォームを実装している場合に、リクエスト内容を検証するテストコードを MSW を使って実装できないという課題があった。

## MSW 2 を Jest 向けに設定する

Jest から MSW 2 のサーバーを動作させるといくつかのエラーが発生した。[Frequent issues](https://mswjs.io/docs/migrations/1.x-to-2.x/#frequent-issues) に記載の方法で解消できた。

また、MSW 2 を Jest で動作させるためには undici のインストールが必要だが、undici v6 は動作しないため v5 を利用する必要がある。

[Always results in Network Error when using undici 6.x #2172](https://github.com/mswjs/msw/issues/2172#issuecomment-2225185717)

## Request Body をテストする

例として、ユーザが名前と年齢を入力して送信できるフォームのテストコードを実装する。

まず、Next.js (App Router) の Client Component で以下のようにフォームを実装した。ちなみに App Router なので [Server Action](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#forms) でも実装できるが、この記事の趣旨から外れるのでクライアントサイドから API をリクエストしている。

また、サンプル実装なのでバリデーションや異常系等は考慮していない。

```tsx:page.tsx
"use client";

import { useState } from "react";

export default function Home() {
  const [name, setName] = useState("");
  const [age, setAge] = useState(0);

  async function handleSubmit() {
    const body = new FormData();
    body.append("name", name);
    body.append("age", age.toString());

    await fetch("http://localhost:3000/api/test", {
      method: "POST",
      body,
    });
  }

  return (
    <main>
      <form>
        <div>
          <label>
            name
            <input
              name="name"
              type="text"
              onChange={(e) => {
                setName(e.target.value);
              }}
            />
          </label>
        </div>
        <div>
          <label>
            age
            <input
              name="age"
              type="number"
              onChange={(e) => {
                setAge(Number(e.target.value));
              }}
            />
          </label>
        </div>
      </form>
      <button onClick={handleSubmit}>submit</button>
    </main>
  );
}
```

次に以下のようにテストコードを実装した。重要な箇所には ⭐ 付きのコメントを付与した。

```tsx:page.test.tsx
import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { http, HttpResponse } from "msw";
import { server } from "../mocks/node";
import Home from "@/app/page";

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test("test", async () => {
  const mockfn = jest.fn();

  server.use( // ⭐ 一時的に API をモックする
    http.post("http://localhost:3000/api/test", async ({ request }) => {
      const formData = await request.formData(); // ⭐ multipart/form-data 形式の Request Body をパース

      mockfn({
        name: formData.get("name"),
        age: formData.get("age"),
      }); // ⭐ Request Body から値を取得してモック関数を呼び出す

      return HttpResponse.json(
        {
          result: "ok",
        },
        { status: 200 }
      );
    })
  );

  const inputData = {
    name: "foo",
    age: "30",
  };

  render(<Home />);

  await userEvent.type(screen.getByLabelText("name"), inputData.name); // ⭐ フォームに値を入力
  await userEvent.type(screen.getByLabelText("age"), inputData.age);

  await userEvent.click(screen.getByText("submit"));

  expect(mockfn).toHaveBeenCalledWith(inputData); // ⭐ フォームに入力した値と API に送信された値が同一であることをモック関数を利用して確認
});

```

このように、`multipart/form-data` 形式の Request Body をテストできる。

## Jest のプロセスが終了しない問題

実はサンプルコードの実装では、`npm run test` で Jest を実行するとテストはパスするが、以下の文言が表示されプロセスが終了しない。

```bash
Jest did not exit one second after the test run has completed.

'This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
```

調査はしてみたが、原因は不明だった。おそらく MSW 2 に起因しているのではないかと推測している。

仕方がないので、`--forceExit` オプションを付与して強制的に終了させている。
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[グロービス社の GMAP BF 編を受験した]]></title>
            <link>https://blog.yn2011.com/2024-07-29-gmap</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2024-07-29-gmap</guid>
            <pubDate>Mon, 29 Jul 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# グロービス社の GMAP BF 編を受験した

グロービス社が実施している [GMAP BF 編](https://gce.globis.co.jp/service/gmap/exam/?tab=tab2) という試験を受験した。

受験料は会社負担。会場は自社のオフィスだった。

## 受験科目

以下の科目を受験した。科目は会社で規定されていたので自分が選んだわけではない。

- マーケティング
- 経営戦略
- 人的資源管理
- 組織行動学
- 企業会計

## 成績

| 科目 | スコア |
| --- | :--: |
| マーケティング | 759 |
| 経営戦略 | 660 |
| 人的資源管理 | 630 |
| 組織行動学 | 654 |
| 企業会計 | 718 |

スコア 650 以上が A 評価とのことで、人的資源管理のみ B 評価となり悔しい結果となった。人的資源管理はあまり難しくなかった印象だったのだが...

マーケティングは 1 番自信がなかったが、偏差値換算で 75.9 (母集団に対して上位 0.6 %) という謎の好成績だった。

企業会計は 3 問不正解で上位 1.3 % 前後の成績。科目の性質としては難易度が高そうだったが、まずまず良かった。これは日本の中小型株のファンダメンタルズ投資に真剣に取り組んでいた時代の賜物かもしれない（多分関係ない）

## 試験対策

GMAP BF 編では、[グロービスMBAマネジメント・ブック 改訂3版](https://books.globis.co.jp/2008/08/29/3956/)という書籍の内容を元に問題が出題される。この書籍は会社の経費で購入できた。自分は電子版を購入したが、特に Kindle で読んでいて不便はなかった。

書籍の全体が試験範囲なわけではなく、受験科目に対応した章のみが試験範囲になる。

### 暗記

まずは点検読書として、試験範囲に該当する章のみざっくりと拾い読みをした。次に [Anki](https://apps.ankiweb.net/) というアプリを使用して暗記したい内容をカード化した。この時点では書籍の本文にはあまり目を通さず、見出しになっているキーワード（や定義）、図を中心にカードにした。

カード作成は PC (Mac) で行い、暗記作業は iPhone で行った。

カード作成を PC で行う利点としては以下があった。

- キーボードが使えるので文字の入力速度が速い
- 図をスクリーンショットとしてカードに貼り付け可能
- （デュアルモニター環境なら）一方の画面で Kindle を開き、もう一方の画面でカードを作成可能

暗記は 5~10 分程度のスキマ時間を見つけてコツコツやっていた。iPhone だとスキマ時間にすぐ取り出してアプリを起動できるので良かった。

### 演習

ある程度暗記が完了した後は [GMAP 練習問題](https://scorpion140309.github.io/GMAP/Test_GMAP.html)で知識の定着具合と抜け漏れを確認した。書籍の本文にしか載っていない内容も練習問題に含まれているので、ここで間違った内容を元に本文を参照して Anki のカードを補充していった。

ここまでの作業を大体 2~3 週間ぐらいで実施し、大体記憶に定着させた。

GAMP BF 編の出題は半分が知識問題で、残り半分は提示された文章や資料の読解問題（ケース問題）となっている。出題の半分を占めているにも関わらず、ケース問題の練習問題はほとんど存在しない。一応ケース問題の対策に有効（と言われている）書籍も存在はしているようだが、どれだけ GMAP BF 編の出題傾向と合っているか不明なのと会社の経費で購入できないことから、購入は見送った。

ケース問題対策としては、[Web 上にあった例題](https://gms.globis.co.jp/entry/check-test.html)で雰囲気を把握すること以外はしなかった。

### その他

受験日の 2 日前ぐらいから書籍（の試験範囲の章）を通読して、記憶したキーワードに関する背景や具体例、章を跨いだ繋がりなどを確認した。

他には暗記した知識を体系的に整理するために、Cosense (Scrapbox) で箇条書きのマインドマップを描いた。例えば、PPM というフレームワークでいうと、全社戦略 -> 資源配分 -> 事業ポートフォリオ -> ポートフォリオマトリクス -> PPM のように上位概念との紐づけを確認した。

このようにマインドマップで整理して気付いたが、この書籍は（経営大学院が出版しているので当然かもしれないが）アカデミックな傾向が強く、しっかりと体系的な構成になっている。見出しの順序や構成には意味があるし、用語の定義は曖昧さが少ない。

一方で、この書籍を読みにくいと感じる人は少なくないようだが、その一因は「今自分は全体の中のどこにいるんだろう？この章はいったいいつ終わるんだろう」と迷子になってしまうからなのかもしれない。章ごとのボリュームが多いので単に通読していると、頭の中で体系的な整理が追いつかない。

## 出題に対する印象

実際に受験して、確かに書籍の内容が身についていれば一定のスコアは取れるが、それ以上のスコアは学習量に比例しないだろうな、という印象を受けた。知識量ではなく読解力や論理的思考力でしか対処できない問題も含まれているので、準備だけでなく試験時のパフォーマンスや個人の資質に依存する部分が大きい。

後は、数問ではあるが、明確な根拠を持って選択肢を絞込めないようなものもあった気がする。単純に自分の能力不足でそう感じただけかもしれない。

企業会計の計算問題は、B/S に関するものと損益分岐点売上高に関するものが出題された。こちらも単純に公式を暗記して適用するというよりは、一段階程度は工夫が必要だったと思う。自分が正解していたのか分からないので多分そうだったんじゃないかなという感じだけれど。

単純に書籍を丸暗記すれば高いスコアが取れるわけではないが、学習によって少なくとも一定のスコアは保証されるという意味では、GMAP BF 編は良い試験と言えるかもしれない。

## 所感

久しぶりの試験勉強は思ったより楽しかった。試験が終わってもう iPhone で Anki アプリを起動しなくて良いのか、と思うと少し寂しさすら感じた。時期的に大学の期末試験と同じなので懐かしさもあった。この後に夏休みが待っているわけではないけれども、試験が終わった後の開放感も懐かしさがあった。

学習内容については試験以外でも役に立ちそうなものが多かったので有意義だった。自分は技術職なので、例えば経営戦略や組織構造についての知識が直接的に業務で役に立つわけではないが、経営者や人事、マネージャー等、他の職種の人たちが持つ基礎的な知識や考え方を知ることができたという点で良かった。

学習時間については、スキマ時間の暗記を除くと平日に 1 時間程度は集中して取り組んだ。基本的に平日のみで完結させることができたので良かった。1 歳の子がいるので休日に試験勉強をする時間が取れない。

金銭面については、学習から受験まで私費は一切使わないことにしていて、それを守れたのも良かった。正確には iPhone の Anki は有料アプリだが数年前に購入済みだったのでノーカウントとしている。電卓は自宅にはなかったが、会社の電卓をレンタルして済ませた。書籍は「グロービス MBA マネジメント・ブック 改訂 3 版」だけでそれなりのスコアが取れたので、結果的には他の書籍を購入しなくて良かった。

まとめると、時間的にも金銭的にも最小限のコストでビジネスに関する知識が得られたので受験して良かった。
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ESLint で空行に関するルールを設定する]]></title>
            <link>https://blog.yn2011.com/2024-05-02-eslint-padding-line</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2024-05-02-eslint-padding-line</guid>
            <pubDate>Thu, 02 May 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# ESLint で空行に関するルールを設定する

## モチベーション

コードの文と文の間に適切な空行が挿入されているとコードの可読性が高まる。

例えば、空行をまったく挿入せずにコードを書くと以下のようになるだろう。

```js
function foo() {
  setup();
  const params = {
    a: 1;
  };
  bar1(params);
  if (hoge) {
    bar2();
    return
  }
  return
}
```

以下のように空行を挿入したコードの方が読みやすいと感じる人は多いだろう。

```js
function foo() {
  setup();

  const params = {
    a: 1;
  };

  bar1(params);

  if (hoge) {
    bar2();

    return
  }

  return
}
```

しかし、どこに空行を挿入するかは実装者の感覚に依存しているため、以下の課題がある。

- コードレビューやモブプログラミングで「ここは空行があってもいいんじゃないか」と感じても指摘しにくい
- チーム開発をしている場合はコードベース全体で統一するのが難しい

## ESLint

ESLint に空行に関するルールがないか調べてみたところ見つかった。

@stylistic/js に [padding-line-between-statements](https://eslint.style/rules/js/padding-line-between-statements) というルールがある。

例えば、`return` 文の前には必ず空行を挿入したい場合は以下のように設定する。

```json
{ "blankLine": "always", "prev": "*", "next": ["return"] }
```

これで、`eslint --fix` すると自動的に `return` 文の前には空行を挿入してくれる。

## 設定例

業務で関わっているコードベースには以下の設定を適用している。

```json
"plugins": ["@typescript-eslint", "@stylistic"],
   "rules": {
     "@stylistic/padding-line-between-statements": [
       "error",
         { "blankLine": "always", "prev": "*", "next": ["return", "multiline-expression", "block-like", "try", "throw"] },
         { "blankLine": "always", "prev": ["multiline-expression", "block-like", "const", "let"], "next": "*" },
         { "blankLine": "any", "prev": ["const", "let"], "next": ["const", "let"] }
     ]
   }
```

ざっくりと説明すると以下のようなルールになっている。

- `return`, `if`, `try`, 複数行の式文等は前方に空行
- `if`, 変数宣言, 複数行の式文等は後方に空行
- 変数宣言文同士の間には空行は不要

ちなみに、 `multiline-expression` が複数行の式文を指す。主に関数の実行 `foo();` 等が該当する。`block-like` が主に `if`, `while` を指す。

最後のルールの `{ "blankLine": "any", "prev": ["const", "let"], "next": ["const", "let"] }` で変数宣言文同士の間には空行を強制しないように設定している。変数宣言は固まりとして書きたいことが多いと思ったためそうしている。

例えば、以下のコードでは `a`, `b` の間は空行が入らないが、1 番目のルールによって式文の前には空行が入る。また、2 番目のルールによって `b` の次には空行が入る（結果的に 1 つの空行になる）

```js
const a = 1;
const b = 2;

foo();
```
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[react-error-boundary の fallbackRender と FallbackComponent の違い]]></title>
            <link>https://blog.yn2011.com/2024-04-05-error-boundary-fallback</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2024-04-05-error-boundary-fallback</guid>
            <pubDate>Fri, 05 Apr 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# react-error-boundary の fallbackRender と FallbackComponent の違い

## updated

2024/12/13

本文を大幅に修正。

## react-error-bounadry

React [ErrorBoundary](https://ja.react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) は Class Component で実装する必要がある。
[react-error-bounadry](https://www.npmjs.com/package/react-error-boundary) は React の ErrorBoundary を Functional Component で実装するためのライブラリである。

## 結論

`fallbackRender` と `FallbackComponent` で出来ることに違いはない。

`fallbackRender` は意図しない動作をすることがあるので `FallbackComponent` を使っておけばいい。

## fallbackRender

react-error-boundary の `fallbackRender` を利用して、以下のように ErrorBoundary を実装できる。

```tsx
import { ErrorBoundary } from "react-error-boundary";

<ErrorBoundary fallbackRender={() => <p>Error Boundary</p>}>
  <SomeComponent />
</ErrorBoundary>;
```

上記の `<SomeComponent>` で例外を throw すると `fallbackRender` に渡された関数（`type '(props: FallbackProps) => ReactNode'`）を実行する。
以下のように直接 Functional Component に Component を渡すこともできる。

```tsx
import { useEffect } from 'react';
import { ErrorBoundary } from "react-error-boundary";

function Fallback() {
  return <p>Error Boundary</p>;
}

<ErrorBoundary fallbackRender={Fallback}>
  <SomeComponent />
</ErrorBoundary>;
```

ただし、上記の実装には制限があるので注意が必要なようだ。

例えば、以下のように `FallbackProps` を受け取る `Fallback` コンポーネントを実装した場合、
実行時エラーが発生し正常にレンダリングされなかった。
`fallbackRender` に直接 `Fallback` を渡しても型定義エラーは発生しないので分かりにくい。

```tsx
import { FallbackProps } from "react-error-boundary";

export function Fallback({ error }: FallbackProps) {
  ...
}
```

また、以下のように Hook を実装した場合にも同様に実行時エラーが発生した。

```tsx
export function Fallback() {
  useEffect(() => {
    console.log("hi");
  });
```

`fallbackRender` を利用して `FallbackProps` と Hook を動作させる場合には以下のように実装する。

```tsx
<ErrorBoundary fallbackRender={(props) => <Fallback {...props} />}>
```

## FallbackComponent

react-error-boundary には、`fallbackRender` とは別に `FallbackComponent` という Props が存在する。

`FallbackComponent` を利用して、以下のように ErrorBoundary を実装できる。

```tsx
import { useEffect } from 'react';
import { ErrorBoundary, FallbackProps } from "react-error-boundary";

function Fallback({ error }: FallbackProps)) {
  useEffect(() => {
    console.log("hello");
  }, []);

  return <p>Error Boundary</p>;
}

<ErrorBoundary fallbackComponent={Fallback}>
  <SomeComponent />
</ErrorBoundary>;
```

`fallbackRender` とは異なり、`FallbackComponent` は上記の実装でも React の Hook や Context を利用できる。`FallbackProps` も上記の実装で扱うことができる。

したがって、`FallbackComponent` は `fallbackRender` より記述量が少なく済む。

追加の Props を渡すためには、以下のように実装する。この場合は `fallbackRender` と同じ。

```tsx
<ErrorBoundary FallbackComponent={(props) => <Fallback {...props} text={"hoge"} />}>
```

## 経緯

結局、`fallbackRender` と `FallbackComponent` で同じことが実現できるのに、なぜ react-error-boundary は 2 つの Props を定義したのだろうか。

[Provide extra props to fallback component](https://github.com/bvaughn/react-error-boundary/issues/26) によると、`FallbackComponent` は Props として最初から存在していたことが分かる。

詳細は分からないが、Component に独自の Props を渡す上で、より適切な命名の Props として `fallbackRender` が実装されたという経緯のように解釈できた。

## 結論 (再掲)

命名を気にしなければ常に `FallbackComponent` を使うでも問題ないと思う。
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Mac でデュアルモニター環境を作った]]></title>
            <link>https://blog.yn2011.com/2024-01-22-dual-monitor</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2024-01-22-dual-monitor</guid>
            <pubDate>Mon, 22 Jan 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# Mac でデュアルモニター環境を作った

![著者の作業デスクの画像](/images/posts/dual-monitor.jpg)

## 環境

- Macbook Pro
- macOS 12.7.2

## 今まで

外部ディスプレイ 1 つと Macbook の画面で作業していた。

業務のミーティングで、共有された画面をメインの外部ディスプレイに写し、Macbook の画面からブラウザを開いて共同作業をしたいときがある。23 インチの外部ディスプレイに対して、Macbook の画面が小さいので作業がやりにくかった。そこで、もう 1 つ外部ディスプレイを用意したいと考えた。

## 課題

調べてみると、デイジーチェーンという仕組みに対応しているディスプレイ同士は直接 HDMI ケーブル等で接続してデュアルモニターとして使えることが分かった。しかし、macOS はこの仕組みに対応していないので仮にデイジーチェーンに対応しているディスプレイを 2 台用意しても、MacBook と 1 つのディスプレイのミラーリングにしか使えないようだ。[^1]

## 解決策

[Anker 563](https://www.ankerjapan.com/products/a8386) というアダプターを利用して macOS でも 2 台の外部ディスプレイを利用したデュアルモニター環境を作ることができた。

Anker 563 に HDMI ケーブルを 2 本接続し、Silicon Motion ドライバをインストールすると macOS のディスプレイ設定から外部ディスプレイ 2 台と MacBook の 3 つのスクリーン設定が可能になる。

## DisplayPort to HDMI 変換ケーブルは使えない

 Anker 563 は DisplayPort to HDMI 変換ケーブルだとデュアルモニターとして認識しなかった。通常の HDMI ケーブルなら正常に認識した。

[^1]: [Displayport MST support](https://discussions.apple.com/thread/254942699?sortBy=best)
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ESLint で path alias を使っていない import 文を検出する]]></title>
            <link>https://blog.yn2011.com/2023-12-16-eslint-path-alias</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2023-12-16-eslint-path-alias</guid>
            <pubDate>Sat, 16 Dec 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# ESLint で path alias を使っていない import 文を検出する

## path alias とは

TypeScript では import 文で alias を使うことができる。

例えば、`tsconfig.json` の [paths](https://www.typescriptlang.org/tsconfig#paths) で `@` を alias として、以下のように from 句を記述するように設定できる。

```typescript
// before
import Foo from '../components/Foo';

// after
import Foo from '@/Foo';
```

ただし、これまで通り相対パスを使った import 文を書くこともできるため path alias を使った import 文と混在してしまうという課題がある。

## eslint-import-alias

[steelsojka/eslint-import-alias](https://github.com/steelsojka/eslint-import-alias) を使うと、path alias を使っていない import 文の検出が可能になる。

例えば以下のように `eslintrc.json` にルールを追加する。

```json:eslintrc.json
{
  ...
  "plugins": ["eslint-plugin-import-alias"],
  "rules": {
    ...
    "import-alias/import-alias": [
      "error",
      {
        "relativeDepth": 0
      }
    ]
  }
}
```

すると、相対パスを利用している import 文に対して ESLint がエラーを出す。

```typescript
// error
import Foo from '../components/Foo';

// OK
import Foo from '@/Foo';
```

`relativeDepth` を `0` に設定すると同一階層への相対パスは許容になる。

```typescript
// error
import Foo from '../components/Foo';

// OK
import Foo from './Foo';
```

`relativeDepth` が未指定の場合は同一階層もエラーを出す。

```typescript
// error
import Foo from './Foo';
```

## VSCode の `importModuleSpecifier`

ちなみに、相対パスを使った import 文が生成される原因の 1 つに VSCode の Quick Fix がある。

{/* <!-- textlint-disable sentence-length --> */}

VSCode はデフォルトだと相対パスで import 文を生成する。`settings.json` の `typescript.preferences.importModuleSpecifier` の値を `non-relative` に設定すると、プロジェクトの `tsconfig.json` の `paths` と `baseUrl` を元にして自動的にパスを生成できる。

{/* <!-- textlint-enable sentence-length --> */}

こうしておくだけでも、相対パスを使った import 文の混入を未然に防ぐことができる。

## まとめ

たかが import 文だが、こういった細かい部分を統一していくことは、コードベース全体をクリーンな状態に保っていくための基本だと思う。

割れ窓をできるだけ少なくしていくことで、新たに書かれるコードの品質低下を防ぐことができる。コードベースの内部品質を高めて開発生産性の向上へと繋げていきましょう。
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Next.js で SWR の Conditional Fetching をする]]></title>
            <link>https://blog.yn2011.com/2023-10-27-swr-request-hydration-errror</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2023-10-27-swr-request-hydration-errror</guid>
            <pubDate>Fri, 27 Oct 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# Next.js で SWR の Conditional Fetching をする

## 前提

- Next.js v13.4.x
- SWR v2.2.x

## SWR の Conditional Fetching

[SWR](https://swr.vercel.app/) には [Conditional Fetching](https://swr.vercel.app/docs/conditional-fetching.en-US) という機能がある。

例えば、Conditional Fetching を利用して `API_KEY` という Cookie を持つ場合に fetch を行う hooks を実装をすると以下になる。

```tsx
import Cookies from "js-cookie";

export function useUserInfo(options?: any) {
   return useSWR<UserInfo, APIError>(
     () => {
       if (Cookies.get("API_KEY")) {
         return "/user/info";
       } else {
         return null;
       }
     },
     fetcher,
     options
   );
 }
```

Conditional Fetching を利用することで、無駄な API リクエストを減らすことができる。

## Hydration エラー

Next.js で SWR の Conditional Fetching を利用していると、以下のような Hydration エラーに遭遇することがある。

`app-index.js:32 Warning: Expected server HTML to contain a matching <p> in <div>.`

これは SSR のレンダリング結果とクライアントサイドでのレンダリング結果が異なるために発生する。

例えば、上記の Cookie の例では SSR 時は常にリクエストを行わないので `useUserInfo` はデータを取得できない。一方で、クライアントサイドでのレンダリング時に `API_KEY` という Cookie をブラウザが持っていた場合はリクエストが行われる。そのため、レスポンスを利用してコンポーネントをレンダリングしている場合は当然 SSR 時のレンダリング結果とは違ってしまう。

## 回避策

要件にもよるとは思うが、クライアントサイドに依存してレンダリング結果が決まるものは SSR しないのが 1 番良いと思う。

例えば、`useEffect` を利用して以下のように実装できる。

```tsx
function App() {
  const [isSSR, setIsSSR] = useState(true);

  useEffect(() => {
    setIsSSR(false);
  }, []);

  return isSSR ? (<Loading />) : (<Component />)
}
```

この場合は `<Component />` はクライアントサイドでのみレンダリングされるので、`<Component />` で Conditional Fetching をしても Hydration エラーは発生しない。

## `fallbackData` について

`useSWR` の [options](https://swr.vercel.app/docs/api#options) に `fallbackData` がある。

Conditional Fetching では、`fallbackData` が設定されていると条件を満たさなかった場合にその値が利用される。

また、SSR 時に Conditional Fetching をすると無条件で `fallbackData` が利用される。

上記の回避策のようにクライアントサイドでのレンダリング時のみに Conditional Fetching をしなくても、`fallbackData` を利用して SSR するという選択もある。ただし、クライアントサイドで条件を満たさなかったから `fallbackData` が利用されているのか、SSR 時に `fallbackData` が利用されているのかは区別できない点は注意が必要。

要件によっては、最初は `fallbackData` を表示しておきユーザイベントに応じてリクエストを行うという実装でも良いかもしれない。
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[CSS で中央寄せしたいときのフローチャート]]></title>
            <link>https://blog.yn2011.com/2023-10-24-css-center</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2023-10-24-css-center</guid>
            <pubDate>Tue, 24 Oct 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# CSS で中央寄せしたいときのフローチャート

CSS による要素の中央寄せには様々な実装方法がある。状況によって使える実装が異なるし、水平方向と垂直方向でも若干実装が異なる部分があり難しい。中央寄せを実装したいときに迷わないように、どういう場合にどの実装が使えるのかをフローチャートに整理した。

## 水平方向

![水平方向の中央寄せ](/images/posts/horizontal-center.png)

## 垂直方向

![垂直方向の中央寄せ](/images/posts/vertical-center.png)

## 両方向

![両方向の中央寄せ](/images/posts/center.png)

上記のフローチャートは [mermaid](https://github.com/mermaid-js/mermaid) で記述し、画像として export した。
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[PG BATTLE 2023 に参加した]]></title>
            <link>https://blog.yn2011.com/2023-10-21-pgbattle2023</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2023-10-21-pgbattle2023</guid>
            <pubDate>Sat, 21 Oct 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# PG BATTLE 2023 に参加した

[PG BATTLE 2023](https://products.sint.co.jp/pg_battle) に参加した。

PG BATTLE は個人ではなく所属企業や学校からチーム単位で参加する。1 チームは 3 人。出題される問題は合計 12 問で、それが 4 問ずつ 3 つの種別に分類されている。事前に種別ごとに担当者を申請して固定するので、1 人 4 問を解くことになる。チーム参加ではあるが個人戦を前提としていて、1 つの種別を複数人で回答することは認められていない。

自分は「せんべい」という中難度の種別を担当した。

## 準備

3 年前に AtCoder でのコンテストに参加して以来の競技プログラミングで、言語を含めて全てを忘れていた。流石にそのままの状態で参加しても面白くないしチームにも申し訳ないので、復習をすることにした。

言語は 3 年前も C++ を使っていたので今回も C++ を使うことにした。

復習は標準入出力、全探索、bit 全探索、二分探索、累積和、貪欲法などをやった。3 年前にある程度身についていたものだけを選び、DFS, BFS, Union Find, DP など学習中で定着してなかった分野は復習対象から除外した。もし出題されたら潔く捨てることになる。

環境構築を始めたのがコンテストの 1 か月前ぐらいで、それから週 5 日ぐらいは問題を解くようにしていた。

最終的には diff 500 ぐらいの問題が解けることもある状態になり、思ったより良い結果が出せそうな気がしていた（フラグ）

具体的な目標としては担当する 4 問の中で、1~2 問は正答しようと思っていた。3,4 問目は無理。

## 当日

1 問目は（実はギリギリでミスに気付いて）正答できたが、[2 問目](https://products.sint.co.jp/hubfs/resource/topsic/pgb2023/2_2.pdf)に苦しんだ。

種別や難易度によらず、PG BATTLE 全体に言えることだが、AtCoder の ABC よりは数学がテーマになっていることが多い。2 問目はまさに数学で、確率と期待値の線形性に関する問題だった。

競プロで期待値を扱う問題自体が初見だったので、まず問題を正確に理解するのに時間がかかった。

実装方針は愚直にやると 3^(10^5) を扱う必要があり、絶対にオーバーフローすると分かっていたが、愚直に計算する以外の方法を検討できなかった。

3 文字ずつで期待値計算できたらいいんだけどな、と思いつつ数学的に確証（確信）を持てなくてやらなかった。こういう場面で、愚直にやって駄目なんだからとりあえず出題者の意図に近そうな実装をしてしまう、という選択を取れなかったのはコンテストの参加者としては判断ミスだった。

「コンテストの参加者としては」というのは、それが常に判断ミスと言えるかというとそうではないとも思っているからだ。実際の問題解決の場面で、数学的に自明でないのにとりあえず出来そうな実装をする、という行動は取れない。仮に正しい答えを出すコードが書けていたとしても他人に説明できず自分自身もなぜ正しいのか理解できないのでは問題を解決したとは言えない。

より本質的な敗因は、問題を分割して扱うことが正しいと判断できなかった数学力の不足だと思う。期待値の線形性と言ってしまうのは簡単だが、本当に 3 文字ずつに分割して加算するだけで良いのか？文字列全体の ABC の数が期待値なのに？そのときの確率は 3 文字分の中だけで考えていいのか？例えばその 3 文字以降に ABC が存在したらそれは加算しなくて問題ないのか？重複カウントはないと言えるのか？など考えていくと、本番時の自分は「何か怪しい」と思ってしまった。

2 問目の解説動画で直大さんが「ちょっと難しかったかも」と言っていたので、それなりにせんべい担当者は苦労したのかもしれない。

## 所感

振り返ってみると、約 1 ヶ月間に学習した内容は全然出題されなかった。むしろ 10 年以上前に学習した高校数学の確率と期待値に関する理解度を問われるという事態で、何だかコンテストが終わってもやり切った感が薄かった。それなりに準備して挑んだだけに、理不尽な感じがする。PG BATTLE は 2 年分の過去問も解いたのだが、AtCoder の ABC と微妙に雰囲気が違っていて対策が難しい。結局できることは AtCoder の過去問を中心とした演習になってしまう。今更高校数学まで復習しようという気持ちにはならない。PG BATTLE も普通に ABC と同じ傾向の出題でも良いと思うんだが...

ただ、ブランクがあるとはいえ高校数学はそれなりに勉強した過去はあるわけで、悔しさはある。何となく自己肯定感は下がったが、逆に社会人として謙虚な気持ちにもなった。算数・数学的思考については 3 年前に AtCoder をやっていたときも苦手だと感じることが多かった。人には向き不向きがあるんだろうと思う。

## 試験の辛み

こういう話をしていると、高校 3 年の大学受験の頃を思い出す。どれだけ必死に準備してきても何が出題されるかは分からないし、予想もしないような出題やケアレスミスで得点につながらないことも多かった。

なんでこんなに頑張っているのに成績が伸びないのか、と模擬試験の後に鬱々として寝込んだ日もあった。まさに筆記試験の厳しさだが、村上春樹さん風に言うと”神経症的ゲーム”だったとも言えると思う。

もちろん、出題されなかった範囲でも学習したこと自体は無駄ではない。しかし、解けた・解けなかったという事実だけが得点に反映され、他者と比較され、順位や偏差値として評価される。これは特殊なルールのゲームに過ぎないと思う。実際の（特に現代の）社会でこのゲームが得意なことにそこまで意味があるとはあまり思えない。本当に、当時はよく 1 年間も受験生やってたよなぁ。

## まとめ

結果はともかく、PG BATTLE がきっかけで、C++ と基本的なアルゴリズムの復習の機会にはなった。数理的な問題を解くときの頭の使い方も普段の生活や仕事ではしないので、良い刺激になった。AC できたときの嬉しさも久しぶりに味わった。

一方で、準備しても十分には報われるとは限らないという試験の厳しさを思い出すこともできた。自分の向き不向きについても再認識できた。

そういうわけで、自分は競プロよりも別のことに時間を使った方が幸せになれると思っているので引退状態のままでいるつもりだが、また来年誘われたら期間限定で競プロをやることはあるかもしれない。もっと簡単な準備だけして、数学ゲーだと割り切って気軽に参加する方が楽しめそう。
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[業務で MSW を使ってみた感想]]></title>
            <link>https://blog.yn2011.com/2023-10-17-msw</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2023-10-17-msw</guid>
            <pubDate>Tue, 17 Oct 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# 業務で MSW を使ってみた感想

## 前提

- MSW v1.3.0
- REST API

## 経緯

業務で管理画面を開発した。自分はフロントエンドの開発メンバーとして参加した。

始めに Open API で API の I/F をバックエンドチームと共に定義し、その後はフロントエンドとバックエンドでチームを分けて並行して開発を進めた。

並行して開発を進めるので、フロントエンドチームが自由に API のモックを実装できた方が効率良く開発できそうだった。[^1]

また、フロントエンドの自動テストも整備していきたいと思っていた。

## 採用理由

API のモックを実装するために、[MSW](https://mswjs.io/) を採用することにした。採用理由としては、モックの再利用が可能なことと、他社事例が複数あったことが挙げられる。

### 再利用可能

MSW はブラウザ(Service Worker) と Node.js 環境の両方で動作する。
なので、1 度 API のモックを実装すると、自動テスト（Unit テストや E2E テスト）から再利用できる。

[React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) の
[Example](https://testing-library.com/docs/react-testing-library/example-intro/#full-example) では MSW によるテストの実装例が紹介されている。このドキュメントでは `window.fetch` をモックするよりも MSW を使うことを推奨している。

自動テストを実装する際にも API のモックは必要なので、開発時とテスト実装時でモックを再利用可能なのは嬉しい。

### 他社事例

複数の企業から MSW を採用した開発事例が公表されている。

- [Mock Service WorkerでAPIをモックして開発をスムーズに進められた話](https://tech.classi.jp/entry/2022/03/30/120000)
- [OpenAPIとMSWを使ってユーザーのフォロー機能を実装した話](https://engineer.crowdworks.jp/entry/2023/02/28/120000)
- [OpenAPI × Orval × MSW × Next.jsでのスキーマ駆動開発実践](https://techtekt.persol-career.co.jp/entry/tech/221215_01)

概ね MSW に対して肯定的な内容であることと、業務での利用にも耐えられるライブラリであることが分かった。

## 使ってみてどうだったか

ここからは、実際に自分が MSW を利用した開発をしてみて、出来たこと・出来なかったことを書く。

### 出来たこと

- フロントエンドチーム内で簡単に API のモックを実装できた
  - JavaScript で実装可能なので、バックエンドが別言語で実装されている場合は特に嬉しい
  - エラーレスポンスの再現が簡単にできるので、異常系の実装が楽だった
- Jest と React Testing Library を使った単体テストで、API のモックを再利用できた
  - `server.use` で一時的にレスポンスを変更できるので、異常系のテストケース実装が楽だった

### 出来なかったこと

- 現時点では Next.js の App Router で動作しなかった（[Support Next.js 13 (App directory) #1644](https://github.com/mswjs/msw/issues/1644)）
- `Location` ヘッダーを付与したレスポンスに対して Chrome がリダイレクト処理を行わない
  - Service Worker の制約？
- `Content-Type` ヘッダーの値が `text/csv` のレスポンスに対して、Chrome が CSV ダウンロードに失敗する
- 現時点では `multipart/form-data` 形式の Request Body を取得できない（[Support "req.formData()" #1327](https://github.com/mswjs/msw/issues/1327)）
  - Jest で画面から正しいリクエストが送信されているかをテストできない

上記の Chrome が意図した動作をしない API については、Next.js の API Route の場合は正しく動作したのでこちらで代用した。

MSW 固有の問題ではないが、フロントエンドの開発完了後に実際の API と結合テストを行った際に認証系のバグがいくつか発生した。これは認証が必要な API も常に正常レスポンスを返すようにモックを実装していたからだった。認証が必要な API はモック実装でも Cookie やヘッダーの値を参照して認証エラーを返すような分岐を実装した方が事前にバグを発見できたはずなので、1 つ学びになった。

## まとめ

結論として、今回の管理画面開発では MSW を利用して良かったと思う。

事前に OpenAPI を利用して API の I/F を決めておき、MSW で API のモックを実装することで、フロントエンド実装の依存先がなくなり、全てフロントエンドチーム内で作業を完結できるようになった。バックエンドチームとのコミュニケーションも有意義なものが多くなり、効率が良かったと感じる。

MSW を利用して開発を終えた後に実際の API との結合テストした際には、認証系のバグは出てしまったが、レイヤー間での認識齟齬によるバグはほとんどなかった。

また、Jest と React Testing Library を利用したコンポーネントのテスト実装では、開発に使用した API のモック実装を再利用できたため工数の短縮にも繋がった。

API の I/F が事前に定義されている場合は、今後のフロントエンド開発の業務でも使っていきたいと思う。

[^1]: 便宜上モックという用語を使うが、スタブやフェイクの方が適切な用語かもしれない。 Gerard Meszaros (2007). x Unit Test Patterns: Refactoring Test. Addison-Wesley Professional
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[React の Hydration エラーと HTML のコンテンツモデル違反]]></title>
            <link>https://blog.yn2011.com/2023-10-04-react-hydration-error-html</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2023-10-04-react-hydration-error-html</guid>
            <pubDate>Wed, 04 Oct 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# React の Hydration エラーと HTML のコンテンツモデル違反

## Hydration エラー

Next.js を使って開発していると、以下のような Hydration エラーが発生することがある。

`Text content does not match server-rendered HTML`

サーバーサイドでレンダリングされた HTML とブラウザでレンダリングされた HTML に差異が発生している場合に起きる。

差異が発生する理由はいくつかあるが、HTML のコンテンツモデル違反によりブラウザが HTML を補完している場合がある。

## ブラウザが HTML を補完する

いくつかパターンがあるので列挙する。

### 親子関係の違反

サーバーで以下の HTML を生成する。

```html
<p>
  <div>test</div>
</p>
```

これをブラウザが補完するとこうなる。

```html
<p></p> <!-- 終了タグが追加されている -->
<div>test</div>
<p></p>
```

p タグの子要素にはフレージング・コンテンツのみが許可されているが、div タグはフレージング・コンテンツではない。
したがってこれはコンテンツモデルに違反していることになるので、ブラウザが違反していない形に補完している。

他にも例として、[foster parenting](https://html.spec.whatwg.org/multipage/parsing.html#creating-and-inserting-nodes) と呼ばれる仕様がある。

サーバーで以下の HTML を生成する。

```html
<table>
  <p>test</p>
</table>
```

これをブラウザが補完すると以下になる。

```html
<p>test</p> <!-- 追い出されている -->
<table></table>
```

table タグの子要素に p タグを配置するのはコンテンツモデル違反のため、p タグが外側に移動する。
ちなみに foster parent は里親・育ての親という意味らしく、子要素を別の親要素に託す仕様の比喩と思われる。

### 終了タグの補完

サーバーで以下の HTML を生成する。

```html
<p>
  <b>
    <b>
      <b>
        <b>4
```

これをブラウザが補完すると以下になる。

```html
<p>
  <b>
    <b>
      <b>
        <b>4</b>
      </b> <!-- 終了タグが追加されている -->
    </b>
  </b>
</p>
```

ちなみに、終了タグの補完についてはノアの方舟(Noah's Ark) と呼ばれる補完仕様が存在する。[^1]
同一要素の開始タグが 4 つ以上ある場合に、2 番目以降の子に対しては終了タグに対応するタグが 3 つまでしか補完されない。

サーバーで以下の HTML を生成する。

```html
<p>
  <b>
    <b>
      <b>
        <b>
          4</p>
          3 <!-- 2番目 -->
```

ブラウザが補完すると以下になる。直前に b タグが 4 つあるが 3 つまでしか補完されない（残りは無視される）

```html
<!-- 省略 -->
        <b>4</b>
      </b>
    </b>
  </b>
</p>
<b>
  <b>
    <b>3</b>
  </b>
</b> <!-- 終了タグの補完は 3 つだけ -->
```

## まとめ

ブラウザは HTTP レスポンスの HTML をそのままパースするのではなく、いくつかの仕様に沿って HTML を改変することがある。
サーバーサイドレンダリング時に、コンテンツモデル違反に該当する HTML を生成すると Hydration エラーに繋がる場合があるので
注意しよう。

[^1]: 太田 良典, 中村 直樹 (2022). HTML 解体新書 ボーンデジタル]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Next.js の NEXT_PUBLIC_* という環境変数は何なのか]]></title>
            <link>https://blog.yn2011.com/2023-09-29-nextjs-public-env</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2023-09-29-nextjs-public-env</guid>
            <pubDate>Fri, 29 Sep 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# Next.js の `NEXT_PUBLIC_*` という環境変数は何なのか

## `NEXT_PUBLIC_` の意味

[Bundling Environment Variables for the Browser](https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables#bundling-environment-variables-for-the-browser) によると
`NEXT_PUBLIC_` という prefix を環境変数名に付けることでブラウザから該当の環境変数の値を参照可能になる。

ただし、環境変数の定義方法によっては `NEXT_PUBLIC_` という prefix は不要な場合がある。

## Next.js における環境変数の定義方法

Next.js では以下の方法で環境変数を定義できる。

- `.env` ファイル
- ターミナル (例：`NEXT_PUBLIC_TEST=foo next dev`)
- `next.config.js`

`next.config.js` では以下のように定義する。

```javascript
module.exports = {
  env: {
    API_URL: 'https://examle.com'
  }
};
```

`next.config.js` で環境変数を定義した場合は、デフォルトでクライアントコードに含まれる。
当然ブラウザから参照可能になるため、この場合は `NEXT_PUBLIC_` という prefix は不要になる。

公開するとセキュリティ上問題がある環境変数 (API Key 等) が意図せず含まれないように注意する必要がある。

## `NEXT_PUBLIC_` が有効なケース

`.env` ファイルやターミナルから環境変数を定義する場合はサーバーサイド（ビルド時の Node.js 環境）からしか参照できない。

この場合に `NEXT_PUBLIC` という prefix は意味を持ち、クライアントコードに対象の環境変数の値を含めることができる。

```bash
NEXT_PUBLIC_TEST=foo next dev
```

```jsx
<p>{process.env.NEXT_PUBLIC_TEST}</p>; // foo
```

`.env` ファイルで定義できる環境の種類に制限があり、[^1]業務では `next.config.js` を利用して環境変数を定義することが多いので、
`NEXT_PUBLIC_` prefix を利用することは少ない。

`next.config.js` を利用しない場合は prefix を利用しないとブラウザから参照できない点に注意したい。

[^1]: [Environment Variables](https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables#environment-variable-load-order)
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[JavaScript の関数がオブジェクト型の引数を取る場合に初期値を設定する]]></title>
            <link>https://blog.yn2011.com/2023-09-25-js-args-default-value</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2023-09-25-js-args-default-value</guid>
            <pubDate>Mon, 25 Sep 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# JavaScript の関数がオブジェクト型の引数を取る場合に初期値を設定する

JavaScript の関数は引数が与えられなかった場合に使用する初期値 (Default parameters) を定義できる。

```javascript
function multiply(a, b = 1) {
  return a * b;
}

console.log(multiply(5)); // 5
```

引数がオブジェクトの場合は [Destructuring 構文](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) を利用してプロパティごとに初期値を定義できる。

```javascript
function a({ foo = true, bar } = {}) {
  return foo;
}

console.log(a()); // true
console.log(a({ bar: 1 })); // true
```

## 参考

[Default parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters)
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Markdown Test]]></title>
            <link>https://blog.yn2011.com/2023-03-01-first-post</link>
            <guid isPermaLink="false">https://blog.yn2011.com/2023-03-01-first-post</guid>
            <pubDate>Wed, 01 Mar 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[
# タイトル

![melon](/images/twitter-melon.jpg)

## Autolink literals

www.example.com, https://example.com, and contact@example.com.

## Footnote

A note[^1]

[^1]: Big note.

## Strikethrough

~one~ or ~~two~~ tildes.

## Table

| aaa | bbb | cccc | dddd |
| --- | :-- | ---: | :--: |
| aaa | bbb | cccc | dddd |
| aaa | bbb | cccc | dddd |

## Tasklist

- [ ] to do
- [x] done c | d |

```tsx:components/Code.tsx
"use client";

import SyntaxHighlighter from "react-syntax-highlighter";
import { ocean } from "react-syntax-highlighter/dist/cjs/styles/hljs";

export function Code({ lang, children }: { lang: string; children: string }) {
  return (
    <SyntaxHighlighter language={lang} style={ocean}>
      {children}
    </SyntaxHighlighter>
  );
}
```
]]></content:encoded>
        </item>
    </channel>
</rss>