sns 프로젝트

메세지 채팅 기능 구현하기(백엔드 코드, redis 연결 )

우주속공간 2024. 1. 24. 20:17

이 글은 아래의 메세지 채팅 기능 구현하기 (프론트 코드)와 이어지는 글입니다!  이번 글에서는 백엔드 코드에 대해서 적어볼 예정입니다.

 

 

메세지 채팅 기능 (웹소켓, redis, 실시간 알람 기능)

미리보기 📁 채팅 기능 미리보기 실시간 소통을 위해서 웹소켓 통신 방법을 사용하였고, 채팅 데이터를 저장하기 위해서 redis를 사용했다. redis 를 사용하게 된 이유는 기존에 사용하던 mysql 데

in-my-universe23.tistory.com

 

미리보기


 

📁 채팅 기능 미리보기

 

 

 

백엔드

 

웹소켓을 사용해서 실시간 통신을 구축하고 redis를 통해서 유저들 소켓 아이디, 소켓 채팅 데이터 등을 

  • Redis 클라이언트 설정 및 연결 : 데이터 베이스 연결을 설정하고 관리
  • 소켓 이벤트 핸들러 : 클라이언트와 실시간 통신 관리
    • login : 사용자가 로그인할때마다 사용자 소켓 아이디를 저장하여 나중에 사용자에게 메세지를 보낼 수 있도록 함
    • REQUEST_DATA : 사용자가 채팅페이지를 열 때 이전 채팅 데이터를 불러오는데 사용
    • START_CHAT : 새로운 채팅방을 개설할려고 했을때 사용
    • SEND_MESSAGE : 사용자가 채팅방에 메세지를 보낼때 사용

 

실제 구현한 코드


 

Redis 클라이언트 설정 및 연결 : 서버와 데이터베이스 사이의 연결 담당

const redisClient = redis.createClient({
  url: `redis://${process.env.REDIS_USERNAME}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
  legacyMode: true, // 반드시 설정 !!
});
redisClient.on("connect", () => {
  console.info("Redis connected!");
});
redisClient.on("error", (err: any) => {
  console.error("Redis Client Error", err);
});
redisClient.connect().then(); // redis v4 연결 (비동기)
const redisCli = redisClient.v4; // 기본 redisClient 객체는 콜백기반인데 v4버젼은 프로미스 기반이라 사용

io.on("connection", (socket: any) => {
  //connection

  socket.on("disconnect", async () => {
    await redisCli.SREM("currentUser", `${socket.id}`).then(() => {
      clearInterval(socket.interval);
    });
    console.log("클라이언트 접속 해제");
  });

  //* 에러 시
  socket.on("error", (error: any) => {
    console.error(error);
  });

 

 

login 이벤트 : 로그인하면 유저 소켓아이디 데이터 저장하고 현재 접속한 유저들을 보여주는 currentUser 집합에도 넣어줌

  • key: 유저 아이디, value: 유저 소켓아이디 형식으로 저장
  • currentUser 키를 중심으로 소켓아이디를 계속 넣어줌.

🚨 만약 현재 유저에 대한 소켓아이디 데이터가 존재한다면, 이전 데이터는 지우고 새로 저장

socket.on("login", async (data: any) => {
    await socket.join("client");

    await Users.findOne({
      where: {
        email: data.email,
      },
    }).then(async (r: any) => {
      const checkKey = await redisCli.EXISTS(`${r.user_id}`);

      if (checkKey) {
        await redisCli.GET(`$(r.user_id)`).then(() => {
          redisCli.DEL(`${r.user_id}`);
        });
      }
      await redisCli.SET(`${r.user_id}`, `${socket.id}`);

      await redisCli.SADD("currentUser", `${socket.id}`);

      const checkCurrent = await redisCli.SMEMBERS("currentUser");
    });

    // Redis에 userID와 socketID를 저장한다.
  });

 

START_CHAT : 유저가 새로운 채팅을 시작할려고 할 경우, 해당 채팅방 존재 유무를 확인하고 이미 존재할 경우 해탕 채팅방의 메세지 데이터를 넘겨주고 채팅방이 존재하지 않는 경우 새로운 채팅방 정보를 redis에 저장. 

 socket.on("START_CHAT", async (data: any) => {
    const roomId = `chat-${data.users.sort().join("-")}`;

    // Redis에서 해당 roomId의 존재 여부를 확인
    const chatExist = await redisCli.EXISTS(roomId);

    if (chatExist) {
      // 채팅방이 이미 존재하는 경우, 해당 채팅방의 메시지 데이터 불러오기
      const allData = await redisCli.ZRANGE(roomId, 0, -1);
      if (allData.length > 0) {
        io.to(socket.id).emit("BEFORE_DATA", roomId);
      }
    } else {
      // 채팅방이 존재하지 않는 경우, 새로운 채팅방 정보를 Redis에 저장
      console.log("Creating new chat room:", roomId);
      await redisCli.SET(`roomId:${roomId}`, JSON.stringify(data.users));
    }
  });

 

REQUEST_DATA :  페이지가 처음 로딩될때 프론트로부터 현재 로그인한 유저 정보를 받아서 유저가 속한 모든 채팅방 데이터를 조회함

⇒ 조회 후에 RESPOND_DATA 통신으로 프론트에 조회한 데이터를 보내줌

  // 메세지 페이지 처음 로딩할때 체팅 데이터 가져오기
  socket.on("REQUEST_DATA", async (data: any) => {
    const findData = await redisCli.KEYS(`chat-*${data.id}*`);

    let respondData: any = [];
    await Promise.all(
      findData.map(async (t: any) => {
        let chatData = await redisCli.ZRANGE(`${t}`, 0, -1);

        if (chatData[0] === undefined) {
          return redisCli.DEL(t);
        } else {
          chatData.forEach((item: any) => {
            const parsedItem = JSON.parse(item);
            return respondData.push(parsedItem);
          });
        }
      })
    );
     io.to(socket.id).emit("RESPOND_DATA", respondData);

  });

 

SEND_MESSAGE : 프론트에서 보낸 채팅 데이터를 받고 redis에 저장후 메세지를 받는 사람의 소켓 id로 실시간으로 메세지 전송

socket.on("SEND_MESSAGE", async (m: any) => {
    console.log(`채팅 도착 ${m.message}`);

    let messageData = {
      send: `${m.send}`,
      receive: `${m.receive}`,
      message: `${m.message}`,
      date: `${m.date}`,
      roomId: `${m.roomId}`,
    };

    let change = JSON.stringify(messageData);

    let score = Number(m.score);
    await redisCli.ZADD(`${m.roomId}`, { score: score, value: change });


    // 메시지를 받는 사람의 소켓 ID를 Redis에서 조회
    let getSocketId = await redisCli.GET(`${m.receive}`);

    // 조회한 소켓 ID로 실시간으로 메시지 전송
    if (getSocketId) {
      io.to(getSocketId).emit("RECEIVE_MESSAGE", change);
    } else {
      console.log("소켓 ID를 찾을 수 없습니다.");
    }



});

 

 

참고사이트


채팅 시스템 설계

DynamoDB를 사용하기 전에 알았으면 좋았을 것들

NodeJS를 이용한 채팅서버 구축 하기(express, Socket.io, Redis) -2-

Socket.IO - Redis adapter Redis 어댑터는 Pub/Sub 메커니즘에 의존합니다.

[REDIS] 📚 Node.js 에서 redis 모듈 사용법 (캐싱 & 세션 스토어)

[REDIS] 📚 레디스 소개 & 사용처 (캐시 / 세션) - 한눈에 쏙 정리

AWS Elasticbeanstalk + Node.js + Socket.io + Redis를 이용한 채팅서버 (2)

Redis adapter | Socket.IO

[React] socket.io로 통신

[Spring] WebSocket 채팅 데이터 캐싱 전략 With Redis 1편

Redis는 언제 써야할까?

구현한 채팅에 캐시 적용해서 성능 개선하기