웹소켓 사용법 / WebSocket으로 실시간 소통 채팅앱 개발 방법
HTTP, 웹소켓 프로토콜 차이
HTTP 프로토콜
웹에서 클라이언트가 서버에 요청을 보내면 응답을 반환하는 요청-응답 기반의 단방향 통신 프로토콜입니다.
HTTP 통신은 서버 응답 후 연결이 끊기므로 연결에 지속성이 없습니다.
웹소켓 프로토콜
클라이언트와 서버가 초기 연결을 맺은 뒤, 양방향으로 실시간 데이터를 주고받을 수 있는 프로토콜입니다.
Socket.IO 라이브러리는 네임스페이스로 채널을 구분하여 클라이언트, 서버를 그룹핑 합니다.
Node.js 백엔드 웹소켓 서버 구축
Node.js 서버에서 HTTP 서버 실행 후, 웹소켓 서버를 실행해야 합니다.
Node.js 프로젝트 초기화
npm init -y
백엔드 프로젝트 폴더 생성 후 터미널에서 해당 폴더에 접근하여 명령어를 실행합니다.
Node.js 프로젝트 모듈 설치
npm i express mongoose cors dotenv socket.io
Node.js 프로젝트에서 필요한 모듈들을 설치하는 명령어입니다.
Node.js 프레임워크 및 라이브러리 종류
express | 서버 라우팅, 미들웨어 관리를 도와주는 Node.js 기반 웹 프레임워크 |
mongoose | Node.js와 MongoDB를 연결해서 CRUD 작업을 도와주는 라이브러리 |
cors | Javascript 클라이언트, 서버 통신 시 교차 출처 요청 CORS 정책을 설정하기 위한 미들웨어 |
dotenv | .env 파일에 작성한 환경변수를 Node.js 애플리케이션에서 사용할 수 있게 해주는 라이브러리 |
http |
HTTP 서버를 만들 때 사용하는 Node.js 내장 모듈 생성된 HTTP 서버 인스턴스 위에서 express 앱, 웹소켓 서버가 함께 동작 |
socket.io | Node.js 환경에서 웹소켓 기반 실시간 양방향 통신을 쉽게 구현할 수 있는 라이브러리 |
환경변수 파일 생성
PORT=5000
DB_URL=mongodb://DB서버IP:27017/DB명
.env 파일을 생성하여 Node 포트, 데이터베이스 주소 등을 작성합니다.
js 코드에서 dotenv 모듈로 환경변수 로드 후, process.env.환경병수명으로 사용할 수 있습니다.
Express 앱 인스턴스 생성
// express 모듈 불러오기
const express = require("express");
// 익스프레스 인스턴스 생성
const app = express();
// 라우팅
// CORS 미들웨어 생성
const cors = require("cors");
// 익스프레스 인스턴스에 CORS 미들웨어 등록
// 기본값 : 모든 도메인 요청 허용
app.use(cors());
module.exports = app;
app.js 파일을 만들고, Node.js 서버에서 실행할 Express 앱 인스턴스를 생성합니다.
웹소켓 이벤트 핸들러 작성
// DB 컨트롤러 import
const userController = require("../controllers/userController");
const chatController = require("../controllers/chatController");
// server.js에서 socket.io 서버 인스턴스 전달 받을 예정
module.exports = (io) => {
// 웹소켓 연결 이벤트 수신 리스너
// 매개변수 socket : 연결된 클라이언트 정보
io.on("connection", (socket) => {
console.log("방금 연결된 클라이언트 연결 ID : ", socket.id);
// 클라이언트에서 보낸 "join" 이벤트 수신 리스너
socket.on("join", async (userName, callback) => {
try {
// 소켓 입장 유저 정보 DB 저장 및 조회
const user = await userController.saveUser(userName, socket.id);
// 시스템 메시지 생성
const welcomeChat = {
chat: "${user.name} 님이 채팅방에 들어왔습니다.",
user: { id: null, name: "system" };
};
// 소켓에 접속한 모든 클라이언트에게 "message" 이벤트 전송 (Broadcast)
io.emit("message", welcomeChat);
// 콜백함수 호출 및 데이터 전달
callback({ succese: true, user: user });
} catch (error) {
callback({ succese: false, error: error.message });
}
});
// 클라이언트에서 보낸 "sendMessage" 이벤트 수신 리스너
socket.on("sendMessage", async (msg) => {
console.log("받은 메시지 : ", msg);
// 메시지 보낸 유저 정보 조회
const user = await userController.getUser(socket.id);
// 소켓 메시지 DB 저장
const chat = await chatController.savechat(msg, user);
// 소켓에 접속한 모든 클라이언트에게 "message" 이벤트 전송 (Broadcast)
io.emit("message", chat);
});
// 연결 해제 이벤트 수신 리스너
socket.on("disconnect", () => {
console.log("클라이언트 연결 종료 : ", socket.id);
});
});
}
server.js에서 사용할 소켓 이벤트 핸들러 코드를 별도의 파일로 작성합니다.
/utils/socketHandler.js 파일로 생성하는 것이 권장됩니다.
HTTP 서버 생성 및 웹소켓 서버 실행
const http = require("http");
const app = require("./app");
// 환경변수 로드
require("dotenv").config();
// HTTP 서버 인스턴스 생성
const httpServer = http.createServer(app);
// HTTP 서버에 socket.io 서버 인스턴스 실행
const { Server } = require("socket.io");
const io = new Server(httpServer, {
cors: {
origin: "*", // 또는 ["http://소켓연결허용클라이언트IP:3000", "https://소켓연결허용클라이언트도메인.com"],
methods: ["GET", "POST"]
}
});
// 웹소켓 이벤트 핸들러 모듈 실행
require("./utils/socketHandler.js")(io);
// HTTP 서버 실행 및 클라이언트 요청 대기
httpServer.listen(process.env.PORT, () => {
console.log("Node.js HTTP 서버 실행 완료. 포트 : ", process.env.PORT);
});
server.js 파일을 만들고, Node.js 서버 실행 시 HTTP 서버가 실행되도록 합니다.
HTTP 서버 위에서 Express 앱 인스턴스, Socket.io 웹소켓 서버가 동시에 동작하게 됩니다.
Node.js 서버 실행
node server.js
노드를 이용해서 Javascript 파일을 실행할 수 있습니다.
Ctrl + C로 Node.js 서버를 종료할 수 있습니다.
nodemon 모듈 설치 명령어
npm i --save-dev nodemon
node 대신 nodemon 모듈로 실행하면, 코드 변경 시 자동 재시작 되어서 편합니다.
Node.js 서버 MongoDB 사용법
백엔드 프로젝트에서 유저 정보, 채팅 메시지 정보를 저장하는 데이터베이스입니다.
MongoDB에서는 테이블이 아니라 컬렉션을 사용합니다.
컬렉션 스키마 및 모델 생성
프로젝트 폴더에 models 폴더 생성 후 user.js, chat.js 파일을 생성합니다.
user.js
const mongoose = require("mongoose");
// 유저 정보 스키마 정의
const userSchema = new mongoose.Schema({
userName: {
type: String,
required: [true, "에러메세지"], // 필수 입력값, 없으면 에러 출력
unique: true // 중복 불가
},
socketId: {
type: String
},
isOnline: {
type: Boolean,
default: false
}
});
// 모델 생성 및 내보내기
module.exports = mongoose.model("User", userSchema);
유저 정보 스키마가 적용된 모델을 생성하여 내보내서, 다른 파일에서 사용할 수 있게 합니다.
chat.js
const mongoose = require("mongoose");
// 채팅 메시지 정보 스키마 정의
const chatSchema = new mongoose.Schema(
{
chat: String, // 메시지 내용
user: { // 보낸 유저
id: {
type: mongoose.Schema.Types.ObjectId,
ref: "User" // User 테이블 참조
},
userName: String
}
},
{ timestamps: true }
)
// 모델 생성 및 내보내기
module.exports = mongoose.model("Chat", chatSchema);
채팅 메시지 정보 스키마가 적용된 모델을 생성하여 내보내서, 다른 파일에서 사용할 수 있게 합니다.
MongoDB 데이터베이스 연결
const mongoose = require("mongoose");
// 환경변수 로드
require("dotenv").config();
// MongoDB 연결 및 DB 옵션 설정
mongoose.connect(process.env.DB_URL, {
useNewUrlParser: true, // MongoDB 연결 URL을 최신 방식으로 해석
useUnifiedTopology: true // 연결 관리 방식을 최신 엔진으로 변경
})
.then(() => console.log("데이터베이스 연결 성공"))
.catch(err => console.error("DB 연결 에러 : ", err));
Node.js 백엔드 서버 app.js 파일에 mongoose 모듈을 추가하고 몽고DB에 연결합니다.
DB 연결은 별도 db.js 파일이나 server.js에서 처리하는 경우도 많습니다.
DB 컨트롤러 함수 생성
유저 컨트롤러 개발
// 유저 모델 import
const User = require('../models/User');
const userController = {}
// 유저 정보 저장 함수
userController.saveUser = async (userName, socketId) => {
// 이미 있는 유저인지 확인
let user = await User.findOne({ userName: userName });
// 신규 유저 정보 생성
if (!user) {
user = new User({
userName: userName,
socketId: socketId,
isOnline: true
}
}
// 기존 유저 정보 업데이트
user.socketId = socketId;
user.isOnline = true;
// 유저 정보 DB 저장
await user.save();
return user;
}
// 소켓ID로 유저 정보 조회 함수
userController.getUser = async (socketId) => {
const user = await User.findOne({ socketId: socketId });
if (!user) throw new Error("DB에서 유저 정보를 찾을 수 없습니다.");
return user;
}
module.exports = userController;
/controllers/userController.js 파일 생성 후 유저 테이블 조작 함수를 작성합니다.
채팅 컨트롤러 개발
// 채팅 모델 import
const Chat = require('../models/Chat');
const chatController = {}
// 채팅 메시지 저장 함수
chatController.saveChat = async (msg, user) => {
const chat = new Chat({
chat: msg,
user: {
id: user._id, // mongoDB에서 부여해주는 데이터 키값
userName: user.userName
}
});
// 체팅 메시지 DB 저장
await chat.save();
return chat;
}
/controllers/chatController.js 파일 생성 후 유저 테이블 조작 함수를 작성합니다.
React.js 웹소켓 클라이언트 개발
웹소켓 서버에 연결하여 통신을 주고받을 프론트엔드 프로젝트는 리액트, 뷰 등으로 개발할 수 있습니다.
소켓 클라이언트 라이브러리 추가
"socket.io-client": "^4.7.2",
package.json 파일 dependencies에 Socket.IO 클라이언트 라이브러리를 추가합니다.
소켓 클라이언트 연결
import {io} from "socket.io-client";
// 소켓 클라이언트 인스턴스 생성
// 기본 Namespace "/"가 아닌 경우, 소켓 서버에 Namespace가 있어야만 입장 가능
const socket = io("http://백엔드서버주소:포트/소켓방고유값(Namespace)", {
query: {classCode}, // 백엔드 서버 연결 시 쿼리 파라미터 함께 전송
autoConnect: false, // 기본값 : true (소켓 클라이언트 인스턴스 생성 시 백엔드 서버 자동 연결)
transports: ['websocket','polling'], // 연결 방식 우선순위 지정
withCredentials: true, // 크로스 도메인 요청 시 쿠키, 인증 헤더 등 포함
timeout: 5000, // 서버 연결 시도 후 5초 안에 응답이 없으면 실패 처리
reconnection: true, // 연결이 끊겼을 때, 자동 재연결 허용
reconnectionAttempts: 10, // 자동 재연결 시도 횟수 (10번)
reconnectionDelay: 2000, // 재연결 시도 간격 (2초)
});
// 소켓 클라이언트 인스턴스 내보내기
export default socket;
src/utils/socket.js 파일 생성 후 Socket.IO 클라이언트 코드를 작성합니다.
소켓 클라이언트 통신 예시
import socket from "./utils/socket";
function App() {
// 리액트 상태 선언
const [user, setUser] = useState(null);
const [message, setMessage] = useState("");
const [messageList, setMessageList] = useState([]);
useEffect(() => {
// 소켓 백엔드 서버 수동 연결 (소켓 인스턴스 옵션 autoConnect: false인 경우)
// 소켓 서버 연결 시 "connection" 이벤트 발생
socket.connect();
// 소켓 서버에서 보낸 "message" 이벤트 수신 리스너 등록
socket.on("message", (msg) => {
// 받은 메시지 객체 리스트 추가
setMessageList((prevState) => prevState.concat(msg));
});
// 유저 입장
userJoin();
// 컴포넌트 unmount 시
return () => {
// 소켓에 등록한 이벤트 리스너 제거
socket.off("message");
// 소켓 연결 해제
socket.disconnect();
};
}, []);
// 유저 입장 시 호출되는 함수
const userJoin = () => {
const userName = prompt("이름을 입력하세요.");
socket.emit("join", userName, (res) => {
// 서버에서 acknowledgement로 응답한 경우 실행되는 콜백함수
console.log("콜백 데이터 : ", res);
// 서버 작업 성공 시 처리
if (res?.succese) {
// 유저 상태 저장
setUser(res.user);
}
});
}
// 메시지 전송 버튼 클릭 시 함수
const sendMessage = () => {
// 소켓 서버에 "sendMessage" 이벤트 전송
socket.emit("sendMessage", message);
}
return (
<div>
<div className="App">
<채팅리스트컴포넌트 messageList={messageList} user={user} />
<채팅입력컴포넌트 message={message} setMessage={setMessage} sendMessage={sendMessage} />
</div>
</div>
)
}
소켓을 사용하려는 컴포넌트에서 소켓 생성 유틸 import 후, 백엔드 서버에 연결합니다.
소켓 클라이언트 인스턴스는 브라우저 탭마다 하나씩 생성될 수 있습니다.
브라우저 탭을 종료하면 TCP 연결이 끊기면서 소켓 백엔드 서버가 “disconnect” 이벤트를 감지합니다.