【第9回】TypeScript入門:Node.js + React で本格的なチャットアプリを開発

TypeScript

Webアプリ開発において、リアルタイム性はユーザー体験を大きく向上させます。チャットアプリはその代表例でしょう。

今回は、Node.jsReact、そして Socket.io を使って、シンプルながらもリアルタイムなチャットアプリを構築する手順を、Node.jsの解説を中心に進めていきます。

簡素なアプリですが以下のようなチャットアプリを作成していきます。

フォルダ構成と初期セットアップ

まず、プロジェクトの全体像を把握しましょう。フロントエンドとバックエンドを明確に分けるため、以下のようなフォルダ構成で進めます。

AIT-RealtimeChatApp├── client/     ← React(フロントエンド)
└── server/     ← Node.js + Express + Socket.io(バックエンド)

プロジェクトフォルダを作成

プロジェクトのルートとなるフォルダを作成し、その中に移動します。

mkdir AIT-RealtimeChatApp
cd AIT-RealtimeChatApp

Step 1:サーバー側の準備(Node.js + Express + Socket.io)

リアルタイムな通信を司るバックエンドの準備です。

server フォルダ作成と初期化

mkdir server
cd server
npm init -y

npm init -yは、対話形式の質問をスキップして、デフォルトの設定でpackage.jsonファイルを生成してくれます。

必要なパッケージをインストール

WebサーバーフレームワークのExpressと、リアルタイム通信ライブラリのSocket.ioをインストールします。

npm install express socket.io

もし開発中にサーバーの再起動を自動化したい場合は、nodemonをインストールすると便利です。

npm install --save-dev nodemon

これらのコマンドを実行すると、以下のようにpackage.jsonpackage-lock.jsonが作成され、必要なモジュールがnode_modulesフォルダに格納されます。

以下の画像のファイル構成となっていれば成功です。

サーバーファイルを作成(server/index.js

次に、サーバーのメイン処理を記述するファイルを作成します。

下記コマンドでindex.jsファイルを作成します。

touch index.js

現時点では空のファイルで問題ありません。


Step 2:フロントエンド(React)の準備

次に、ユーザーインターフェースを担当するフロントエンドの準備に移ります。

client フォルダを React で作成

プロジェクトのルートに戻り、Reactプロジェクトを新規作成します。

TypeScriptを使うため、--template typescriptオプションを付与しています。

cd ..
npx create-react-app client --template typescript

WebSocket クライアントをインストール

作成したclientフォルダに移動し、Socket.ioと連携するためのクライアントライブラリをインストールします。

cd client
npm install socket.io-client

ここまでの手順で、プロジェクトのフォルダ構成は以下のようになっているはずです。

realtime-chat-app/
├── client/
│ └── src/
│ ├── App.tsx
│ └── index.tsx
│ └── ...(Reactのコード)
├── server/
│ └── index.js(Express + Socket.ioサーバー)
├── package-lock.json

── package.json

Step 3:サーバーコードを追加

いよいよ、Node.jsでチャットサーバーのロジックを実装していきます。

server/index.jsファイルに以下のコードを記述してください。

// server/index.js
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const cors = require("cors");

const app = express();
app.use(cors());

const server = http.createServer(app);
const io = new Server(server, {
  cors: {
    origin: ["http://localhost:3000", "http://192.168.000.000:3000"], // ReactアプリのURL
    methods: ["GET", "POST"],
  },
});

// Socket.ioの接続イベントをリッスンします
io.on("connection", (socket) => {
  console.log("ユーザー接続:", socket.id); 

  // クライアントから「chat message」イベントが送信されたときの処理
  socket.on("chat message", (msg) => {
    console.log("受信:", msg);
    io.emit("chat message", msg);
  });

  // クライアントが切断したときの処理
  socket.on("disconnect", () => {
    console.log("ユーザー切断:", socket.id);
  });
});

// サーバーをリッスンするポート番号
const PORT = 4000;
server.listen(PORT, () => {
  console.log(`🚀 サーバー起動中 http://localhost:${PORT}`);
});

[“http://localhost:3000”, “http://192.168.000.000:3000”]

http://localhost:3000は、あなたのPC自身を指す特別なアドレスです。

http://192.168.000.000:3000は、ネットワーク上の他のデバイスや、場合によってはPCの別のネットワークインターフェースから見たあなたのPCのアドレスです。
192.168.000.000には適切な値を入力してください。

Step 4:サーバーを起動する

サーバーコードが記述できたら、起動してみましょう。

cd ..
cd server
node index.js 

サーバーが正常に起動すると、以下のようなメッセージがターミナルに表示されます。

🚀 サーバー起動中 http://localhost:4000

これで、Node.jsのバックエンドサーバーが、クライアントからの接続とメッセージの送受信を待機する状態になりました!

ここからは React + TypeScript + Socket.IO Client を使ってチャット送受信 UI を作っていきましょう。

Step 5:React 側の実装(クライアント側)

ここからは、サーバーと通信するためのフロントエンド(React)の準備です。

サーバーからのメッセージを受け取り、入力フォームからメッセージを送信するUIを構築します。

フォルダ構成(client/src/

Reactプロジェクトのsrcフォルダ内に、以下の構成でファイルを作成していきます。

src/
├── components/
│ ├── Chat.tsx ← メインチャット画面
│ └── Message.tsx ← 各メッセージ表示用コンポーネント
├── App.tsx ← アプリのルート
└── index.tsx

フォルダ&ファイル作成コマンド

VSCodeターミナル上で以下を順番に実行してください。

cd ../client
mkdir -p src/components
touch src/components/Chat.tsx
touch src/components/Message.tsx

src/components/Chat.tsx

チャットのメイン画面を構築します。

import React, { useEffect, useState } from "react";
import io from "socket.io-client";
import Message from "./Message";

const socket = io("http://localhost:4000");

const Chat: React.FC = () => {
  const [messages, setMessages] = useState<string[]>([]);
  const [input, setInput] = useState("");

  useEffect(() => {
    socket.on("chat message", (msg: string) => {
      setMessages((prev) => [...prev, msg]);
    });

    // クリーンアップ(不要なリスナー削除)
    return () => {
      socket.off("chat message");
    };
  }, []);

  const sendMessage = () => {
    if (input.trim()) {
      socket.emit("chat message", input);
      setInput("");
    }
  };

  return (
    <div>
      <div className="chat-box">
        {messages.map((msg, idx) => (
          <Message key={idx} text={msg} />
        ))}
      </div>
      <div className="chat-input">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="メッセージを入力"
        />
        <button onClick={sendMessage}>送信</button>
      </div>
    </div>
  );
};
import React, { useEffect, useState } from "react";
import io from "socket.io-client"; // Socket.ioクライアントライブラリをインポート
import Message from "./Message";

// サーバーのURLを指定してSocket.ioクライアントを初期化
const socket = io("http://localhost:4000");

// ChatMessageインターフェースを定義(ニックネーム機能追加時に使用)
interface ChatMessage {
  user: string;
  text: string;
}

// 親コンポーネントからusernameを受け取るためのPropsインターフェース
interface Props {
  username: string;
}

const Chat: React.FC<Props> = ({ username }) => {
  // メッセージのリストを管理するstate
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  // 入力フォームの値を管理するstate
  const [input, setInput] = useState("");

  useEffect(() => {
    // サーバーから「chat message」イベントが送信されたら
    // 受信したメッセージをstateに追加するリスナーを設定
    socket.on("chat message", (msg: ChatMessage) => {
      setMessages((prev) => [...prev, msg]);
    });

    // コンポーネントがアンマウントされる際にリスナーをクリーンアップ
    return () => {
      socket.off("chat message");
    };
  }, []); // 空の依存配列で、コンポーネントマウント時に一度だけ実行

  // メッセージ送信ボタンがクリックされたときの処理
  const sendMessage = () => {
    if (input.trim()) { // 入力値が空でない場合のみ送信
      const msg: ChatMessage = { user: username, text: input };
      socket.emit("chat message", msg); // サーバーに「chat message」イベントとメッセージを送信
      setInput(""); // 入力フォームをクリア
    }
  };

  return (
    <div>
      <div className="chat-box">
        {messages.map((msg, idx) => (
          // 受信したメッセージを表示するMessageコンポーネントをレンダリング
          <Message key={idx} msg={msg} isSelf={msg.user === username} />
        ))}
      </div>
      <div className="chat-input">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)} // 入力値の変更をstateに反映
          placeholder="メッセージを入力"
        />
        <button onClick={sendMessage}>送信</button>
      </div>
    </div>
  );
};

export default Chat;

src/components/Message.tsx

個々のメッセージを表示するためのコンポーネントです。

import React from "react";

interface Props {
  text: string;
}

const Message: React.FC<Props> = ({ text }) => {
  return <div className="message">{text}</div>;
};

export default Message;

src/App.tsx

アプリのルートコンポーネントです。ニックネーム入力モーダルとチャットコンポーネントを管理します。

import React, { useState } from "react";
import "./App.css";
import Chat from "./components/Chat";
import UsernameModal from "./components/UsernameModal"; // 後で作成するモーダルコンポーネント

function App() {
  // ユーザー名を管理するstate。初期値はnullで、未設定状態を示す
  const [username, setUsername] = useState<string | null>(null);

  return (
    <div className="App">
      <h1>AITチャットアプリ</h1>
      {/* ユーザー名が設定されていなければUsernameModalを表示 */}
      {!username && <UsernameModal onSetUsername={setUsername} />}
      {/* ユーザー名が設定されていればChatコンポーネントを表示し、usernameをpropsとして渡す */}
      {username && <Chat username={username} />}
    </div>
  );
}

export default App;

src/index.tsx

Reactアプリケーションのエントリーポイントです。

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./App.css"; // アプリ全体のCSSをインポート

const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

src/App.css

アプリの基本的なスタイルを定義します。

.App {
  font-family: sans-serif;
  text-align: center;
  padding: 2rem;
}

.chat-box {
  border: 1px solid #ccc;
  max-width: 500px;
  height: 300px;
  overflow-y: auto;
  margin: 0 auto 1rem;
  padding: 1rem;
  background-color: #f9f9f9;
}

.chat-input {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
}

.chat-input input {
  width: 300px;
  padding: 0.5rem;
  font-size: 1rem;
}

.chat-input button {
  padding: 0.5rem 1rem;
  font-size: 1rem;
}

.message {
  text-align: left;
  margin-bottom: 0.5rem;
}

/* ニックネーム機能追加に伴うスタイル */
.message.self {
  background-color: #dcf8c6; /* 自分のメッセージの背景色 */
  text-align: right;
}
.message.other {
  background-color: #fff; /* 他のユーザーのメッセージの背景色 */
  text-align: left;
}

起動手順

いよいよ、作成したチャットアプリを起動してみましょう。サーバーとクライアントの両方を同時に起動する必要があります。

まずはNode.jsサーバーを起動します。serverフォルダに移動して実行してください。

cd server
node index.js

サーバーが起動したら、新しいターミナルを開き、Reactクライアントを起動します。

cd client
npm start

npm startを実行すると、ブラウザが自動的に開かれ、チャットアプリの画面が表示されます。

Screenshot

Step 6:ブラウザを開いてチャットアプリを確認

画像のように簡素なチャットアプリが完成しました。

Step 7:チャットアプリを改良する

作成したチャットアプリは、メッセージを送受信できますが、誰がどのメッセージを送ったのかが分かりません。

そこで、ニックネーム付きのログイン機能を追加して、より使いやすく改良しましょう。

ChatMessage.ts ファイルを作成

srcフォルダ内にtypesフォルダを作成し、その中にChatMessage.tsを作成します。

export interface ChatMessage {
  user: string;
  text: string;
}

使用側でインポート

Chat.tsxなどのファイルでこの型を使いたい場合は、以下のようにインポートします。

import { ChatMessage } from "../types/ChatMessage";

ニックネーム付きのログイン機能を React + WebSocket チャットアプリに追加するには、以下のステップを順番に実行すれば OK です。

ユーザー名を入力するモーダルを作成

src/components/UsernameModal.tsxを作成し、以下のコードを記述してください。

import React, { useState } from "react";

interface Props {
  onSetUsername: (name: string) => void;
}

const UsernameModal: React.FC<Props> = ({ onSetUsername }) => {
  const [input, setInput] = useState("");

  const handleSubmit = () => {
    if (input.trim()) {
      onSetUsername(input.trim());
    }
  };

  return (
    <div style={styles.overlay}>
      <div style={styles.modal}>
        <h2>ニックネームを入力</h2>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="例: たろう"
        />
        <button onClick={handleSubmit}>OK</button>
      </div>
    </div>
  );
};

const styles: { [key: string]: React.CSSProperties } = {
  overlay: {
    position: "fixed",
    top: 0,
    left: 0,
    width: "100vw",
    height: "100vh",
    background: "rgba(0,0,0,0.5)",
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
  },
  modal: {
    background: "#fff",
    padding: "2rem",
    borderRadius: "8px",
    textAlign: "center",
  },
};

export default UsernameModal;

App.tsx にステートとモーダルの表示を追加

src/App.tsxを修正して、ユーザー名が入力されるまでモーダルを表示するようにします。

tsxコピーする編集するimport React, { useState } from "react";
import "./App.css";
import Chat from "./components/Chat";
import UsernameModal from "./components/UsernameModal";

function App() {
  const [username, setUsername] = useState<string | null>(null);

  return (
    <div className="App">
      <h1>AITチャットアプリ</h1>
      {!username && <UsernameModal onSetUsername={setUsername} />}
      {username && <Chat username={username} />}
    </div>
  );
}

export default App;

Chat.tsx に username を受け取るように変更

src/components/Chat.tsxを修正し、usernameをpropsとして受け取り、メッセージ送信時に含めるようにします。

import React, { useEffect, useState } from "react";
import io from "socket.io-client";
import Message from "./Message";

const socket = io("http://localhost:4000");

interface ChatMessage {
  user: string;
  text: string;
}

interface Props {
  username: string;
}

const Chat: React.FC<Props> = ({ username }) => {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [input, setInput] = useState("");

  useEffect(() => {
    // サーバーから受け取ったメッセージを state に追加
    socket.on("chat message", (msg: ChatMessage) => {
      setMessages((prev) => [...prev, msg]);
    });

    // クリーンアップ
    return () => {
      socket.off("chat message");
    };
  }, []);

  const sendMessage = () => {
    if (input.trim()) {
      const msg: ChatMessage = { user: username, text: input };
      socket.emit("chat message", msg);
      setInput("");
    }
  };

  return (
    <div>
      <div className="chat-box">
        {messages.map((msg, idx) => (
          <Message key={idx} msg={msg} isSelf={msg.user === username} />
        ))}
      </div>
      <div className="chat-input">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="メッセージを入力"
        />
        <button onClick={sendMessage}>送信</button>
      </div>
    </div>
  );
};

export default Chat;

Message.tsx を修正して自分と他人を見分ける

src/components/Message.tsxを修正し、自分のメッセージと他人のメッセージで見た目を変更できるようにします。

interface ChatMessage {
  user: string;
  text: string;
}

interface Props {
  msg: ChatMessage;
  isSelf: boolean;
}

const Message: React.FC<Props> = ({ msg, isSelf }) => {
  return (
    <div className={`message ${isSelf ? "self" : "other"}`}>
      <p><strong>{msg.user}</strong></p>
      <p>{msg.text}</p>
    </div>
  );
};

export default Message;

CSS を追加して見た目を分ける

src/App.cssに以下のスタイルを追加し、自分のメッセージと他人のメッセージの背景色や配置を調整します。

/* src/App.css に追記 */
.message.self {
  background-color: #dcf8c6; /* 自分のメッセージの背景色 */
  text-align: right; /* 右寄せ */
  margin-left: auto; /* 右寄せ */
  width: fit-content; /* 内容の幅に合わせる */
  padding: 8px 12px;
  border-radius: 12px;
}
.message.other {
  background-color: #fff; /* 他のユーザーのメッセージの背景色 */
  text-align: left; /* 左寄せ */
  margin-right: auto; /* 左寄せ */
  width: fit-content; /* 内容の幅に合わせる */
  padding: 8px 12px;
  border-radius: 12px;
  border: 1px solid #eee;
}

最後にサーバー/index.jsを確認

クライアント側でChatMessageオブジェクトを送信するように変更したので、サーバー側でもそれを受け取って全員にブロードキャストするようにします。server/index.jsが以下のようになっていることを確認してください。

const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const cors = require("cors");

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  cors: {
    origin: ["http://localhost:3000", "http://192.168.000.000:3000"], // Reactのポート
    methods: ["GET", "POST"]
  }
});

io.on("connection", (socket) => {
  console.log("クライアント接続:", socket.id);

  // クライアントから「chat message」イベントとChatMessageオブジェクトを受信
  socket.on("chat message", (msg) => {
    console.log(`受信(${msg.user}):`, msg.text); // ユーザー名とメッセージテキストをログに出力
    io.emit("chat message", msg); // 受信したChatMessageオブジェクトを全クライアントに送信
  });

  socket.on("disconnect", () => {
    console.log("クライアント切断:", socket.id);
  });
});

server.listen(4000, () => {
  console.log("🚀 サーバー起動中 http://localhost:4000");
});

これで、ニックネーム付きのチャットアプリが完成です!ぜひ両方のターミナルを起動して、ブラウザで動作を確認してみてください。

作成したアプリでメッセージのやり取りを実践

複数のブラウザタブや異なるデバイスからアクセスして、リアルタイム通信の醍醐味を味わってみましょう。

http://192.168.000.000:3000側では、やまださんがログインしています。

“http://localhost:3000″はたなかさんがログインしてメッセージのやり取りをしています。

画像のようにそれぞれ別のアドレスからチャットのやり取りができることまで確認できました。

まとめ

いかがでしたでしょうか? Node.jsとReact、Socket.ioを組み合わせることで、手軽にリアルタイムなチャットアプリを構築できることがお分かりいただけたかと思います。

さらに機能を拡張して、あなただけのチャットアプリを開発してみてくださいね!

タイトルとURLをコピーしました