SPA・非同期通信・UIデザインの基本を独学で身につける完全ガイド|Fetch APIからアニメーションまで
📋 この記事の目次
これまでの回で、見た目・データ保存・簡単な画面操作までが揃いました。今回は、自己紹介カードを中心にしたポートフォリオサイトを、ページ遷移なしでヌルヌル動く「SPA(Single Page Application)」 に磨き上げていきます。
「TwitterやGmailのような、画面が切り替わってもURLが変わって見えるのに、実際にはページの再読み込みが起きていないアプリ、あれはどういう仕組みなのか」
この疑問に答えながら、ゲストブックの表示を非同期通信で読み込み、メッセージが増えたら自動で追加読み込みする「無限スクロール」まで実装します。
SPAは「舞台のセットを裏で入れ替える演劇」だと考える
- 従来のWeb(Multi-Page Application) = 場面が変わるたびに、劇場ごと移動する
- SPA(Single Page Application) = 同じ舞台(1つのHTMLページ)の上で、幕間に裏方がセットだけ素早く入れ替える
観客(ユーザー)から見ると、場面(画面の内容)は変わっているのに、劇場自体(ページの土台)は移動していません。これにより、画面切り替えが速く、滑らかに感じられます。
従来のWeb vs SPAの利点・欠点
| 従来のWeb(MPA) | SPA | |
|---|---|---|
| 画面遷移 | ページ全体を再読み込み | 必要な部分だけ書き換え |
| 体感速度 | 遷移のたびに読み込み待ち | 一度読み込めば高速 |
| 実装の複雑さ | 比較的シンプル | 状態管理など考慮点が増える |
| 初期表示 | 比較的速い | JavaScriptの読み込み待ちが発生しやすい |
🧓 ベテランの現場コラム SPAは「なんでもSPAにすれば良い」というものではありません。単純な情報発信サイトであれば、従来型のページ遷移の方がシンプルで検索エンジンにも扱いやすい場合があります。「動きの滑らかさが本当に必要な体験か」を見極める視点は、技術を選ぶ側になったときに重要です。
Part A: SPAの構造とページ切り替え
SPAの構造とDOM操作によるページ切り替え
<nav>
<a href="#" data-section="home">ホーム</a>
<a href="#" data-section="skills">スキル</a>
<a href="#" data-section="guestbook">ゲストブック</a>
</nav>
<main id="app"></main>
const sections = {
home: "<h1>やまだこうじ</h1><p>未経験からエンジニアを目指し中</p>",
skills: "<h2>スキル一覧</h2>",
guestbook: "<h2>ゲストブック</h2>",
};
function showSection(sectionName) {
document.getElementById("app").innerHTML = sections[sectionName];
}
document.querySelectorAll("nav a").forEach((link) => {
link.addEventListener("click", (event) => {
event.preventDefault();
showSection(event.target.dataset.section);
});
});
ページを移動する代わりに、innerHTMLを書き換えることで「別のページに見える」表現をしています。
Part B: 非同期データ読み込みとHistory API
Fetch API
async function loadGuestbookMessages() {
const response = await fetch("/api/messages/");
const messages = await response.json();
return messages;
}
fetchは、ページを再読み込みせずにサーバーと通信するための仕組みです。awaitをつけることで、「通信が終わるのを待ってから次の処理に進む」という流れを、上から下に読める形で書けます。
async function renderGuestbook() {
const messages = await loadGuestbookMessages();
const list = messages.map((m) => `<li>${m.visitor_name}: ${m.body}</li>`).join("");
document.getElementById("app").innerHTML = `<h2>ゲストブック</h2><ul>${list}</ul>`;
}
History API
function navigateTo(sectionName) {
showSection(sectionName);
history.pushState({ sectionName }, "", `#${sectionName}`);
}
window.addEventListener("popstate", (event) => {
if (event.state) {
showSection(event.state.sectionName);
}
});
history.pushStateは、ページを再読み込みせずにブラウザのアドレスバーのURLだけを書き換える仕組みです。これにより、SPAでも「戻る」ボタンが自然に機能するようになります。
Part C: windowオブジェクトとスクロール検出
windowオブジェクトとonscroll
window.addEventListener("scroll", () => {
const scrolledToBottom =
window.innerHeight + window.scrollY >= document.body.offsetHeight - 100;
if (scrolledToBottom) {
loadMoreMessages();
}
});
windowはブラウザのウィンドウ自体を表すオブジェクトで、画面サイズやスクロール位置など、ページ全体に関わる情報を持っています。
無限スクロールの実装
let currentPage = 1;
async function loadMoreMessages() {
const response = await fetch(`/api/messages/?page=${currentPage}`);
const newMessages = await response.json();
if (newMessages.length === 0) return; // これ以上データがない
appendMessagesToList(newMessages);
currentPage += 1;
}
ゲストブックのメッセージが増えてきたとき、最初から全件を読み込むのではなく、画面の下端に近づくたびに少しずつ追加で読み込む、という仕組みです。
🧓 ベテランの現場コラム 無限スクロールは体験としては心地よい一方、「同じデータを二重に読み込んでしまう」「読み込み中に連続でイベントが発火してしまう」といった不具合が起きやすい機能でもあります。実務では、読み込み中かどうかを示すフラグ変数を用意し、「読み込み中は追加のリクエストを送らない」制御を入れるのが定石です。
Part D: CSSアニメーション
keyframesとアニメーションプロパティ
@keyframes slideIn {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
.message-item {
animation: slideIn 0.3s ease-out;
}
JavaScriptでの制御
function appendMessagesToList(messages) {
messages.forEach((m) => {
const li = document.createElement("li");
li.textContent = `${m.visitor_name}: ${m.body}`;
li.classList.add("message-item"); // ここでアニメーションが発火する
document.getElementById("message-list").appendChild(li);
});
}
CSSだけで完結するアニメーションに、JavaScriptから「いつクラスを付けるか」というタイミングの制御を組み合わせることで、「新しく追加された要素だけがふわっと現れる」ような表現ができます。
Part E: UIデザインのベストプラクティス
- レスポンシブデザイン: 画面幅を問わず使えることは、SPAでも当然の前提
- ユーザーへのフィードバック: ボタンを押したら何か反応がある(読み込み中はローディング表示を出す等)。反応がないと、ユーザーは「壊れている」と誤解する
- 一貫性: ボタンの色・配置・文言のルールをサイト内で統一する
- アクセシビリティ: SPAは特に、キーボード操作だけで全機能に到達できるかを見落としがち
- シンプルさ: 機能を足しすぎず、本当に必要な操作だけを画面に出す
- パフォーマンス最適化: 画像の遅延読み込み、不要なアニメーションの削減など
実践プロジェクト:ポートフォリオをSPA化する
- ナビゲーションのクリックで、ページ遷移せずに表示内容を切り替える
history.pushStateでURLも連動させ、ブラウザの「戻る」に対応する- ゲストブックのメッセージを
fetchで非同期に取得して表示する - スクロールが下端に近づいたら追加のメッセージを読み込む無限スクロールを実装する
- 新しく追加されたメッセージにだけ、フェードイン系のアニメーションをつける
🧠 理解度チェック(一部抜粋)
- Q. SPAが「ページ遷移していないのに見た目が変わる」仕組みは? → JavaScriptでDOMの内容を書き換えているため
- Q.
fetchにawaitをつける理由は? → 通信が完了するのを待ってから、次の処理(結果の利用)に進むため - Q.
history.pushStateは何をしているか? → ページを再読み込みせずに、ブラウザのURL表示と履歴だけを更新する
挫折ポイントTOP3と対処法
1. 「非同期処理の結果を使おうとしたらundefinedだった」問題
awaitのつけ忘れ、またはasync関数の外でawaitを使おうとしているケースが典型的です。「通信が終わる前に次の行が実行されてしまっている」と考えて処理の順番を見直します。
2. 「戻るボタンを押しても表示が変わらない」問題
pushStateだけしてpopstateイベントの処理を書き忘れているケースです。URLの変更と、実際の画面更新処理は別々に実装する必要があります。
3. 「無限スクロールで同じデータが重複して表示される」問題
読み込み中に複数回リクエストが発火してしまうことが原因です。「今読み込み中かどうか」を示すフラグで制御します。
🧓 ベテランの現場コラム SPAの不具合は、画面をぱっと見ただけでは気づきにくいものが多く、「ブラウザの戻るボタンだけ挙動がおかしい」「連打すると壊れる」といった形で後から発覚しがちです。実装した機能は、正常な操作だけでなく「ちょっと意地悪な操作(連打・戻る・進むの繰り返し)」でも試しておくと安心です。
この先、何を学べばいいか
動きのあるSPAができたら、次は 「テストとCI/CD」 で、ここまで作った機能が壊れていないかを自動でチェックする仕組みを整えます。機能が増えるほど、手作業での確認だけでは限界が来ます。
まとめ
- SPAは「舞台のセットを裏で入れ替える」ように、1つのページの中身をJavaScriptで書き換える仕組み
fetch+awaitで非同期通信を、history.pushStateでURLとの整合性を取る- 無限スクロールは便利だが、二重読み込みなどの事故が起きやすいため制御フラグが必須
- アニメーションは「新しく増えた部分」など、意味のある場面に絞って使う
- UIデザインは「反応がある」「一貫している」「シンプル」の3点を軸に見直す
参考にした学習リソースについて
本記事は、ハーバード大学が提供する人気講座「CS50’s Web Programming with Python and JavaScript」の内容を参考にしつつ、独自の題材・独自の構成・自作のサンプルで書き下ろしたオリジナル解説記事です。CS50そのものに興味を持った方は、以下の公式サイトも覗いてみてください。
- CS50 公式サイト: https://cs50.harvard.edu/x/
🎓 理解度テスト
学んだ内容を確認しましょう。全6問。