はじめに
フロントエンド開発では、プロジェクトの規模が大きくなるにつれ、コードの可読性や保守性が低下し、開発スピードが落ちることがよくあります。最初はシンプルだったコードも、機能追加や仕様変更を繰り返すうちに複雑化し、「この処理、どこかで見たような気がする…」「この関数、何をしているのかわかりづらい…」といった問題が発生しがちです。
こうした課題を解決するためには、コードを継続的に見直し、整理・最適化していく 「リファクタリング」 が欠かせません。リファクタリングを適切に行うことで、可読性が向上し、重複コードが削減され、パフォーマンスが改善される など、プロジェクト全体の品質を高めることができます。
本記事では、リファクタリングの具体的な手法として、命名規則の見直し、関数の分割、共通処理のモジュール化、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)
意味のある命名をする
data
や temp
などの抽象的な名前を避け、具体的な意味を持たせる。
- 悪い例: 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
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"
テストコード
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リクエスト用の関数
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);
}
}
テストコード
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');
});
});
例)メールアドレスのバリデーション
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
テストコード
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);
});
});
例)ローカルストレージのユーティリティ
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 }
テストコード
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();
});
});
例)エラーハンドリング関数
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"
}
テストコード
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>
);
}
分割前の問題点
-
責務が多すぎる
- useUserData を呼び出してデータ取得を行う
- ロード中やエラー時のハンドリングを行う
- UIの描画を行う
-
再利用が難しい
- UserProfile を他の画面で使い回すことが困難
- 例えば、別のページでユーザー情報だけを表示したい場合でも useUserData を内部で呼び出してしまうため、自由に user を渡せない
-
テストがしづらい
- データ取得の処理と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>
);
}
メリット
-
コンポーネントの役割が明確になる
UserProfileContainer
はデータ取得と状態管理のみ担当UserProfile
は表示のみを担当
-
再利用性が向上
UserProfile
は異なるデータ(例: 他のユーザー情報)を渡せばそのまま使い回せる
-
テストがしやすい
UserProfile
はprops
だけで動作するため、テストが簡単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
内の dependencies
と devDependencies
を確認し、どのライブラリが実際に使用されているかを把握します。
ステップ 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: 主要ライブラリのリリースノートを確認
React
や Next.js
などの主要なライブラリは、新バージョンのリリース時に breaking changes を含むことがあります。アップデート前にリリースノートを確認し、移行ガイドに従うようにしましょう。
any
の排除と型の適用範囲拡大
TypeScript における TypeScript を活用する際、any
型の使用は柔軟性を提供する一方で、型安全性を損なう原因になります。本記事では、any
の排除方法と、型の適用範囲を拡大する手法について詳しく解説します。
any
を排除すべきか?
なぜ any
型を使用すると、TypeScript の型チェックが無効化され、以下のような問題を引き起こします。
- 型安全性の欠如:
any
を使用すると、型のミスマッチをコンパイル時に検知できず、ランタイムエラーの原因になる。 - コードの可読性の低下: 型情報が失われることで、関数やオブジェクトの構造が不明瞭になる。
- 型推論が働かない: TypeScript の強力な型推論機能を活用できなくなる。
any
を削減するための手法
any
を安全に排除し、型の適用範囲を拡大するための手法をいくつか紹介します。
unknown
の活用
unknown
はany
の代替として使える型で、適切な型チェックが行われるまで型安全性を保つことができます。
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); // 型チェック後にキャスト
}
- ジェネリクスを活用する
関数の戻り値や引数にジェネリクスを使うことで、型の適用範囲を広げられます。
function identity<T>(value: T): T {
return value;
}
const result = identity<string>("Hello"); // 型が string になる
- 明示的な型アノテーションを追加
適切な型を明示することで、any
の使用を防げます。
interface User {
id: number;
name: string;
}
const getUser = (id: number): User => {
return { id, name: "John Doe" };
};
- ユーティリティ型の活用
TypeScript には型を拡張・制約するためのユーティリティ型が用意されています。
type PartialUser = Partial<User>; // すべてのプロパティをオプショナルに
const user: PartialUser = { name: "Alice" }; // OK
- 型推論を活用する
TypeScript の型推論を活かし、明示的なany
の使用を避けることができます。
const numbers = [1, 2, 3];
const doubled = numbers.map(num => num * 2); // `num` は number 型として推論される
any
を使用せざるを得ない場合の対処法
どうしても any
を使わざるを得ない場合でも、適切な方法で制御することが重要です。
- 型ガードを使用する
型チェックを行い、安全に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); // 安全に型を適用できる
}
as
を乱用しない
as any
の使用は極力避け、適切な型を設定する。
// NG 例
const data: any = getData();
const user = data as User; // 型安全性が失われる
- ライブラリの型定義を活用する
外部ライブラリを使用する際、適切な型定義ファイル (@types/...
) をインストールし、型の適用範囲を広げる。
npm install --save-dev @types/lodash
JavaScriptからTypeScriptに移行する際の注意点などをまとめた記事があります。
型に関する様々なテクニックをご紹介しています。合わせてご参照ください。
リファクタリング計画の策定(段階的な適用をサポート)
ソフトウェア開発において、コードの可読性や保守性を向上させるためにリファクタリングは不可欠です。しかし、大規模なプロジェクトでは一度にすべてを変更することは困難であり、適切な計画が求められます。本記事では、段階的な適用を考慮したリファクタリング計画の策定方法について詳しく解説します。
リファクタリングの目的を定義する
- コードの可読性向上
- 保守性の向上
- パフォーマンスの最適化
- バグの削減
- 技術的負債の解消
優先度の決定
- 影響範囲の大きい箇所(頻繁に変更されるコードや、バグの温床になりやすい部分)を優先
- ビジネス価値の高い機能のコードを優先
- 技術的負債が大きい箇所(旧来の設計の影響で開発速度が落ちている部分)を優先
現状のコードベースの分析
- コードの品質分析(SonarQube、ESLint、Stylelint などを活用)
- 依存関係の可視化(Dependency Cruiser や Graphviz を利用)
- テストカバレッジの確認(Jest、React Testing Library など)
影響範囲を考慮したリファクタリングの設計
- モジュール単位で分割(コンポーネントごと、機能ごとに変更を段階的に適用)
- 依存関係が少ない部分から順に適用
- 新旧コードの共存を考慮した設計(Feature Toggle, Strangler Pattern の活用)
リファクタリングの進め方
- PoC(概念実証)を実施
- 小規模な変更で影響を確認
- 小さな変更を段階的に適用
- 例: 関数のリネームやコードの整理から開始
- 主要な機能や依存関係が強い部分をリファクタリング
- 例: 旧来のクラスコンポーネントから関数コンポーネントへの移行
- アーキテクチャの改善
- 例: Redux から Zustand への移行、API の整理
リファクタリングを安全に進めるためのポイント
- テストの充実
- 単体テスト、結合テスト、E2E テストの整備
- Feature Flag の活用
- 段階的に適用しながら動作を切り替えられる仕組みを導入
- コードレビューとペアプログラミングの活用
- 第三者の視点を入れてリファクタリングの質を向上
- リリースごとの影響範囲を最小限にする
- マイクロリリースの戦略を採用
リファクタリング後のコード管理
リファクタリングが適切に行われたかを確認するために、以下の指標を用いて成果を評価します。
-
コードの品質指標を測定
- Cyclomatic Complexity(循環的複雑度): コードの分岐の複雑さを測定し、リファクタリングによる簡素化を評価
- Maintainability Index(保守性指標): コードの理解しやすさや変更しやすさを数値化
- コードの重複率: コードの重複を削減し、メンテナンス性を向上
-
開発速度の向上を確認
- PR マージ速度の変化: 変更がスムーズにレビュー・統合されるか
- バグ報告の減少: リファクタリングによるバグの削減効果を測定
- リリースサイクルの短縮: 新機能の開発がスムーズに進むか
リファクタリングによる変更を適切に記録し、チーム全体で共有することで、将来的な保守性を向上させます。
-
リファクタリングの記録を残す
- 変更理由: なぜリファクタリングを行ったのかを明確にする
- 影響範囲: どのモジュールや機能に影響があるのかを整理
- 移行手順: 旧コードから新コードへ移行する手順を記述
- 後方互換性の確保: 既存の機能に影響を与えずに変更するための配慮点
-
コードコメントや README の更新
- API 仕様の変更: エンドポイントの仕様変更やパラメータの変更をドキュメントに反映
- コンポーネントの使用方法: 変更されたコンポーネントの使い方を明記
- 環境設定の更新: 設定ファイルや CI/CD スクリプトの変更を記録
さいごに
リファクタリングは一度行えば終わりではなく、継続的に改善を積み重ねることが重要 です。可読性・再利用性・パフォーマンスの向上を意識しながらコードを整理していくことで、開発体験が向上し、バグの発生を抑えながら新機能の追加がしやすくなります。
特に、以下のポイントを意識すると、効果的なリファクタリングが行えます。
- 小さな改善を積み重ねる:一度に大きく変えすぎず、段階的にリファクタリングを適用する
- コードの意図を明確にする:命名や関数の分割を工夫し、可読性を重視する
- チームでリファクタリングの方針を共有する:統一感を持たせ、開発効率を維持する
- 自動テストを活用する:リファクタリング後の動作を保証し、安全に進める
プロジェクトの成長とともにコードは変化し続けるものです。その変化を制御し、より良い形へと進化させるために、リファクタリングを習慣化し、「メンテナブルなコードを維持する文化」 を築いていきましょう。
関連する技術ブログ
ExpressとMongoDBで簡単にWeb APIを構築する方法【TypeScript対応】
本記事では、MongoDB Atlasを活用してREST APIを構築する方法を、初心者の方にも分かりやすいステップで解説します。プロジェクトの初期設定からMongoDBの接続設定、Expressを使用したルートの作成、さらにTypeScriptを用いた型安全な実装まで、実践的なサンプルコードを交えて丁寧に説明します。
shinagawa-web.com
Express(+TypeScript)入門ガイド: Webアプリケーションを素早く構築する方法
Node.jsでWebアプリケーションを構築するための軽量フレームワーク、Expressの基本的な使い方を解説。シンプルなサーバー設定からルーティング、ミドルウェアの活用方法、TypeScriptでの開発環境構築まで、実践的なコード例とともに学べます。
shinagawa-web.com
JestとTypeScriptで始めるテスト自動化:基本設定から型安全なテストの書き方まで徹底解説
JestとTypeScriptを使ったテスト自動化の基本を学びたい方へ。環境のセットアップ方法、型安全なテストを書くメリット、コードの信頼性を高める実践的なテクニックを初心者向けに丁寧に解説します。テストカバレッジの活用で、品質の高い開発を目指しましょう。
shinagawa-web.com
ESLint / Prettier 導入ガイド: Husky, CI/CD 統合, コード品質の可視化まで徹底解説
開発チームでコードの品質を統一するために、Linter(ESLint)と Formatter(Prettier)は欠かせません。Linter はコードの構文やスタイルの問題を検出し、Formatter はコードの整形を統一します。本記事では、9つの取り組みについて詳しく解説します。
shinagawa-web.com
フロントエンド開発に役立つモックサーバー構築:@graphql-tools/mock と Faker を使った実践ガイド
フロントエンド開発を先行させるために、バックエンドが未完成でもモックサーバーを立ち上げる方法を解説。@graphql-tools/mock と Faker を使用して、実際のデータに近い動作をシミュレートします。
shinagawa-web.com
Mock Service Worker (MSW) を使ったAPIモックとテストの効率化
MSW(Mock Service Worker)を使用して、フロントエンド開発やテスト環境でのAPIモックを効率的に行う方法を解説します。Mock Service Workerの基本的な使い方から、Jestテストでの活用方法、さらにテストを簡単にするための設定手順を紹介します。
shinagawa-web.com
Next.jsとAuth.jsで認証機能を実装するチュートリアル
Next.jsでアプリケーションを作る時に必要となる認証機能をどのように実装するかをご紹介する記事となります。アカウント登録から始まり、ログイン、ログアウト、ページごとのアクセス制御、OAuth、二要素認証、パスワードリセットなど認証に関連する様々な機能をコードベースでご紹介します。
shinagawa-web.com
Next.js × Squareで決済を実装:Square Web Payments SDKの導入ガイド
Next.jsアプリにSquare Web Payments SDKを導入し、安全でスムーズな決済機能を実装する方法を解説。セットアップから支払い処理の実装、エラーハンドリングまでの実践的なガイド。
shinagawa-web.com