본문 바로가기

개발 공부

Node.js, Redis

Node.js — 자바스크립트의 서버 확장

Node.js는 구글 크롬의 V8 자바스크립트 엔진을 기반으로 만들어진 비동기 이벤트 기반의 런타임입니다.

Node는 기존의 자바스크립트를 브라우저 밖, 즉 서버 환경에서도 실행할 수 있도록 만든 플랫폼입니다.

특징

  • 단일 스레드 기반의 논블로킹 I/O
    • Node는 하나의 스레드로 동작하지만, 작업을 이벤트 루프에 비동기로 위임하면서 수천 개의 동시 연결을 처리할 수 있습니다.
    • 이는 특히 I/O 중심 애플리케이션(예: API 서버, 채팅 서버)에 최적화되어 있습니다.
  • NPM(Node Package Manager)
    • 방대한 생태계를 통해 다양한 라이브러리를 즉시 도입할 수 있습니다.
    • Redis 클라이언트부터 파일 처리, 인증까지 대부분 패키지로 빠르게 구축 가능합니다.
  • 빠른 프로토타이핑과 유지보수성
    • 프론트엔드와 같은 언어(Typescript/JS)를 쓰기 때문에 전체 스택의 통일성이 좋아 협업과 유지보수가 수월합니다.
  • Express, Nest.js, Fastify 등 다양한 프레임워크 지원

 


 

Redis — 인메모리 기반의 초고속 데이터 저장소

 

Redis는 Remote Dictionary Server의 약자로, 인메모리(In-Memory) 데이터 저장소입니다.

데이터를 디스크가 아닌 메모리에 저장하기 때문에 읽기/쓰기 속도가 극도로 빠릅니다.

특징

  • 다양한 데이터 구조
    • 단순 문자열뿐 아니라 List, Hash, Set, Sorted Set, Bitmap, HyperLogLog, Stream 등 다양한 자료구조 지원
  • TTL(Time To Live)
    • 캐시 데이터의 자동 만료 기능 제공. 예를 들어 로그인 세션은 30분 후 자동 삭제 등 설정 가능.
  • Persistence 옵션
    • Redis는 in-memory지만 AOF(Append Only File)와 RDB(Snapshot) 기반으로 디스크에 백업해 장애 복구도 가능합니다.
  • Pub/Sub 모델
    • 비동기 메시징 시스템 구현 가능. 채팅, 알림, 실시간 데이터 전파에 적합
  • 분산 시스템 구성 가능
    • Redis Sentinel(고가용성), Cluster(수평 확장)를 통해 대규모 서비스 구성 가능

 

 


 

Node.js + Redis 사용 케이스

기능 설명
세션 저장소 Express와 Redis를 연동해 로그인 세션을 Redis에 저장하고 TTL로 자동 만료
API 응답 캐싱 자주 조회되는 데이터(DB 조회) 결과를 Redis에 저장해 트래픽 감소
실시간 채팅/알림 Redis의 Pub/Sub을 활용해 WebSocket과 연동
Background Job Queue Bull, Agenda.js 등의 Redis 기반 큐를 활용해 비동기 작업 처리 (이메일, 썸네일 처리 등)

 

 


 

예시: 실시간 알림 시스템

사용자가 로그인하면, 서버에서 실시간 알림을 Redis pub/sub으로 구독하고, 캐시를 우선 참조하여 알림을 제공하며, 새로운 알림은 웹소켓으로 프론트에 push됩니다.

 

1. Nest.js 백엔드: Redis 캐시 + pub/sub 구성

1) 의존성 설치

npm install ioredis @nestjs/websockets @nestjs/platform-socket.io

 

 

2) Redis 모듈화 (redis.module.ts)

import { Module, Global } from '@nestjs/common';
import Redis from 'ioredis';

@Global()
@Module({
  providers: [
    {
      provide: 'REDIS_PUB',
      useFactory: () => new Redis({ host: 'localhost', port: 6379 }),
    },
    {
      provide: 'REDIS_SUB',
      useFactory: () => {
        const sub = new Redis({ host: 'localhost', port: 6379 });
        sub.subscribe('notifications');
        return sub;
      },
    },
  ],
  exports: ['REDIS_PUB', 'REDIS_SUB'],
})
export class RedisModule {}
  • @Global()을 사용하면 이 모듈이 NestJS 애플리케이션 전체에서 싱글톤으로 공유됩니다.
  • 즉, 다른 모듈에서 imports: [RedisModule] 없이도 주입해서 사용할 수 있습니다.
  • REDIS_PUB : 퍼블리셔 전용 Redis 인스턴스입니다.
  • 주로 publish(channel, message) 같은 명령을 실행할 때 사용합니다.
  • useFactory는 NestJS의 팩토리 프로바이더로, 직접 객체를 생성해 반환합니다.
  • REDIS_SUB : 서브스크라이버 전용 Redis 인스턴스입니다.
  • subscribe('notifications')를 통해 알림 채널을 구독합니다.
  • 이후 이 인스턴스에 .on('message', callback)을 붙여 수신 이벤트를 처리할 수 있습니다.

 

 

3) 실시간 알림 서비스 (notification.service.ts)

@Injectable()
export class NotificationService {
  constructor(
    @Inject('REDIS_PUB') private readonly redisPub: Redis,
    @Inject('REDIS_SUB') private readonly redisSub: Redis,
  ) {}

  async sendNotification(userId: string, message: string) {
    const payload = { userId, message };
    await this.redisPub.publish('notifications', JSON.stringify(payload));
  }
}

 

  • 'REDIS_PUB'과 'REDIS_SUB'는 우리가 RedisModule에서 정의한 토큰입니다.
  • redisPub: 알림을 발행하는 퍼블리셔 (publish)
  • redisSub: 알림을 수신하는 서브스크라이버 (subscribe)
  • 여기서는 사용하지 않지만, Gateway 등에서 .on('message')로 메시지를 받을 때 사용됩니다.

 

 

4) Gateway로 WebSocket 연결 (Nest.js)

@WebSocketGateway({ cors: true })
export class NotificationGateway implements OnModuleInit {
  @WebSocketServer() server: Server;

  constructor(@Inject('REDIS_SUB') private readonly redisSub: Redis) {}

  onModuleInit() {
    this.redisSub.on('message', (channel, message) => {
      const data = JSON.parse(message);
      this.server.to(data.userId).emit('notification', data.message);
    });
  }

  handleConnection(socket: Socket) {
    const userId = socket.handshake.query.userId;
    socket.join(userId);
  }
}

 

  • NestJS에서 Socket.IO 기반 웹소켓 서버를 생성합니다.
  • 애플리케이션 시작 후 Redis로부터 메시지를 수신할 수 있도록 리스너를 등록합니다.
  • data.userId에 해당하는 WebSocket room에 접속한 클라이언트에게 메시지를 전송합니다.
  • handleConnection()
    • 클라이언트가 WebSocket으로 연결될 때 호출되는 함수입니다.
    • 클라이언트가 ?userId=abc123 형태로 연결 요청을 보냈다고 가정
    • 이 유저의 소켓을 userId라는 이름의 "room"에 가입시켜 이후 특정 유저 타겟팅이 가능하게 만듭니다.

 

 

 

2.Next.js(React) 프론트에서 WebSocket 수신

1) Socket.IO 클라이언트 설치

npm install socket.io-client

 

2) React Hook으로 알림 수신

import { useEffect } from 'react';
import io from 'socket.io-client';

export default function NotificationClient({ userId }: { userId: string }) {
  useEffect(() => {
    const socket = io('http://localhost:3000', {
      query: { userId },
    });

    socket.on('notification', (msg) => {
      alert(`새 알림: ${msg}`);
    });

    return () => socket.disconnect();
  }, [userId]);

  return null;
}

 

  • useEffect
    • 컴포넌트가 마운트될 때 WebSocket 연결을 초기화하고,
    • 언마운트되면 연결을 해제(disconnect) 합니다.
    • userId가 바뀔 경우에도 재연결됩니다.
  • io('http://localhost:3000', { query: { userId } })
    • 서버와 Socket.IO로 연결을 시도합니다.
    • 서버로는 다음과 같은 쿼리스트링이 전달됩니다:
      ws://localhost:3000/socket.io/?userId=abc123
    • 서버의 handleConnection()에서 이 값을 읽어 room에 join 시킵니다
  • socket.on('notification', callback)
    • 서버에서 다음과 같이 emit한 메시지를 수신합니다
    • 클라이언트에서는 alert으로 사용자에게 알림을 띄웁니다
  • return () => socket.disconnect();
    • 컴포넌트가 언마운트될 때 소켓 연결을 정리(disconnect) 합니다.
    • 이를 하지 않으면 메모리 누수 또는 중복 수신 문제가 생길 수 있습니다.

'개발 공부' 카테고리의 다른 글

Pub/Sub (2) - Angular + RxJS  (0) 2025.06.29
Pub/Sub (1)  (0) 2025.06.28
트랜잭션(Transaction)  (0) 2025.06.19
MCP (Model Context Protocol) (3)  (1) 2025.06.17
MCP (Model Context Protocol) (1)  (11) 2025.06.12