💡

5分で分かる!Next.jsでYAMLファイルを読み込む実践的アプローチ

2024/09/02に公開

1. はじめに

この記事では、Next.jsアプリケーションでYAMLファイルを読み込み、JavaScriptオブジェクトとして取得する方法について詳しく解説します。具体的には以下の内容を学べます。

  • Next.jsでYAMLファイルを読み込む方法
  • fs、yaml、pathモジュールを使用したファイル操作
  • 非同期処理を用いたデータ取得の実装
  • 型安全なデータ操作(TypeScriptの活用)
  • プログラミング言語データの効率的な処理方法

2. Next.jsでYAMLファイルを読み込むコード例

2.1 YAMLファイルの内容

まず、扱うYAMLファイルの内容を見てみましょう。

public/data/languages.yml
JavaScript:
  type: programming
  color: "#f1e05a"
  language_id: 183

Python:
  type: programming
  color: "#3572A5"
  language_id: 303

HTML:
  type: markup
  color: "#e34c26"
  language_id: 146

CSS:
  type: markup
  color: "#563d7c"
  language_id: 50

Ruby:
  type: programming
  color: "#701516"
  language_id: 326

2.2 コード例の概要と全体像

次に、このYAMLファイルを読み込み、処理するためのTypeScriptコードを見てみましょう。

app/services/programmingLangService.ts
import fs from "fs";
import yaml from "js-yaml";
import path from "path";
import { ProgrammingLang } from "../types/programmingLang";

export class ProgrammingLangService {
  static async getProgrammingLangs(): Promise<ProgrammingLang[]> {
    try {
      const yamlPath = path.join(
        process.cwd(),
        "public",
        "data",
        "languages.yml"
      );
      const yamlContent = fs.readFileSync(yamlPath, "utf8");
      const languages = yaml.load(yamlContent) as Record<string, any>;

      return Object.entries(languages)
        .filter(
          ([_, lang]) => lang.type === "programming" || lang.type === "markup"
        )
        .map(([name, lang]) => ({
          label: name,
          value: lang.language_id.toString(),
          color: lang.color || "#000000",
        }))
        .sort((a, b) => a.label.localeCompare(b.label));
    } catch (error) {
      console.error("Error loading programmingLangs:", error);
      return []; // エラー時は空の配列を返す
    }
  }
}

このコードは、public/data/languages.ymlからプログラミング言語データを読み込み、処理します。

2.3 コード解説

モジュールのインポート

import fs from "fs";
import yaml from "js-yaml";
import path from "path";
import { ProgrammingLang } from "../types/programmingLang";
  • fs (File System): Node.jsのコアモジュールで、ファイルシステムの操作を可能にします。
  • js-yaml: YAMLファイルをパースするための外部ライブラリです。
  • path: Node.jsのコアモジュールで、ファイルパスを操作するためのユーティリティです。
  • ProgrammingLang: カスタムの型定義をインポートしています。

https://github.com/nodeca/js-yaml

型定義

interface ProgrammingLang {
  label: string;
  value: string;
  color: string;
}

この型定義により、プログラミング言語データの構造を明確に指定し、型安全性を確保しています。

ファイル読み込み、YAMLをオブジェクトに変換する

const yamlPath = path.join(
  process.cwd(),
  "public",
  "data",
  "languages.yml"
);
const yamlContent = fs.readFileSync(yamlPath, "utf8");
const languages = yaml.load(yamlContent) as Record<string, any>;
  • path.join: 複数のパス部分を結合して1つのパスを作成します。
  • fs.readFileSync: 同期的にファイルを読み込みます。
  • yaml.load: YAMLテキストをJavaScriptオブジェクトに変換します。
  • as Record<string, any>: yaml.load(yamlContent)で取得できるオブジェクトの型を定義しています。もし、もうちょっと詳しく知りたい方は以下の資料がわかりやすいので参考にしてみてください。

YAMLデータの構造理解とデータ処理

return Object.entries(languages)
  .filter(
    ([_, lang]) => lang.type === "programming" || lang.type === "markup"
  )
  .map(([name, lang]) => ({
    label: name,
    value: lang.language_id.toString(),
    color: lang.color || "#000000",
  }))
  .sort((a, b) => a.label.localeCompare(b.label));
  • Object.entries: オブジェクトを [key, value] ペアの配列に変換します。
  • filter: lang.typeが、"programming"、"markup"であるデータ抽出します。
  • map: 必要な情報だけを新しいオブジェクト形式に変換します。
  • sort: 言語名でアルファベット順にソートします。

エラーハンドリング

} catch (error) {
  console.error("Error loading programmingLangs:", error);
  return []; // エラー時は空の配列を返す
}

3. ProgrammingLangServiceの使用例と出力例

3.1 使用例

ProgrammingLangServiceの使用例を、以下、Next.jsのページコンポーネントで示します。

  • この例では、ProgrammingLangServiceを使用してYAMLファイルのプログラミング言語のリストを取得し、Next.jsで表示しています。
  • page.tsx(Server Component)を呼び出し、さらにその中でProgrammingLangs.tsx(Client Component)を呼び出す構造です。
  • この構造にすることでimport fs from "fs";などサーバー側のライブラリを含んだProgrammingLangServiceを直接、呼び出すことができます。

app/programmings/page.tsx(Server Component)

app/programmings/page.tsx
import { ProgrammingLangService } from "../services/programmingLangService";
import ProgrammingLangs from "./ProgrammingLangs";

export default async function ProgrammingLangsPage() {
  const languages = await ProgrammingLangService.getProgrammingLangs();

  return (
    <div className="flex min-h-screen flex-col items-center justify-center bg-gray-100">
      <div className="w-full max-w-2xl rounded-lg bg-white p-8 shadow-md">
        <h1 className="mb-8 text-center text-2xl font-bold">
          Programming Languages
        </h1>
        <ProgrammingLangs initialLanguages={languages} />
      </div>
    </div>
  );
}

app/programmings/ProgrammingLangs.tsx(Client Component)

app/programmings/ProgrammingLangs.tsx
"use client";

import { useState } from "react";
import { ProgrammingLang } from "../types/programmingLang";

interface ProgrammingLangsProps {
  initialLanguages: ProgrammingLang[];
}

export default function ProgrammingLangs({
  initialLanguages,
}: ProgrammingLangsProps) {
  const [languages] = useState<ProgrammingLang[]>(initialLanguages);

  return (
    <ul>
      {languages.map((lang) => (
        <li key={lang.value} style={{ color: lang.color }}>
          {lang.label} (ID: {lang.value})
        </li>
      ))}
    </ul>
  );
}

3.2 出力例

/programmings にアクセスすると、以下の画面が出力されます。

出力例

4. コード改善と機能拡張の例

4.1 非同期処理の改善

import { promises as fsPromises } from 'fs';

// ...

static async getProgrammingLangs(): Promise<ProgrammingLang[]> {
  try {
    const yamlPath = path.join(process.cwd(), "public", "data", "languages.yml");
    const yamlContent = await fsPromises.readFile(yamlPath, 'utf8');
    const languages = yaml.load(yamlContent) as Record<string, any>;

    // 以下は同じ
  } catch (error) {
    // エラーハンドリング
  }
}

この改善では、fs.promises APIを使用して、ファイル読み込みを非同期で行います。

4.2 型安全性の強化

interface LanguageData {
  type: string;
  color?: string;
  language_id: number;
}

// ...

const languages = yaml.load(yamlContent) as Record<string, LanguageData>;

return Object.entries(languages)
  .filter(([_, lang]) => lang.type === "programming" || lang.type === "markup")
  .map(([name, lang]): ProgrammingLang => ({
    label: name,
    value: lang.language_id.toString(),
    color: lang.color || "#000000",
  }))
  .sort((a, b) => a.label.localeCompare(b.label));

LanguageData インターフェースを導入することで、YAMLから読み込んだデータの構造をより厳密に定義し、型安全性を高めています。

4.3 エラーハンドリングの改善

static async getProgrammingLangs(): Promise<ProgrammingLang[]> {
  try {
    // ... 既存のコード ...
  } catch (error) {
    console.error("Error loading programmingLangs:", error);
    if (error instanceof Error) {
      // エラーの種類に応じた処理
      if (error.message.includes("ENOENT")) {
        console.error("File not found. Please check the file path.");
      } else if (error.message.includes("YAML")) {
        console.error("Invalid YAML format. Please check the file content.");
      }
    }
    return []; // エラー時は空の配列を返す
  }
}

エラーハンドリングを改善することで、エラーの種類に応じてより詳細なログを出力し、問題の診断が容易になります。

5. Tips

キャッシュの導入

キャッシュを実装することで、頻繁なファイル読み込みを避け、パフォーマンスを向上させることができます。

export class ProgrammingLangService {
  private static cache: ProgrammingLang[] | null = null;
  private static lastFetchTime: number = 0;
  private static CACHE_TTL = 60 * 60 * 1000; // 1時間のTTL

  static async getProgrammingLangs(): Promise<ProgrammingLang[]> {
    const now = Date.now();
    if (this.cache && (now - this.lastFetchTime) < this.CACHE_TTL) {
      return this.cache; // キャッシュが有効な場合はキャッシュを返す
    }

    try {
      // YAMLファイルの読み込みと処理(既存のコード)
      const result = // ... 既存の処理 ...

      this.cache = result;
      this.lastFetchTime = now;
      return result;
    } catch (error) {
      // エラーハンドリング
    }
  }
}

このキャッシュは、一定時間内の再取得要求に対してキャッシュされたデータを返すことで、ファイルシステムへのアクセスを最小限に抑えます。

5. まとめ

この記事では、Next.jsでYAMLファイルを読み込み、オブジェクトとして取得する方法について学びました。

主なポイントは以下の通りです。

  • fs、yaml、pathモジュールを組み合わせたファイル操作技術
  • 非同期処理を用いたファイルのデータ取得実装
  • TypeScriptを活用した型安全なデータ操作の重要性
  • 実際のYAMLファイル構造(type、color、language_id)の理解と効果的な処理方法

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

Discussion