Today I Learned

16 TIL 소켓 socket.io 쇼핑몰 실시간 구매 알림 기능

냥냥's 2021. 10. 2. 09:53

TCP란?

  • 서버와 클라이언트간 신뢰성 있는 데이터 송수신을 위해 만들어진 프로토콜입니다.
  • 연결 지향성 프로토콜이라고도 부릅니다.
  • 데이터를 나눠서 보낼수 있으며, 데이터를 받는쪽에서 나눠 받은 데이터를 재조립합니다. 만약 누락된 데이터가 존재하면 다시 요청해서 받아와 완전한 데이터를 만듭니다.
  • TCP로 서버/클라이언트간 연결이 된 경우 데이터를 양방향으로 주고 받을수 있습니다.
  • 데이터의 순서가 뒤바뀌는 일 없이 안정적이라 신뢰가 가능합니다.
  • UDP에 비해 데이터 송수신 비용(부담)이 크다는 특성을 가졌습니다.
  • UDP보다 전송 속도가 느립니다.

UDP란?

  • TCP와 다르게 비연결성 프로토콜입니다.
  • 데이터를 보내고 제대로 받았는지 확인하지 않아, 데이터가 제대로 도착했는지 보장하지 않는 신뢰도가 비교적 낮습니다.
  • 데이터를 순차적으로 보내도 받는 쪽에서는 다른 순서로 전달받을 수 있습니다.
  • 데이터를 보내기만 하고 별 다른 처리를 하지 않기 때문에 TCP에 비해 비용(부담)이 적다는 특성을 가졌습니다.
  • TCP보다 전송 속도가 빠릅니다.

소켓이란 무엇인가?

    1. 소켓에 대한 간단한 설명
    • 소켓의 역할?그럼 네트워크에서의 소켓은? 우리가 네트워크에서 데이터를 송수신하기 위해 반드시 거쳐야 하는 연결부에 해당합니다!
    • 현실로 비유하자면 마치 벽에 있는 콘센트 구멍과 비슷합니다! 우리가 전기를 사용하기 위해 반드시 거쳐야 하는 연결부에 해당하죠!
    • 소켓의 종류?아주 일반적으로는 안정적인 데이터 송수신을 위해 TCP 소켓을 사용하는 경우가 대부분이지만, 일부 패킷이 손실되어도 괜찮거나 빠른 전송 속도가 필요한 경우 UDP 소켓을 사용하기도 합니다.
    • 소켓의 역할은 언제나 같지만 종류는 여러가지가 있습니다! 대표적으로 TCP, UDP 프로토콜을 사용하는 2가지의 소켓이 있는데요,
    • 패킷이란?
    • 네트워크 소켓이 현실의 콘센트와 비슷하다면, 패킷은 쉽게 말해 콘센트 배선에 흐르는 전기와 비슷합니다. 소켓을 통해 송수신하는 데이터 덩어리 하나가 한개의 패킷이라고 표현합니다.
    1. 웹소켓은 무엇인가?
    실시간 웹 서비스를 제공하기 위해 만들어진 Socket이라고 생각하면 됩니다.
  • 최근 Google Docs 등 여러 협업툴이 실시간 공동 편집 기능, 웹 메신저 등에서 많이 사용되는 기술로 최근 점점 많이 사용하는 기술이지만 일부 브라우저들이 웹소켓을 지원하지 않기 때문에 모든 브라우저에서의 동작을 보장하지는 못합니다.
    1. socket.io?
    자바스크립트를 사용해 웹소켓을 사용하길 원한다면 가장 많이 사용되는 라이브러리입니다. 그러나 이 라이브러리는 순수한 웹소켓 기술만 이용한 라이브러리가 아닙니다.이 어려움을 해결하기 위해 socket.io는 웹소켓을 사용할 수 없는 브라우저인 경우 서버에서 데이터를 일정 간격마다 받아오는 polling 기능으로 실시간 기능 구현을 가능케 해줍니다.
  • 위에서 말했듯 웹소켓 기술은 아직 모든 브라우저에서 동작하지는 못하기 때문에, 모든 사용자를 고려해야 하는 경우 실시간성 기능 구현에 어려움이 생기게 됩니다.
    1. socket.io는 웹소켓과 다르다?
    그렇습니다! 엄밀히 따지면 socket.io는 웹소켓을 포함하여, 웹소켓을 사용하지 못하는 환경에서도 웹소켓과 비슷하게 사용이 가능하도록 구현해놓은 라이브러리입니다.추가 참고 내용: https://d2.naver.com/helloworld/1336
  • 그렇기 때문에 socket.io는 웹소켓과 완전히 동일하다고 오해하지 않으시길 바랍니다!

프로젝트 폴더를 생성하고

모듈부터 설치한다.

npm i socket.io -S

간단한 app.js를 만들었다.

const io = require("socket.io")(3000, {
  cors: {
    origin: "*",
    methods: ["GET", "POST"],
  },
});

io.on("connection", (socket) => {
  console.log("새로운 소켓이 연결됐어요!");

  socket.on("message", (data) => {
    console.log(data);
  });
});
io.on("connection", (socket) => {
  socket.send("Hello!");

  socket.on("message", (data) => {
    console.log(data);
  });
});
node app.js

쇼핑몰 실시간 구매 알림 구현

저번 시간에서 클라이언트와 소켓 연결도 하고, 소켓으로 데이터를 간단하게 주고 받는 방법에 대해서 알아봤다면, 이번에는 정말로 누군가 구매를 하면 실시간으로 알림이 뜨도록 구현을 해볼게요!

app.js를 고친다!

const express = require("express");
const { Server } = require("http"); // 1. 모듈 불러오기
const socketIo = require("socket.io"); // 1. 모듈 불러오기

... // 생략

const app = express();
const http = Server(app); // 2. express app을 http 서버로 감싸기
const io = socketIo(http); // 3. http 객체를 Socket.io 모듈에 넘겨서 소켓 핸들러 생성

// 4. 소켓 연결 이벤트 핸들링
io.on("connection", (sock) => {
  console.log("새로운 소켓이 연결됐어요!");

  sock.on("disconnect", () => {
    console.log(sock.id, "연결이 끊어졌어요!");
  });
});

... // 생략

// 5. app 대신 http 객체로 서버 열기
http.listen(8080, () => {
  console.log("서버가 요청을 받을 준비가 됐어요");
});

 

sock.emit("BUY_GOODS", {
    nickname: "서버가 보내준 구매자 닉네임",
    goodsId: 10, // 서버가 보내준 상품 데이터 고유 ID
    goodsName: "서버가 보내준 구매자가 구매한 상품 이름",
    date: "서버가 보내준 구매 일시",
  });

이런식으로 이벤트 데이터를 줘야 한다!

sock.on("BUY", (data) => {
    console.log(data);
  });

 

const express = require("express");
const Http = require("http");
const socketIo = require("socket.io");
const { Op } = require("sequelize");
const jwt = require("jsonwebtoken");
const { User, Goods, Cart } = require("./models");
const authMiddleware = require("./middlewares/auth-middleware");

const app = express();
const http = Http.createServer(app);
const io = socketIo(http);
const router = express.Router();

const socketIdMap = {};

function emitSamePageViewerCount() {
  const countByUrl = Object.values(socketIdMap).reduce((value, url) => {
    return {
      ...value,
      [url]: value[url] ? value[url] + 1 : 1,
    };
  }, {});

  for (const [socketId, url] of Object.entries(socketIdMap)) {
      const count = countByUrl[url];
      io.to(socketId).emit("SAME_PAGE_VIEWER_COUNT", count);
  }
}

io.on("connection", (socket) => {
  socketIdMap[socket.id] = null;
  console.log("누군가 연결했어요!");

  socket.on("CHANGED_PAGE", (data) => {
    console.log("페이지가 바뀌었대요", data, socket.id);
    socketIdMap[socket.id] = data;

    emitSamePageViewerCount();
  });

  socket.on("BUY", (data) => {
    const payload = {
      nickname: data.nickname,
      goodsId: data.goodsId,
      goodsName: data.goodsName,
      date: new Date().toISOString(),
    };
    console.log("클라이언트가 구매한 데이터", data, new Date());
    socket.broadcast.emit("BUY_GOODS", payload);
  });

  socket.on("disconnect", () => {
    delete socketIdMap[socket.id];
    console.log("누군가 연결을 끊었어요!");
    emitSamePageViewerCount();
  });
});

완성하였다!