Node.js 애플리케이션에서 Redis를 연동하면 캐싱, 세션 관리, 실시간 메시징 등 다양한 기능을 구현할 수 있습니다. 이 글에서는 ioredis 패키지를 사용하여 Redis를 효과적으로 활용하는 방법을 알아봅니다.
1. Redis 소개와 특징
Redis(Remote Dictionary Server)는 오픈소스 인메모리 데이터 저장소입니다. 모든 데이터를 메모리에 저장하기 때문에 읽기와 쓰기 속도가 매우 빠릅니다.
Redis의 주요 특징:
- 인메모리 저장: 데이터를 RAM에 저장하여 마이크로초 단위의 응답 시간 제공
- 다양한 데이터 구조: String, Hash, List, Set, Sorted Set, Stream 등 지원
- 영속성: RDB 스냅샷과 AOF 로그를 통한 데이터 영구 저장 옵션
- 복제: 마스터-슬레이브 복제를 통한 고가용성 구현
- Pub/Sub: 메시지 발행/구독 패턴 지원
- 클러스터: 수평 확장을 위한 클러스터 모드 지원
- 트랜잭션: MULTI/EXEC 명령어를 통한 원자적 연산
2. ioredis 패키지 설치 및 설정
Node.js에서 Redis를 사용하기 위해 ioredis 패키지를 사용합니다. ioredis는 기능이 풍부하고 성능이 우수한 Redis 클라이언트입니다.
2.1 설치
npm install ioredis
TypeScript를 사용하는 경우 타입 정의가 패키지에 포함되어 있어 별도 설치가 필요 없습니다.
2.2 기본 연결 설정
const Redis = require('ioredis');
// 기본 연결 (localhost:6379)
const redis = new Redis();
// 호스트와 포트 지정
const redis = new Redis(6379, '127.0.0.1');
// URL 형식으로 연결
const redis = new Redis('redis://username:password@host:6379/0');
// 옵션 객체로 연결
const redis = new Redis({
host: '127.0.0.1',
port: 6379,
password: 'your-password',
db: 0,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
maxRetriesPerRequest: 3,
enableReadyCheck: true,
connectTimeout: 10000
});
2.3 연결 이벤트 처리
const Redis = require('ioredis');
const redis = new Redis();
redis.on('connect', () => {
console.log('Redis 연결 시작');
});
redis.on('ready', () => {
console.log('Redis 연결 준비 완료');
});
redis.on('error', (err) => {
console.error('Redis 에러:', err);
});
redis.on('close', () => {
console.log('Redis 연결 종료');
});
redis.on('reconnecting', () => {
console.log('Redis 재연결 시도 중');
});
3. 기본 명령어
3.1 SET과 GET
const Redis = require('ioredis');
const redis = new Redis();
async function basicOperations() {
// 값 저장
await redis.set('name', 'John');
// 값 조회
const name = await redis.get('name');
console.log(name); // 'John'
// 존재하지 않는 키 조회
const notExist = await redis.get('notExist');
console.log(notExist); // null
// NX 옵션: 키가 없을 때만 저장
await redis.set('name', 'Jane', 'NX'); // 실패 (이미 존재)
// XX 옵션: 키가 있을 때만 저장
await redis.set('name', 'Jane', 'XX'); // 성공
// EX 옵션: 초 단위 만료 시간 설정
await redis.set('session', 'abc123', 'EX', 3600);
// PX 옵션: 밀리초 단위 만료 시간 설정
await redis.set('temp', 'data', 'PX', 5000);
}
basicOperations();
3.2 DEL과 EXISTS
async function deleteAndCheck() {
await redis.set('key1', 'value1');
await redis.set('key2', 'value2');
await redis.set('key3', 'value3');
// 키 삭제 (단일)
const deleted = await redis.del('key1');
console.log(deleted); // 1 (삭제된 키 개수)
// 키 삭제 (다중)
const deletedMultiple = await redis.del('key2', 'key3');
console.log(deletedMultiple); // 2
// 키 존재 확인
const exists = await redis.exists('key1');
console.log(exists); // 0 (존재하지 않음)
await redis.set('active', 'true');
const activeExists = await redis.exists('active');
console.log(activeExists); // 1 (존재함)
}
3.3 EXPIRE와 TTL
async function expireOperations() {
await redis.set('token', 'xyz789');
// 만료 시간 설정 (초 단위)
await redis.expire('token', 3600);
// 남은 시간 확인 (초 단위)
const ttl = await redis.ttl('token');
console.log(ttl); // 3600 (또는 그보다 작은 값)
// 남은 시간 확인 (밀리초 단위)
const pttl = await redis.pttl('token');
console.log(pttl); // 밀리초 단위 값
// 특정 시점에 만료 (Unix timestamp)
const expireAt = Math.floor(Date.now() / 1000) + 7200;
await redis.expireat('token', expireAt);
// 만료 시간 제거 (영구 저장)
await redis.persist('token');
const afterPersist = await redis.ttl('token');
console.log(afterPersist); // -1 (만료 시간 없음)
}
3.4 INCR과 DECR
async function counterOperations() {
// 카운터 초기화
await redis.set('visitors', '0');
// 1씩 증가
const incr = await redis.incr('visitors');
console.log(incr); // 1
// 지정 값만큼 증가
const incrby = await redis.incrby('visitors', 10);
console.log(incrby); // 11
// 1씩 감소
const decr = await redis.decr('visitors');
console.log(decr); // 10
// 지정 값만큼 감소
const decrby = await redis.decrby('visitors', 5);
console.log(decrby); // 5
// 부동소수점 증가
await redis.set('price', '10.5');
const incrbyfloat = await redis.incrbyfloat('price', 0.5);
console.log(incrbyfloat); // '11'
}
4. 데이터 구조 활용
4.1 Hash
Hash는 필드-값 쌍의 집합으로, 객체를 저장하기에 적합합니다.
async function hashOperations() {
// 단일 필드 설정
await redis.hset('user:1001', 'name', 'John');
await redis.hset('user:1001', 'email', 'john@example.com');
// 다중 필드 설정
await redis.hset('user:1002', {
name: 'Jane',
email: 'jane@example.com',
age: '30'
});
// 단일 필드 조회
const name = await redis.hget('user:1001', 'name');
console.log(name); // 'John'
// 다중 필드 조회
const fields = await redis.hmget('user:1002', 'name', 'email');
console.log(fields); // ['Jane', 'jane@example.com']
// 모든 필드-값 조회
const user = await redis.hgetall('user:1002');
console.log(user); // { name: 'Jane', email: 'jane@example.com', age: '30' }
// 필드 존재 확인
const exists = await redis.hexists('user:1001', 'name');
console.log(exists); // 1
// 필드 삭제
await redis.hdel('user:1001', 'email');
// 모든 필드명 조회
const keys = await redis.hkeys('user:1002');
console.log(keys); // ['name', 'email', 'age']
// 모든 값 조회
const values = await redis.hvals('user:1002');
console.log(values); // ['Jane', 'jane@example.com', '30']
// 필드 개수
const len = await redis.hlen('user:1002');
console.log(len); // 3
// 숫자 필드 증가
await redis.hincrby('user:1002', 'age', 1);
}
4.2 List
List는 순서가 있는 문자열 집합으로, 큐나 스택 구현에 적합합니다.
async function listOperations() {
// 왼쪽에 추가 (스택처럼 사용)
await redis.lpush('tasks', 'task1', 'task2', 'task3');
// 결과: ['task3', 'task2', 'task1']
// 오른쪽에 추가 (큐처럼 사용)
await redis.rpush('tasks', 'task4');
// 결과: ['task3', 'task2', 'task1', 'task4']
// 범위 조회 (0부터 시작, -1은 마지막)
const allTasks = await redis.lrange('tasks', 0, -1);
console.log(allTasks); // ['task3', 'task2', 'task1', 'task4']
// 인덱스로 조회
const first = await redis.lindex('tasks', 0);
console.log(first); // 'task3'
// 왼쪽에서 꺼내기
const leftPop = await redis.lpop('tasks');
console.log(leftPop); // 'task3'
// 오른쪽에서 꺼내기
const rightPop = await redis.rpop('tasks');
console.log(rightPop); // 'task4'
// 리스트 길이
const len = await redis.llen('tasks');
console.log(len); // 2
// 인덱스로 값 변경
await redis.lset('tasks', 0, 'updated-task');
// 특정 범위만 유지
await redis.ltrim('tasks', 0, 99); // 처음 100개만 유지
// 블로킹 팝 (큐 대기)
// 5초 동안 대기하며 값이 오면 꺼냄
const blocked = await redis.blpop('queue', 5);
console.log(blocked); // ['queue', 'value'] 또는 null
}
4.3 Set
Set은 중복 없는 문자열 집합입니다.
async function setOperations() {
// 멤버 추가
await redis.sadd('tags', 'nodejs', 'javascript', 'redis');
await redis.sadd('tags', 'nodejs'); // 중복은 무시됨
// 모든 멤버 조회
const members = await redis.smembers('tags');
console.log(members); // ['nodejs', 'javascript', 'redis']
// 멤버 존재 확인
const isMember = await redis.sismember('tags', 'nodejs');
console.log(isMember); // 1
// 멤버 개수
const count = await redis.scard('tags');
console.log(count); // 3
// 멤버 삭제
await redis.srem('tags', 'redis');
// 랜덤 멤버 조회
const random = await redis.srandmember('tags');
console.log(random); // 랜덤 값
// 랜덤 멤버 꺼내기
const popped = await redis.spop('tags');
console.log(popped); // 랜덤 값 (집합에서 제거됨)
// 집합 연산
await redis.sadd('set1', 'a', 'b', 'c');
await redis.sadd('set2', 'b', 'c', 'd');
// 합집합
const union = await redis.sunion('set1', 'set2');
console.log(union); // ['a', 'b', 'c', 'd']
// 교집합
const inter = await redis.sinter('set1', 'set2');
console.log(inter); // ['b', 'c']
// 차집합
const diff = await redis.sdiff('set1', 'set2');
console.log(diff); // ['a']
}
4.4 Sorted Set
Sorted Set은 점수를 기준으로 정렬되는 집합입니다.
async function sortedSetOperations() {
// 멤버 추가 (점수, 멤버)
await redis.zadd('leaderboard', 100, 'player1');
await redis.zadd('leaderboard', 200, 'player2');
await redis.zadd('leaderboard', 150, 'player3');
// 다중 추가
await redis.zadd('leaderboard', 180, 'player4', 120, 'player5');
// 점수순 조회 (오름차순)
const ascending = await redis.zrange('leaderboard', 0, -1);
console.log(ascending); // ['player1', 'player5', 'player3', 'player4', 'player2']
// 점수순 조회 (내림차순)
const descending = await redis.zrevrange('leaderboard', 0, -1);
console.log(descending); // ['player2', 'player4', 'player3', 'player5', 'player1']
// 점수와 함께 조회
const withScores = await redis.zrange('leaderboard', 0, -1, 'WITHSCORES');
console.log(withScores); // ['player1', '100', 'player5', '120', ...]
// 특정 멤버 점수 조회
const score = await redis.zscore('leaderboard', 'player1');
console.log(score); // '100'
// 순위 조회 (0부터 시작)
const rank = await redis.zrank('leaderboard', 'player1');
console.log(rank); // 0 (오름차순 기준)
const revRank = await redis.zrevrank('leaderboard', 'player1');
console.log(revRank); // 4 (내림차순 기준)
// 점수 증가
await redis.zincrby('leaderboard', 50, 'player1');
// 점수 범위로 조회
const byScore = await redis.zrangebyscore('leaderboard', 100, 200);
console.log(byScore);
// 멤버 삭제
await redis.zrem('leaderboard', 'player5');
// 멤버 개수
const count = await redis.zcard('leaderboard');
console.log(count);
}
5. Pub/Sub 패턴 구현
Pub/Sub은 실시간 메시징을 위한 패턴입니다. 발행자가 메시지를 발행하면 구독자가 이를 받습니다.
5.1 기본 Pub/Sub
const Redis = require('ioredis');
// 구독자 (별도 연결 필요)
const subscriber = new Redis();
// 발행자
const publisher = new Redis();
// 채널 구독
subscriber.subscribe('news', 'updates', (err, count) => {
if (err) {
console.error('구독 실패:', err);
return;
}
console.log(`${count}개 채널 구독 중`);
});
// 메시지 수신
subscriber.on('message', (channel, message) => {
console.log(`[${channel}] ${message}`);
});
// 메시지 발행
async function publish() {
await publisher.publish('news', '새로운 소식입니다');
await publisher.publish('updates', '업데이트가 있습니다');
}
publish();
5.2 패턴 구독
const Redis = require('ioredis');
const subscriber = new Redis();
const publisher = new Redis();
// 패턴으로 구독 (와일드카드 사용)
subscriber.psubscribe('chat:*', (err, count) => {
console.log(`패턴 구독 완료: ${count}`);
});
// 패턴 매칭 메시지 수신
subscriber.on('pmessage', (pattern, channel, message) => {
console.log(`패턴: ${pattern}, 채널: ${channel}, 메시지: ${message}`);
});
// 다양한 채널에 발행
async function publishToRooms() {
await publisher.publish('chat:room1', '안녕하세요');
await publisher.publish('chat:room2', '반갑습니다');
await publisher.publish('chat:lobby', '로비 메시지');
}
publishToRooms();
5.3 실시간 알림 시스템 예제
const Redis = require('ioredis');
class NotificationService {
constructor() {
this.subscriber = new Redis();
this.publisher = new Redis();
this.handlers = new Map();
}
subscribe(channel, handler) {
if (!this.handlers.has(channel)) {
this.handlers.set(channel, []);
this.subscriber.subscribe(channel);
}
this.handlers.get(channel).push(handler);
}
init() {
this.subscriber.on('message', (channel, message) => {
const handlers = this.handlers.get(channel) || [];
const data = JSON.parse(message);
handlers.forEach(handler => handler(data));
});
}
async notify(channel, data) {
await this.publisher.publish(channel, JSON.stringify(data));
}
async close() {
await this.subscriber.quit();
await this.publisher.quit();
}
}
// 사용 예시
const notifications = new NotificationService();
notifications.init();
notifications.subscribe('orders', (data) => {
console.log('새 주문:', data);
});
notifications.notify('orders', {
orderId: '12345',
product: 'Node.js 책',
quantity: 1
});
6. 파이프라인과 트랜잭션
6.1 파이프라인
파이프라인은 여러 명령을 한 번에 전송하여 네트워크 왕복을 줄입니다.
const Redis = require('ioredis');
const redis = new Redis();
async function pipelineExample() {
// 파이프라인 생성
const pipeline = redis.pipeline();
// 명령 추가
pipeline.set('key1', 'value1');
pipeline.set('key2', 'value2');
pipeline.get('key1');
pipeline.get('key2');
pipeline.incr('counter');
// 실행 및 결과 받기
const results = await pipeline.exec();
console.log(results);
// [
// [null, 'OK'],
// [null, 'OK'],
// [null, 'value1'],
// [null, 'value2'],
// [null, 1]
// ]
// 체이닝 방식
const chainResults = await redis.pipeline()
.set('a', '1')
.set('b', '2')
.get('a')
.get('b')
.exec();
console.log(chainResults);
}
pipelineExample();
6.2 트랜잭션 (MULTI/EXEC)
트랜잭션은 여러 명령을 원자적으로 실행합니다.
async function transactionExample() {
// 기본 트랜잭션
const results = await redis.multi()
.set('foo', 'bar')
.get('foo')
.incr('counter')
.exec();
console.log(results);
// [
// [null, 'OK'],
// [null, 'bar'],
// [null, 1]
// ]
// 트랜잭션 취소
const multi = redis.multi();
multi.set('key', 'value');
multi.discard(); // 트랜잭션 취소
}
6.3 WATCH를 사용한 낙관적 잠금
async function watchExample() {
// 계좌 이체 예제
async function transfer(from, to, amount) {
while (true) {
try {
// 키 감시 시작
await redis.watch(from, to);
const fromBalance = parseInt(await redis.get(from)) || 0;
const toBalance = parseInt(await redis.get(to)) || 0;
if (fromBalance < amount) {
await redis.unwatch();
throw new Error('잔액 부족');
}
// 트랜잭션 실행
const result = await redis.multi()
.set(from, fromBalance - amount)
.set(to, toBalance + amount)
.exec();
if (result === null) {
// 다른 클라이언트가 값을 변경함, 재시도
console.log('충돌 발생, 재시도...');
continue;
}
console.log('이체 성공');
return true;
} catch (error) {
await redis.unwatch();
throw error;
}
}
}
// 초기 잔액 설정
await redis.set('account:A', 1000);
await redis.set('account:B', 500);
// 이체 실행
await transfer('account:A', 'account:B', 100);
}
7. 연결 관리 및 에러 처리
7.1 연결 풀 설정
const Redis = require('ioredis');
// 클러스터 모드
const cluster = new Redis.Cluster([
{ host: '127.0.0.1', port: 7000 },
{ host: '127.0.0.1', port: 7001 },
{ host: '127.0.0.1', port: 7002 }
], {
redisOptions: {
password: 'password'
},
scaleReads: 'slave' // 읽기는 슬레이브에서
});
// 센티넬 모드
const sentinel = new Redis({
sentinels: [
{ host: '127.0.0.1', port: 26379 },
{ host: '127.0.0.1', port: 26380 }
],
name: 'mymaster',
password: 'password'
});
7.2 재연결 전략
const redis = new Redis({
retryStrategy: (times) => {
if (times > 10) {
// 10번 이상 실패하면 재연결 중단
return null;
}
// 재연결 대기 시간 (최대 3초)
const delay = Math.min(times * 300, 3000);
return delay;
},
reconnectOnError: (err) => {
// READONLY 에러 시 재연결
const targetError = 'READONLY';
if (err.message.includes(targetError)) {
return true;
}
return false;
}
});
7.3 에러 처리 패턴
const Redis = require('ioredis');
class RedisClient {
constructor(options) {
this.redis = new Redis({
...options,
lazyConnect: true,
maxRetriesPerRequest: 3
});
this.setupEventHandlers();
}
setupEventHandlers() {
this.redis.on('error', (err) => {
console.error('Redis 에러:', err.message);
});
this.redis.on('connect', () => {
console.log('Redis 연결됨');
});
this.redis.on('ready', () => {
console.log('Redis 준비 완료');
});
this.redis.on('close', () => {
console.log('Redis 연결 종료');
});
}
async connect() {
try {
await this.redis.connect();
} catch (err) {
console.error('연결 실패:', err.message);
throw err;
}
}
async get(key) {
try {
return await this.redis.get(key);
} catch (err) {
console.error(`GET 실패 [${key}]:`, err.message);
return null;
}
}
async set(key, value, ttl = null) {
try {
if (ttl) {
return await this.redis.set(key, value, 'EX', ttl);
}
return await this.redis.set(key, value);
} catch (err) {
console.error(`SET 실패 [${key}]:`, err.message);
return null;
}
}
async del(key) {
try {
return await this.redis.del(key);
} catch (err) {
console.error(`DEL 실패 [${key}]:`, err.message);
return 0;
}
}
async disconnect() {
await this.redis.quit();
}
}
// 사용 예시
async function main() {
const client = new RedisClient({
host: '127.0.0.1',
port: 6379
});
await client.connect();
await client.set('greeting', 'Hello Redis', 3600);
const value = await client.get('greeting');
console.log(value);
await client.disconnect();
}
main();
7.4 타임아웃 설정
const redis = new Redis({
host: '127.0.0.1',
port: 6379,
connectTimeout: 10000, // 연결 타임아웃 10초
commandTimeout: 5000, // 명령 타임아웃 5초
enableOfflineQueue: true, // 오프라인 시 명령 큐에 저장
maxRetriesPerRequest: 3
});
7.5 정상 종료 처리
const Redis = require('ioredis');
const redis = new Redis();
async function gracefulShutdown() {
console.log('종료 시작...');
// 새로운 명령 거부
redis.disconnect();
// 대기 중인 명령 완료 후 종료
await redis.quit();
console.log('Redis 연결 정상 종료');
process.exit(0);
}
process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);
결론
Node.js에서 Redis를 연동하면 빠른 캐싱, 세션 관리, 실시간 메시징 등 다양한 기능을 구현할 수 있습니다. ioredis는 풍부한 기능과 안정적인 연결 관리를 제공하여 프로덕션 환경에서도 신뢰할 수 있습니다. 파이프라인과 트랜잭션을 적절히 활용하고, 에러 처리와 재연결 전략을 잘 설정하면 안정적인 Redis 기반 애플리케이션을 구축할 수 있습니다.
'Node.js' 카테고리의 다른 글
| Node.js SQLite 연동 완벽 가이드 (0) | 2026.03.19 |
|---|---|
| Node.js PostgreSQL 연동 (0) | 2026.03.19 |
| Node.js Sequelize ORM 완벽 가이드 (0) | 2026.03.18 |
| Node.js MySQL 연동 완벽 가이드 (0) | 2026.03.18 |
| Node.js의 Mongoose 사용법 완벽 가이드 (0) | 2026.03.17 |