Next.js × socket.io で学ぶ「WebSocket」/ SSE との違いは?
突然ですが、上記のようなチャット、、、リアルタイムで取り交わされていますが、どんな実装イメージがつきますか?「リアルタイム通信」と一言で言っても、実装にはいくつか選択肢があり、特に WebSocket と SSE(Server-Sent Events)が代表的です。勉強中に両者の違いをざっくり眺めていたら少し混乱したので、整理して記事にまとめました。今回は Next.js + Socket.io を使ったシンプルなチャットアプリを例に、WebSocket の基本的な使い方を解説します。※ Next.js・tailwindcss・shadcnの構築部分は省略します…ドキュメントご覧くださいNextじゃなくて全然OKです〜〜1. WebSocket とは?💡WebSocket は クライアントとサーバー間で双方向通信を常時確立する技術 です。特徴双方向通信:クライアントもサーバーも自由にメッセージを送れる常時接続:HTTP のようにリクエストごとに接続する必要がないリアルタイム性が高い:チャットや位置情報共有、ゲームなどに最適SSE との違い項目WebSocketSSE通信方向双方向サーバー → クライアントのみデータ形式バイナリ・テキスト両方テキストのみ接続方法常時接続HTTP ベースで常時接続(受信専用)適した用途チャット、ゲーム、位置情報共有ニュース配信、株価、ログ表示つまり、「クライアントからサーバーに情報を送る必要がある」場合は WebSocket が必須です。SSE は「サーバーから通知を送るだけ」の用途に向いています。2. Socket.io を使う理由Socket.io は WebSocket をラップして、開発しやすくしたライブラリです。接続管理や再接続処理が自動化されているブラウザ間の互換性が担保されているemit/on で簡単にメッセージ送受信できる生の WebSocket を使う場合、セキュリティー問題や接続失敗時のリトライやブラウザ互換性の考慮が必要ですが、Socket.io はそれを内部で処理してくれます。3. チャットアプリの通信フロー今回作るチャットアプリでは、次のフローで通信します。ユーザーA ------> サーバー ------> ユーザーB
<------ <------ユーザーがメッセージを送信 → サーバーに送信サーバーが接続中の他のクライアントに転送他のユーザーの画面にリアルタイムで表示されるここで「双方向通信が必要」なため、WebSocket が適しています。4. サーバー側の実装(Node.js + Socket.io)必要なパッケージをインストールします💫# Express と HTTP サーバー
npm install express
# Socket.io
npm install socket.io
# 開発時に自動再起動
npm install -D nodemonnodemon を使うとサーバーコードを変更したときに自動で再起動されます(便利すぎ〜〜)package.jsonの中も変更します!これで npm start でサーバーが起動可能! "scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js"
},// 必要なモジュールを読み込む
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
// Socket.ioサーバーを作成
// CORS設定でフロントエンド(localhost:3000)からのアクセスを許可
const io = new Server(server, {
cors: {
origin: 'http://localhost:3000',
},
});
const PORT = 5000;
// クライアントが接続してきたときの処理
io.on('connection', (socket) => {
console.log('クライアントと接続しました');
// クライアントから送信されたメッセージを受信
socket.on('send_message', (data) => {
console.log('受信メッセージ:', data);
// 送信者以外の全クライアントにメッセージを送信
socket.broadcast.emit('receive_message', {
message: data.message,
sender: 'other', // 他人のメッセージとして扱う
timestamp: Date.now(), // 受信時刻を追加
});
});
// クライアントが切断したときの処理
socket.on('disconnect', () => {
console.log('クライアントが切断しました');
});
});
// サーバーを起動
server.listen(PORT, () => console.log(`Server running on port ${PORT}`));socket.broadcast.emit は「送信者以外に送る」ために便利CORS 設定でフロントエンドからの接続を許可5. クライアント側の実装(Next.js + TypeScript)必要なパッケージをインストール# Socket.io クライアント
npm install socket.io-client'use client';
import { useState, useEffect, useRef } from 'react';
import io, { Socket } from 'socket.io-client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
// チャットメッセージの型を定義
interface Chat {
message: string; // メッセージ本文
sender: 'me' | 'other'; // 送信者
timestamp?: number; // メッセージの送信時刻
}
export default function Home() {
// メッセージ入力値
const [message, setMessage] = useState('');
// チャットメッセージリスト
const [list, setList] = useState<Chat[]>([]);
// socket.ioの接続を保持するref
const socketRef = useRef<Socket | null>(null);
// コンポーネントがマウントされた時に一度だけ実行される
useEffect(() => {
// サーバーに接続
const socket = io('http://localhost:5000');
socketRef.current = socket;
// サーバーから受信したメッセージをリストに追加
socket.on('receive_message', (data: { message: string }) => {
setList((prev) => [
...prev,
{ message: data.message, sender: 'other', timestamp: Date.now() },
]);
});
// コンポーネントがアンマウントされる時に接続を切断
return () => {
socket.disconnect();
};
}, []); // 空配列なので初回レンダリング時のみ実行
// メッセージ送信処理
const handleSendMessage = () => {
// 空メッセージや未接続の場合は送信しない
if (!message.trim() || !socketRef.current) return;
// サーバーにメッセージ送信
socketRef.current.emit('send_message', { message });
// 自分のメッセージをローカルリストに追加
setList((prev) => [
...prev,
{ message, sender: 'me', timestamp: Date.now() },
]);
// 入力欄を空にする
setMessage('');
};
return (
<div className='min-h-screen flex flex-col'>
{/* メッセージリスト表示エリア */}
<div className='flex-1 overflow-y-auto p-4 space-y-2'>
{list.map((chat, index) => (
<div
key={index}
className={`flex items-end ${
chat.sender === 'me' ? 'justify-end' : 'justify-start'
}`}
>
{chat.sender === 'other' && (
<Avatar className='w-8 h-8 mr-2'>
<AvatarFallback>👤</AvatarFallback>
</Avatar>
)}
{/* メッセージの内容 */}
<div
className={`px-4 py-2 rounded-lg max-w-xs wrap-break-word ${
chat.sender === 'me'
? 'bg-primary text-primary-foreground'
: 'bg-secondary text-secondary-foreground'
}`}
>
{chat.message}
<div className='text-xs mt-1 text-right text-muted-foreground'>
{new Date(chat.timestamp!).toLocaleTimeString('ja-JP')}
</div>
</div>
{chat.sender === 'me' && (
<Avatar className='w-8 h-8 ml-2'>
<AvatarFallback>👤</AvatarFallback>
</Avatar>
)}
</div>
))}
</div>
{/* 入力エリア */}
<div className='fixed bottom-0 left-0 w-full p-4 bg-white border-t flex gap-2 max-w-sm mx-auto'>
<Input
type='text'
placeholder='message...'
value={message}
onChange={(e) => setMessage(e.target.value)} // 入力値をstateに反映
className='flex-1'
/>
<Button type='button' variant='outline' onClick={handleSendMessage}>
Send
</Button>
</div>
</div>
);
}これで記事の冒頭にあったチャットアプリが完成します🥳6. まとめWebSocket は双方向通信が必要なリアルタイムアプリに最適SSE は「サーバーから通知するだけ」の用途に向くSocket.io は WebSocket をラップし、再接続やブラウザ互換性を自動で処理Next.js のクライアントコンポーネントでは useEffect と useRef で Socket を管理7. もうすぐ冬休み ㊗️今年もお疲れ様でした〜〜年末はゆっくり休みましょうね!