フロントエンドエンジニアとして働き始めた頃は、実装速度が評価へ結びつきやすいものです。ReactやVueの扱いに慣れ、CSSを短時間で整え、APIレスポンスから型定義と画面を組み立てられるようになると、任せられる仕事の範囲が広がります。

ただし、経験年数が増えるほど、単純な実装速度だけでは差がつきにくくなります。定型的なコンポーネント、型定義、テストの雛形、管理画面の基本構造は、AIを使って短時間で生成できるようになりました。手で書く速度は今後も必要ですが、それだけを強みにすると、道具の進歩によって優位性が小さくなります。

経験5年前後から問われるのは、コード量ではなく、曖昧な要件を整理し、壊れにくい仕組みを選び、事業へ継続的に届ける力です。この記事では、その力を四つに分けて考えます。

手作業の速さだけでは差がつきにくい

画面実装には、一定量の反復作業があります。

  • デザインをコンポーネントへ分解する
  • API仕様からTypeScriptの型を作る
  • 配列を一覧や表へ描画する
  • フォームのバリデーションを組み込む
  • Storybookやテストの雛形を用意する

これらは重要な仕事ですが、過去の実装例と規則がそろっていれば自動化しやすい領域でもあります。AIへ設計ルール、既存コンポーネント、アクセシビリティ要件を渡せば、最初の実装案を短時間で作れます。

ここで必要なのは、AIと実装速度を競うことではありません。生成結果が要件を満たすか、既存設計と矛盾しないか、障害時にどう振る舞うかを判断できることです。実装そのものよりも、実装へ渡す条件と受け入れ基準の質が重要になります。

1. 画面ではなくデータ仕様を設計する

フロントエンドの複雑さは、UIだけから生まれるわけではありません。一つの画面を表示するために複数のAPIを呼び出し、レスポンスを結合し、欠損値を補正し、表示用の形式へ変換する構成では、画面側へ多くの責務が集まります。

この状態でコンポーネントだけを整理しても、データ取得部分の複雑さは残ります。経験を積んだエンジニアには、受け取ったAPIをそのまま使うだけでなく、画面に必要なデータ仕様を整理する力が求められます。

ここでいうデータ仕様とは、APIが返す項目、必須値と任意値、欠損やエラーの表現、更新頻度など、画面とデータ提供側で合意する内容です。データ基盤やAPI設計では「データ契約(data contract)」と呼ぶこともありますが、この記事では意味が伝わりやすい「データ仕様」を使います。

たとえば、次の点をバックエンド担当者と合意します。

  • 画面が必要とする項目と更新頻度
  • 必須値、欠損値、エラー時の表現
  • ページング、並び順、検索条件の責務
  • 複数リソースを集約する場所
  • キャッシュ可能な範囲と失効条件

既存APIをすぐに変更できない場合は、BFF(Backend for Frontend)やサーバー関数、エッジ処理を間に置く選択肢があります。ただし、BFFを追加すれば必ず改善するわけではありません。運用対象が一つ増えるため、単純な変換だけならクライアント側で処理した方がよい場合もあります。

重要なのは技術名ではなく、どの層がどの責務を持つと変更しやすいかを判断することです。データ仕様を整理できる人は、画面の実装者から、システム全体の設計へ関与する人へ役割を広げられます。

2. 正常系ではなく失敗条件を設計する

AIが生成するコードは、正常系の実装を始めるには便利です。一方で、実際のサービスでは次のような条件が発生します。

  • 通信が途中で止まる
  • 応答順が入れ替わる
  • 同じ操作が連続して実行される
  • APIが一時的に429 Too Many Requestsを返す
  • タブがバックグラウンドへ移動する
  • コンポーネントの破棄後にレスポンスが返る

リアルタイムに近い画面で、setInterval()から無条件にfetch()を呼び出すと、前のリクエストが終了する前に次のリクエストが始まる可能性があります。弱い回線ではリクエストが重なり、サーバーとブラウザーの双方へ負荷をかけます。

次の例では、リクエストの完了後に次回を予約し、バックグラウンド中の処理を抑え、失敗が続いた場合は待機時間を延ばします。コード全体を追わなくても流れが分かるよう、主要な処理へコメントを付けています。

import { useEffect, useState } from "react";

type PollingResult<T> = {
  data: T | null;
  error: Error | null;
};

export function usePolling<T>(
  url: string,
  intervalMs = 5_000,
): PollingResult<T> {
  // 画面へ返す最新データと直近のエラーを保持する
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    // Effectの破棄後にタイマーや通信を継続しないための状態
    let stopped = false;
    // 前回の通信が完了する前に次の通信を始めないための状態
    let running = false;
    // 連続失敗数に応じて待機時間を延ばす
    let failures = 0;
    let timer: ReturnType<typeof setTimeout> | undefined;
    let controller: AbortController | null = null;

    // setIntervalではなく、現在の処理が終わってから次回を予約する
    const schedule = (delay: number) => {
      if (!stopped) {
        timer = setTimeout(() => void poll(), delay);
      }
    };

    const poll = async () => {
      // 停止済み、または通信中なら新しいリクエストを開始しない
      if (stopped || running) return;

      // 非表示のタブでは通信せず、次回の確認だけを予約する
      if (document.visibilityState === "hidden") {
        schedule(intervalMs);
        return;
      }

      running = true;
      // アンマウント時に進行中のfetchを中断できるようにする
      controller = new AbortController();

      try {
        const response = await fetch(url, {
          signal: controller.signal,
          // ポーリングではブラウザーキャッシュを使わず最新値を取得する
          cache: "no-store",
        });

        if (!response.ok) {
          throw new Error(`Request failed: ${response.status}`);
        }

        const nextData = (await response.json()) as T;
        setData(nextData);
        // 成功したらエラーと連続失敗数をリセットする
        setError(null);
        failures = 0;
      } catch (cause) {
        // アンマウントに伴う中断は、画面へ表示するエラーに含めない
        if (
          stopped ||
          (cause instanceof DOMException && cause.name === "AbortError")
        ) {
          return;
        }

        const nextError =
          cause instanceof Error ? cause : new Error(String(cause));
        setError(nextError);
        failures += 1;
      } finally {
        running = false;

        if (!stopped) {
          // 失敗回数に応じて待機時間を延ばし、最大60秒に制限する
          const delay = Math.min(intervalMs * 2 ** failures, 60_000);
          schedule(delay);
        }
      }
    };

    // タブへ戻ったときは待機中のタイマーを破棄し、すぐ再取得する
    const handleVisibilityChange = () => {
      if (document.visibilityState !== "visible") return;

      if (timer) clearTimeout(timer);
      void poll();
    };

    document.addEventListener("visibilitychange", handleVisibilityChange);
    // コンポーネント表示時の初回取得を開始する
    void poll();

    return () => {
      // タイマー、通信、イベントリスナーをまとめて解除する
      stopped = true;
      if (timer) clearTimeout(timer);
      controller?.abort();
      document.removeEventListener(
        "visibilitychange",
        handleVisibilityChange,
      );
    };
  }, [intervalMs, url]);

  return { data, error };
}

これは汎用ライブラリの代替ではなく、設計上の論点を示す例です。実運用では、Retry-Afterの処理、認証切れ、ログ出力、リトライ対象の分類、オンライン状態、テスト方法なども決める必要があります。更新頻度が高い場合は、ポーリングよりServer-Sent EventsやWebSocketが適することもあります。

価値が出るのはコードを長く書いた部分ではありません。失敗条件を洗い出し、負荷と利用者体験を両立する方針を選んだ部分です。

3. 作らない判断を持つ

経験を積むほど、すべての要望を自前実装することが最適ではないと分かってきます。新しい機能を作る前には、少なくとも次の選択肢を比較します。

  • 既存画面の運用で代替する
  • 要件を小さくして検証する
  • 外部SaaSを利用する
  • オープンソースを導入する
  • 一時的な手動運用で需要を確認する
  • 自社開発する

外部サービスやiframeを使えば開発期間を短縮できることがありますが、常に低コストとは限りません。認証連携、個人情報、アクセシビリティ、障害時の責任範囲、利用料金、サービス終了時の移行まで含めて判断する必要があります。

自社開発も同様です。初期実装が一週間でも、その後の監視、問い合わせ対応、ブラウザー更新への追従、仕様変更が続けば、保有コストは大きくなります。

「作れるか」ではなく、作り続ける価値があるかを説明できることが、経験者の役割です。不要な実装を減らす判断は、コードを書く能力と同じくらい重要です。

4. 個人の速さをチームの再現性へ変える

一人だけが速く実装できても、その人が休むと作業が止まる状態では、組織として安定しません。経験年数が増えるほど、個人の技術をチームが再利用できる形へ変える必要があります。

具体的には、次のような仕事です。

  • 要件と受け入れ条件を文章にする
  • コンポーネントとAPIの境界を共通化する
  • 障害時の確認手順をRunbookへ残す
  • ログ、メトリクス、トレースから原因を追えるようにする
  • 小さくリリースし、問題があれば戻せる手順を用意する
  • AIへ渡す指示とレビュー観点をチームで共有する

AIを導入すると、実装の初速は上がります。その一方で、生成されたコードの設計や品質が人によってばらつく可能性があります。コーディング規約だけでなく、どこまでAIへ任せるか、何を人が確認するか、どのテストが通れば採用するかを決めておく必要があります。

経験者の価値は、自分だけが難しい問題を解けることではありません。問題を分解し、判断基準を残し、別の人でも同じ品質で進められる状態を作ることにあります。

AIに任せる範囲と人が引き受ける責任

AIへ任せやすいのは、入力と完成条件が明確な作業です。

  • 定型コンポーネントの初稿
  • 型定義やAPIクライアントの雛形
  • テストケースの候補
  • 既存コードの説明と変更案
  • ドキュメントの下書き

人が引き受けるべきなのは、完成条件そのものを決める仕事です。

  • 誰のどの問題を解決するのか
  • 失敗時に何を守るのか
  • どのデータをどこまで扱ってよいか
  • 将来の変更へどの程度備えるか
  • 開発と運用へどれだけ費用をかけるか

AIが出したコードを読んで修正できるだけでは十分ではありません。そもそもそのコードが必要か、別の構成の方が安全か、運用できるかまで判断する必要があります。

経験年数より責任範囲が差を作る

「5年働けば自動的に市場価値が上がる」という意味ではありません。同じ作業を繰り返していれば、年数だけ増えても判断できる範囲は広がりません。一方で、短い経験年数でも、要件整理、データ設計、運用、障害対応まで担当すれば、早い段階から広い視点を持てます。

差がつくのは、次の問いへ答えられる範囲です。

  • この機能は本当に必要か
  • どの層へ責務を置くべきか
  • 通信や依存サービスが失敗したらどうなるか
  • リリース後に状態を観測できるか
  • 問題が起きたとき安全に戻せるか
  • チームの別の人が保守できるか

フレームワークの知識や実装速度は、これらを実現するための土台です。AIによって土台を作る時間が短くなった分、エンジニアは判断と検証へ時間を使えるようになります。

まとめ

AI時代にフロントエンドエンジニアの価値がなくなるわけではありません。ただし、定型的な実装を速くこなすことだけでは、長期的な差を作りにくくなります。

経験を価値へ変えるには、次の四つを意識する必要があります。

  1. 画面だけでなくデータ仕様を設計する
  2. 正常系ではなく失敗条件から考える
  3. 作ることと作らないことを比較する
  4. 個人の速さをチームの再現性へ変える

AIは実装候補を増やしてくれます。どの候補を採用し、どのリスクを引き受け、どの品質で継続運用するかを決めるのは、引き続きエンジニアの仕事です。