SvelteKitでモダンWebアプリを構築する:セットアップから本番デプロイまで

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

SvelteKitが私のメインフレームワークになった理由

数年前、クライアントの中規模Reactアプリを保守していた。バンドルサイズは少しずつ膨れ上がり、ハイドレーションエラーが不定期に発生し、新しく参加した開発者は状態管理の仕組みを理解するだけで一週間かかっていた。同僚に次のプロジェクトでSvelteKitを試してみるよう勧められた。また別のJavaScriptフレームワークか、と正直懐疑的だった。しかし一週末試してみたら、もう後には戻れなくなった。

SvelteKitはビルド時にコンポーネントをバニラJavaScriptにコンパイルする。仮想DOMもなく、ランタイムのフレームワークオーバーヘッドもなく、メンタルモデルがすっきりとシンプルだ。拡張されたHTMLのように見える.svelteファイルを書けば、あとはコンパイラが処理してくれる。サーバーサイドレンダリング、静的生成、APIルートも、ファイルベースのルーティングシステムを使って一つのプロジェクトにまとまっており、直感的に理解できる。

三ヶ月後、新しいプロジェクトはリリースされた。エントリーバンドルのビルド出力は80KB未満に収まった。置き換えたReactアプリの340KBと比べると雲泥の差だ。新しいメンバーも一週間ではなく一日で生産的に動けるようになった。SvelteKitは初日から複雑なエコシステムを押し付けることなく、フルスタック開発をカバーしてくれる。

インストールとプロジェクトのセットアップ

Node.js 18以上が必要だ。まずバージョンを確認しよう:

node --version
npm --version

公式CLIを使って新しいSvelteKitプロジェクトを作成する:

npm create svelte@latest my-sveltekit-app
cd my-sveltekit-app
npm install

CLIからいくつか質問される。実際のプロジェクトでは通常こう選ぶ:

  • テンプレート: Skeleton project(クリーンな状態から始められる)
  • 型チェック: TypeScript(保守するプロジェクトならセットアップコストに見合う)
  • Prettier + ESLint: Yes — コードレビューでの無駄な議論を防げる

開発サーバーを起動する:

npm run dev

http://localhost:5173を開くとデフォルトページが表示される。ファイルを保存すると変更が即座に反映される — 開発サーバーはViteのホットモジュールリプレースメントを使っているため、編集間でアプリケーションの状態がリセットされない。

理解しておくべきプロジェクト構成

ファイルベースのルーティングはSvelteKitの中核だ。src/routesディレクトリがURLパスに直接対応する:

src/
├── routes/
│   ├── +page.svelte          ← / でレンダリング
│   ├── about/
│   │   └── +page.svelte      ← /about でレンダリング
│   ├── blog/
│   │   ├── +page.svelte      ← /blog でレンダリング
│   │   └── [slug]/
│   │       └── +page.svelte  ← /blog/任意のスラッグ でレンダリング
│   └── api/
│       └── posts/
│           └── +server.ts    ← /api/posts のAPIエンドポイント
├── lib/
│   └── components/           ← 再利用可能なコンポーネント
└── app.html                  ← ベースHTMLテンプレート

ファイル名の+プレフィックスは意図的なもので、同じディレクトリ内の自分のファイルとSvelteKit専用ファイルを区別するためだ。

設定:SSR、アダプター、環境変数

svelte.config.jsファイルはSvelteKit設定の核心だ。デフォルトではautoアダプターが使われており、ほとんどのNode.js環境で動作する:

import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter()
  }
};

export default config;

本番デプロイには特定のアダプターが必要になる。よく使われるものを挙げる:

  • @sveltejs/adapter-node — Node.jsサーバー(VPS、Docker)
  • @sveltejs/adapter-vercel — Vercel Edge/Serverless
  • @sveltejs/adapter-static完全な静的出力(Nginx、CDN)

VPSデプロイ向けにNodeアダプターへ切り替える:

npm install -D @sveltejs/adapter-node
import adapter from '@sveltejs/adapter-node';

const config = {
  kit: {
    adapter: adapter({
      out: 'build'  // 出力ディレクトリ
    })
  }
};

環境変数の扱い方

鋭い設計上の選択がある:SvelteKitはサーバーの秘密情報と公開変数を厳密に分離することを強制する。.envファイルでは:

# 公開 — ブラウザに公開される
PUBLIC_API_URL=https://api.example.com

# 非公開 — サーバーサイドのみ
DATABASE_URL=postgresql://user:pass@localhost/mydb
SECRET_KEY=your-secret-here

コード内でのアクセス方法:

// +page.server.ts または +server.ts 内(サーバーサイドのみ)
import { DATABASE_URL } from '$env/static/private';

// +page.svelte 内(クライアントセーフ)
import { PUBLIC_API_URL } from '$env/static/public';

クライアントサイドのコードにプライベートな変数を誤ってインポートするとビルドエラーになる。このガードレールのおかげで、APIキーの漏洩を何度も防いできた。

データ読み込みを使った最初のページを作る

SvelteKitの最もスマートなパターンの一つがload関数だ。ブログ一覧ページを作ってみよう:

// src/routes/blog/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ fetch }) => {
  const response = await fetch('/api/posts');
  const posts = await response.json();
  return { posts };
};
<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';
  export let data: PageData;
</script>

<h1>ブログ</h1>
{#each data.posts as post}
  <article>
    <h2><a href="/blog/{post.slug}">{post.title}</a></h2>
    <p>{post.excerpt}</p>
  </article>
{/each}

データはサーバーからコンポーネントへ、自動生成された完全なTypeScript型と共に流れる。ReduxもContext APIの配線も、三層のコンポーネントを貫くpropsのバケツリレーも不要だ。バックエンドAPIと組み合わせる場合は、このload関数からそのままfetchするだけで連携できる。

本番環境へのビルドとデプロイ

本番バンドルをビルドする:

npm run build

# 本番ビルドをローカルでプレビュー
npm run preview

Nodeアダプターを使うと、build/ディレクトリにスタンドアロンのNode.jsサーバーが生成される。以下のコマンドでデプロイできる:

# サーバー上で実行
node build/index.js

またはシンプルなDockerfileを作成する:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY build ./build
EXPOSE 3000
CMD ["node", "build/index.js"]
docker build -t my-sveltekit-app .
docker run -p 3000:3000 -e PORT=3000 my-sveltekit-app

環境変数PORTでサーバーがバインドするポートを制御できる。Nginxの背後に置く場合は、リバースプロキシブロックを追加する:

server {
  listen 80;
  server_name yourdomain.com;

  location / {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
  }
}

動作確認とモニタリング

デプロイ後はSSRが正常に動いているか確認しよう。curlでページをリクエストして、HTMLコンテンツが事前レンダリングされた状態で返ってくるか確認する:

curl -s https://yourdomain.com/blog | grep -o '<h1>.*</h1>'

生のHTMLレスポンスに見出しが含まれていれば(空の<div>コンテナではなく)、SSRは正常に動作している。本文が空の場合はクライアントサイドのみでレンダリングされている。load関数とアダプターの設定を確認しよう。

パフォーマンスのベースライン計測

コマンドラインから手軽にLighthouseチェックを実行する:

npx lighthouse https://yourdomain.com --output json --quiet | \
  node -e "const d=require('fs').readFileSync('/dev/stdin','utf8'); \
           const r=JSON.parse(d); \
           console.log('パフォーマンス:', r.categories.performance.score * 100);"

パフォーマンスのスコアは標準構成で90〜98が普通だ。ダウンロードするランタイムフレームワークはなく、コンパイルされたコンポーネントコードと自分でインポートしたものだけだからだ。

ログとエラートラッキング

エラーフックはsrc/hooks.server.tsに書く — 組み込み機能なので追加パッケージは不要だ:

import type { HandleServerError } from '@sveltejs/kit';

export const handleError: HandleServerError = ({ error, event }) => {
  console.error('サーバーエラー:', error, 'パス:', event.url.pathname);
  // Sentry、Datadogなどに送信
  return {
    message: '問題が発生しました。担当者に通知済みです。'
  };
};

本番環境のモニタリングにはPM2をビルド出力に向ける:

npm install -g pm2
pm2 start build/index.js --name sveltekit-app -i max
pm2 save
pm2 startup

-i maxフラグでCPUコアの数だけインスタンスを起動する — アプリケーションコードを一行も変えずに水平スケーリングが実現できる。

SvelteKitのプロジェクトを二年間リリースし続けてきて、繰り返し感じることは「戦わなくて済むことの少なさ」だ。小さなAPI、充実したドキュメント、プロジェクトが大きくなっても速さを保つVite搭載の開発サーバー。今のフレームワークがツールではなく重荷に感じ始めているなら、ぜひ一週末きちんと試してみてほしい。比較は自ずとついてくる。

Share: