Skip to content

第2章: 開発環境の構築

2.1 技術スタックの選定

React + TypeScript を選んだ理由

財務会計システムのフロントエンドには、以下の要件があります。

要件 説明
型安全性 金額計算、API 連携での型不整合防止
保守性 長期運用を見据えたコードベース
開発効率 コンポーネントの再利用性
エコシステム 豊富なライブラリ・ツール

これらの要件を満たすため、React + TypeScript を採用しました。

uml diagram

Vite を選んだ理由

従来の Create React App(CRA)に代わり、Vite を採用しました。

比較項目 CRA (webpack) Vite
開発サーバー起動 30秒〜1分 1秒以下
HMR(ホットリロード) 数秒 即時
ビルド速度 遅い 高速(esbuild)
設定の複雑さ 隠蔽(eject 必要) シンプル
TypeScript サポート 追加設定必要 ネイティブ対応

Vite の仕組み:

uml diagram


2.2 プロジェクト初期化

Vite プロジェクトの作成

# プロジェクト作成
npm create vite@latest accounting-frontend -- --template react-ts

# ディレクトリ移動
cd accounting-frontend

# 依存関係インストール
npm install

生成されるファイル構成

accounting-frontend/
├── public/
│   └── vite.svg
├── src/
│   ├── assets/
│   │   └── react.svg
│   ├── App.css
│   ├── App.tsx
│   ├── index.css
│   ├── main.tsx
│   └── vite-env.d.ts
├── .gitignore
├── eslint.config.js
├── index.html
├── package.json
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

TypeScript 設定

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    /* Path aliases */
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@/api/*": ["src/api/*"],
      "@/components/*": ["src/components/*"],
      "@/views/*": ["src/views/*"],
      "@/hooks/*": ["src/hooks/*"],
      "@/utils/*": ["src/utils/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

vite.config.ts(パスエイリアス対応):

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
});

2.3 依存パッケージのインストール

コア依存関係

# ルーティング
npm install react-router-dom

# API 連携
npm install @tanstack/react-query axios

# UI コンポーネント
npm install react-modal react-tabs react-icons react-spinners

# ユーティリティ
npm install dayjs decimal.js

開発依存関係

# Orval(API クライアント生成)
npm install -D orval

# ESLint / Prettier
npm install -D eslint prettier eslint-config-prettier eslint-plugin-react-hooks

# 型定義
npm install -D @types/react-modal

# テスト
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
npm install -D msw@latest

# E2E テスト
npm install -D cypress

package.json

{
  "name": "accounting-frontend",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage",
    "cypress": "cypress open",
    "cypress:run": "cypress run",
    "api:generate": "orval",
    "api:fetch": "curl http://localhost:8080/api-docs.yaml -o openapi.yaml && npm run api:generate"
  },
  "dependencies": {
    "@tanstack/react-query": "^5.0.0",
    "axios": "^1.7.0",
    "dayjs": "^1.11.0",
    "decimal.js": "^10.4.0",
    "react": "^18.3.0",
    "react-dom": "^18.3.0",
    "react-icons": "^5.0.0",
    "react-modal": "^3.16.0",
    "react-router-dom": "^6.26.0",
    "react-spinners": "^0.14.0",
    "react-tabs": "^6.0.0"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^6.0.0",
    "@testing-library/react": "^16.0.0",
    "@types/react": "^18.3.0",
    "@types/react-dom": "^18.3.0",
    "@types/react-modal": "^3.16.0",
    "@vitejs/plugin-react": "^4.3.0",
    "cypress": "^14.5.0",
    "eslint": "^9.0.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-react-hooks": "^5.0.0",
    "jsdom": "^24.0.0",
    "msw": "^2.0.0",
    "orval": "^7.0.0",
    "prettier": "^3.0.0",
    "typescript": "^5.5.0",
    "vite": "^5.4.0",
    "vitest": "^2.0.0"
  }
}

2.4 ESLint / Prettier 設定

ESLint 設定

eslint.config.js:

import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  { ignores: ['dist', 'src/api/generated', 'src/api/model'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
      '@typescript-eslint/no-unused-vars': [
        'error',
        { argsIgnorePattern: '^_' },
      ],
      '@typescript-eslint/explicit-function-return-type': 'off',
      '@typescript-eslint/no-explicit-any': 'warn',
    },
  }
);

ポイント: - src/api/generatedsrc/api/model を除外(Orval 自動生成コード) - React Hooks のルールを適用 - 未使用変数は _ プレフィックスで除外可能

Prettier 設定

.prettierrc:

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf"
}

.prettierignore:

dist
node_modules
src/api/generated
src/api/model
openapi.yaml

2.5 開発サーバー設定

環境変数

.env.development:

# API ベース URL
VITE_API_BASE_URL=http://localhost:8080/api

# アプリケーション名
VITE_APP_NAME=財務会計システム

# 開発モード
VITE_DEV_MODE=true

.env.production:

# API ベース URL(本番)
VITE_API_BASE_URL=/api

# アプリケーション名
VITE_APP_NAME=財務会計システム

# 開発モード
VITE_DEV_MODE=false

環境変数の使用:

// src/config.ts
export const config = {
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL || '/api',
  appName: import.meta.env.VITE_APP_NAME || '財務会計システム',
  isDev: import.meta.env.VITE_DEV_MODE === 'true',
};

プロキシ設定

開発時はバックエンド API へのリクエストをプロキシします。

vite.config.ts:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        secure: false,
        // ログ出力(デバッグ用)
        configure: (proxy) => {
          proxy.on('proxyReq', (proxyReq, req) => {
            console.log(`[Proxy] ${req.method} ${req.url} -> ${proxyReq.path}`);
          });
        },
      },
    },
  },
  build: {
    outDir: 'dist',
    sourcemap: true,
  },
});

2.6 テスト環境の構築

Vitest 設定

vite.config.ts(テスト設定追加):

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/api/generated/',
        'src/api/model/',
        'src/test/',
      ],
    },
  },
  // ... server, build 設定
});

src/test/setup.ts:

import '@testing-library/jest-dom';
import { afterAll, afterEach, beforeAll } from 'vitest';
import { server } from '../mocks/server';

// MSW サーバーの起動・停止
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

MSW(Mock Service Worker)設定

src/mocks/handlers.ts:

import { http, HttpResponse } from 'msw';
import type { Account } from '@/api/model';

export const handlers = [
  // 勘定科目一覧取得
  http.get('/api/accounts', () => {
    return HttpResponse.json<Account[]>([
      {
        accountCode: '111',
        accountName: '現金預金',
        bsplType: 'B',
        debitCreditType: '借',
        elementType: '資産',
        displayOrder: 1,
        version: 1,
      },
      {
        accountCode: '211',
        accountName: '買掛金',
        bsplType: 'B',
        debitCreditType: '貸',
        elementType: '負債',
        displayOrder: 10,
        version: 1,
      },
    ]);
  }),

  // 勘定科目詳細取得
  http.get('/api/accounts/:code', ({ params }) => {
    const { code } = params;
    return HttpResponse.json<Account>({
      accountCode: code as string,
      accountName: '現金預金',
      bsplType: 'B',
      debitCreditType: '借',
      elementType: '資産',
      displayOrder: 1,
      version: 1,
    });
  }),

  // 認証
  http.post('/api/auth/login', async ({ request }) => {
    const body = await request.json();
    if (body.username === 'admin' && body.password === 'password') {
      return HttpResponse.json({
        accessToken: 'mock-access-token',
        refreshToken: 'mock-refresh-token',
        user: {
          id: 1,
          username: 'admin',
          role: 'ADMIN',
        },
      });
    }
    return new HttpResponse(null, { status: 401 });
  }),
];

src/mocks/server.ts:

import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);

src/mocks/browser.ts(ブラウザ用):

import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);

Cypress 設定

cypress.config.ts:

import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    supportFile: 'cypress/support/e2e.ts',
    specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: false,
    screenshotOnRunFailure: true,
  },
  component: {
    devServer: {
      framework: 'react',
      bundler: 'vite',
    },
  },
});

cypress/support/e2e.ts:

// カスタムコマンドの型定義
declare global {
  namespace Cypress {
    interface Chainable {
      login(username: string, password: string): Chainable<void>;
    }
  }
}

// ログインコマンド
Cypress.Commands.add('login', (username: string, password: string) => {
  cy.visit('/login');
  cy.get('input[name="username"]').type(username);
  cy.get('input[name="password"]').type(password);
  cy.get('button[type="submit"]').click();
  cy.url().should('not.include', '/login');
});

export {};

2.7 初期ファイルの作成

エントリポイント

src/main.tsx:

import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { BrowserRouter } from 'react-router-dom';
import { queryClient } from './queryClient';
import { AuthProvider } from './providers/AuthProvider';
import { App } from './App';
import './styles/global.css';

// 開発時のみ MSW を起動
async function enableMocking() {
  if (import.meta.env.DEV && import.meta.env.VITE_ENABLE_MSW === 'true') {
    const { worker } = await import('./mocks/browser');
    return worker.start({
      onUnhandledRequest: 'bypass',
    });
  }
  return Promise.resolve();
}

enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
      <QueryClientProvider client={queryClient}>
        <BrowserRouter>
          <AuthProvider>
            <App />
          </AuthProvider>
        </BrowserRouter>
        <ReactQueryDevtools initialIsOpen={false} />
      </QueryClientProvider>
    </React.StrictMode>
  );
});

TanStack Query 設定

src/queryClient.ts:

import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // キャッシュの有効期間(5分)
      staleTime: 5 * 60 * 1000,
      // ガベージコレクション期間(30分)
      gcTime: 30 * 60 * 1000,
      // リトライ回数
      retry: 1,
      // フォーカス時の再取得を無効化
      refetchOnWindowFocus: false,
      // マウント時の再取得
      refetchOnMount: true,
    },
    mutations: {
      // ミューテーションのリトライ
      retry: 0,
    },
  },
});

グローバルスタイル

src/styles/global.css:

/* リセット */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* ルート設定 */
:root {
  /* カラー */
  --color-primary: #1976d2;
  --color-primary-dark: #1565c0;
  --color-secondary: #dc004e;
  --color-error: #f44336;
  --color-warning: #ff9800;
  --color-success: #4caf50;
  --color-info: #2196f3;

  /* テキスト */
  --color-text-primary: #212121;
  --color-text-secondary: #757575;

  /* 背景 */
  --color-background: #fafafa;
  --color-surface: #ffffff;

  /* ボーダー */
  --color-border: #e0e0e0;

  /* フォント */
  --font-family: 'Noto Sans JP', 'Hiragino Sans', sans-serif;
  --font-size-base: 14px;
  --font-size-small: 12px;
  --font-size-large: 16px;

  /* スペーシング */
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --spacing-xl: 32px;

  /* ボーダー半径 */
  --border-radius: 4px;

  /* シャドウ */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1);
  --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.1);
}

/* ボディ */
body {
  font-family: var(--font-family);
  font-size: var(--font-size-base);
  color: var(--color-text-primary);
  background-color: var(--color-background);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* リンク */
a {
  color: var(--color-primary);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

/* ボタン */
button {
  font-family: inherit;
  font-size: inherit;
  cursor: pointer;
}

/* テーブル */
table {
  border-collapse: collapse;
  width: 100%;
}

th,
td {
  padding: var(--spacing-sm) var(--spacing-md);
  text-align: left;
  border-bottom: 1px solid var(--color-border);
}

th {
  background-color: var(--color-surface);
  font-weight: 600;
}

/* 金額表示(右寄せ) */
.money {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* 借方(青) */
.debit {
  color: #1976d2;
}

/* 貸方(赤) */
.credit {
  color: #d32f2f;
}

2.8 動作確認

開発サーバーの起動

# 開発サーバー起動
npm run dev

# ブラウザで http://localhost:3000 にアクセス

ビルド確認

# プロダクションビルド
npm run build

# ビルド結果のプレビュー
npm run preview

テスト実行

# 単体テスト実行
npm run test

# カバレッジ付きテスト
npm run test:coverage

# E2E テスト(Cypress)
npm run cypress

2.9 まとめ

本章では、財務会計システムのフロントエンド開発環境を構築しました。

構築した環境

項目 内容
ビルドツール Vite 5.4
言語 TypeScript 5.5
リンター ESLint 9 + Prettier 3
テスト Vitest 2 + Cypress 14
API モック MSW 2

設定ファイル一覧

ファイル 用途
vite.config.ts Vite 設定(エイリアス、プロキシ、テスト)
tsconfig.json TypeScript 設定(パスエイリアス)
eslint.config.js ESLint 設定
.prettierrc Prettier 設定
cypress.config.ts Cypress 設定

次章の予告

第3章では、OpenAPI と Orval による API 連携について解説します。バックエンドから OpenAPI 仕様を取得し、Orval で型安全な API クライアントを自動生成します。