🛠️

学生が実務レベルを意識して、React + TypeScript + Dockerで開発者ツールを作ってみた

に公開

1.はじめに


今回は、学生である自分の学習用と便利ツールの両方を兼ねてWebアプリを作成してみました。技術テーマを設定し、目的意識を持って開発に取り組みました。
今回の題材は、開発者向けユーティリティツール「Dev Tool Box」です。

Webアプリリンクはこちらです。
もしよければぜひ使ってみてください。
https://dev-tool-box.pages.dev/

https://github.com/SeigoTakahashi/dev-tool-box
技術文書作成はまだ初心者のため、拙い点があるかもしれませんがご容赦ください。

2.技術テーマ


盛り込みたいと考えた内容は、以下のとおりです。
就職目前の学生ということで、少しでも実務に近づけたらと思った次第です。

  • React(Typescript)
  • GitHub(ブランチ運用、PR、CI/CD、README、copilot-instructions.md etc...)
  • Docker
  • リーダブルコード
  • UI・UX最適化(Material UI、ダークモード対応 etc...)
  • テスト(単体、E2E)

3.機能一覧


機能は以下のとおりです。
普段使うもの、開発であったら便利だと思ったものを用意してみました。
ブラウザのブックマークが各種ツールで溢れかえっていたので、一つに集約して自分好みのUX(ダークモード対応や高速な動作)を実現したかったためです。

テキスト系ツール

  • 文字数、バイト数、単語数カウンター
  • 改行コード変換(LF ⇄ CRLF)
  • 命名規則変換(スネーク・キャメル)
  • diffツール(2つのテキスト比較)
  • Markdown → HTMLプレビュー

JSON系ツール

  • JSON整形(フォーマッタ)
  • JSONバリデータ(try/catch)
  • JSON → CSV変換
  • CSV → JSON変換

Web開発者向けツール

  • URLエンコード・デコード
  • Base64エンコード・デコード
  • 正規表現テスター
  • Cron式ジェネレーター
  • ハッシュ生成器(SHA-1、SHA-256など)

色系ツール

  • カラーコード変換(#hex ⇆ rgb ⇆ hsl)
  • カラーパレット生成(ランダム生成 or ベースカラーから派生)
  • グラデーションプレビュー

画像系

  • 画像圧縮
  • Favicon生成
  • 画像背景除去

生活便利ツール

  • QRコード生成
  • 基底変換(2進数 ⇆ 10進数 など)
  • ランダムジェネレーター(パスワード、UUID)
  • パスワード強度チェッカー

4.システム構成図


システム構成図はご覧のとおりです。(Geminiで生成してみました)

文字ベースでもまとめてみました。

カテゴリ 内容
言語 TypeScript
フレームワーク React
フロントエンド UI Material UI / Tailwind CSS
Docker 開発環境構築用(Dev Container / ローカル開発向け)
CI/CD CI:ESLint、単体テスト、E2Eテスト、ビルド検証
CDmain ブランチへの push をトリガーに Cloudflare Pages へ自動デプロイ

5.フォルダ構成


フォルダ構成は以下のようにしてみました。
可能な限り再利用性や保守性を意識して作成しました。気になる点があれば、ぜひご指摘ください。🙇‍♀️

# 一部抜粋
.
├── .github/
│   ├── workflows/
│   │   └── ci.yml
│   └── copilot-instructions.md
├── src/
│   ├── common/
│   │   ├── components/
│   │   └── utils/
│   ├── features/
│   │   ├── text/
│   │   │   ├── components/
│   │   │   └── utils/
│   │   ├── json/
│   │   │   ├── components/
│   │   │   └── utils/
│   │   ├── web/
│   │   │   ├── components/
│   │   │   └── utils/
│   │   ├── color/
│   │   │   ├── components/
│   │   │   └── utils/
│   │   ├── image/
│   │   │   ├── components/
│   │   │   └── utils/
│   │   └── utility/
│   │       ├── components/
│   │       └── utils/
│   ├── pages/
│   │   ├── text/
│   │   ├── json/
│   │   ├── web/
│   │   ├── color/
│   │   ├── image/
│   │   └── utility/
│   └── tests/
│       ├── unit/     
│       │   ├── text/
│       │   ├── json/
│       │   ├── web/
│       │   ├── color/
│       │   ├── image/
│       │   └── utility/
│       └── e2e/       
│           ├── text/
│           ├── json/
│           ├── web/
│           ├── color/
│           ├── image/
│           ├── utility/
│           └── common/
├── Dockerfile
└── docker-compose.yml

6.GitHub関連


ブランチ運用

個人開発でできる範囲ではありますが、実務を意識してブランチ運用してみました。
意識したこと:

  • 機能ごとにfeature/xxxブランチを切る
  • 現在ブランチを意識する
  • コミット前に差分を確認しておく

以下の流れを繰り返しました。

# ============================================
# 🔥 0. main ブランチを最新にする
# ============================================
git checkout main
git pull origin main


# ============================================
# 🔥 1. 機能ブランチを作成(例:文字数カウンター)
# ============================================
git checkout -b feature/text-counter


# ============================================
# 🔥 2. 実装を進める(コードを書く)
#   - components/TextCounter.tsx
#   - utils/countCharacters.ts
#   - pages/text-tools/TextCounterPage.tsx
#  …
# ============================================
# (作業 → ファイル編集)


# ============================================
# 🔥 3. 作業内容をステージングしてコミット
# ============================================
git add .
git commit -m "文字数カウンターのUI追加"


# ============================================
# 🔥 4. テスト & Lint & 型チェック(ローカルで確認)
# ============================================
npm run lint
npm run test
npm run type-check
npm run build   # ビルドが通るかも必ず確認


# ============================================
# 🔥 5. ブランチを GitHub に Push
# ============================================
git push origin feature/text-counter


# ============================================
# 🔥 6. GitHub 上で PR を作成
# - pull_request_template.mdで作成したテンプレートを使用
# ============================================
# (GitHub で PR を作成)


# ============================================
# 🔥 7. PR のレビュー(今回は自分)
# ============================================
# - 命名は適切?
# - コンポーネント分割はOK?
# - utilsにロジック分離されてる?
# - 冗長な処理はない?
# - 不必要なconsole.logはなし?


# ============================================
# 🔥 8. 修正指摘があれば対応
# ============================================
# 例: コードを修正して…
git add .
git commit -m "PRのレビュー指摘修正"
git push


# ============================================
# 🔥 9. CI がパスしたら main にマージ
#   → Squash merge 推奨(PRごとに1コミットになる)
# ============================================
# (GitHubで "Squash and merge" を押す)


# ============================================
# 🔥 10. 不要になった feature ブランチを削除
# ============================================
git branch -d feature/text-counter
git push origin --delete feature/text-counter


# ============================================
# 🔥 11. main にマージされたので自動デプロイ(CI/CD)
#   → Cloudflare
# ============================================
# (CI/CD が勝手にデプロイ)


# ============================================
# 🎉 12. 次の機能へ(例:JSON Formatter)
# ============================================
git checkout main
git pull origin main
git checkout -b feature/json-formatter

# → 同じ流れで実装していく

CI/CD

CI:ESLint、単体テスト、E2Eテスト、ビルド検証
CDmain ブランチへの 「push」 をトリガーに Cloudflare Pages へ自動デプロイ

※ PlaywrightをローカルとGitHub Actionsの両方で実行したかったため、ドメインを切り替える処理を入れてます。

name: CI

on:
  pull_request:
    branches: [ "main" ]
  push:
    branches: [ "main" ]

jobs:
  ci:
    runs-on: ubuntu-latest

    steps:
      # 1. ソースコードチェックアウト
      - name: Checkout repository
        uses: actions/checkout@v4

      # 2. Node.js セットアップ
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"

      # 3. 依存関係インストール
      - name: Install dependencies
        run: npm ci

      # 4. Lint
      - name: Run ESLint
        run: npm run lint --if-present

      # 5. Unit Test
      - name: Run Unit Tests
        run: npm test --if-present

      # 6. Build (本番ビルドが通るかチェック)
      - name: Build project
        run: npm run build --if-present

      # 7. E2E Test
      - name: Install Playwright browsers
        run: npx playwright install --with-deps

      - name: Run Playwright tests
        run: npm run test:e2e
        env:
          # PRの時は空文字になる → Playwright Config内の webServer が起動する
          PLAYWRIGHT_BASE_URL: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') && 'https://dev-tool-box.pages.dev' || '' }}
単体テストのポイント

「1関数 = 1テスト対象」+「成功系1〜2、失敗系2〜4」

  • 実装の詳細までは踏み込まない
  • 入力 → 出力(Result型)だけを見る
  • UIやログは一切見ない
  • 出力にResult型をとることで呼び出し側でエラー処理を強制させる
json-validator.test.ts
import { describe, it, expect } from "vitest";
import { validateJson } from "../../../features/json/utils/json-validator";

// JSON関数のユニットテスト
describe("validateJson", () => {
  it("正しいJSONを検証できる", () => {
    const input = '{"a":1,"b":2}';

    const result = validateJson(input);

    expect(result.ok).toBe(true);
  });

  it("不正なJSONの場合はエラーを返す", () => {
    const input = '{"a":1,}';

    const result = validateJson(input);

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(typeof result.error).toBe("string");
      expect(result.error.length).toBeGreaterThan(0);
    }
  });

  it("空文字はエラーになる", () => {
    const input = "";

    const result = validateJson(input);

    expect(result.ok).toBe(false);
  });

  it("JSONでない文字列はエラーになる", () => {
    const input = "hello world";

    const result = validateJson(input);

    expect(result.ok).toBe(false);
  });
});

E2Eテスト

「1機能 = 1〜3シナリオ」までで止める

  • 全分岐は追わない
  • エラーメッセージ全文一致はしない
  • UIの見た目(色・細かいDOM構造)は見ない
json-validator.spec.ts
import { test, expect } from "@playwright/test";

test.describe("JSONバリデーターページ", () => {
  // 各テストの前にページへ移動
  test.beforeEach(async ({ page }) => {
    await page.goto("/json/validator");
  });

  test("主要な入力フォームが表示されていること", async ({ page }) => {
    // JSON入力エリアがあるか
    await expect(
      page.locator(
        'textarea[placeholder="ここに検証したいJSONを入力してください"]',
      ),
    ).toBeVisible();

    // 検証ボタンがあるか
    await expect(page.getByText("検証する")).toBeVisible();
  });

  test("有効なJSONを入力した場合、成功メッセージが表示されること", async ({
    page,
  }) => {
    const validJson = `{
  "name": "John",
  "age": 30,
  "city": "New York"
}`;

    // JSON入力エリアに有効なJSONを入力
    await page.fill(
      'textarea[placeholder="ここに検証したいJSONを入力してください"]',
      validJson,
    );

    // 検証ボタンをクリック
    await page.click("text=検証する");

    // 成功メッセージが表示されていることを確認
    await expect(page.getByText("有効なJSONです。")).toBeVisible();
  });

  test("無効なJSONを入力した場合、エラーメッセージが表示されること", async ({
    page,
  }) => {
    const invalidJson = `{
  "name": "John",
  "age": 30,
  "city": "New York"`;

    // JSON入力エリアに無効なJSONを入力
    await page.fill(
      'textarea[placeholder="ここに検証したいJSONを入力してください"]',
      invalidJson,
    );

    // 検証ボタンをクリック
    await page.click("text=検証する");

    // エラーメッセージが表示されていることを確認
    await expect(page.getByText("無効なJSONです。")).toBeVisible();
  });
});

テスト粒度の心掛け

「壊れたら困る仕様」をテストし、
「変えてもいい実装」はテストしない

copilot-instructions.md

プロンプトを最適化して、出力を誘導しました。

.github/copilot-instructions.md
# Copilot Instructions

このリポジトリでは、以下の開発指針・ディレクトリ構成・命名規則・テスト方針に従ってコードを生成すること。

---

# 1. プロジェクト全体方針
...
---

# 2. 機能一覧
...
---

# 3. ディレクトリ構成
...
---

# 4. 命名規則
...
---

# 5. テスト方針
...
---

# 6. Copilot が生成するときの具体的ルール
...
---

# 7. コードスタイル
...
---

# 8. 出力フォーマット(Copilotへの指示)
...
---

これらのルールに従って、コード・補完・説明・テストを生成すること。

pull_request_template.md

プルリクエストのテンプレートを用意し、効率化を図りました。

.github/pull_request_template.md
## 概要
<!-- 何を変更したかを簡潔に -->

## 変更内容
<!-- 具体的な変更点をリストアップ -->
- [ ]
- [ ]
- [ ]

## テスト
<!-- テスト方法や確認事項 -->

## レビューポイント
<!-- 特に見てほしい箇所 -->

## 関連Issue
<!-- 関連するIssueがあれば記載 -->

7.Docker


簡易的な構成ではありますが、導入しました。
正直、現段階では恩恵を最大限受けきれていません。
ただ、WindowsとMacの2台間での環境の不整合が起きなかったので、そこは良かったのかなと。
ゆくゆくはデプロイにもDockerを使うことを検討していますが、今回はローカル実行用だけに使用しました。

Dockerfile
FROM node:20-alpine

WORKDIR /app

# 依存関係インストール
COPY package*.json ./
RUN npm install

# ソースコードをコピー
COPY . .

# Vite の開発サーバー
EXPOSE 5174

CMD ["npm", "run", "dev", "--", "--host"]
docker-compose.yml
version: '3.9'
services:
  app:
    build: .
    ports:
      - "5174:5173"
    volumes:
      - .:/app:cached
      - /app/node_modules
    environment:
      - CHOKIDAR_USEPOLLING=true

8.リーダブルコード


以下のことを念頭に置いて開発しました。
また、定期的なリファクタリングの時間を設けることで保守性・可読性の向上を図りました。

シンプルにする

  • 必要になるまで作らない(YAGNI)
  • 複雑にしない、重複しない(KISS / DRY)

関心ごとを分ける

  • ディレクトリや関数を“役割ごと”に分割
  • 高レベルの流れと細かい処理を分離

名前とコメントは「意図」を伝える

  • 名前はわかりやすく
  • コメントは「なぜそうするか」を書く
  • そのまま読めば分かることは書かない

巨大な式を分解する

  • 説明変数・要約変数で読みやすくする
  • マジックナンバーは定数化

不要なものを捨てる

  • 使わないコード・変数は即削除
  • スコープを最小にする

少しずつ改善する

  • 小さく変更 → テスト → 次へ
  • ゼロベースで「もっと良い書き方は?」を考える

共通処理は関数化して再利用

  • utils / helpers に切り出して整理
  • 過剰な抽象化はしない

フォーマットは自動化

  • Prettier / ESLint などで統一
  • 読みやすさは“スタイルの一貫性”で決まる

9.今後の課題


今回の開発で浮き彫りになった課題は:

  • 機能を詰め込みすぎてユーザーが迷う
    → 今回は学習のためだからよいが、今後は特定の機能に絞る
    → 今回は対策として検索機能を実装
  • テスト粒度やブランチ運用の細かいルールがブレブレ
    → 今回はテストやブランチ運用の概要を知ることができたのでよしとする
    → 今後現場に合わせて学んでいく

10.まとめ


開発全体を通して、目的意識を持ちながら取り組めたと感じています。
ただ、まだまだ経験・学習が足りないことを痛感したので、今後も自己研鑽を続けていきたいです。
ここが実務とは違う、この技術を取り入れたほうがいいなどあればご教示いただけると幸いです。

Discussion