목차
Next.js에서 socket.io를 어떻게 활용할까?
Next.js를 개발하던 도중 소켓 통신을 어떻게 할 수 있을까에 대한 생각을 했었다. 역시 웹개발이기 때문에 socket.io에서 공식으로 Next.js와 연결하는방법이 적혀있었다.
https://socket.io/how-to/use-with-nextjs
그래서 해당 링크를 기반으로 socket.io통신을 하려고한다.
세팅방법
Server.js파일 생성
공식문서에 따르면 그냥 프로젝트 루트에 server.js파일을 만들어주면 된다고 한다.그래서 프로젝트 최상단에 server.js파일을 만들어주었다. 코드는 공식문서의 Next.js활용법에 적혀있지만 io.on("connection")부분은 테스트를위해 코드를 더 작성하였다.
import { createServer } from "node:http";
import next from "next";
import { Server } from "socket.io";
const dev = process.env.NODE_ENV !== "production";
const hostname = "localhost";
const port = 3000;
const app = next({ dev, hostname, port });
const handler = app.getRequestHandler();
app.prepare().then(() => {
const httpServer = createServer(handler);
const io = new Server(httpServer);
let users = {};
function getUsersArray() {
return Object.keys(users).map(id => ({ id, nickname: users[id] }))
}
io.on("connection", (socket) => {
// ...
console.log("a user connected");
/**
* msg는 {type:string, nickName:string}
*/
socket.on('joinAndLeave', (msg) => {
console.log("joinAndLeave", msg.type, msg.nickName);
if (msg.type === 'join' && !users[socket.id]) {
io.emit('chat message', `${msg.nickName}님이 입장하셨습니다.`)
users[socket.id] = msg.nickName;
}
// else if (msg.type === 'leave') {
// io.emit('chat message', `${msg.nickName}님이 퇴장하셨습니다.`)
// users = users.filter((user) => user !== msg.nickName);
// }
io.emit('users', getUsersArray());
})
socket.on('chat message', (msg) => {
console.log('message: ' + msg);
io.emit('chat message', msg);
})
socket.on('disconnect', () => {
console.log('user disconnected')
const nickName = users[socket.id];
if (nickName) {
io.emit('chat message', `${nickName}님이 퇴장하셨습니다.`)
delete users[socket.id];
io.emit('users', getUsersArray());
}
})
});
httpServer
.once("error", (err) => {
console.error(err);
process.exit(1);
})
.listen(port, () => {
console.log(`> Ready on http://${hostname}:${port}`)
})
})
socket.js 파일 생성
/src 폴더에 클라이언트가 사용할 socket.js파일을 작성해준다. 아래 코드처럼 단 3줄이면 된다.
"use client";
import { io } from "socket.io-client";
export const socket = io();
Socket.tsx 및 Socket.styles.ts 파일 생성
Socket.tsx
/src/containers/Socket/Socket.tsx파일을 생성해준다.
'use client';
import { MainLayout } from '@/layout/MainLayout';
import { v4 as uuidv4 } from 'uuid';
import { useEffect, useState } from 'react';
import { faker } from '@faker-js/faker';
import StyledSocket from './Socket.styles';
import { socket } from '../../socket.js';
interface MessageLog {
room: string;
message: string;
nickName: string;
}
export default function SocketPage() {
const [nickName, setNickName] = useState(faker.internet.userName());
const [isConnected, setIsConnected] = useState(false);
const [transport, setTransport] = useState('N/A');
const [logs, setLogs] = useState<string[]>([]);
const [message, setMessage] = useState('');
const [users, setUsers] = useState<string[]>([]);
useEffect(() => {
function onConnect() {
setIsConnected(true);
setTransport(socket.io.engine.transport.name);
socket.io.engine.on('upgrade', (transport) => {
setTransport(transport.name);
});
}
if (socket.connected) {
onConnect();
socket.emit('joinAndLeave', { type: 'join', nickName });
}
function onDisconnect() {
setIsConnected(false);
setTransport('N/A');
}
socket.on('connect', onConnect);
socket.on('disconnect', onDisconnect);
function onSetLog(msg: MessageLog | string) {
if (typeof msg === 'string') {
setLogs((prevLogs) => [...prevLogs, msg]);
} else {
setLogs((prevLogs) => [
...prevLogs,
`${msg.nickName}의 말: ${msg.message}`,
]);
}
}
socket.on('chat message', onSetLog);
function onUsers(inputUsers: { id: string; nickname: string }[]) {
console.log({ inputUsers });
setUsers(inputUsers.map((inputUser) => inputUser.nickname));
}
socket.on('users', onUsers);
return () => {
socket.off('connect', onConnect);
socket.off('disconnect', onDisconnect);
socket.off('chat message', onSetLog);
socket.off('users', onUsers);
};
}, [nickName]);
const onClickSubmitBtn = () => {
socket.emit('chat message', { room: 'room1', message, nickName });
setMessage('');
};
return (
<MainLayout title="Socket 페이지!" tags={['next.js', 'socket']}>
<StyledSocket>
<p>Status: {isConnected ? 'connected' : 'disconnected'}</p>
<p>Transport : {transport}</p>
<div className="socket-nickname-container">
<h3>닉네임</h3>
<input
type="text"
placeholder="닉네임을 적어주세요"
value={nickName}
onChange={(e) => setNickName(e.target.value)}
disabled
/>
</div>
<div className="socket-message-container">
<h3>메시지</h3>
<input value={message} onChange={(e) => setMessage(e.target.value)} />
<button type="submit" onClick={onClickSubmitBtn}>
전송
</button>
</div>
<div className="socket-logs-container">
<div className="socket-logs-container__left">
<h3>채팅 로그</h3>
{logs && logs.map((log) => <p key={uuidv4()}>{log}</p>)}
</div>
<div className="socket-logs-container__right">
<h3>접속 유저</h3>
{users && users.map((user) => <p key={uuidv4()}>{user}</p>)}
</div>
</div>
</StyledSocket>
</MainLayout>
);
}
Socket.styles.ts
/src/containers/Socket/Socket.styles.ts파일을 작성해준다.
import styled from 'styled-components';
const StyledSocket = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
.socket-nickname-container {
display: flex;
gap: 10px;
}
.socket-message-container {
display: flex;
gap: 10px;
input {
width: 80%;
}
}
.socket-logs-container {
display: flex;
gap: 5px;
background-color: white;
.socket-logs-container__left {
width: 70%;
border: 1px solid gray;
padding: 10px;
}
.socket-logs-container__right {
width: 30%;
border: 1px solid gray;
padding: 10px;
}
}
`;
export default StyledSocket;
socket라우트에 적용
import SocketPage from '@/containers/Socket/Socket';
export default function Page() {
return <SocketPage />;
}
이제 Next.js socket 라우터에 아까만든 Socket container를 적용해주면 된다.
package.json변경
{
"scripts": {
- "dev": "next dev",
+ "dev": "node server.js",
"build": "next build",
- "start": "next start",
+ "start": "NODE_ENV=production node server.js",
"lint": "next lint"
}
}
server.js파일을 추가하였기 때문에 실행은 이제 그냥 next dev가 아닌 node server.js로 실행하게 해야 한다. 이부분을 주의해야한다.
구현한 기능
- 입장 및 퇴장
- 클라이언트가 입장 또는 퇴장시 모두에게 알림
- 채팅
- 접속한 유저들에게 채팅이 보여줌
- 채팅 작성 가능
- 접속유저
- 접속한 유저들의 리스트를 알려줌
작동영상
'Next.js' 카테고리의 다른 글
[Next, vitest] Next.js에 vitest 적용해보기(기록용) (0) | 2024.12.04 |
---|---|
[Next.js, styled-components] Next.js에 styled-components 설정하기 (0) | 2024.06.07 |
[Next.js, TS,Zustand] 페이지 이동전 로딩효과 추가하기 (0) | 2024.05.30 |
[Next.js,TS] 특정폴더에만 Eslint 적용하기 (0) | 2024.05.30 |
[Next.js, TypeScript] airbnb 및 커스텀 TS 세팅 (1) | 2024.05.29 |