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 |