1. 概要
今回はSocketIOを使い、シンプルなチャットアプリを作る内容となります。
対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。
2. nodeのインストール
こちらを参考
3. プロジェクトを作成
下記を実行
npx create-next-app@latest
Need to install the following packages:
create-next-app@15.0.2
Ok to proceed? (y)
✔ What is your project named? … socket-sample
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack for next dev? … No / Yes
✔ Would you like to customize the import alias (@/* by default)? … No / Yes
✔ What import alias would you like configured? … @/*
Creating a new Next.js app in /home/sondon/dev/wsl/nodejs/react/socket-sample.
Using npm.
Initializing project with template: app
Installing dependencies:
- react
- react-dom
- next
Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- eslint
- eslint-config-next
added 297 packages, and audited 298 packages in 4m
118 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Initialized a git repository.
Success! Created socket-sample at /home/sondon/dev/wsl/nodejs/react/socket-sample
4. 必要なライブラリをインストール
下記を実行
npm i --save-dev ts-node nodemon
npm i socket.io socket.io-client
5. ソースコード
5-1. 設定
5-1-1. package.json
{
"name": "socket-sample",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"server:dev": "nodemon server/app.ts"
},
"dependencies": {
"next": "15.0.2",
"react": "19.0.0-rc-02c0e824-20241028",
"react-dom": "19.0.0-rc-02c0e824-20241028",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "15.0.2",
"nodemon": "^3.1.7",
"ts-node": "^10.9.2",
"typescript": "^5"
}
}
- サーバーサイドの起動コマンドを追加
5-1-2. tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"ts-node": {
"compilerOptions": {
"module": "CommonJS",
"paths": {
"@/*": ["../src/*"]
}
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
- サーバーサイドのコンパイルのために「ts-node」を追加
5-1-3. nodemon.json
{
"watch": ["server"],
"ext": "ts",
"ignore": ["*.text.ts"],
"delay": "3",
"execMap": {
"ts": "ts-node"
}
}
- Reload, automatically.
5-2. フロントサイト
5-2-1. src/common/constants.ts
export const HOST: string = "localhost";
export const PORT_SERVER: number = 3333;
export const PORT_FRONT: number = 3000;
export const EVENTNAME_CONNECTION: string = "connect";
export const EVENTNAME_DISCONNECT: string = "disconnect";
export const EVENTNAME_MESSAGE: string = "message";
export const EVENTNAME_BROADCAST: string = "broadcast";
5-2-2. src/common/definitions.ts
export type ChatData = {
socketId?: string;
msg: string;
};
5-2-3. src/socket.ts
"use client";
import { io } from "socket.io-client";
import { HOST, PORT_SERVER } from "./common/constants";
const URL_SERVER: string =
process.env.SOCKET_URL || `http://${HOST}:${PORT_SERVER}`;
export const socket = io(URL_SERVER);
5-2-4. src/components/connection-manager.tsx
import { socket } from "../socket";
const ConnectionManager = () => {
const join = () => {
socket.connect();
};
const leave = () => {
socket.disconnect();
};
return (
<>
<button onClick={join}>Join</button>
<button onClick={leave}>Leave</button>
</>
);
};
export default ConnectionManager;
5-2-5. src/components/chats.tsx
import { ChatData } from "@/common/definitions";
type Props = {
chats: ChatData[];
};
const Chats = (props: Props) => {
return (
<ul>
{props.chats.map((chat: ChatData, index) => (
<li key={index}>{`[${chat.socketId}] ${chat.msg}`}</li>
))}
</ul>
);
};
export default Chats;
5-2-6. src/components/chat-form.tsx
import { FormEvent, useState } from "react";
import { socket } from "../socket";
import { EVENTNAME_MESSAGE } from "@/common/constants";
import { ChatData } from "@/common/definitions";
const ChatForm = () => {
const [msg, setMsg] = useState("");
const [isLoading, setIsLoading] = useState(false);
const sendMsg = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
const chatData: ChatData = {
msg: msg,
};
socket.timeout(100).emit(EVENTNAME_MESSAGE, chatData, () => {
setIsLoading(false);
setMsg("");
});
};
return (
<form onSubmit={(e) => sendMsg(e)}>
<input value={msg} onChange={(e) => setMsg(e.target.value)} />
<button type="submit" disabled={isLoading}>
Chat
</button>
</form>
);
};
export default ChatForm;
5-2-7. src/app/page.tsx
"use client";
import styles from "./page.module.css";
import { useEffect, useState } from "react";
import { socket } from "../socket";
import Chats from "@/components/chats";
import ConnectionManager from "@/components/connection-manager";
import ChatForm from "@/components/chat-form";
import {
EVENTNAME_CONNECTION,
EVENTNAME_DISCONNECT,
EVENTNAME_BROADCAST,
} from "@/common/constants";
import { ChatData } from "@/common/definitions";
const Home = () => {
const [isConnected, setIsConnected] = useState(false);
const [transport, setTransport] = useState("N/A");
const [chatData, setChatData] = useState<ChatData[]>([]);
useEffect(() => {
const onConnect = () => {
setIsConnected(true);
setTransport(socket.io.engine.transport.name);
socket.io.engine.on("upgrade", (transport) => {
setTransport(transport.name);
});
};
const onDisconnect = () => {
setIsConnected(false);
setTransport("N/A");
setChatData([]);
};
if (socket.connected) {
onConnect();
}
socket.on(EVENTNAME_CONNECTION, onConnect);
socket.on(EVENTNAME_DISCONNECT, onDisconnect);
return () => {
socket.off(EVENTNAME_CONNECTION, onConnect);
socket.off(EVENTNAME_DISCONNECT, onDisconnect);
};
}, []);
useEffect(() => {
socket.on(EVENTNAME_BROADCAST, (chat: ChatData) => {
setChatData([...chatData, chat]);
});
return () => {
socket.off(EVENTNAME_BROADCAST);
};
}, [chatData]);
return (
<div className={styles.page}>
<main className={styles.main}>
<p>Status: {isConnected ? "joined" : "leaved"}</p>
<p>Transport: {transport}</p>
<hr />
<Chats chats={chatData} />
<ConnectionManager />
<ChatForm />
</main>
</div>
);
};
export default Home;
5-3. サーバーサイト
5-3-1. server/server-socket.ts
import { RequestHandler } from "next/dist/server/next";
import { createServer } from "node:http";
import { Server, Socket } from "socket.io";
import {
HOST,
PORT_FRONT,
EVENTNAME_CONNECTION,
EVENTNAME_DISCONNECT,
EVENTNAME_MESSAGE,
EVENTNAME_BROADCAST,
} from "../src/common/constants";
import { ChatData } from "../src/common/definitions";
type Props = {
handler: RequestHandler;
};
const server = (props: Props) => {
const httpServer = createServer(props.handler);
const io = new Server(httpServer, {
cors: {
origin: `http://${HOST}:${PORT_FRONT}`,
methods: ["GET", "POST"],
},
});
io.on(EVENTNAME_CONNECTION, (socket: Socket) => {
const data: ChatData = { socketId: socket.id, msg: "joined." };
io.emit(EVENTNAME_BROADCAST, data);
socket.on(EVENTNAME_MESSAGE, (chatData: ChatData) => {
const data: ChatData = {
socketId: socket.id,
msg: chatData.msg,
};
io.emit(EVENTNAME_BROADCAST, data);
});
socket.on(EVENTNAME_DISCONNECT, () => {
const data: ChatData = {
socketId: socket.id,
msg: "leaved.",
};
io.emit(EVENTNAME_BROADCAST, data);
});
});
return httpServer;
};
export default server;
5-3-2. server/app.ts
import next from "next";
import { NextServer } from "next/dist/server/next";
import server from "./server-socket";
import { HOST, PORT_SERVER } from "../src/common/constants";
const dev: boolean = process.env.NODE_ENV !== "production";
const hostname: string = process.env.HOSTNAME || HOST;
const port: number = parseInt(process.env.PORT || String(PORT_SERVER), 10);
const app: NextServer = next({ dev, hostname, port });
const handler = app.getRequestHandler();
app.prepare().then(() => {
const httpServer = server({ handler });
httpServer
.once("error", (err) => {
console.error(err);
process.exit(1);
})
.listen(port, () => {
console.log(
`> Ready on http://${hostname}:${port} as ${
dev ? "development" : process.env.NODE_ENV
}`
);
});
});
6. サーバーを起動
6-1. サーバーサイド
npm run server:dev
> socket-sample@0.1.0 server:dev
> nodemon server/app.ts
[nodemon] 3.1.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): server/**/*
[nodemon] watching extensions: ts
[nodemon] starting `ts-node server/app.ts`
> Ready on http://localhost:3333 as development
6-2. フロントサイド
npm run dev
> socket-sample@0.1.0 dev
> next dev
▲ Next.js 15.0.2
- Local: http://localhost:3000
✓ Starting...
✓ Ready in 2.4s
7. ブラウザで確認
- http://localhost:3000
- ブラウザを3つ立ち上げて確認
8. ディレクトリの構造
.
├── README.md
├── next-env.d.ts
├── next.config.ts
├── nodemon.json
├── package-lock.json
├── package.json
├── public
│ ├── file.svg
│ ├── globe.svg
│ ├── next.svg
│ ├── vercel.svg
│ └── window.svg
├── server
│ ├── app.ts
│ └── server-socket.ts
├── src
│ ├── app
│ │ ├── favicon.ico
│ │ ├── fonts
│ │ │ ├── GeistMonoVF.woff
│ │ │ └── GeistVF.woff
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── page.module.css
│ │ └── page.tsx
│ ├── common
│ │ ├── constants.ts
│ │ └── definitions.ts
│ ├── components
│ │ ├── chat-form.tsx
│ │ ├── chats.tsx
│ │ └── connection-manager.tsx
│ └── socket.ts
└── tsconfig.json
7 directories, 27 files
9. 備考
今回はSocketIOを使い、シンプルなチャットアプリを作る内容についてでした。
10. 参考
- Docs | Next.js (nextjs.org)
- Quick Start – React
- Introduction | Socket.IO
- Configuring: Custom Server | Next.js
- nodemon
投稿者プロフィール
-
開発好きなシステムエンジニアです。
卓球にハマってます。