【JavaScript】数千行のコードを数行にしてしまう Proxy の魔力

こんにちは。[ぬこすけ](https://twitter.com/nuko_suke_dev)です。

最近（2022/10/20）に、ゆめみさんがクイズを出しました。

https://qiita.com/Yametaro/items/845b7968720d04241393

ゆめみさんの記事を読むと、 `Proxy` というオブジェクトが出てきました。
記事のタイトル通り「代入したのに、代入されない」、摩訶不思議なオブジェクトです。
`Proxy` って一体何者なんでしょうか？

ゆめみさんのクイズの例では **やめ太郎さんがたかしに改名するのを断固拒否する** 例ですが、
実は `Proxy` には **数千行のコードを数行にしてしまう魔力** も持っているのです。

今回は JavaScript の `Proxy` について魔力についてご紹介したいと思います。

本来、 `Proxy` は難しい内容ですが、 `setTimeout` の時と同じく、 **できるだけ初心者の方にもわかりやすく説明していきたい**と思います。

https://qiita.com/nuko-suke/items/5b16ab9de402547c5797

## Proxy って何？

簡単にいえば、 **オブジェクトを魔改造できる** ものです。
[ゆめみさんの記事の例](https://qiita.com/Yametaro/items/845b7968720d04241393#%E4%BB%A3%E5%85%A5%E3%82%92%E3%83%8F%E3%83%83%E3%82%AF%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8%E3%82%82%E3%81%A7%E3%81%8D%E3%82%8B)から引用させていただきます。

```javascript
const user = new Proxy(obj, { set: (target) => { target.name = "やめ太郎"; return true; } })
```

```javascript
// 元のオブジェクト
const obj = { name: "" }

// Proxyオブジェクトを生成
const user = new Proxy(obj, { set: (target) => { target.name = "やめ太郎"; return true; } })

user.name = "たかし"

console.log(user.name)
// -> "やめ太郎"

console.log(obj.name)
// -> "やめ太郎"
```

この例では `user.name = "たかし"` というように、「たかし」にユーザー名を変更しようとしても、強制的にやめ太郎になっています。
まさしく **`user` というオブジェクトが `Proxy` によって魔改造** させられています。

じゃあ **魔改造って具体的になんなん？** という感じでしょう。

![boy_question.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/851f9cf6-99bd-bb6b-8f2f-6fcf66b18c48.png)

その前に、**そもそも「プロキシ」ってどういう意味** でしょうか？

正解は「 **代理** 」という意味です。

もしかしたら「プロキシサーバー」とかよく聞くかもしれません。
プロキシサーバーとは、クライアントとバックエンドのサーバーを中継する「代理」のサーバーです。

![img_32171_14.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/237411b4-6f16-6cb3-f768-f8ec46657e95.jpeg)
（https://cybersecurity-jp.com/column/32171 より引用）

**`Proxy` も代理をしています** 。
先ほどのゆめみさんのコード例を見てみてください。

```javascript
// 元のオブジェクト
const obj = { name: "" }

// Proxyオブジェクトを生成
const user = new Proxy(obj, { set: (target) => { target.name = "やめ太郎"; return true; } })
```

この例では `obj` に対してセッターが働いた時 `Proxy` が `target.name = "やめ太郎";` するように代理をしています。

>じゃあ **魔改造って具体的になんなん？** という感じでしょう。

少しずつ解像度が上がってきたのではないでしょうか？
ここで「プロキシサーバー」のお話に戻ります。

プロキシサーバーの役割をもう少し詳しく考えます。

>クライアントとバックエンドのサーバーを中継する「代理」のサーバー

プロキシサーバーは中継をします。
**中継といっても、何を起点に何をする** のでしょうか？
例えば、クライアントから「ページの情報くれ」とリクエストが飛んだ時には、ページの情報を得るためにバックエンドのサーバーにリクエストを飛ばします。
また、バックエンドからページの情報が返ってきた時には、クライアントにページの情報を返します。

**`Proxy` にも同じことが言えます**。

>この例では `obj` に対してセッターが働いた時 `Proxy` が `target.name = "やめ太郎";` するように代理をしています。

「〇〇した時に〇〇する」 というような表現になっています。
これが **魔改造の真理** です。

**`Proxy`　は「〇〇した時に〇〇する」と定義して、対象のオブジェクトの代理として動く** のです。
先ほどのゆめみさんのコード例ではセッターでしたが、 **ゲッターや関数実行時にも、好きな処理を定義することできるのです！**

```javascript
const user = new Proxy({ name: "" }, { get: () => "やめ太郎" })
```

こちらも[ゆめみさんの記事のコード例](https://qiita.com/Yametaro/items/845b7968720d04241393#%E5%88%A5%E8%A7%A3-1)です。
この例ではゲッターを魔改造しており、ゲッターが働いた時は `"やめ太郎"` を返却するように代理しています。

## Proxy マジック

`Proxy` の威力をお見せします。

あなたのサイトにはバックエンドの API にリクエストするために、次のような実装をしていました。

```javascript
// 各 API へアクセスするためのパスを定義
const apiPathList = {
  articles: '/api/articles',
  users: '/api/users',
  // ...いっぱい
}

// 各 API へアクセスするためのオブジェクトを定義
const myClient = {
  fetchArticles(queryParams) {
    return new Promise((resolve, reject) => {
      // { page: 1 } などのオブジェクトを page=1 のようなクエリパラメータに変換
      const queryStr = (new URLSearchParams(queryParams)).toString();
      fetch(`${apiPathList.articles}?${queryStr}`)
        .then(res => res.json())
        .then(resolve)
        .catch(reject);
    })
  }
  fetchUsers(queryParams) {
    // ...
  }
  // API のパスごとに関数を用意
  // ...
}

// API へアクセスする時
const artcles = await myClient.fetchArticles({ page: 1 });
```

バックエンドにはいくつか API が用意してあり、 API ごとにリクエストする関数を作成しています。
しかし、サービスが拡大、バックエンドもマイクロサービス化していき、どんどん API が増えてしまいました。
API が増えるごとに各 API へアクセスするための URL の定義やリクエストする関数も増やさなければなりません。

ここで `Proxy` を使ってマジックします。

```javascript
const apiClient = {};

// キャメルケースを小文字にしてスラッシュに変更する関数
// 例： fetchApiArticles -> fetch/api/articles
const convertCamelCaseToLowerCaseSlash = str => {
  return str
    .split(/(?=[A-Z])/)
    .join('/')
    .toLowerCase();
}

// Proxy でゲッターを魔改造して、 fetch する関数を返すようにします
const handler = {
  // prop には、関数名が入ります
  // myClient.fetchApiArticles だったら prop は 'fetchApiArticles' になります
  // target と receiver は使いません
  get(_target, prop, _receiver) {
    return queryParams =>
      new Promise((resolve, reject) => {
        if (prop.indexOf('fetch') !== 0) {
          reject('プロパティ名の先頭は「fetch」をつけてください');
          return;
        }
        // { page: 1 } などのオブジェクトを page=1 のようなクエリパラメータに変換
        const queryStr = queryParams
          ? `?${new URLSearchParams(queryParams).toString()}`
          : '';
        // convertCamelCaseToSlash の結果が fetch/api/articles のようになるので、先頭の fetch は削除
        const requestUrl = convertCamelCaseToLowerCaseSlash(prop).replace('fetch', '');
        fetch(`${requestUrl}${queryStr}`)
          .then(data => data.json())
          .then(resolve)
          .catch(reject);
      });
  },
};

const myClient = new Proxy(apiClient, handler);

// 関数は定義されていないが /api/articles?page=1 でリクエストされる！！
const articles = await myClient.fetchApiArticles({ page: 1 });

// 関数は定義されていないが /api/users?page=1 でリクエストされる！！
const users = await myClient.fetchApiUsers({ page: 1 });

// 「プロパティ名の先頭は「fetch」をつけてください」 のエラーが出る
const response = await myClient.hoge();
```

**`myClient.fetchApiArticles` や `myClient.fetchApiUsers` というような関数が定義されていないのにも関わらず、期待するリクエストが飛びます！！**

そして、 **バックエンドの API が増えても改修する必要がありません**。
例えば、新しく `/api/search` のような API が追加されたとしましょう。
**`myClient.fetchApiSearch` を使うだけで終わり** です。

このコードでは一定のルールに基づいてリクエストする URL を組み立てています。
ルールというのは次の通りです。

* プロパティ名の先頭には「fetch」をつける。
* プロパティ名のキャメルケースがそのままリクエストするパスになる。
  * 例えば、 `fetchHogeFuga` の場合は `/hoge/fuga` へのリクエストパスに変換される。

コード例の `Proxy` ですが、 `handler` をいじればさらに機能を拡張させることができます。
例えば、リクエスト先のドメイン名を変えたいとか、リクエストするメソッドを変えたり、などです。

```javascript
// GET でリクエスト
myClient.fetchGetApiArticles();

// hoge.com のような別ドメインにリクエスト
myClient.fetchHogeDomainApiArticles();
```

テストケースも API ごとに関数を作成していた場合はその関数ごとにテストが必要ですが、この `Proxy` の例ではテキトーなプロパティ名に基づいて期待されるリクエスト URL が発行されているのを確認できれば良いので、テストケースも減らせます。

このように **`Proxy` を使ってかなりのコード量を減らすことができます**。

:::note info
[2022/11/02 追記]
コード例に載せたものを [fetch-magic](https://www.npmjs.com/package/fetch-magic) という npm ライブラリとして公開しました！
コードも公開しているので実装の参考にしたり、インストールして使ってみてください！
バグっていたりこうした方が良いとかあったら教えてもらえると嬉しいです！
:::

## Proxy の使いどころ

正直なんでもできちゃうので発想次第なところはありますが、肝となるのは **オブジェクトの拡張** でしょう。

`Vue` や `MobX` などのライブラリは、コンポーネントに渡すデータを `Proxy` で拡張します。
オブジェクトの変更を検知して、データ依存のあるコンポーネントのみをレンダリングしますが、これは `Proxy` を使って検知しています。
「あるオブジェクトに変更が走った時に〇〇したい！」というケースに使えるかもしれません。

その他、私の例で恐縮ですが、 `WebWorker` のスレッド管理や `WebWorker` 内でメインスレッドの DOM にアクセスする処理に `Proxy` を使ったことはあります。

`WebWorker` のスレッド管理というのは、例えば `WebWorker` で処理したいタスクをキューに積んでおいて、あるスレッドで処理が終了した時にキューにタスクがあれば引き続き実行、なければスレッドを終了させる、みたいな感じです。 `Proxy` でラップしたスレッドの状態管理するオブジェクトに変更が走った時に、「キューにタスクがあれば引き続き実行、なければスレッドを終了」などのスレッドを管理する処理をします。

後者の「`WebWorker` 内でメインスレッドの DOM にアクセスする処理」については下の記事で少し話を触れています。

https://qiita.com/nuko-suke/items/22702472543bfd3e585f#partytown-%E3%82%92%E8%87%AA%E4%BD%9C%E3%81%99%E3%82%8B

Ruby が特にこの分野に強いですが、 `Proxy` は言わば **メタプログラミング** と呼ばれる分野でもあります。
完全に余談ですが、もし Ruby を勉強している方がいれば、「メタプログラミングRuby」という本がおすすめです。
私はこの本を読んで Ruby 技術者認定試験の Gold をとりました！笑

https://nuko-programming.com/ranking/Ruby/book/f4e9ca32-6cfe-4064-b18a-b8f8d091283b

（ここから本買ってもらえると泣いて喜びます！！）

## Proxy 使いまくろう？

「 `Proxy` って便利やん！早速使いまくろう！」

![banzai_kids_boy1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/99679c6f-0aed-5132-8ebb-5d7dbfe2f0f4.png)

**ダメ** です。

まあ、ダメは言いすぎかもしれませんが、私はおすすめしません。

おすすめしない理由はいくつかあります。

一番の理由は **誰もコード読めない** ためです。
[Proxy マジック](#proxy-マジック)での `Proxy` を使ったコード例ですが、正直かなりコード読むの辛かったのではないでしょうか？
`Proxy` 自体なかなか癖がありますし、「こういうプロパティ名で書かなきゃいけない」という暗黙の独自ルールもありました。
`Proxy` の処理を書いた自分自身も数ヶ月後には読めなくなってるんじゃないかと思います😇

その他、 **TypeScript と相性が悪い** です。
`Proxy` で実装するとわかりますが、型定義するのが厳しくなるケースがよくあります。

そもそも `Proxy` というのが動的に、自由さを求めるものに対し、 TypeScript は静的に、厳格さを求めるものであります。
犬猿の仲ではないですが、お互い理念的に相反するものかなーと思います。

強大な力を得るためには代償が伴います。
デメリットを認識した上で `Proxy` は使いましょう。

## 最後に

いかがだったでしょうか？ `Proxy` についてなんとなくでも理解できたら嬉しいです！

JavaScript は深ぼれば新しい発見が得られます。
例えば、 `setTimeout` も実はただの指定した時間に実行する関数でなかったりします。

https://qiita.com/nuko-suke/items/5b16ab9de402547c5797

もし記述に誤りなどありましたらご指摘いただけば幸いです！

今後も記事を書いていこうと思うので、良かったら [Twitter](https://twitter.com/nuko_suke_dev) をフォローしていただければ記事投稿を通知します！

ここまでご覧いただきありがとうございました！