프로그래밍 언어/NODE JS

실시간 경매 시스템 만들기 - 서버센트 이벤트 사용하기(1)

· 코딩마이데이

플링이나 웹 소켓을 통해 서버 시간을 받아올 수도 있지만, 이번 예제에서는 서버센트 이벤트를 사용해 서버의 시간을 받아올 것입니다. 주기적으로 서버 시간을 조회하는 데 양방향 통신이 필요하지 않기 때문입니다.

웹 소켓도 사용합니다. 웹 소켓은 경매를 진행하는 동안에 다른 사람이 참여하거나 입찰했을 때 모두에게 금액을 알리는 약할을 할 것입니다. 서버센트 이벤트와 웹 소켓은 같이 사용할 수 있습니다.

SSE 패키지와 Socket.IO 패키지를 동시에 설치하겠습니다.

$ npm i sse socket.io@2

 

서버와 sse, socket.io 모듈을 연결합니다.

 

app.js

const express = require("express");
const path = require("path");
const morgan = require("morgan");
const cookieParser = require("cookie-parser");
const session = require("express-session");
const passport = require("passport");
const nunjucks = require("nunjucks");
const dotenv = require("dotenv");

dotenv.config();
const indexRouter = require("./routes/index");
const authRouter = require("./routes/auth");
const { sequelize } = require("./models");
const passportConfig = require("./passport");
const sse = require("./see");
const webSocket = require("./socket");

const app = express();
passportConfig();
app.set("port", process.env.PORT || 8010);
app.set("view engine", "html");
nunjucks.configure("views", {
  express: app,
  watch: true,
});
sequelize
  .sync({ force: false })
  .then(() => {
    console.log("데이터베이스 연결 성공");
  })
  .catch((err) => {
    console.error(err);
  });

const sessionMiddleware = session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
    secure: false,
  },
});

app.use(morgan("dev"));
app.use(express.static(path.join(__dirname, "public")));
app.use("/img", express.static(path.join(__dirname, "uploads")));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(sessionMiddleware);
app.use(passport.initialize());
app.use(passport.session());

app.use("/", indexRouter);
app.use("/auth", authRouter);

app.use((req, res, next) => {
  const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
  error.status = 404;
  next(error);
});

app.use((err, req, res, next) => {
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== "production" ? err : {};
  res.status(err.status || 500);
  res.render("error");
});

const server = app.listen(app.get("port"), () => {
  console.log(app.get("port"), "번 포트에서 대기중");
});

webSocket(server, app);
sse(server);

 

see.js

const SSE = require("sse");

module.exports = (server) => {
  const sse = new SSE(server);
  sse.on("conection", (client) => {
    // 서버센트 이벤트 연결
    setInterval(() => {
      client.send(Date.now().toString());
    }, 1000);
  });
};

 

see 모듈을 불러와 new SSE(익스프레스 서버)로 서버 객체를 생성하면 됩니다. 생성한 객체에는 connection 이벤트 리스너를 연결하여 클라이언트와 연결할 때 어떤 동작을 할지 정의할 수 있습니다. 매개변수로 client 객체를 쓸 수 있습니다. 클라이언트에 메시지를 보낼 때 이 객체를 사용합니다. 라우터에서 SSE를 사용하고 싶다면 app.set 메서드로 client 객체를 등록하고, req.app.get 메서드로 가져오면 됩니다.

1초마다 접속한 클라이언트에 서버 시간 타임스탬프를 보내도록 했습니다.

client.send 메서드를 보넬 수 있습니다. 단, 문자열만 보낼 수 있으므로 숫자인 타임스탬프를 toString 메서드를 사용하여 문자열로 변경했습니다.

 

socket.js

const SocketIO = require("socket.io");

module.exports = (server, app) => {
  const io = SocketIO(server, { path: "/socket.io" });
  app.set("io", io);
  io.on("connection", (socket) => {
    // 웹 소켓 연결 시
    const req = socket.request;
    const {
      headers: { referer },
    } = req;
    const roomId = referer.split("/")[referer.split("/").length - 1];
    socket.join(roomId);
    socket.on("disconnect", () => {
      socket.leave(roomId);
    });
  });
};

 

Socket.IO와도 연결합니다. 이번에는 사용자 정의 네임스페이스를 쓰지 않고 기본 네임스페이스(/)로 연결했습니다. 경매 화면에서 실시간으로 입찰 정보를 올리기 위해 웹 소켓을 사용합니다. 클라이언트 연결 시 주소로부터 경매방 아이디를 받아와 socket.join으로 해당 방에 입장합니다.

연결이 끊겼다면 socket.leave로 해당 방에 나갑니다.