🔭

同じ5行のコードが全く違って見える12の瞬間、なぜ私たちは学ぶのか?

に公開

最近、ふとした気づきがありました。

それは、「同じものを見ていても、過去と現在の自分では見えている世界がまったく違っている」ということです。

みなさんには、このコードからどんな世界が見えますか?

async function getUserName(userId) {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  return user.name;
}

はじめに

こんにちは、株式会社ココナラ在籍のKです。

本記事では、冒頭の5行のコードを通して、私たちが学ぶ理由について考えてみたいと思います。

TL;DR

  • 同じコードを見ても、人によって見えるものが違っている
  • 学習を重ねることで、それまで見えなかった世界が見えてくる
  • 学習とは、知識の獲得ではなく、世界に対する認識を変化させること

対象読者

  • 「動くコードが書ければ十分」と感じているエンジニア
  • 学習の必要性は感じつつも、モチベーションが保てない方
  • チームに学習の重要性を伝えたい方

まず、どう見えるか?

async function getUserName(userId) {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  return user.name;
}

プログラミングを学びたての方には、APIからユーザー名を取得する関数が見えるのではないでしょうか。
それ以上でも、それ以下でもありません。

見えなかったものが見えてくる

学習を重ねることで、このコードに隠れていた無数の問題や可能性が見えてきます。

学びとともに、このシンプルなコードがどう違って見えるようになっていくのか。
これから、順を追って見ていきましょう。

エラーハンドリングを学んだら

async function getUserName(userId) {
  // fetchは5xxエラーでも例外を投げないが、大丈夫?
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  // たまたまエラーのJSONにnameという項目があると、ユーザー名として返されてしまう
  return user.name;
}
// 例外が発生する関数なのか、利用側で読み取るのが難しい

APIで500エラーが発生すると、どうなるでしょうか?
エラーレスポンスのJSONに偶然"name": "Internal Server Error"が含まれていたら?

fetchは、通信自体が成功していれば、500エラーであっても例外を投げません。
そのため、この関数はエラーレスポンスの内容をユーザー名として返してしまいます。

エラーハンドリングを学んだら、この関数が意図した通りに動いてくれない野放図なコードだとわかります。
また、Result型(成否を型レベルで保持) のような、エラーを明示的に扱う手法の価値も理解できるようになります。

パフォーマンスを学んだら

async function getUserName(userId) {
  // 毎回ネットワーク通信が発生する
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  // nameだけのためにユーザーの全情報を取得するのは無駄
  return user.name;
}

一覧に50人のユーザーが表示される画面でこの関数が呼ばれたら、どうなるでしょうか?
また、ユーザー情報に画像などの巨大なバイナリデータが含まれていたら?

APIが50回も叩かれ、不要なデータが転送され、ユーザーが待ちぼうけ状態になりそうです。
本来なら、1回のAPIコールで50人分の名前だけをまとめて取得すべきところです。

パフォーマンスを学んだら、このコードが典型的なN+1問題(本来1回でまとめて取得できるが、N回に分けて取得してしまう問題) だと即座に認識できるようになります。
また、フィールドのオーバーフェッチ(必要以上に過剰なデータを取得してしまう問題) を防ぐ必要性も理解できるようになります。

セキュリティを学んだら

async function getUserName(userId) {
  // そのまま文字としてURLに含めて大丈夫?
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  return user.name;
}

userId/admin/1が入って来たらどうなるでしょうか?

意図しないエンドポイントにアクセスしてしまい、冷や汗が止まらなくなります。

セキュリティを学んだら、入力値の検証とサニタイゼーション(悪意のある文字列を無害化する処理) の重要性が理解できるようになります。

認証・認可を学んだら

async function getUserName(userId) {
  // 認証・認可の仕組みがない無防備なAPI呼び出し
  const response = await fetch(`https://api.example.com/users/${userId}`);
  // アクセス権限チェックなしでデータにアクセスできていいの?
  const user = await response.json();
  return user.name;
}

このAPIは、世界中の誰でもアクセスできてよいのでしょうか?

認証(誰であるかの確認)も認可(何ができるかの確認)もなく、玄関のドアを全開にしているようなAPIになってしまっています。

認証・認可を学んだら、JWT(認証情報を含み認可判断にも使えるトークン)OAuth 2.0(ユーザーの代理でアクセス許可を安全に委譲する認可の仕組み) などの、認証・認可の複雑な世界が理解できるようになります。

信頼性を学んだら

async function getUserName(userId) {
  // サーバーから応答がない場合、永久に待ってしまう
  // 一時的なネットワーク障害でユーザーにエラーが返ってしまう
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  return user.name;
}

通信先のAPIから応答がない場合、どうなるでしょうか?

このコードには、タイムアウトもリトライもありません。
APIからの応答がなければ、ユーザーの画面は固まってしまいます。
さらに、サーバーのリソースも枯渇して、夜間に叩き起こされかねません。
また、一度のネットワークエラーで即座に処理が失敗してしまいます。

信頼性を学んだら、タイムアウトリトライ指数バックオフ(負荷を抑えるため、リトライ間隔を2倍ずつ増やす戦略)サーキットブレーカー(障害サービスへのリクエストを自動遮断する仕組み) など、分散システムの信頼性を支える仕組みの必要性を痛感するようになります。

保守性を学んだら

async function getUserName(userId) {
  // ハードコードされたエンドポイント
  const response = await fetch(`https://api.example.com/users/${userId}`);
  // 型がなく、レスポンスの構造が暗黙知になっている
  const user = await response.json();
  // フィールド名の変更で壊れるが、気付けない
  return user.name;
}

環境が変わったら?APIのバージョンが上がったら?namedisplayName になったら?

保守性を学んだら、この関数が環境や暗黙知に依存した、引き継ぐのがためらわれるようなコードだと気づきます。
そして、型安全性設定の外部化などの必要性が理解できるようになります。

テスタビリティを学んだら

async function getUserName(userId) {
  // モックできない外部通信
  // テストが実際のAPIに依存してしまう
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  return user.name;
}

単体テストを書こうとしたら、どうなるでしょうか?

fetchをモックする複雑さ、環境依存するテストの脆さに直面します。
実際のAPIを叩くしかないテストでは、CIの前で祈るしかありません。
仮に失敗が稀だとしても、実行回数が多ければ、それなりの頻度で失敗するものです。

テスタビリティを学んだら、依存性注入(依存を外から渡してテスト時に差し替え可能にする設計) の必要性が、実感として理解できるようになります。

スケーラビリティを学んだら

async function getUserName(userId) {
  // 大量リクエスト時に耐えられる?
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  return user.name;
}

1秒間に1,000回、呼ばれたらどうなるでしょうか?
サーバーは耐えられるでしょうか?ネットワークは?データベースは?

カスケード障害(ドミノ倒しのように障害が連鎖する) の光景が見えます。

スケーラビリティを学んだら、バッチ処理(複数のリクエストをまとめて処理する手法)非同期キュー(リクエストを順番待ちさせて負荷を平準化する仕組み)レート制限(単位時間あたりのリクエスト数を制限する仕組み) などの必要性が理解できるようになります。

可観測性を学んだら

async function getUserName(userId) {
  // レスポンスタイムが計測されていない
  const response = await fetch(`https://api.example.com/users/${userId}`);
  // 失敗がログに残らない
  const user = await response.json();
  // どのユーザーの取得に失敗したか追跡できない
  return user.name;
}

本番環境で「処理が遅い」「エラーが出る」と報告されたら、どうやって調査するのでしょうか?

調査しようとした瞬間、詰んで絶望する未来が見えます。

可観測性を学んだら、構造化されたログメトリクス(レスポンスタイムやエラー率などの性能指標の計測)トレーシング(分散システムでリクエストの流れを追跡する仕組み) などの大切さが実感できるようになります。

チーム開発を学んだら

async function getUserName(userId) {
  // なぜこのコードがリリースされた?
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  return user.name;
}

このコードは、どんなチームで書かれたのでしょうか?

コードレビューの文化があれば、エラーハンドリングの指摘があったはずです。
技術負債(戦略的、あるいは偶発的な後回し) は管理されているでしょうか?
チームに学習する文化はあるでしょうか?

チーム開発を学んだら、コードの奥にあるチーム全体の文化(や深淵)が見えてきます。
同時に、文化を変えることの難しさ一歩ずつ改善していくことの大切さも理解できるようになります。

Vibe Codingを学んだら

async function getUserName(userId) {
  // どんなコンテキストを与えれば、期待するコードが得られるか?
  // AIの出力から、隠れた問題を見抜けるか?
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  return user.name;
}

このコードをAIで改善する際、私たちはどんなコンテキストを与えられるでしょうか?

「信頼性を重視して」
「パフォーマンスが最重視」

これらのコンテキストを与えられるのは、その重要性や必要なコンテキストを理解しているから。
AIが生成したコードの問題に気づけるのは、落とし穴を知っているから。

Vibe Codingを学んだら、これまで学んできたすべてがVibe Codingで要求される 「言語化する力、コードを読み解く力」 の源泉になっていることに気づきます。
将来のことはわかりませんが、少なくとも現時点(2025年8月)では、これらの力はVibe Codingに必須のように思います。


もし、ここまでのすべてを学んだら

// エラーハンドリング: Result型で値を型として明示
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
type UserFetchError = 'UNAUTHORIZED' | 'RATE_LIMITED' | 'INTERNAL_ERROR';

// 保守性: TypeScriptでAPIレスポンスの型安全性を確保
type UserResponse = {
  id: string;
  name: string;
};

async function getUserNames(
  // パフォーマンス: 複数ユーザーIDをまとめて取得することで、N+1問題を回避
  userIds: string[],
  // テストビリティ: 依存性注入により、モックに差し替え可能に
  deps: Dependencies,
): Promise<Result<Map<string, string>, UserFetchError>> {
  // 信頼性: リトライ機構により、一時的な障害に対応
  const maxRetries = 3;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    // 信頼性: タイムアウトにより、ネットワーク遅延からアプリケーションを保護
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 5000);

    try {
      const response = await deps.fetchClient(
        // スケーラビリティ: バッチ処理でリクエスト回数を最小化
        // 保守性: URLのベタ書きを排除
        `${deps.apiBaseUrl}/users/batch`,
        {
          method: 'POST',
          headers: {
            // 認証・認可: JWTでAPIを保護
            Authorization: `Bearer ${await deps.jwtProvider()}`,
            'Content-Type': 'application/json',
          },
          // パフォーマンス: 複数ユーザーIDをまとめて取得することで、N+1問題を回避
          // パフォーマンス: 必要なフィールドのみ取得してデータ転送量を最小化
          body: JSON.stringify({ ids: userIds, fields: ['id', 'name'] }),
          signal: controller.signal,
        },
      );

      // エラーハンドリング: HTTPステータスを適切にハンドリング
      if (!response.ok) {
        if (response.status >= 400 && response.status < 500) {
          switch (response.status) {
            // 認証・認可: APIを保護
            case 403:
              throw new NonRetryableError('UNAUTHORIZED', response.status);
            // スケーラビリティ: レート制限対応
            case 429:
              throw new NonRetryableError('RATE_LIMITED', response.status);
            default:
              throw new NonRetryableError('INTERNAL_ERROR', response.status);
          }
        }
        throw response;
      }

      const users = await response.json();

      // セキュリティ: レスポンスを厳密に検証
      if (!isValidUsersResponse(users)) {
        throw new NonRetryableError('INTERNAL_ERROR');
      }

      const results = new Map(users.map((user) => [user.id, user.name]));
      return { ok: true, value: results };
    } catch (error) {
      // 可観測性: 構造化されたログ
      deps.logger.error('Failed to fetch users', {
        userIds,
        attempt,
        error,
      });
      // 可観測性: メトリクス
      deps.metrics.increment('user_fetch_error', {
        status: error instanceof Response || error instanceof NonRetryableError ? error.status : undefined,
      });

      if (error instanceof NonRetryableError) {
        return { ok: false, error: error.errorType };
      }

      if (attempt < maxRetries) {
        // 信頼性: 指数バックオフ(1秒→2秒→4秒)によるリトライで過負荷を防止
        const delay = 2 ** attempt * 1000;
        await new Promise((resolve) => setTimeout(resolve, delay));
      }
    } finally {
      clearTimeout(timeoutId);
    }
  }

  return { ok: false, error: 'INTERNAL_ERROR' };
}

これはかなり極端な例です。
実際には、もっと抽象化してシンプルにすべきです。
また、ライブラリ、サービスメッシュやAPIゲートウェイ(リトライ、タイムアウト、サーキットブレーカーなどをプラットフォームレベルで自動化) などの利用が推奨されます。

しかし、今まで学んだすべての概念がここに実装されています。

これだけのことを自分の頭だけで考え、問題に気づき、対処法を見出すことは、「超人でもない限り難しい」 です。

だからこそ、自身の経験だけから「がむしゃら」に学ぶのではなく、先人たちが積み重ねてきた知識と経験から学ぶ、「巨人の肩に立とうとする姿勢」 が大切なのかもしれません。


そして、トレードオフを学んだら

async function getUserName(userId) {
  // どこまでやる?:プロトタイプ?本番?
  // 何を重視する?:開発速度?信頼性?パフォーマンス?
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  return user.name;
}

このコードを完璧にしようとしたら、どこまで実装すべきでしょうか?

トレードオフを学んだら、あらゆる状況に有効な 「銀の弾丸は存在しない」 ことが理解できます。

時間とのトレードオフ

完璧なコードを書こうとすると、開発速度が犠牲になります。
エラーハンドリング、パフォーマンス、可観測性など、すべてを実現しようとすれば、それだけ時間がかかります。
そして、時間は最も貴重な資源 です。

だからこそ、状況に応じた判断が必要になります。
小規模サービスなら、簡易的な監視で事足りるかもしれません。
プロトタイプなら、最初の5行のコードで十分な可能性が高いでしょう。

アーキテクチャ特性間のトレードオフ

時間以外のトレードオフもあります。

分散システムのPACELC定理(CAP定理を拡張した、一貫性・可用性・レイテンシのトレードオフ定理) が示すように、すべてを完璧にはできません。

一貫性を重視するならRaft(分散合意アルゴリズム) のような仕組みが必要になりますが、それは可用性やパフォーマンスとのトレードオフになります。

トレードオフを学んだら、「制約の中で最適なバランスを見つける」 という、エンジニアリングの本当の難しさと面白さが見えてきます。

そして、「Vibe Codingで適切なコンテキストを与える」 重要性も実感できます。
すべてを同時に成立させることができないからこそ、「今回は何を優先すべきか」という判断を、具体的なコンテキストとしてAIに与える必然性があるのだと思います。

スキルの制約を超える

しかし、スキルだけは他の制約とは違います。

パフォーマンスと信頼性は、本質的にどちらかを立てればもう一方が犠牲になりやすいトレードオフの関係です。
一方、スキルの制約は自身の成長で乗り越えることができます。
学び続けることで、「知らないから選べない」から 「知った上で選ぶ」 へと変えることができます。

学び続けなかったら

async function getUserName(userId) {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  return user.name;
}

一方で、学び続けなかったら、ここにあるのは、ただの「動くコード」です。
それ以上でも、それ以下でもありません。

結果は同じでも、「あらゆる選択肢を知り尽くした上で、あえて何もしない」、あるいは「最小限に留める」という意思決定を知的な誠実さをもって下せるようになった状態とは、結果に至るまでの過程や思考の深さがまったく異なります。

同じコード、だが違う世界

初心者が書くコードと、熟練者が書くコード。
時に、まったく同じ5行になります。

でも、初心者は「他に方法を知らないから」 これを書きます。
熟練者は「すべての選択肢を知った上で、今回はこれで十分だ」 と判断して書きます。

見えている世界がまったく違います。

学び続ける理由

同じ5行のコードから、
学ぶほどに、無限の世界が浮かび上がってきます。
学ぶほどに、避けるべき落とし穴が明らかになります。
学ぶほどに、予想に反して難しさを感じます。

私たちが学び続ける理由。
それは、「世界の見え方が変わっていく、その瞬間瞬間が素晴らしく面白いから」 なのかもしれません。

あなたは、なぜ学ぶのでしょうか?

今、どんな世界が見えていますか?
そして明日、見えているのはどんな世界でしょうか?


以上、ある夏の暑い日にふと思ったことでした。

ココナラでは積極的にエンジニアを採用しています。
私たちと一緒に学んでみませんか?

採用情報はこちら。
https://coconala.co.jp/recruit/engineer/

カジュアル面談希望の方はこちら。
まずはココナラの魅力やキャリアパスについて、お気軽にお話ししませんか?
https://open.talentio.com/r/1/c/coconala/pages/70417

Discussion