행궁동 데이터 엔지니어

반응형

저희 회사는 셀러가 자사몰을 쉽게 만들고 상품을 판매할 수 있도록 있도록 홈페이지 빌더 서비를 제공하고 있습니다.

 

때때로 셀러들이 현재 자사몰에 접속 중인 유저가 몇 명인지 궁금해하는 경우가 있어 쇼핑몰별 실시간 접속자 수를 가져오는 코드를 테스트해봤습니다.

 

- 실시간 접속자는 최근 30분 이내 접속한 유저로 정의했습니다.

 

 구현 목표

  • 셀러의 자사몰별로 접속자 추적: 개별 셀러 페이지에 접속한 유저를 Redis에 저장 
  • 30분 기준 세션 관리: 30분이 지난 유저 세션을 자동 삭제
  • API를 통해 실시간 접속 유저 조회 
  • 기술 스택: Vue (Frontend) + Node.js (Backend) + Redis
  • Test Code Link (GitHub)

(테스트 결과 샘플)

ABCShop에 방문
(왼쪽: Redis, 오른쪽: Active user 가져오기): 최근 30분 동안 ABCShop에 4명의 유저가 접속

 

📌 테스트 개요

Redis의 Sorted Set (ZSET)을 활용하여 실시간 접속자 수를 관리

 

유저가 자사몰에 접속하면 session_id를 발급
Redis ZSET(active_users:sellerId)에 session_id와 timestamp를 저장

(POST /track-active-users)


30분이 지난 유저 세션은 삭제 (접속자 목록을 조회하는 시점에 삭제)
API를 호출하여 현재 접속자 목록을 확인 가능
(GET /get-active-users/:sellerId)

 

1.  Backend (Node.js + Express + Redis)

✅ 주요 기능

  • POST /track-active-users → 유저 접속 기록을 Redis에 저장
  • GET /get-active-users/:sellerId → 최근 30분 내 접속한 유저 목록 조회
  • ZSET(ZADD, ZREMRANGEBYSCORE, ZRANGEBYSCORE)을 활용하여 유저 관리

📌 server.ts (Backend 코드)

import express, { Request, Response, Application } from "express";
import cors from "cors";
import { createClient } from "redis";
import dotenv from "dotenv";

dotenv.config(); // .env 환경 변수 로드

const app: Application = express(); // ✅ Application 타입 지정
app.use(express.json()); // JSON 요청을 처리하기 위한 미들웨어 추가
app.use(cors());

// 🔹 Redis 클라이언트 설정 (비밀번호 포함)
const redisClient = createClient({
  url: `redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`, // Redis 인증 추가
});

redisClient.on("error", (err) => console.error("Redis error:", err));

(async () => {
  try {
    await redisClient.connect();
    console.log("✅ Connected to Redis");
  } catch (err) {
    console.error("❌ Redis connection failed:", err);
  }
})();

// 🔹 요청 데이터 타입 정의
interface TrackActiveUsersRequest {
  sellerId: string;
  sessionId: string;
}

// 🔹 방문자 추적 API (타입 오류 해결)
app.post(
  "/track-active-users",
  async (req: Request<{}, {}, TrackActiveUsersRequest>, res: Response): Promise<void> => {
    try {
      const { sellerId, sessionId } = req.body;

      // 🔹 요청 데이터 유효성 검사
      if (typeof sellerId !== "string" || typeof sessionId !== "string") {
        res.status(400).json({ error: "Invalid sellerId or sessionId" });
        return;
      }

      const now = Math.floor(Date.now() / 1000); // 현재 타임스탬프 (초 단위)

      // 🔹 Redis ZSET에 추가 (배열 형식 사용)
      await redisClient.zAdd(`active_users:${sellerId}`, [
        { score: now, value: sessionId },
      ]);

      res.json({ message: "User tracked successfully" });
    } catch (error) {
      console.error("❌ Error tracking active users:", error);
      res.status(500).json({ error: "Internal server error" });
    }
  }
);

// 🔹 요청 데이터 타입 정의
interface SellerRequest {
  sellerId: string;
}

// 🔹 최근 30분 내 접속 유저 가져오는 API
app.get("/active-users/:sellerId", async (req: Request<SellerRequest>, res: Response): Promise<void> => {
  try {
    const { sellerId } = req.params;

    if (!sellerId) {
      res.status(400).json({ error: "Missing sellerId" });
      return;
    }

    const now = Math.floor(Date.now() / 1000); // 현재 타임스탬프 (초 단위)
    const cutoffTime = now - 1800; // 30분 전 시간 계산

    // 🔹 1️⃣ 30분이 지난 유저 삭제
    await redisClient.zRemRangeByScore(`active_users:${sellerId}`, 0, cutoffTime);

    // 🔹 2️⃣ 최근 30분 내 접속한 유저 목록 가져오기
    const activeUsers = await redisClient.zRangeByScore(`active_users:${sellerId}`, cutoffTime, now);

    res.json({ sellerId, activeUsers, count: activeUsers.length });
  } catch (error) {
    console.error("❌ Error fetching active users:", error);
    res.status(500).json({ error: "Internal server error" });
  }
});

// 🔹 서버 실행
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`🚀 Server running on http://localhost:${PORT}`);
});

 

 

2. Frontend (Vue)

✅ 주요 기능

  • 페이지 접속 시 session_id 발급 (로컬 스토리지에 저장)
  • 유저가 셀러 페이지에 방문하면 서버에 접속 정보 전송

📌 SellerPage.vue (Frontend 코드)

<script setup>
import { onMounted } from "vue";
import { useRoute } from "vue-router";
import { v4 as uuidv4 } from "uuid";

const route = useRoute();
const sellerId = route.params.sellerId;

onMounted(() => {
  let sessionId = localStorage.getItem("session_id");

  if (!sessionId) {
    sessionId = uuidv4();
    localStorage.setItem("session_id", sessionId);
  }

  fetch("http://localhost:3000/track-active-users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ sellerId, sessionId }),
  });
});
</script>

<template>
  <div>
    <h2>Welcome to {{ sellerId }}'s Shop</h2>
    <p>Tracking your visit...</p>
  </div>
</template>

 

 

📌 사용한 REDIS ZSET (Sorted Set) 명령어 정리

Redis ZSET을 이용하니 생각보다 간단하게 실시간 접속자를 추적할 수 있었고 아래에 사용한 Redis ZSET 명령어를 정리해 봤습니다.

- ZSET 특징: 집합의 멤버들이 Score순으로 자동 정렬되고 O(N)이 아닌 O(log N + M)  탐색(log N) + 삭제 개수(M)의 효율적인 시간복잡도로 멤버를 삭제할 수 있습니다.

기능 Redis 명령어 명령어 샘플 설명
유저 접속 기록 저장 ZADD ZADD active_users:ABCShop 1717215600 "session_123" 유저 세션을 ZSET에 저장
30분이 지난 유저 삭제 ZREMRANGEBYSCORE ZREMRANGEBYSCORE active_users:ABCShop 0 {current_timestamp - 1800} 30분 이전 데이터 삭제
최근 30분 접속자 조회 ZRANGEBYSCORE ZRANGEBYSCORE active_users:ABCShop {current_timestamp - 1800} {current_timestamp} 특정 시간 범위의 유저 조회

 

ZSET으로 저장된 데이터 샘플: 집합의 멤버들이 Score순으로 자동 정렬됩니다.

 

 

 

 

상세 코드 링크도 아래에 남깁니다.

혹시 테스트하고 싶은 분들은 편하게 사용하세요.
GitHub: https://github.com/menthamin/live-user-monitor

 

GitHub - menthamin/live-user-monitor

Contribute to menthamin/live-user-monitor development by creating an account on GitHub.

github.com

 

 

Github

 

반응형

이 글을 공유합시다

facebook twitter kakaoTalk kakaostory naver band