👋

サマーインターン@マネーフォワードの参加記

に公開

はじめに

マネーフォワードのエンジニアサマーインターンシップ(8/15〜8/28)にフロントエンド領域で参加させていただきました。

参加記を書くのが推奨されているとのことで、せっかくなので外国人留学生としての視点も交えて書いてみようと思います。

短い期間でしたが、メンターやチームメンバーの皆さんの丁寧なサポートのおかげで多くの学びを得ることができました。言語面・文化面での挑戦はありましたが、それ以上に「学ぶ楽しさ」と「安心して挑戦できる環境」を感じられたのは大きな喜びでした。

同じように日本でエンジニアを目指す留学生の方の参考になればうれしいです。

インターン参加までの経緯

私は外国人留学生として日本でのキャリアを考えており、特に英語と日本語が共存する環境に強く惹かれていました。マネーフォワードがエンジニアで英語を公用語として採用していることを知り、自然と関心を持つようになりました。事前の人事面接やコーディングテスト、技術面接を通じて会社の雰囲気を感じ、インターンシップに参加したい気持ちがさらに高まりました。

これまで私はバックエンド開発や機械学習の経験が中心でしたが、フロントエンドエンジニアリングにも強い関心を持っていました。実際の現場で経験を積むことで、自分が将来的にどの領域でキャリアを築いていきたいかを確かめられると考え、この機会に挑戦することを決めました。フロントエンド未経験という不安はありましたが、むしろそれを成長のチャンスと捉え、新しい挑戦を前向きに楽しむことができました。

タスク概要

  • マネーフォワード クラウド経費のフロントエンド開発に参加しました。
  • 既存の UI/仕様を尊重しつつ、Next.js + TypeScript で画面一部の作成と API 連携を担当しました。
  • データ取得は、既存プロジェクトの API を前提に、TypeScript で fetch などの取得処理(型定義/エラー処理/ローディング)を書きました。
  • なるべく小さく差分を出して、PR → レビュー → CI(GitHub Actions)を何度も回しました。

大きくやったこと

  1. プロダクトの把握
    まず既存の PR とドキュメントを読み、担当モジュールの API の返り値(フィールド・null 可否・ページネーションなど)を確認しました。旧実装の意図も確認し、fetch のリクエスト/レスポンスの型を先に決めてから実装に入りました。

  2. Select(プルダウン)コンポーネント
    再利用しやすい形に整え、API から取得した選択肢の表示、状態の置き場所などをはっきりさせました。

  3. データ取得の実装と接続
    検索条件の整形 → 取得 → API連携の流れをそろえ、どこで何をしているか追いやすくしました。

  4. テストと CI の整備
    MSW と React Testing Library で必要なケースだけ最小限にカバーし、Lint/型/ユニットは常にグリーンを保ちました。

具体的にやったこと

  • fetch 関数の実装:検索条件を受け取り、候補データを返す取得処理を TypeScript で書きました(型付け・エラー・ローディング込み)。
  • Select の拡張:API の結果を表示できるようにし、ローディングとエラーの出し方を統一しました。
  • つなぎ込みとデリバリー:Select と取得処理をつなぎ、ローカル確認 → PR → レビュー対応 → マージまで担当しました。
  • テストの追加:成功/失敗/キャンセルを MSW でモックして、UI の表示が期待どおりになるか確認しました。

使った技術

  • Next.js / React / TypeScript:コンポーネントの分割、状態の置き場所を意識して実装しました。
  • MSW / React Testing Library:壊れやすいところを押さえる最小テストを書きました。
  • AbortController / AbortSignal:多重リクエストを止め、最新の結果だけを反映させました。
  • pnpm / Node.js / GitHub:PR ベースで開発しました。

実装の流れ

Step 1:データ取得(fetch)の実装 → 単体テスト → PR

  • TypeScript で fetch 関数を実装しました(型定義/エラー時の扱い/ローディングの想定)。
  • 単体テストでは 正常・空データ・エラー を用意し、必要なところは 呼び出し順も toHaveBeenNthCalledWith で確認しました。
  • 背景・変更点・確認手順を書いて PR を作成し、レビュー後にマージしました。

Step 2:コンポーネントの実装 → テスト → PR

  • Select 相当のコンポーネントを作り、props と状態の役割をはっきりさせました。
  • React Testing Library で ローディング/エラー/結果表示 を確認しました。アクセシビリティの観点で getByRole を中心に書いています。
  • PR を出してレビュー対応し、マージしました。

Step 3:統合(取得関数 × コンポーネント) → テスト → PR

  • Step 1 の取得関数と Step 2 のコンポーネントをつなぎ込みました。
  • 連続操作で古いリクエストが残らないよう、必要に応じて Abort を入れて最新の結果だけ反映させました。
  • 統合テスト(正常パス+エラーパス1つ)を用意し、PR を出してマージしました。

印象に残ったこと①:toHaveBeenNthCalledWith で呼び出し順をはっきりさせる(最小サンプル)

本番コードではなく、動きを説明するための最小例です。
実際のプロジェクトだと分岐やエラーハンドリングはもっとあります。
ちなみに、この関数を使わなくてもテストは通せますが、チームでは「第何回でこう呼ばれるか」まで書くやり方が推されていて、自分もそれを真似しました(メンター/TL からの学び)。

サンプルコード(擬似コード)

// api.ts(実装は別ファイル・匿名化)
// 画面側はこの関数だけを使う想定にしておくと、テストでここをスパイできて書きやすかったです。
export async function getOptions(params: { q: string }) {
  const res = await fetch(`/api/options?q=${encodeURIComponent(params.q)}`);
  // 本番では try/catch やキャンセルなどを入れますが、ここでは省略しています。
  return res.json() as Promise<Array<{ id: string; label: string }>>;
}
// test.tsx(1回目=空、2回目=データ、以降=空の順序を残すサンプル)
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";
import * as api from "./api";            // ← fetch ではなく自作 API 関数をスパイ
import { SelectBox } from "./SelectBox"; // ← コンポーネント本体

beforeEach(() => {
  vi.clearAllMocks();
});

// 戻り値を順番に積んでおく(1回目=空、2回目=ダミーデータ、以降=空)
vi.spyOn(api, "getOptions")
  .mockResolvedValueOnce([])                                   // 1回目
  .mockResolvedValueOnce([{ id: "dummy1", label: "Sample" }])  // 2回目
  .mockResolvedValue([]);                                      // 3回目以降

test("初回は空→2回目でデータ(呼び出し順チェック付き)", async () => {
  render(<SelectBox />);

  // 1) 初回:空(プレースホルダしかない)
  await userEvent.click(screen.getByRole("combobox"));
  expect(screen.getByText(/select|選択してください|no options/i)).toBeInTheDocument();

  // 2) 条件を変えて再取得 → 2回目でデータ
  await userEvent.click(screen.getByRole("button", { name: /reload|更新/i }));
  const opts = await screen.findAllByRole("option");
  expect(opts.map(o => o.textContent)).toEqual(["Sample"]);

  // 3) 呼び出し順を断言しておくと、後で順番が変わったときに気づきやすい
  expect(api.getOptions).toHaveBeenCalledTimes(2);
  expect(api.getOptions).toHaveBeenNthCalledWith(1, { q: "" });
  expect(api.getOptions).toHaveBeenNthCalledWith(
    2,
    expect.objectContaining({ q: "sample" })
  );
});

使ってよかった理由

  • 意図をそのまま書ける
    「1回目はこの引数、2回目はこの引数」とテストに残せるので、レビューで話が早いです。

  • 後からの順序変更に強い
    UI はたまたま通っても、呼び出し順が入れ替わっていたら落ちるため、順序の回帰をちゃんと拾えます。

  • レビューしやすい
    見た目だけのテストより、どの関数がどう呼ばれたかまで書いてある方がレビューの根拠になります。

つまずきやすいところ

  • toHaveBeenCalledWith は 回数に無関係。順番を見るなら toHaveBeenNthCalledWith を使う。
  • 非同期はちゃんと待つ(findBy… / waitFor など)。待たずに検証するとフレークになりがち。

感想

最初は「画面に出ていれば十分では?」と思っていましたが、順序と引数まで書くようにすると、 レビューのやり取りが短くなって、後からの変更にも安心して対応できました。

これはメンターと TL から教わったポイントで、「仕様はテストに残す」という意識が自分の中で定着しました。

印象に残ったこと②:Next.js × GraphQL の「URL を気にしない」開発体験

ここで書くのは実際のプロダクトのコードではなくメンターさんとの勉強雑談後の感想です。
本番では型生成やキャッシュ、エラー処理がもっと増えます。
自分が一番「なるほど」と思ったのは、GraphQL だとエンドポイントを意識せずに、スキーマとクエリに集中できたところです。

サンプルコード(擬似コード)

// ここでは文字列クエリで擬似コードだけ。
const query = /* GraphQL */ `
  query Options($limit: Int!) {
    options(limit: $limit) {
      id
      label
    }
  }
`;

export default async function Page() {
  // 「どの URL に行くか」ではなく「何を取るか」を書く感じが楽でした
  const data = await graphqlClient.request(query, { limit: 20 });

  return (
    <ul>
      {data.options.map((o: { id: string; label: string }) => (
        <li key={o.id}>{o.label}</li>
      ))}
    </ul>
  );
}

使ってみて良かった点

  • 毎回 URL を組み立てなくていい
    /api/v1/... を考えるより、「このフィールドが欲しい」に集中できます。頭の切り替えが少なくて楽でした。

  • 型が自動でついて安心
    スキーマ変更があればビルドで教えてくれます。
    「取れると思ってたフィールドが無い」みたいな事故が減りました。

  • Next.js と合わせやすい
    サーバー側で取る/クライアントでキャッシュする、の切り替えが素直でした。SSR/ISR と相性が良いと感じました。

感想

最初は「REST の URL を書けば十分では?」と思っていましたが、GraphQL を使うと「URL を忘れて、スキーマとクエリに集中する」感じが想像以上に楽でした。

UI の近くにクエリを置ける設計も、レビューの理解コストが下がるので好きです。

チームの書き方に合わせて少しずつ直していく中で、型とスキーマに寄りかかって開発する安心感を実感しました。

インターンシップで得た学び

技術的な側面

コンポーネント設計
今回の開発では、コンポーネントごとに責務を明確に分けることを意識しました。props の設計などの扱いを整理し、「データを取得する部分」と「画面に描画する部分」を切り分けるようにしました。これによってコードの見通しが良くなり、後から読み返しても理解しやすくなっただけでなく、修正やテストの際にも影響範囲を把握しやすくなりました。

API 連携の UX
API との連携においては、失敗時にどうユーザーに伝えるか、リトライを許可するか、キャンセルやローディングの表示をどう設計するかなど、細かい部分まで考える必要がありました。普段の個人開発ではあまり意識していなかったポイントですが、実際のプロダクトではユーザーが「今どういう状態なのか」を理解できるだけで体験が大きく変わると実感しました。

テスト戦略
MSW と React Testing Library を使い、ユースケースを壊さないための最小限のテストを設計しました。成功・失敗・キャンセルといった代表的なパターンをしっかり抑えることで、大きな不具合を防げることを学びました。テストは単に数を増やすのではなく「壊れやすい部分を守る」という考え方が重要だと感じました。

CI / GitHub Actions / ローカルテスト
業務では CI(GitHub Actions)が常にグリーンであることが前提なので、push 前にできるだけローカルでテストや Lint・型チェックを実行し、問題がない状態に整えてから小さな単位で PR を出すようにしました。その結果、GitHub Actions でもエラーが出にくくなり、レビューもスムーズに進み、開発全体のリズムが安定することを実感しました。こうした意識は個人開発ではあまり持っていなかったので、大きな学びになりました。

技術以外の側面

  • 英語環境での開発: 技術的な議論を英語で行う機会が多く、グローバルな環境での開発を実際に体験できました。Japanese English でもほとんど聞き返されることなく意思疎通ができ、自信につながりました。外国人でも安心して発言できる雰囲気があるのは大きな学びでした。

  • 仕様のすり合わせ: 開発を進める中で、仕様について細かく確認し合い、考え方の違いをなくすためのコミュニケーションを繰り返しました。最初は難しさもありましたが、相手に伝わりやすい説明や質問の仕方を工夫することで、徐々に精度が高まったと感じます。

  • 効率的な時間活用: 短期間のインターンだったので、時間の使い方を常に意識しました。午前中の作業時間は集中してコードや資料を進め、午後はレビュー対応や打ち合わせに充てるなど、メリハリをつけることで効率良く進められました。限られた日数の中でもやりたいことをやり切る工夫ができたのは良い経験でした。

  • 社内の知識ベースへのアクセス: 社内記事や Slack のチャンネルが適切に公開されており、インターン生でも多くの情報にアクセスできました。日々の記事を読むことで会社の文化や価値観を理解できました。隙間時間に社内記事を読む習慣がつき、会社理解が深まったのはありがたかったです。

その他

  • AI ツールの活用
    今回のインターンが、AI ツールを本格的に使って開発する最初の経験でした。開発支援の AI ツールが日常的に利用されており、AI がどれくらい自然に開発の一部になっているか実感しました。利用時は会社としての承認などセキュリティ面の運用も徹底されていることを学びました。

  • グローバルな雰囲気のある拠点
    拠点全体がとてもグローバルで、外国人でも自然に受け入れられていると感じました。皆さんが親切に接してくださり、ランチや雑談を通じて他チームの方とも交流でき、会社のカルチャーを肌で知ることができました。

  • 1on1 文化の厚さ
    インターン期間中は多くの 1on1 を設定していただきました。タスクの進捗確認にとどまらず、キャリアや技術の相談まで幅広く話せる時間で、1on1 文化の厚さを強く感じました。

  • 学びを支援する文化
    英語学習会や開発勉強会など社員が自主的に立ち上げる場と、会社が体系的に提供する学びの場の両方がありました。インターンでありながらそうした活動に参加させていただき、学びを重視し成長を後押しする文化が根付いていると実感しました。

最後に

2週間という短い期間でしたが、実際のプロジェクトの開発フローを体験し、レビューやデプロイまで一連の流れに関わることができました。バックエンド経験しかなかった自分にとって、フロントエンドの開発を現場で体験できたことは非常に貴重であり、今後のキャリアに大きくつながると感じています。

今回のインターンでは、技術的な成長に加えて、グローバルな環境でのコミュニケーションや、チーム開発の文化を学ぶこともできました。これからも今回の経験を糧に、学習やキャリアをさらに伸ばしていきたいと思います。

最後まで読んでいただき、ありがとうございました!

Discussion