T-CREATOR

Svelte と GraphQL:最速データ連携のススメ

Svelte と GraphQL:最速データ連携のススメ

最新の Web 開発において、ユーザーエクスペリエンスを向上させるためには、効率的なデータ取得と軽量なフロントエンドフレームワークの組み合わせが重要です。今回は、コンパイル時最適化により高速な Web アプリケーションを構築できる Svelte と、データフェッチを革新的に変化させる GraphQL の組み合わせについて解説いたします。

この記事では、従来の REST API の課題から GraphQL の利点、そして Svelte との相性の良さまで、実際のコード例を交えながら詳しくご紹介していきます。最後まで読んでいただければ、あなたの Web アプリケーションも劇的に高速化できるでしょう。

GraphQL の背景と Svelte との相性

なぜ GraphQL が注目されているのか

GraphQL は、Facebook(現 Meta)が開発したデータクエリ言語およびランタイムとして、現代の Web 開発において注目を集めています。従来の REST API とは異なり、クライアントが必要なデータを正確に指定できる点が最大の特徴です。

GraphQL が注目される理由は以下の通りです。

#理由具体的なメリット
1単一エンドポイント複数の API エンドポイントを管理する必要がない
2必要なデータのみ取得オーバーフェッチやアンダーフェッチの解決
3強力な型システム開発時の型安全性と API ドキュメントの自動生成
4リアルタイム機能サブスクリプションによるリアルタイムデータ更新

GraphQL のデータフロー図を以下に示します。

mermaidflowchart LR
    client[クライアント] -->|GraphQLクエリ| endpoint[単一エンドポイント]
    endpoint -->|スキーマ解析| resolver[リゾルバー]
    resolver -->|データ取得| db1[(ユーザーDB)]
    resolver -->|データ取得| db2[(商品DB)]
    resolver -->|データ取得| api[外部API]
    resolver -->|結合データ| endpoint
    endpoint -->|JSON形式| client

この図からもわかるように、GraphQL は複数のデータソースから必要な情報だけを効率的に取得できます。

Svelte の特性と GraphQL のメリット

Svelte は「消えるフレームワーク」として知られ、コンパイル時に最適化されたバニラ JavaScript を生成します。この特性が GraphQL と組み合わさることで、驚くべき効果を発揮するのです。

Svelte の主な特徴を以下にまとめました。

#特徴GraphQL との相性
1コンパイル時最適化GraphQL のコード生成と相性が良い
2小さなバンドルサイズ軽量な GraphQL クライアントと組み合わせて高速化
3反応的更新GraphQL サブスクリプションとの親和性が高い
4簡潔な記法GraphQL クエリを直感的に扱える

Svelte と GraphQL の組み合わせによる処理フローを図で示します。

mermaidsequenceDiagram
    participant U as ユーザー
    participant S as Svelteコンポーネント
    participant G as GraphQLクライアント
    participant API as GraphQL API

    U->>S: ページアクセス
    S->>G: データ要求
    G->>API: GraphQLクエリ実行
    API->>G: 必要なデータのみ返却
    G->>S: データ受信
    S->>S: リアクティブ更新
    S->>U: 最適化されたUI表示

この組み合わせにより、従来のフレームワークでは実現できない高速なデータ連携が可能になります。

データ連携における課題

従来の REST API の限界

REST API は長い間 Web 開発のスタンダードとして使われてきましたが、現代の Web アプリケーションの要求に対して、いくつかの限界が明らかになっています。

オーバーフェッチ問題

REST API では、エンドポイントごとに固定されたデータ構造が返されるため、必要以上のデータを取得してしまう「オーバーフェッチ」が発生します。

以下の REST API の例をご覧ください。

javascript// ユーザー名だけが欲しいのに、全てのユーザー情報を取得
const response = await fetch('/api/users/123');
const userData = await response.json();
// 実際に使用するのは userData.name のみ
console.log(userData.name);

この API レスポンスは以下のような大量の不要なデータを含みます。

json{
  "id": 123,
  "name": "田中太郎",
  "email": "tanaka@example.com",
  "phone": "090-1234-5678",
  "address": {
    "prefecture": "東京都",
    "city": "渋谷区",
    "street": "道玄坂1-2-3"
  },
  "profileImage": "base64文字列...",
  "preferences": {
    "notifications": true,
    "theme": "dark"
  }
}

アンダーフェッチ問題

逆に、必要なデータが一回のリクエストで取得できない「アンダーフェッチ」も問題となります。

javascript// ユーザー情報を取得
const user = await fetch('/api/users/123').then((r) =>
  r.json()
);

// ユーザーの投稿を別途取得(追加のHTTPリクエスト)
const posts = await fetch(
  `/api/users/${user.id}/posts`
).then((r) => r.json());

// 各投稿のコメントをさらに取得(N+1問題)
const postsWithComments = await Promise.all(
  posts.map(async (post) => {
    const comments = await fetch(
      `/api/posts/${post.id}/comments`
    ).then((r) => r.json());
    return { ...post, comments };
  })
);

エンドポイント管理の複雑化

プロジェクトが成長するにつれ、API エンドポイントの数が増え、管理が困難になります。

javascript// 複数のエンドポイントを管理する必要がある
const API_ENDPOINTS = {
  USERS: '/api/v1/users',
  POSTS: '/api/v1/posts',
  COMMENTS: '/api/v2/comments', // バージョンが異なる場合も
  CATEGORIES: '/api/v1/categories',
  TAGS: '/api/v1/tags',
};

Svelte アプリケーションでのデータフェッチの問題点

Svelte アプリケーションにおいても、従来のデータフェッチ手法では以下のような問題が発生します。

複雑な状態管理

複数の API からデータを取得する際、ローディング状態やエラー状態の管理が複雑になります。

javascript// 複数のAPIからデータを取得するSvelteコンポーネント
<script>
  import { onMount } from 'svelte';

  let user = null;
  let posts = [];
  let loading = {
    user: true,
    posts: true
  };
  let errors = {
    user: null,
    posts: null
  };

  onMount(async () => {
    // ユーザー情報の取得
    try {
      const userResponse = await fetch('/api/users/123');
      user = await userResponse.json();
      loading.user = false;
    } catch (error) {
      errors.user = error.message;
      loading.user = false;
    }

    // 投稿情報の取得
    try {
      const postsResponse = await fetch('/api/posts?userId=123');
      posts = await postsResponse.json();
      loading.posts = false;
    } catch (error) {
      errors.posts = error.message;
      loading.posts = false;
    }
  });
</script>

このような実装は、各 API ごとにローディングとエラーの状態管理が必要となり、コードが冗長になってしまいます。

データの一貫性の問題

異なるタイミングで複数の API からデータを取得する場合、データの一貫性を保つのが困難です。

javascript// データの一貫性を保つためのコード例
let userVersion = 0;
let postsVersion = 0;

const fetchUserData = async () => {
  userVersion++;
  const currentVersion = userVersion;

  const response = await fetch('/api/users/123');
  const userData = await response.json();

  // 最新のリクエストでない場合は無視
  if (currentVersion === userVersion) {
    user = userData;
  }
};

ネットワーク効率の悪化

REST API の制約により、ウォーターフォール的な API コールが発生し、ページの読み込み時間が長くなります。

REST API における典型的な問題フローを図で示します。

mermaidsequenceDiagram
    participant U as ユーザー
    participant C as Svelteコンポーネント
    participant API as REST API

    U->>C: ページアクセス
    C->>API: GET /api/users/123
    API->>C: 大量のユーザーデータ(不要な情報含む)
    C->>API: GET /api/users/123/posts
    API->>C: 投稿一覧
    loop 各投稿に対して
        C->>API: GET /api/posts/{id}/comments
        API->>C: コメント一覧
    end
    C->>U: データ表示(複数回のローディング)

    Note over C,API: N+1問題とウォーターフォール読み込み

これらの問題を解決するために、次章では GraphQL と Svelte の組み合わせによる解決策について詳しく解説いたします。

Svelte × GraphQL の解決策

GraphQL クエリによる効率的なデータ取得

GraphQL は前章で説明した REST API の問題を根本的に解決します。必要なデータを 1 回のリクエストで効率的に取得できるのが最大のメリットです。

必要なデータのみを取得

GraphQL では、クライアントが必要なフィールドだけを指定してデータを取得できます。

以下の GraphQL クエリの例をご覧ください。

graphql# ユーザー名とその投稿のタイトルだけが必要な場合
query GetUserWithPosts($userId: ID!) {
  user(id: $userId) {
    name
    posts {
      title
      createdAt
    }
  }
}

このクエリは、上記で説明した REST API の複数リクエストを 1 つに統合し、必要なデータのみを返却します。

レスポンス例:

json{
  "data": {
    "user": {
      "name": "田中太郎",
      "posts": [
        {
          "title": "GraphQLの魅力について",
          "createdAt": "2023-11-01T10:00:00Z"
        },
        {
          "title": "Svelteで始めるモダン開発",
          "createdAt": "2023-11-02T15:30:00Z"
        }
      ]
    }
  }
}

複雑な関連データの一括取得

GraphQL では、関連するデータを一度のクエリで効率的に取得できます。

graphql# 投稿とそのコメント、さらにコメント投稿者の情報まで一度に取得
query GetPostWithCommentsAndAuthors($postId: ID!) {
  post(id: $postId) {
    title
    content
    author {
      name
    }
    comments {
      content
      createdAt
      author {
        name
        avatar
      }
      replies {
        content
        author {
          name
        }
      }
    }
  }
}

従来の REST API では複数回のリクエストが必要だった処理を、GraphQL なら 1 回で完了できます。

型安全性の確保

GraphQL スキーマにより、データの型が保証され、開発時に型チェックが可能になります。

graphql# GraphQLスキーマ定義例
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
  createdAt: DateTime!
}

type Comment {
  id: ID!
  content: String!
  author: User!
  post: Post!
  createdAt: DateTime!
}

Svelte の反応性との組み合わせ

Svelte のリアクティブシステムと GraphQL のデータフェッチ機能を組み合わせることで、非常に効率的な Web アプリケーションを構築できます。

自動的な UI 更新

Svelte のリアクティブ変数と GraphQL のサブスクリプション機能を組み合わせると、データの変更が自動的に UI に反映されます。

javascript<script>
  import { onMount } from 'svelte';
  import { graphqlClient } from './graphql-client.js';

  // リアクティブ変数として定義
  let posts = [];
  let loading = true;

  onMount(async () => {
    // GraphQLクエリの実行
    const query = `
      query GetPosts {
        posts {
          id
          title
          content
          author {
            name
          }
        }
      }
    `;

    try {
      const result = await graphqlClient.request(query);
      posts = result.posts; // Svelteが自動的にUIを更新
      loading = false;
    } catch (error) {
      console.error('データの取得に失敗しました:', error);
      loading = false;
    }
  });
</script>

HTML テンプレート部分:

html{#if loading}
<p>読み込み中...</p>
{:else}
<div class="posts-container">
  {#each posts as post (post.id)}
  <article class="post">
    <h2>{post.title}</h2>
    <p>投稿者: {post.author.name}</p>
    <p>{post.content}</p>
  </article>
  {/each}
</div>
{/if}

リアルタイムデータ更新

GraphQL サブスクリプションと Svelte の組み合わせにより、リアルタイムでデータを更新できます。

javascript<script>
  import { onMount, onDestroy } from 'svelte';
  import { graphqlClient } from './graphql-client.js';

  let messages = [];
  let subscription;

  onMount(() => {
    // 初期データの取得
    loadInitialMessages();

    // リアルタイムサブスクリプション
    subscribeToNewMessages();
  });

  const loadInitialMessages = async () => {
    const query = `
      query GetMessages {
        messages {
          id
          content
          author {
            name
          }
          createdAt
        }
      }
    `;

    const result = await graphqlClient.request(query);
    messages = result.messages;
  };

  const subscribeToNewMessages = () => {
    const subscriptionQuery = `
      subscription OnMessageAdded {
        messageAdded {
          id
          content
          author {
            name
          }
          createdAt
        }
      }
    `;

    subscription = graphqlClient.subscribe({
      query: subscriptionQuery,
      next: (data) => {
        // 新しいメッセージをリアクティブ配列に追加
        messages = [...messages, data.messageAdded];
      }
    });
  };

  onDestroy(() => {
    // コンポーネント破棄時にサブスクリプションを解除
    if (subscription) {
      subscription.unsubscribe();
    }
  });
</script>

効率的なデータフローを図で示します。

mermaidflowchart TD
    A[Svelteコンポーネント] -->|GraphQLクエリ| B[GraphQLクライアント]
    B -->|HTTP/WebSocket| C[GraphQLサーバー]
    C -->|データベース操作| D[(データベース)]

    C -->|リアルタイム更新| B
    B -->|リアクティブ更新| A
    A -->|自動UI再描画| E[ユーザーインターフェース]

    F[他のクライアント] -->|データ変更| C
    C -->|サブスクリプション通知| B

    style A fill:#ff6b6b
    style C fill:#4ecdc4
    style E fill:#45b7d1

このフローにより、他のユーザーがデータを変更した際も、即座に UI 不適合が反映される仕組みを実現できます。

次章では、実際に Svelte と GraphQL を組み合わせたアプリケーションの実装方法について、具体的なコード例とともに詳しく解説いたします。

具体例:Svelte GraphQL アプリの実装

ここからは、実際に Svelte と GraphQL を組み合わせたアプリケーションを構築する方法について、具体的なコード例とともに解説いたします。今回は、シンプルなブログアプリケーションを例に進めてまいります。

プロジェクト環境構築

まずは、Svelte と GraphQL を組み合わせた開発環境を構築いたします。

新しい Svelte プロジェクトの作成

Svelte プロジェクトを作成し、必要な依存関係をインストールします。

bash# Svelteプロジェクトを作成
yarn create svelte@latest my-svelte-graphql-app

# プロジェクトディレクトリに移動
cd my-svelte-graphql-app

作成時のオプション選択:

#オプション推奨設定理由
1TypeScriptYesGraphQL との型安全性を向上
2ESLintYesコード品質の維持
3PrettierYesコードフォーマットの統一

GraphQL 関連パッケージのインストール

GraphQL クライアントとしてgraphql-requestと、開発支援ツールとして@graphql-codegenを使用します。

bash# GraphQLクライアントのインストール
yarn add graphql graphql-request

# 開発用依存関係のインストール
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-graphql-request

基本的なプロジェクト構成

プロジェクトの構成を以下のように整理します。

perlmy-svelte-graphql-app/
├── src/
│   ├── lib/
│   │   ├── graphql/
│   │   │   ├── client.ts          # GraphQLクライアント設定
│   │   │   ├── queries/           # GraphQLクエリファイル
│   │   │   └── generated/         # 自動生成された型定義
│   │   ├── components/            # Svelteコンポーネント
│   │   └── stores/                # Svelteストア
│   ├── routes/                    # SvelteKitのルーティング
│   └── app.html
├── codegen.yml                    # GraphQL Code Generatorの設定
└── package.json

GraphQL クライアント設定

GraphQL クライアントを設定し、型安全なデータフェッチを実現します。

GraphQL クライアントの基本設定

src​/​lib​/​graphql​/​client.tsファイルを作成し、GraphQL クライアントを設定します。

typescript// src/lib/graphql/client.ts
import { GraphQLClient } from 'graphql-request';

// GraphQLエンドポイントURL(環境変数から取得)
const GRAPHQL_ENDPOINT =
  import.meta.env.VITE_GRAPHQL_ENDPOINT ||
  'http://localhost:4000/graphql';

// GraphQLクライアントのインスタンス作成
export const graphqlClient = new GraphQLClient(
  GRAPHQL_ENDPOINT,
  {
    headers: {
      'Content-Type': 'application/json',
    },
  }
);

// 認証トークンを設定する関数
export const setAuthToken = (token: string) => {
  graphqlClient.setHeader(
    'Authorization',
    `Bearer ${token}`
  );
};

// 認証トークンを削除する関数
export const clearAuthToken = () => {
  graphqlClient.setHeader('Authorization', '');
};

環境変数の設定

.envファイルを作成し、GraphQL エンドポイントを定義します。

bash# .env
VITE_GRAPHQL_ENDPOINT=http://localhost:4000/graphql

GraphQL Code Generator の設定

codegen.ymlファイルを作成し、TypeScript 型の自動生成を設定します。

yaml# codegen.yml
overwrite: true
schema: 'http://localhost:4000/graphql'
documents: 'src/lib/graphql/queries/**/*.graphql'
generates:
  src/lib/graphql/generated/graphql.ts:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-graphql-request'
    config:
      rawRequest: false
      inlineFragmentTypes: combine
      skipTypename: false
      exportFragmentSpreadSubTypes: true
      dedupeFragments: true
      preResolveTypes: false

Svelte コンポーネントでのクエリ実装

実際の Svelte コンポーネントで GraphQL クエリを使用する方法について解説します。

GraphQL クエリの定義

まず、使用する GraphQL クエリを定義します。

graphql# src/lib/graphql/queries/blog.graphql
query GetPosts {
  posts {
    id
    title
    content
    createdAt
    author {
      id
      name
      email
    }
  }
}

query GetPost($id: ID!) {
  post(id: $id) {
    id
    title
    content
    createdAt
    author {
      id
      name
      email
    }
    comments {
      id
      content
      createdAt
      author {
        id
        name
      }
    }
  }
}

型定義の自動生成

GraphQL Code Generator を実行して、TypeScript 型定義を生成します。

bash# 型定義の生成
yarn graphql-codegen

# package.jsonにスクリプトを追加することも可能
# "scripts": {
#   "codegen": "graphql-codegen"
# }

生成される型定義の例:

typescript// src/lib/graphql/generated/graphql.ts(自動生成)
export type Post = {
  __typename?: 'Post';
  id: Scalars['ID'];
  title: Scalars['String'];
  content: Scalars['String'];
  createdAt: Scalars['DateTime'];
  author: User;
  comments: Array<Comment>;
};

export type GetPostsQuery = {
  __typename?: 'Query';
  posts: Array<Post>;
};

export type GetPostQuery = {
  __typename?: 'Query';
  post?: Maybe<Post>;
};

投稿一覧コンポーネントの実装

型安全な GraphQL クエリを使用した Svelte コンポーネントを実装します。

typescript<!-- src/lib/components/PostList.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { graphqlClient } from '$lib/graphql/client';
  import type { GetPostsQuery } from '$lib/graphql/generated/graphql';
  import { GET_POSTS } from '$lib/graphql/queries/blog.graphql';

  // 型安全なデータ管理
  let posts: GetPostsQuery['posts'] = [];
  let loading = true;
  let error: string | null = null;

  // データフェッチ関数
  const fetchPosts = async () => {
    try {
      loading = true;
      error = null;

      const result = await graphqlClient.request<GetPostsQuery>(GET_POSTS);
      posts = result.posts;
    } catch (err) {
      error = err instanceof Error ? err.message : '投稿の取得に失敗しました';
      console.error('投稿取得エラー:', err);
    } finally {
      loading = false;
    }
  };

  // コンポーネントマウント時にデータを取得
  onMount(fetchPosts);

  // 日付フォーマット関数
  const formatDate = (dateString: string): string => {
    return new Date(dateString).toLocaleDateString('ja-JP', {
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    });
  };
</script>

HTML テンプレート部分:

html<div class="post-list">
  <h1>ブログ投稿一覧</h1>

  {#if loading}
  <div class="loading">
    <p>投稿を読み込み中...</p>
  </div>
  {:else if error}
  <div class="error">
    <p>エラーが発生しました: {error}</p>
    <button on:click="{fetchPosts}">再読み込み</button>
  </div>
  {:else if posts.length === 0}
  <div class="empty">
    <p>投稿がありません。</p>
  </div>
  {:else}
  <div class="posts">
    {#each posts as post (post.id)}
    <article class="post-card">
      <header>
        <h2>
          <a href="/posts/{post.id}" class="post-title">
            {post.title}
          </a>
        </h2>
        <div class="post-meta">
          <span class="author"
            >投稿者: {post.author.name}</span
          >
          <span class="date"
            >{formatDate(post.createdAt)}</span
          >
        </div>
      </header>
      <div class="post-content">
        <p>{post.content.substring(0, 200)}...</p>
      </div>
    </article>
    {/each}
  </div>
  {/if}
</div>

スタイル定義:

css<style>
  .post-list {
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem;
  }

  .loading, .error, .empty {
    text-align: center;
    padding: 2rem;
    background-color: #f8f9fa;
    border-radius: 0.5rem;
    margin: 1rem 0;
  }

  .error {
    background-color: #f8d7da;
    color: #721c24;
  }

  .post-card {
    border: 1px solid #e1e8ed;
    border-radius: 0.75rem;
    padding: 1.5rem;
    margin-bottom: 1.5rem;
    background-color: white;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    transition: box-shadow 0.2s ease;
  }

  .post-card:hover {
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
  }

  .post-title {
    color: #1da1f2;
    text-decoration: none;
    font-size: 1.25rem;
    font-weight: bold;
  }

  .post-title:hover {
    text-decoration: underline;
  }

  .post-meta {
    display: flex;
    gap: 1rem;
    margin-top: 0.5rem;
    font-size: 0.875rem;
    color: #657786;
  }

  .post-content {
    margin-top: 1rem;
    line-height: 1.6;
  }
</style>

ミューテーションとサブスクリプション

GraphQL の強力な機能であるミューテーション(データの変更)とサブスクリプション(リアルタイム更新)の実装について解説します。

投稿作成のミューテーション実装

新しい投稿を作成するミューテーションと Svelte コンポーネントを実装します。

GraphQL ミューテーションの定義:

graphql# src/lib/graphql/queries/blog.graphql に追加
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
    content
    createdAt
    author {
      id
      name
    }
  }
}

投稿作成フォームコンポーネント:

typescript<!-- src/lib/components/CreatePostForm.svelte -->
<script lang="ts">
  import { createEventDispatcher } from 'svelte';
  import { graphqlClient } from '$lib/graphql/client';
  import type { CreatePostMutation, CreatePostMutationVariables } from '$lib/graphql/generated/graphql';
  import { CREATE_POST } from '$lib/graphql/queries/blog.graphql';

  // イベントディスパッチャーの作成
  const dispatch = createEventDispatcher<{
    postCreated: CreatePostMutation['createPost'];
  }>();

  // フォームデータ
  let formData = {
    title: '',
    content: ''
  };

  let isSubmitting = false;
  let error: string | null = null;

  // フォーム送信処理
  const handleSubmit = async () => {
    // バリデーション
    if (!formData.title.trim() || !formData.content.trim()) {
      error = 'タイトルと内容を入力してください。';
      return;
    }

    try {
      isSubmitting = true;
      error = null;

      const variables: CreatePostMutationVariables = {
        input: {
          title: formData.title.trim(),
          content: formData.content.trim()
        }
      };

      const result = await graphqlClient.request<CreatePostMutation>(CREATE_POST, variables);

      // 成功時の処理
      dispatch('postCreated', result.createPost);

      // フォームをリセット
      formData = {
        title: '',
        content: ''
      };
    } catch (err) {
      error = err instanceof Error ? err.message : '投稿の作成に失敗しました';
      console.error('投稿作成エラー:', err);
    } finally {
      isSubmitting = false;
    }
  };
</script>

フォームの HTML テンプレート:

html<form
  on:submit|preventDefault="{handleSubmit}"
  class="create-post-form"
>
  <h2>新しい投稿を作成</h2>

  {#if error}
  <div class="error-message">{error}</div>
  {/if}

  <div class="form-group">
    <label for="title">タイトル</label>
    <input
      id="title"
      type="text"
      bind:value="{formData.title}"
      placeholder="投稿のタイトルを入力"
      disabled="{isSubmitting}"
      required
    />
  </div>

  <div class="form-group">
    <label for="content">内容</label>
    <textarea
      id="content"
      bind:value="{formData.content}"
      placeholder="投稿の内容を入力"
      disabled="{isSubmitting}"
      rows="6"
      required
    ></textarea>
  </div>

  <button
    type="submit"
    disabled="{isSubmitting}"
    class="submit-button"
  >
    {isSubmitting ? '作成中...' : '投稿を作成'}
  </button>
</form>

リアルタイムコメント機能の実装

GraphQL サブスクリプションを使用したリアルタイムコメント機能を実装します。

サブスクリプションの定義:

graphql# src/lib/graphql/queries/blog.graphql に追加
subscription OnCommentAdded($postId: ID!) {
  commentAdded(postId: $postId) {
    id
    content
    createdAt
    author {
      id
      name
    }
  }
}

mutation AddComment($input: AddCommentInput!) {
  addComment(input: $input) {
    id
    content
    createdAt
    author {
      id
      name
    }
  }
}

リアルタイムコメントコンポーネント:

typescript<!-- src/lib/components/Comments.svelte -->
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { graphqlClient } from '$lib/graphql/client';
  import type {
    Comment,
    OnCommentAddedSubscription,
    AddCommentMutation,
    AddCommentMutationVariables
  } from '$lib/graphql/generated/graphql';

  export let postId: string;

  let comments: Comment[] = [];
  let newComment = '';
  let isSubmitting = false;
  let subscription: any = null;

  onMount(() => {
    // 既存コメントの取得
    loadComments();

    // リアルタイムサブスクリプションの開始
    subscribeToComments();
  });

  onDestroy(() => {
    // サブスクリプションの解除
    if (subscription) {
      subscription.unsubscribe();
    }
  });

  const loadComments = async () => {
    try {
      const query = `
        query GetComments($postId: ID!) {
          post(id: $postId) {
            comments {
              id
              content
              createdAt
              author {
                id
                name
              }
            }
          }
        }
      `;

      const result = await graphqlClient.request(query, { postId });
      comments = result.post?.comments || [];
    } catch (error) {
      console.error('コメント取得エラー:', error);
    }
  };

  const subscribeToComments = () => {
    const subscriptionQuery = `
      subscription OnCommentAdded($postId: ID!) {
        commentAdded(postId: $postId) {
          id
          content
          createdAt
          author {
            id
            name
          }
        }
      }
    `;

    subscription = graphqlClient.subscribe({
      query: subscriptionQuery,
      variables: { postId },
      next: (data: OnCommentAddedSubscription) => {
        // 新しいコメントをリアクティブに追加
        comments = [...comments, data.commentAdded];
      },
      error: (error: any) => {
        console.error('サブスクリプションエラー:', error);
      }
    });
  };

  const addComment = async () => {
    if (!newComment.trim()) return;

    try {
      isSubmitting = true;

      const variables: AddCommentMutationVariables = {
        input: {
          postId,
          content: newComment.trim()
        }
      };

      await graphqlClient.request<AddCommentMutation>(`
        mutation AddComment($input: AddCommentInput!) {
          addComment(input: $input) {
            id
            content
            createdAt
            author {
              id
              name
            }
          }
        }
      `, variables);

      // フォームをリセット(リアルタイム更新でコメントは自動追加される)
      newComment = '';
    } catch (error) {
      console.error('コメント投稿エラー:', error);
    } finally {
      isSubmitting = false;
    }
  };
</script>

コメント表示とフォーム:

html<div class="comments-section">
  <h3>コメント ({comments.length})</h3>

  <!-- コメント投稿フォーム -->
  <div class="comment-form">
    <textarea
      bind:value="{newComment}"
      placeholder="コメントを入力..."
      disabled="{isSubmitting}"
      rows="3"
    ></textarea>
    <button
      on:click="{addComment}"
      disabled="{isSubmitting"
      ||
      !newComment.trim()}
    >
      {isSubmitting ? '投稿中...' : 'コメント'}
    </button>
  </div>

  <!-- コメント一覧 -->
  <div class="comments-list">
    {#each comments as comment (comment.id)}
    <div class="comment">
      <div class="comment-header">
        <strong>{comment.author.name}</strong>
        <span class="comment-date">
          {new
          Date(comment.createdAt).toLocaleDateString('ja-JP')}
        </span>
      </div>
      <p class="comment-content">{comment.content}</p>
    </div>
    {/each} {#if comments.length === 0}
    <p class="no-comments">まだコメントはありません。</p>
    {/if}
  </div>
</div>

GraphQL を活用した Svelte アプリケーションの構成図を以下に示します。

mermaidflowchart TB
    subgraph "Svelteアプリケーション"
        A[PostList.svelte] --> B[CreatePostForm.svelte]
        A --> C[Comments.svelte]
        B --> D[GraphQLクライアント]
        C --> D
        A --> D
    end

    subgraph "GraphQL機能"
        D --> E[Query: データ取得]
        D --> F[Mutation: データ変更]
        D --> G[Subscription: リアルタイム更新]
    end

    subgraph "バックエンド"
        E --> H[GraphQLサーバー]
        F --> H
        G --> H
        H --> I[(データベース)]
    end

    style A fill:#ff6b6b
    style H fill:#4ecdc4
    style D fill:#45b7d1

これまでの実装により、Svelte と GraphQL を組み合わせた効率的な Web アプリケーションが完成いたします。型安全性、リアルタイム機能、そして優れたユーザーエクスペリエンスを兼ね備えた、最速のデータ連携を実現できるでしょう。

まとめ

本記事では、Svelte と GraphQL を組み合わせた最速データ連携について、基礎から実装まで詳しく解説いたしました。これらの技術を組み合わせることで、従来の REST API では解決困難だった課題を効率的に解決できます。

主要なメリットの再確認

Svelte と GraphQL の組み合わせがもたらす主要なメリットをまとめます。

#メリット具体的な効果
1データフェッチの効率化必要なデータのみを一回のリクエストで取得
2型安全性の向上GraphQL スキーマによる自動型生成とコンパイル時チェック
3リアルタイム機能サブスクリプションによる即座のデータ同期
4開発効率の向上コード生成と Svelte の簡潔な記法による生産性向上
5パフォーマンス最適化Svelte のコンパイル時最適化と GraphQL の効率的データ取得

技術選択時の考慮点

実際のプロジェクトでこれらの技術を採用する際の考慮点をご紹介いたします。

採用を推奨するケース:

  • 複雑なデータ関係を持つアプリケーション
  • リアルタイム機能が重要な要件となる場合
  • 型安全性を重視する開発チーム
  • 高いパフォーマンスが求められる Web アプリケーション

注意が必要なケース:

  • シンプルな CRUD 操作のみのアプリケーション
  • GraphQL の学習コストを考慮する必要がある場合
  • 既存の REST API 資産を活用したい場合

今後の発展性

Svelte と GraphQL の組み合わせは、現在も活発に発展を続けています。今後注目すべき技術動向をご紹介いたします。

SvelteKit との統合強化:

  • サーバーサイドレンダリング(SSR)での最適化
  • スタティックサイトジェネレーション(SSG)対応
  • エッジコンピューティングへの対応

GraphQL エコシステムの進化:

  • フェデレーション機能による大規模システム対応
  • キャッシュ戦略の洗練化
  • セキュリティ機能の強化

実装を通じて体感いただけたように、Svelte と GraphQL の組み合わせは、従来の開発手法と比較して大幅な効率化と品質向上をもたらします。ぜひ皆さまのプロジェクトでも、この最速データ連携の手法を活用していただき、素晴らしいユーザーエクスペリエンスを提供する Web アプリケーションを構築してください。

関連リンク

公式ドキュメント

GraphQL クライアント

開発支援ツール

学習リソース

パフォーマンス最適化