可読性・再利用性・パフォーマンスを向上させる!フロントエンドリファクタリングの実践ガイド

2025/01/13に公開

はじめに

フロントエンド開発では、プロジェクトの規模が大きくなるにつれ、コードの可読性や保守性が低下し、開発スピードが落ちることがよくあります。最初はシンプルだったコードも、機能追加や仕様変更を繰り返すうちに複雑化し、「この処理、どこかで見たような気がする…」「この関数、何をしているのかわかりづらい…」といった問題が発生しがちです。

こうした課題を解決するためには、コードを継続的に見直し、整理・最適化していく 「リファクタリング」 が欠かせません。リファクタリングを適切に行うことで、可読性が向上し、重複コードが削減され、パフォーマンスが改善される など、プロジェクト全体の品質を高めることができます。

本記事では、リファクタリングの具体的な手法として、命名規則の見直し、関数の分割、共通処理のモジュール化、React のパフォーマンス改善、非同期処理の最適化、依存関係の整理、型の適用強化、設計パターンの適用 など、実践的なアプローチを紹介します。さらに、リファクタリングを段階的に適用するための計画策定についても解説します。

コードの品質を向上させ、よりメンテナブルで拡張しやすいフロントエンドを目指していきましょう。

コードの可読性向上

コードの可読性を向上させることは、チーム開発や長期的な保守性の観点から非常に重要です。可読性の高いコードは、開発者間での理解を容易にし、バグの発生を減らし、リファクタリングをしやすくします。ここでは、可読性向上のための具体的なポイントを詳しく解説し、言語ごとの命名規則の違いについても触れます。

命名規則の適用

プロジェクトで統一されたルールを適用することで、コードの可読性とメンテナンス性が向上します。以下は、言語ごとの命名規則の例です。

  • JavaScript / TypeScript
    • 変数・関数: camelCase(例: fetchUserData)
    • クラス・コンポーネント: PascalCase(例: UserProfileComponent)
    • 定数: UPPER_SNAKE_CASE(例: MAX_RETRY_COUNT)
  • Python
    • 変数・関数: snake_case(例: fetch_user_data)
    • クラス: PascalCase(例: UserProfile)
    • 定数: UPPER_SNAKE_CASE(例: MAX_RETRY_COUNT)
  • Java / C#
    • 変数・メソッド: camelCase(例: fetchUserData)
    • クラス・インターフェース: PascalCase(例: UserProfile)
    • 定数: UPPER_SNAKE_CASE(例: MAX_RETRY_COUNT)
  • Go
    • 変数・関数: camelCase(例: fetchUserData)
    • クラスに相当するものが少なく、構造体は PascalCase(例: UserProfile)
    • 定数: PascalCase もしくは UPPER_SNAKE_CASE(例: MaxRetryCount または MAX_RETRY_COUNT)

意味のある命名をする

datatemp などの抽象的な名前を避け、具体的な意味を持たせる。

  • 悪い例: getData(), processTemp()
  • 良い例: fetchUserList(), convertTemperatureToCelsius()

状態を示す変数には is, has, should などを使用すると、意図が伝わりやすい。

  • 例: isAdminUser, hasPendingRequests, shouldRetry

関数分割の適用

関数が1つの責務 (Single Responsibility Principle, SRP) を持つように設計することで、コードの可読性と保守性が向上します。

  • 1つの関数に1つの責務を持たせる
    1つの関数が複数の役割を持つと、コードの意図が不明確になり、テストが困難になります。

関数名の命名ルール

  • 動詞 + 名詞 の形式で関数名を定義すると、関数の役割が明確になります。
    • 例: fetchUserProfile(), validateEmail(), sendNotification()
  • 悪い例: processData() → 何を処理するのか不明瞭
  • 良い例: calculateOrderTotal() → 何を計算するのか明確
// ❌ 悪い例: 1つの関数に複数の責務がある
function getUserDataAndFormat(userId: string) {
  const user = fetchFromDatabase(userId);
  user.name = `${user.firstName} ${user.lastName}`;
  return user;
}

// ✅ 良い例: 関数を分割し、それぞれの責務を明確にする
function fetchUserData(userId: string): User {
  return fetchFromDatabase(userId);
}

function formatUserName(user: User): string {
  return `${user.firstName} ${user.lastName}`;
}

コードの整然化 (フォーマットとスタイル)

可読性を向上させるためには、適切なフォーマットやスタイルガイドの遵守も重要です。

インデントの統一

  • チームで使用するインデントを統一する(一般的には 2スペース か 4スペース)
  • JavaScript / TypeScript / Python では スペース推奨
  • Go や C では タブ推奨

適切な改行と空白

  • 主要な処理ブロックの間に適切な空行を入れる

コメントの活用

  • 必要な部分にはコメントを入れるが、冗長すぎないようにする
  • コードの意図を説明するコメント を書く(「何をしているか」ではなく「なぜそうしているか」)

悪い例

// ユーザーデータを取得する
const user = fetchUserData(userId);

良い例

// API からユーザーデータを取得(キャッシュが無効な場合のみリクエスト)
const user = fetchUserData(userId);

共通処理のモジュール化

コードの重複を減らし、保守性と再利用性を高めるために、共通処理を適切にモジュール化することが重要です。ここでは、どのように共通処理をモジュール化するかについて、具体例を交えて詳しく説明します。

共通で利用する関数をutilsフォルダなどにまとめ、コードの重複を防ぎます。

ポイントとしては下記3点が挙げられます。

  • 頻繁に使用する処理(例: 日付フォーマット、データ変換、バリデーション)を切り出す
  • utilsフォルダの中でカテゴリごとに整理する(例: dateUtils.ts, stringUtils.ts など)
  • テストを書いて意図した動作を保証する

例)日付フォーマットの関数

utils/dateUtils.ts
// utils/dateUtils.ts
export function formatDate(date: unknown, format: string = 'YYYY-MM-DD'): string {
  if (!(date instanceof Date) || isNaN(date.getTime())) {
    return '-';
  }

  return new Intl.DateTimeFormat('ja-JP', { dateStyle: 'short' }).format(date);
}

利用例

import { formatDate } from '@/utils/dateUtils';

console.log(formatDate(new Date())); // 例: "2024/03/10"

テストコード

tests/utils/dateUtils.spec.ts
import { formatDate } from '@/utils/dateUtils';

describe('formatDate', () => {
  it('正しい日付フォーマットに変換されること', () => {
    const date = new Date(2024, 2, 10);
    expect(formatDate(date)).toBe('2024/03/10');
  });

  it('不正な日付を渡した場合に "-" を返すこと', () => {
    expect(formatDate(new Date('invalid-date'))).toBe('-');
  });

  it('null を渡した場合に "-" を返すこと', () => {
    expect(formatDate(null)).toBe('-');
  });

  it('undefined を渡した場合に "-" を返すこと', () => {
    expect(formatDate(undefined)).toBe('-');
  });

  it('文字列を渡した場合に "-" を返すこと', () => {
    expect(formatDate('2024-03-10')).toBe('-');
  });

  it('数値を渡した場合に "-" を返すこと', () => {
    expect(formatDate(0)).toBe('-');
  });
});

例)APIリクエスト用の関数

utils/apiClient.ts
export async function fetchData<T>(url: string, options?: RequestInit): Promise<T> {
  const response = await fetch(url, options);
  if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status}`);
  }
  return response.json();
}
  • APIとの通信処理を共通化することで、エラーハンドリングやリクエストの管理を一元化できます。

利用例

import { fetchData } from '@/utils/apiClient';

async function getUserData() {
  try {
    const user = await fetchData<{ name: string; age: number }>('https://api.example.com/user');
    console.log(user);
  } catch (error) {
    console.error('Error fetching user data:', error);
  }
}

テストコード

tests/utils/apiClient.spec.ts
import { fetchData } from '@/utils/apiClient';

global.fetch = jest.fn();

describe('fetchData', () => {
  it('APIからデータを取得できること', async () => {
    (fetch as jest.Mock).mockResolvedValueOnce({
      ok: true,
      json: async () => ({ name: 'John Doe' }),
    });

    const data = await fetchData<{ name: string }>('https://api.example.com/user');
    expect(data).toEqual({ name: 'John Doe' });
  });

  it('APIリクエストが失敗した場合にエラーを投げること', async () => {
    (fetch as jest.Mock).mockResolvedValueOnce({
      ok: false,
      status: 500,
    });

    await expect(fetchData('https://api.example.com/user')).rejects.toThrow('HTTP error! Status: 500');
  });

  it('ネットワークエラーが発生した場合にエラーを投げること', async () => {
    (fetch as jest.Mock).mockRejectedValueOnce(new Error('Network Error'));

    await expect(fetchData('https://api.example.com/user')).rejects.toThrow('Network Error');
  });
});

例)メールアドレスのバリデーション

utils/validation.ts
export function isValidEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

利用例

import { isValidEmail } from '@/utils/validation';

console.log(isValidEmail('test@example.com')); // true
console.log(isValidEmail('invalid-email')); // false

テストコード

tests/utils/validation.spec.ts
import { isValidEmail } from '@/utils/validation';

describe('isValidEmail', () => {
  it('正しいメールアドレス形式を受け入れること', () => {
    expect(isValidEmail('test@example.com')).toBe(true);
    expect(isValidEmail('user.name+alias@domain.co.jp')).toBe(true);
  });

  it('不正なメールアドレス形式を拒否すること', () => {
    expect(isValidEmail('invalid-email')).toBe(false);
    expect(isValidEmail('user@domain,com')).toBe(false);
    expect(isValidEmail('user@.com')).toBe(false);
  });
});

例)ローカルストレージのユーティリティ

utils/storage.ts
export function getStorageItem<T>(key: string): T | null {
  const item = localStorage.getItem(key);
  return item ? JSON.parse(item) : null;
}

export function setStorageItem<T>(key: string, value: T): void {
  localStorage.setItem(key, JSON.stringify(value));
}

export function removeStorageItem(key: string): void {
  localStorage.removeItem(key);
}

利用例

import { getStorageItem, setStorageItem } from '@/utils/storage';

setStorageItem('user', { name: 'John Doe', age: 30 });

const user = getStorageItem<{ name: string; age: number }>('user');
console.log(user); // { name: 'John Doe', age: 30 }

テストコード

tests/utils/storage.spec.ts
import { getStorageItem, setStorageItem, removeStorageItem } from '@/utils/storage';

describe('localStorage utils', () => {
  beforeEach(() => {
    localStorage.clear();
  });

  it('データを正しく保存できること', () => {
    setStorageItem('testKey', { foo: 'bar' });
    expect(localStorage.getItem('testKey')).toBe(JSON.stringify({ foo: 'bar' }));
  });

  it('データを正しく取得できること', () => {
    localStorage.setItem('testKey', JSON.stringify({ foo: 'bar' }));
    expect(getStorageItem<{ foo: string }>('testKey')).toEqual({ foo: 'bar' });
  });

  it('データを削除できること', () => {
    localStorage.setItem('testKey', JSON.stringify({ foo: 'bar' }));
    removeStorageItem('testKey');
    expect(localStorage.getItem('testKey')).toBeNull();
  });
});

例)エラーハンドリング関数

utils/errorHandler.ts
export function handleError(error: unknown): string {
  if (error instanceof Error) {
    return error.message;
  }
  return 'An unknown error occurred';
}

利用例

import { handleError } from '@/utils/errorHandler';

try {
  throw new Error('Something went wrong');
} catch (error) {
  console.error(handleError(error)); // "Something went wrong"
}

テストコード

tests/utils/errorHandler.spec.ts
import { handleError } from '@/utils/errorHandler';

describe('handleError', () => {
  it('Errorオブジェクトからメッセージを取得できること', () => {
    const error = new Error('Something went wrong');
    expect(handleError(error)).toBe('Something went wrong');
  });

  it('未知のエラーの際にデフォルトメッセージを返すこと', () => {
    expect(handleError(null)).toBe('An unknown error occurred');
    expect(handleError(undefined)).toBe('An unknown error occurred');
  });
});

長大なコンポーネントの分割(関心の分離)

Reactコンポーネントが肥大化すると、以下のような問題が発生します。

  • 可読性の低下:コードの見通しが悪くなり、理解しづらくなる。
  • 再利用性の低下:特定のコンポーネントが1つの用途に閉じてしまい、他の箇所で流用しにくい。
  • テストが困難になる:1つのコンポーネントが多くの責務を持つと、単体テストがしにくくなる。

プレゼンテーションとロジックの分離

コンポーネントを Container Component(コンテナコンポーネント) と Presentational Component(プレゼンテーションコンポーネント) に分割することで、役割を明確にし、責務を限定します。

  • Container Component(コンテナコンポーネント)
    • データの取得や状態管理を担う
    • 外部APIやカスタムフックからデータを取得する
    • ビジネスロジックを処理する
    • プレゼンテーションコンポーネントをラップし、必要なデータや関数を渡す
  • Presentational Component(プレゼンテーションコンポーネント)
    • UIの描画のみを担当する
    • 受け取った props をそのまま表示する
    • ユーザー入力やイベント処理を受け取るが、状態管理はしない

例)分割前のユーザープロフィールコンポーネント

// ❌ 悪い例
function UserProfile() {
  const { data: user, isLoading, error } = useUserData();

  if (isLoading) {
    return <Loading />;
  }

  if (error) {
    return <ErrorMessage message="ユーザー情報を取得できませんでした。" />;
  }

  return (
    <div className="p-4 border rounded-lg">
      <h2 className="text-xl font-bold">{user.name}</h2>
      <p className="text-gray-600">{user.email}</p>
    </div>
  );
}

分割前の問題点

  1. 責務が多すぎる

    • useUserData を呼び出してデータ取得を行う
    • ロード中やエラー時のハンドリングを行う
    • UIの描画を行う
  2. 再利用が難しい

    • UserProfile を他の画面で使い回すことが困難
    • 例えば、別のページでユーザー情報だけを表示したい場合でも useUserData を内部で呼び出してしまうため、自由に user を渡せない
  3. テストがしづらい

    • データ取得の処理とUIの描画が混ざっているため、UIのテストを書く際にデータ取得部分も考慮しないといけない

例)分割後のユーザープロフィールコンポーネント

// ✅ Container Component(データ取得と状態管理)
function UserProfileContainer() {
  const { data: user, isLoading, error } = useUserData();

  if (isLoading) return <Loading />;
  if (error) return <ErrorMessage message="ユーザー情報を取得できませんでした。" />;

  return <UserProfile user={user} />;
}

// ✅ Presentational Component(UIの描画)
function UserProfile({ user }: { user: User }) {
  return (
    <div className="p-4 border rounded-lg">
      <h2 className="text-xl font-bold">{user.name}</h2>
      <p className="text-gray-600">{user.email}</p>
    </div>
  );
}

メリット

  1. コンポーネントの役割が明確になる

    • UserProfileContainer はデータ取得と状態管理のみ担当
    • UserProfile は表示のみを担当
  2. 再利用性が向上

    • UserProfile は異なるデータ(例: 他のユーザー情報)を渡せばそのまま使い回せる
  3. テストがしやすい

    • UserProfileprops だけで動作するため、テストが簡単
    • UserProfileContainer はモックデータを使ってデータ取得部分をテスト可能

設計パターンの適用

ソフトウェア開発において、設計パターンはシステムの保守性や拡張性を向上させる重要な要素です。本記事では、特にフロントエンド・バックエンドの開発において有用な「クリーンアーキテクチャ」と「リポジトリパターン」について解説し、それらを実際に適用する方法を紹介します。

クリーンアーキテクチャ

クリーンアーキテクチャ(Clean Architecture)は、Robert C. Martinによって提唱されたアーキテクチャパターンで、以下の4つの主要な層で構成されます。

  • エンティティ(Entities):ビジネスルールを表現する最も重要な部分
  • ユースケース(Use Cases):アプリケーション固有のビジネスルール
  • インターフェースアダプター(Interface Adapters):フレームワークやデータベースとの橋渡し
  • フレームワーク & ドライバ(Frameworks & Drivers):外部のシステムやUIなどの詳細実装

メリット

  • 保守性が高い:各層が明確に分離されているため、変更の影響範囲が限定される。
  • テストが容易:ビジネスロジックがUIやDBと分離されているため、ユニットテストがしやすい。
  • 技術選定の自由度が高い:データベースやフレームワークの変更が容易。

クリーンアーキテクチャの適用例

// エンティティ層(ドメインモデル)
class User {
  constructor(
    public id: string,
    public name: string,
    public email: string
  ) {}
}

// ユースケース層(アプリケーションのビジネスロジック)
class GetUserUseCase {
  constructor(private userRepository: UserRepository) {}

  async execute(userId: string): Promise<User> {
    return this.userRepository.findById(userId);
  }
}

// インターフェースアダプター層(リポジトリの実装)
class UserRepositoryImpl implements UserRepository {
  constructor(private db: Database) {}

  async findById(id: string): Promise<User> {
    const userData = await this.db.query("SELECT * FROM users WHERE id = ?", [id]);
    return new User(userData.id, userData.name, userData.email);
  }
}

リポジトリパターン

リポジトリパターン(Repository Pattern)は、データアクセスの抽象化を行い、ビジネスロジックがデータベースの具体的な実装に依存しないようにする設計パターンです。

メリット

  • データアクセスの分離:ビジネスロジックとデータアクセスロジックを分離し、疎結合にする。
  • テストが容易:データベースをモック化しやすくなる。
  • データソースの変更が容易:例えば、SQL DB から NoSQL DB に変更する際の影響を抑えられる。

リポジトリパターンの適用例

interface UserRepository {
  findById(id: string): Promise<User>;
  save(user: User): Promise<void>;
}

class InMemoryUserRepository implements UserRepository {
  private users = new Map<string, User>();

  async findById(id: string): Promise<User | null> {
    return this.users.get(id) || null;
  }

  async save(user: User): Promise<void> {
    this.users.set(user.id, user);
  }
}

クリーンアーキテクチャとリポジトリパターンの組み合わせ

クリーンアーキテクチャのユースケース層とリポジトリパターンを組み合わせることで、より柔軟な設計が可能になります。

class CreateUserUseCase {
  constructor(private userRepository: UserRepository) {}

  async execute(name: string, email: string): Promise<User> {
    const user = new User(uuid(), name, email);
    await this.userRepository.save(user);
    return user;
  }
}

Reactのパフォーマンス改善(不要なレンダリングの削減、メモ化の適用)

Reactアプリケーションのパフォーマンスを向上させるためには、不要なレンダリングを削減し、適切なメモ化を適用することが重要です。本記事では、Reactのレンダリング最適化に役立つテクニックを紹介します。

不要なレンダリングの削減

  • React.memo を活用する
    React.memo を使用すると、コンポーネントの props が変更されない限り再レンダリングを防ぐことができます。
import React from 'react';

const ExpensiveComponent = React.memo(({ value }: { value: number }) => {
  console.log('Rendering ExpensiveComponent');
  return <div>{value}</div>;
});

export default ExpensiveComponent;

ポイント

  • React.memo は props の比較を行い、変更がなければ再レンダリングを抑制する。

  • React.memo でラップしただけでは、関数 props(イベントハンドラなど)が変更されると再レンダリングされるため、useCallback と併用する。

  • useCallback で関数のメモ化
    関数が毎回新しく作られると、子コンポーネントの再レンダリングが発生する可能性があります。そのため、useCallback を活用して関数をメモ化します。

import { useState, useCallback } from 'react';
import ExpensiveComponent from './ExpensiveComponent';

const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState(10);

  const increment = useCallback(() => setCount((prev) => prev + 1), []);

  return (
    <div>
      <button onClick={increment}>Increment: {count}</button>
      <ExpensiveComponent value={value} />
    </div>
  );
};

export default ParentComponent;

ポイント

  • useCallback を使用すると、依存配列が変更されない限り、新しい関数が生成されない。

  • React.memo でラップされたコンポーネントの不要な再レンダリングを防ぐことができる。

  • useMemo で計算結果をキャッシュ
    計算コストが高い処理を useMemo でメモ化し、不要な再計算を防ぎます。

import { useState, useMemo } from 'react';

const ExpensiveCalculation = (num: number) => {
  console.log('Calculating...');
  return num * 2;
};

const MemoizedComponent = () => {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState(10);

  const computedValue = useMemo(() => ExpensiveCalculation(value), [value]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment: {count}</button>
      <div>Computed Value: {computedValue}</div>
    </div>
  );
};

export default MemoizedComponent;

ポイント

  • useMemo は依存配列の値が変わらない限り、計算結果をキャッシュする。
  • useMemo の使用は、計算コストが高い場合に限定する。

key の適切な設定

リストレンダリング時に key を適切に設定することで、Reactの仮想DOMが効率的に差分検出を行えるようになります。

const items = ['Apple', 'Banana', 'Orange'];

const ItemList = () => {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
};

ポイント

  • key はユニークで変わらない値を使用する(インデックスは推奨されない)。
  • 適切な key を指定することで、不要な再レンダリングを防ぐ。

JavaScriptの非同期処理の最適化(Promise.all の適切な活用、エラーハンドリング強化)

JavaScript における非同期処理の最適化は、アプリケーションのパフォーマンスや安定性に直結します。特に Promise.all を適切に活用し、エラーハンドリングを強化することで、効率的で堅牢な非同期処理を実装できます。

Promise.all の基本

Promise.all は複数の Promise を並列実行し、すべての処理が成功した場合に結果を返します。

const promises = [fetchData1(), fetchData2(), fetchData3()];
Promise.all(promises)
  .then(results => {
    console.log("すべての処理が完了", results);
  })
  .catch(error => {
    console.error("エラーが発生", error);
  });

メリット

  • 並列実行: 処理を同時に実行するため、パフォーマンスが向上する。
  • シンプルなコード: 各処理の結果を配列でまとめて取得できる。

デメリット

  • 1つの処理が失敗すると全体が失敗する: 途中のエラーで catch に入るため、他の成功した処理の結果を得られない。

Promise.allSettled を活用したエラーハンドリング

Promise.allSettled は、すべての Promise の処理が完了するまで待ち、成功・失敗に関係なく結果を取得できます。

const promises = [fetchData1(), fetchData2(), fetchData3()];
Promise.allSettled(promises)
  .then(results => {
    results.forEach((result, index) => {
      if (result.status === "fulfilled") {
        console.log(`成功: ${index}`, result.value);
      } else {
        console.error(`失敗: ${index}`, result.reason);
      }
    });
  });

Promise.allSettled のメリット

  • すべての処理が終了するまで待機: 失敗した処理があっても、他の処理は継続。
  • 個別のエラー処理が可能: 各 Promise の成功・失敗を個別に処理できる。

Promise.all のエラーハンドリングを強化する方法

Promise.all で個別のエラーを処理するために、各 Promise で catch を使う方法もあります。

const safeFetch = (promise) => {
  return promise.catch(error => ({ error }));
};

const promises = [
  safeFetch(fetchData1()),
  safeFetch(fetchData2()),
  safeFetch(fetchData3()),
];

Promise.all(promises).then(results => {
  results.forEach((result, index) => {
    if (result.error) {
      console.error(`失敗: ${index}`, result.error);
    } else {
      console.log(`成功: ${index}`, result);
    }
  });
});

個別のエラーハンドリングを適用する

すべてのエラーをキャッチするのではなく、特定の処理ごとに適切なハンドリングを行うことが重要です。

const fetchDataWithRetry = async (fetchFunction, retries = 3) => {
  for (let i = 0; i < retries; i++) {
    try {
      return await fetchFunction();
    } catch (error) {
      console.warn(`リトライ ${i + 1} 回目に失敗`, error);
    }
  }
  throw new Error("最大リトライ回数を超えました");
};

const promises = [
  fetchDataWithRetry(fetchData1),
  fetchDataWithRetry(fetchData2),
  fetchDataWithRetry(fetchData3),
];

Promise.all(promises)
  .then(results => console.log("すべて成功", results))
  .catch(error => console.error("エラー", error));

依存関係の整理:不要なライブラリの削除と最新バージョンへの更新

ソフトウェア開発において、プロジェクトの依存関係を適切に管理することは非常に重要です。不要なライブラリを削除し、必要なライブラリを最新の安定バージョンに更新することで、パフォーマンスの向上、セキュリティリスクの低減、保守性の向上が期待できます。

メリット

  • パフォーマンスの向上
    不要なライブラリが増えると、ビルド時間やアプリケーションの起動時間が長くなります。また、ランタイムのメモリ使用量も増加するため、動作が重くなる原因になります。

  • セキュリティリスクの低減
    古いライブラリには脆弱性が含まれている可能性があります。最新バージョンへ更新することで、既知の脆弱性を解消し、セキュリティを向上させることができます。

  • 保守性の向上
    使われていないライブラリや古いバージョンのライブラリが多いと、コードの管理が煩雑になります。また、将来的に他の開発者がプロジェクトを引き継ぐ際の障害となる可能性があります。

ステップ 1: 現在の依存関係をリストアップ

まず、プロジェクトの依存関係を確認しましょう。

npm list --depth=0  # npmの場合
pnpm list --depth=0  # pnpmの場合
yarn list --depth=0  # yarnの場合

また、package.json 内の dependenciesdevDependencies を確認し、どのライブラリが実際に使用されているかを把握します。

ステップ 2: 不要なライブラリの特定と削除

不要なライブラリを見つける方法として、以下の手順を試してみましょう。

  • プロジェクト内で使用していないライブラリを探す

    npx depcheck
    

    depcheck は、使用されていない依存関係を特定する便利なツールです。

  • package.json で手動チェック

    • 過去に試験的に導入したが今は使われていないライブラリ
    • フレームワークの変更に伴い不要になったライブラリ
    • devDependencies にあるが本番環境で使われていないライブラリ

不要なライブラリが特定できたら削除します。

npm uninstall <package_name>
pnpm remove <package_name>
yarn remove <package_name>

ライブラリを更新した後は、テストを実行してアプリケーションが正常に動作することを確認します。

npm test  # 事前に用意したテストスクリプトを実行

ステップ 3: 依存関係の最新バージョンへの更新

古いライブラリを最新の安定バージョンに更新するには、以下のコマンドを実行します。

npm outdated  # アップデート可能なパッケージを確認
npm update    # マイナーバージョンの更新

ステップ 4: 動作確認とリグレッションテスト

ライブラリを更新した後は、テストを実行してアプリケーションが正常に動作することを確認します。

npm test  # 事前に用意したテストスクリプトを実行

また、E2E テストや手動テストを行い、バージョンアップによる影響がないか確認することも重要です。

ステップ 5: 依存関係の定期的な見直し

プロジェクトの規模に応じて、定期的に依存関係を見直すことをおすすめします。

ステップ 6: ロックファイルの管理

package-lock.json(npm)や yarn.lock(Yarn)を適切に管理し、意図しないバージョンアップを防ぎます。

ステップ 7: 主要ライブラリのリリースノートを確認

ReactNext.js などの主要なライブラリは、新バージョンのリリース時に breaking changes を含むことがあります。アップデート前にリリースノートを確認し、移行ガイドに従うようにしましょう。

TypeScript における any の排除と型の適用範囲拡大

TypeScript を活用する際、any 型の使用は柔軟性を提供する一方で、型安全性を損なう原因になります。本記事では、any の排除方法と、型の適用範囲を拡大する手法について詳しく解説します。

なぜ any を排除すべきか?

any 型を使用すると、TypeScript の型チェックが無効化され、以下のような問題を引き起こします。

  • 型安全性の欠如: any を使用すると、型のミスマッチをコンパイル時に検知できず、ランタイムエラーの原因になる。
  • コードの可読性の低下: 型情報が失われることで、関数やオブジェクトの構造が不明瞭になる。
  • 型推論が働かない: TypeScript の強力な型推論機能を活用できなくなる。

any を削減するための手法

any を安全に排除し、型の適用範囲を拡大するための手法をいくつか紹介します。

  1. unknown の活用
    unknownany の代替として使える型で、適切な型チェックが行われるまで型安全性を保つことができます。
function parseJson(json: string): unknown {
  return JSON.parse(json);
}

const data: unknown = parseJson('{"name":"Alice"}');
if (typeof data === 'object' && data !== null && 'name' in data) {
  console.log((data as { name: string }).name); // 型チェック後にキャスト
}
  1. ジェネリクスを活用する

関数の戻り値や引数にジェネリクスを使うことで、型の適用範囲を広げられます。

function identity<T>(value: T): T {
  return value;
}

const result = identity<string>("Hello"); // 型が string になる
  1. 明示的な型アノテーションを追加
    適切な型を明示することで、any の使用を防げます。
interface User {
  id: number;
  name: string;
}

const getUser = (id: number): User => {
  return { id, name: "John Doe" };
};
  1. ユーティリティ型の活用
    TypeScript には型を拡張・制約するためのユーティリティ型が用意されています。
type PartialUser = Partial<User>; // すべてのプロパティをオプショナルに

const user: PartialUser = { name: "Alice" }; // OK
  1. 型推論を活用する
    TypeScript の型推論を活かし、明示的な any の使用を避けることができます。
const numbers = [1, 2, 3];
const doubled = numbers.map(num => num * 2); // `num` は number 型として推論される

any を使用せざるを得ない場合の対処法

どうしても any を使わざるを得ない場合でも、適切な方法で制御することが重要です。

  1. 型ガードを使用する
    型チェックを行い、安全に any をキャストする方法。
function isUser(obj: any): obj is User {
  return typeof obj.id === "number" && typeof obj.name === "string";
}

const maybeUser: any = { id: 1, name: "Alice" };
if (isUser(maybeUser)) {
  console.log(maybeUser.name); // 安全に型を適用できる
}
  1. as を乱用しない
    as any の使用は極力避け、適切な型を設定する。
// NG 例
const data: any = getData();
const user = data as User; // 型安全性が失われる
  1. ライブラリの型定義を活用する
    外部ライブラリを使用する際、適切な型定義ファイル (@types/...) をインストールし、型の適用範囲を広げる。
npm install --save-dev @types/lodash

JavaScriptからTypeScriptに移行する際の注意点などをまとめた記事があります。

型に関する様々なテクニックをご紹介しています。合わせてご参照ください。

https://shinagawa-web.com/blogs/typescript-migration-support

リファクタリング計画の策定(段階的な適用をサポート)

ソフトウェア開発において、コードの可読性や保守性を向上させるためにリファクタリングは不可欠です。しかし、大規模なプロジェクトでは一度にすべてを変更することは困難であり、適切な計画が求められます。本記事では、段階的な適用を考慮したリファクタリング計画の策定方法について詳しく解説します。

リファクタリングの目的を定義する

  • コードの可読性向上
  • 保守性の向上
  • パフォーマンスの最適化
  • バグの削減
  • 技術的負債の解消

優先度の決定

  • 影響範囲の大きい箇所(頻繁に変更されるコードや、バグの温床になりやすい部分)を優先
  • ビジネス価値の高い機能のコードを優先
  • 技術的負債が大きい箇所(旧来の設計の影響で開発速度が落ちている部分)を優先

現状のコードベースの分析

  • コードの品質分析(SonarQube、ESLint、Stylelint などを活用)
  • 依存関係の可視化(Dependency Cruiser や Graphviz を利用)
  • テストカバレッジの確認(Jest、React Testing Library など)

影響範囲を考慮したリファクタリングの設計

  • モジュール単位で分割(コンポーネントごと、機能ごとに変更を段階的に適用)
  • 依存関係が少ない部分から順に適用
  • 新旧コードの共存を考慮した設計(Feature Toggle, Strangler Pattern の活用)

リファクタリングの進め方

  1. PoC(概念実証)を実施
    • 小規模な変更で影響を確認
  2. 小さな変更を段階的に適用
    • 例: 関数のリネームやコードの整理から開始
  3. 主要な機能や依存関係が強い部分をリファクタリング
    • 例: 旧来のクラスコンポーネントから関数コンポーネントへの移行
  4. アーキテクチャの改善
    • 例: Redux から Zustand への移行、API の整理

リファクタリングを安全に進めるためのポイント

  • テストの充実
    • 単体テスト、結合テスト、E2E テストの整備
  • Feature Flag の活用
    • 段階的に適用しながら動作を切り替えられる仕組みを導入
  • コードレビューとペアプログラミングの活用
    • 第三者の視点を入れてリファクタリングの質を向上
  • リリースごとの影響範囲を最小限にする
    • マイクロリリースの戦略を採用

リファクタリング後のコード管理

リファクタリングが適切に行われたかを確認するために、以下の指標を用いて成果を評価します。

  • コードの品質指標を測定

    • Cyclomatic Complexity(循環的複雑度): コードの分岐の複雑さを測定し、リファクタリングによる簡素化を評価
    • Maintainability Index(保守性指標): コードの理解しやすさや変更しやすさを数値化
    • コードの重複率: コードの重複を削減し、メンテナンス性を向上
  • 開発速度の向上を確認

    • PR マージ速度の変化: 変更がスムーズにレビュー・統合されるか
    • バグ報告の減少: リファクタリングによるバグの削減効果を測定
    • リリースサイクルの短縮: 新機能の開発がスムーズに進むか

リファクタリングによる変更を適切に記録し、チーム全体で共有することで、将来的な保守性を向上させます。

  • リファクタリングの記録を残す

    • 変更理由: なぜリファクタリングを行ったのかを明確にする
    • 影響範囲: どのモジュールや機能に影響があるのかを整理
    • 移行手順: 旧コードから新コードへ移行する手順を記述
    • 後方互換性の確保: 既存の機能に影響を与えずに変更するための配慮点
  • コードコメントや README の更新

    • API 仕様の変更: エンドポイントの仕様変更やパラメータの変更をドキュメントに反映
    • コンポーネントの使用方法: 変更されたコンポーネントの使い方を明記
    • 環境設定の更新: 設定ファイルや CI/CD スクリプトの変更を記録

さいごに

リファクタリングは一度行えば終わりではなく、継続的に改善を積み重ねることが重要 です。可読性・再利用性・パフォーマンスの向上を意識しながらコードを整理していくことで、開発体験が向上し、バグの発生を抑えながら新機能の追加がしやすくなります。

特に、以下のポイントを意識すると、効果的なリファクタリングが行えます。

  • 小さな改善を積み重ねる:一度に大きく変えすぎず、段階的にリファクタリングを適用する
  • コードの意図を明確にする:命名や関数の分割を工夫し、可読性を重視する
  • チームでリファクタリングの方針を共有する:統一感を持たせ、開発効率を維持する
  • 自動テストを活用する:リファクタリング後の動作を保証し、安全に進める

プロジェクトの成長とともにコードは変化し続けるものです。その変化を制御し、より良い形へと進化させるために、リファクタリングを習慣化し、「メンテナブルなコードを維持する文化」 を築いていきましょう。

記事に関するお問い合わせ📝

記事の内容に関するご質問、ご意見などは、下記よりお気軽にお問い合わせください。
ご質問フォームへ

技術支援などお仕事に関するお問い合わせ📄

技術支援やお仕事のご依頼に関するお問い合わせは、下記よりお気軽にお問い合わせください。
お問い合わせフォームへ

関連する技術ブログ