NOTE
Bun 의 Redis 클라이언트는 Redis 서버 버전 7.2 이상을 지원합니다.Bun 은 현대적인 Promise 기반 API 를 사용하여 Redis 데이터베이스를 작업하기 위한 네이티브 바인딩을 제공합니다. 이 인터페이스는 간단하고 성능이 뛰어나도록 설계되었으며, 내장 연결 관리, 완전히 타입 지정된 응답, TLS 지원을 제공합니다.
import { redis } from "bun";
// 키 설정
await redis.set("greeting", "Hello from Bun!");
// 키 가져오기
const greeting = await redis.get("greeting");
console.log(greeting); // "Hello from Bun!"
// 카운터 증가
await redis.set("counter", 0);
await redis.incr("counter");
// 키 존재 여부 확인
const exists = await redis.exists("greeting");
// 키 삭제
await redis.del("greeting");시작하기
Redis 클라이언트를 사용하려면 먼저 연결을 생성해야 합니다:
import { redis, RedisClient } from "bun";
// 기본 클라이언트 사용 (환경에서 연결 정보 읽기)
// 기본적으로 process.env.REDIS_URL 가 사용됨
await redis.set("hello", "world");
const result = await redis.get("hello");
// 커스텀 클라이언트 생성
const client = new RedisClient("redis://username:password@localhost:6379");
await client.set("counter", "0");
await client.incr("counter");기본적으로 클라이언트는 다음 환경 변수에서 연결 정보를 읽습니다 (우선순위 순):
REDIS_URLVALKEY_URL- 설정되지 않은 경우 기본적으로
"redis://localhost:6379"로 설정됨
연결 라이프사이클
Redis 클라이언트는 백그라운드에서 연결을 자동으로 처리합니다:
// 명령이 실행될 때까지 연결되지 않음
const client = new RedisClient();
// 첫 명령으로 연결 시작
await client.set("key", "value");
// 후속 명령을 위해 연결 유지
await client.get("key");
// 완료 시 명시적으로 연결 닫기
client.close();연결 라이프사이클을 수동으로 제어할 수도 있습니다:
const client = new RedisClient();
// 명시적으로 연결
await client.connect();
// 명령 실행
await client.set("key", "value");
// 완료 시 연결 끊기
client.close();기본 연산
문자열 연산
// 키 설정
await redis.set("user:1:name", "Alice");
// 키 가져오기
const name = await redis.get("user:1:name");
// Uint8Array 로 키 가져오기
const buffer = await redis.getBuffer("user:1:name");
// 키 삭제
await redis.del("user:1:name");
// 키 존재 여부 확인
const exists = await redis.exists("user:1:name");
// 만료 설정 (초 단위)
await redis.set("session:123", "active");
await redis.expire("session:123", 3600); // 1 시간 후 만료
// 잔여 수명 가져오기 (초 단위)
const ttl = await redis.ttl("session:123");숫자 연산
// 초기값 설정
await redis.set("counter", "0");
// 1 증가
await redis.incr("counter");
// 1 감소
await redis.decr("counter");해시 연산
// 해시에 여러 필드 설정
await redis.hmset("user:123", ["name", "Alice", "email", "alice@example.com", "active", "true"]);
// 해시에서 여러 필드 가져오기
const userFields = await redis.hmget("user:123", ["name", "email"]);
console.log(userFields); // ["Alice", "alice@example.com"]
// 해시에서 단일 필드 가져오기 (값 직접 반환, 없으면 null)
const userName = await redis.hget("user:123", "name");
console.log(userName); // "Alice"
// 해시에서 숫자 필드 증가
await redis.hincrby("user:123", "visits", 1);
// 해시에서 float 필드 증가
await redis.hincrbyfloat("user:123", "score", 1.5);집합 연산
// 집합에 멤버 추가
await redis.sadd("tags", "javascript");
// 집합에서 멤버 제거
await redis.srem("tags", "javascript");
// 집합에 멤버 존재 여부 확인
const isMember = await redis.sismember("tags", "javascript");
// 집합의 모든 멤버 가져오기
const allTags = await redis.smembers("tags");
// 랜덤 멤버 가져오기
const randomTag = await redis.srandmember("tags");
// 랜덤 멤버 팝 (제거 및 반환)
const poppedTag = await redis.spop("tags");Pub/Sub
Bun 은 Redis Pub/Sub 프로토콜에 대한 네이티브 바인딩을 제공합니다. Bun 1.2.23 에서 새로 추가됨
기본 사용법
메시지 게시를 시작하려면 publisher.ts 에서 게시자를 설정할 수 있습니다:
import { RedisClient } from "bun";
const writer = new RedisClient("redis://localhost:6739");
await writer.connect();
writer.publish("general", "Hello everyone!");
writer.close();다른 파일에서 subscriber.ts 에 구독자를 생성합니다:
import { RedisClient } from "bun";
const listener = new RedisClient("redis://localhost:6739");
await listener.connect();
await listener.subscribe("general", (message, channel) => {
console.log(`Received: ${message}`);
});한 셸에서 구독자를 실행합니다:
bun run subscriber.ts그리고 다른 셸에서 게시자를 실행합니다:
bun run publisher.tsNOTE
구독 모드는 `RedisClient` 연결을 차지합니다. 구독이 있는 클라이언트는 `RedisClient.prototype.subscribe()` 만 호출할 수 있습니다. 즉, Redis 에 메시지를 보내야 하는 애플리케이션은 별도의 연결이 필요하며, `.duplicate()` 를 통해 얻을 수 있습니다:import { RedisClient } from "bun";
const redis = new RedisClient("redis://localhost:6379");
await redis.connect();
const subscriber = await redis.duplicate();
await subscriber.subscribe("foo", () => {});
await redis.set("bar", "baz");게시
메시지 게시는 publish() 메서드를 통해 수행됩니다:
await client.publish(channelName, message);구독
Bun RedisClient 는 .subscribe() 메서드를 통해 채널을 구독할 수 있습니다:
await client.subscribe(channel, (message, channel) => {});.unsubscribe() 메서드를 통해 구독을 해제할 수 있습니다:
await client.unsubscribe(); // 모든 채널에서 구독 해제.
await client.unsubscribe(channel); // 특정 채널 구독 해제.
await client.unsubscribe(channel, listener); // 특정 리스너 구독 해제.고급 사용법
명령 실행 및 파이프라이닝
클라이언트는 자동으로 명령을 파이프라이닝하여 여러 명령을 배치로 보내고 응답이 도착하는 대로 처리하여 성능을 향상시킵니다.
// 명령은 기본적으로 자동으로 파이프라이닝됨
const [infoResult, listResult] = await Promise.all([redis.get("user:1:name"), redis.get("user:2:email")]);자동 파이프라이닝을 비활성화하려면 enableAutoPipelining 옵션을 false 로 설정할 수 있습니다:
const client = new RedisClient("redis://localhost:6379", {
enableAutoPipelining: false,
});원시 명령
편의 메서드가 없는 명령을 사용해야 할 때는 send 메서드를 사용할 수 있습니다:
// 모든 Redis 명령 실행
const info = await redis.send("INFO", []);
// 리스트에 LPUSH
await redis.send("LPUSH", ["mylist", "value1", "value2"]);
// 리스트 범위 가져오기
const list = await redis.send("LRANGE", ["mylist", "0", "-1"]);send 메서드를 사용하면 클라이언트에 전용 메서드가 없는 명령을 포함한 모든 Redis 명령을 사용할 수 있습니다. 첫 번째 인수는 명령 이름이고 두 번째 인수는 문자열 인수 배열입니다.
연결 이벤트
연결 이벤트에 대한 핸들러를 등록할 수 있습니다:
const client = new RedisClient();
// Redis 서버에 성공적으로 연결되면 호출됨
client.onconnect = () => {
console.log("Redis 서버에 연결됨");
};
// Redis 서버에서 연결이 끊기면 호출됨
client.onclose = error => {
console.error("Redis 서버에서 연결 끊김:", error);
};
// 수동으로 연결/연결 끊기
await client.connect();
client.close();연결 상태 및 모니터링
// 연결 여부 확인
console.log(client.connected); // 연결 상태를 나타내는 부울
// 버퍼링된 데이터 양 확인 (바이트 단위)
console.log(client.bufferedAmount);타입 변환
Redis 클라이언트는 Redis 응답에 대한 자동 타입 변환을 처리합니다:
- 정수 응답은 JavaScript 숫자로 반환됨
- 벌크 문자열은 JavaScript 문자열로 반환됨
- 심플 문자열은 JavaScript 문자열로 반환됨
- null 벌크 문자열은
null로 반환됨 - 배열 응답은 JavaScript 배열로 반환됨
- 오류 응답은 적절한 오류 코드로 JavaScript 오류를 throw 함
- 부울 응답 (RESP3) 은 JavaScript 부울로 반환됨
- 맵 응답 (RESP3) 은 JavaScript 객체로 반환됨
- 집합 응답 (RESP3) 은 JavaScript 배열로 반환됨
특정 명령에 대한 특별 처리:
EXISTS는 숫자 대신 부울을 반환함 (1 은 true, 0 은 false)SISMEMBER는 부울을 반환함 (1 은 true, 0 은 false)
다음 명령은 자동 파이프라이닝을 비활성화합니다:
AUTHINFOQUITEXECMULTIWATCHSCRIPTSELECTCLUSTERDISCARDUNWATCHPIPELINESUBSCRIBEUNSUBSCRIBEUNPSUBSCRIBE
연결 옵션
클라이언트를 생성할 때 연결을 구성하기 위한 다양한 옵션을 전달할 수 있습니다:
const client = new RedisClient("redis://localhost:6379", {
// 밀리초 단위의 연결 타임아웃 (기본값: 10000)
connectionTimeout: 5000,
// 밀리초 단위의 유휴 타임아웃 (기본값: 0 = 타임아웃 없음)
idleTimeout: 30000,
// 연결 끊김 시 자동으로 재연결 여부 (기본값: true)
autoReconnect: true,
// 최대 재연결 시도 횟수 (기본값: 10)
maxRetries: 10,
// 연결 끊김 시 명령 큐잉 여부 (기본값: true)
enableOfflineQueue: true,
// 명령을 자동으로 파이프라이닝 여부 (기본값: true)
enableAutoPipelining: true,
// TLS 옵션 (기본값: false)
tls: true,
// 또는 커스텀 TLS 구성 제공:
// tls: {
// rejectUnauthorized: true,
// ca: "path/to/ca.pem",
// cert: "path/to/cert.pem",
// key: "path/to/key.pem",
// }
});재연결 동작
연결이 손실되면 클라이언트는 지수 백오프로 자동으로 재연결을 시도합니다:
- 클라이언트는 작은 지연 (50ms) 으로 시작하여 각 시도마다 두 배로 증가시킵니다
- 재연결 지연은 2000ms(2 초) 로 제한됨
- 클라이언트는 최대
maxRetries번 (기본값: 10) 재연결을 시도합니다 - 연결 끊김 중 실행된 명령은:
enableOfflineQueue가 true 이면 큐에 추가됨 (기본값)enableOfflineQueue가 false 이면 즉시 거부됨
지원되는 URL 형식
Redis 클라이언트는 다양한 URL 형식을 지원합니다:
// 표준 Redis URL
new RedisClient("redis://localhost:6379");
new RedisClient("redis://localhost:6379");
// 인증 포함
new RedisClient("redis://username:password@localhost:6379");
// 데이터베이스 번호 포함
new RedisClient("redis://localhost:6379/0");
// TLS 연결
new RedisClient("rediss://localhost:6379");
new RedisClient("rediss://localhost:6379");
new RedisClient("redis+tls://localhost:6379");
new RedisClient("redis+tls://localhost:6379");
// Unix 소켓 연결
new RedisClient("redis+unix:///path/to/socket");
new RedisClient("redis+unix:///path/to/socket");
// Unix 소켓 위의 TLS
new RedisClient("redis+tls+unix:///path/to/socket");
new RedisClient("redis+tls+unix:///path/to/socket");오류 처리
Redis 클라이언트는 다양한 시나리오에 대해 타입 지정된 오류를 throw 합니다:
try {
await redis.get("non-existent-key");
} catch (error) {
if (error.code === "ERR_REDIS_CONNECTION_CLOSED") {
console.error("Redis 서버와의 연결이 닫혔습니다");
} else if (error.code === "ERR_REDIS_AUTHENTICATION_FAILED") {
console.error("인증에 실패했습니다");
} else {
console.error("예상치 못한 오류:", error);
}
}일반적인 오류 코드:
ERR_REDIS_CONNECTION_CLOSED- 서버와의 연결이 닫혔음ERR_REDIS_AUTHENTICATION_FAILED- 서버 인증에 실패함ERR_REDIS_INVALID_RESPONSE- 서버로부터 잘못된 응답을 받음
예제 사용 사례
캐싱
async function getUserWithCache(userId) {
const cacheKey = `user:${userId}`;
// 먼저 캐시에서 가져오기 시도
const cachedUser = await redis.get(cacheKey);
if (cachedUser) {
return JSON.parse(cachedUser);
}
// 캐시에 없음, 데이터베이스에서 가져오기
const user = await database.getUser(userId);
// 1 시간 동안 캐시에 저장
await redis.set(cacheKey, JSON.stringify(user));
await redis.expire(cacheKey, 3600);
return user;
}속도 제한
async function rateLimit(ip, limit = 100, windowSecs = 3600) {
const key = `ratelimit:${ip}`;
// 카운터 증가
const count = await redis.incr(key);
// 이 창의 첫 요청이면 만료 설정
if (count === 1) {
await redis.expire(key, windowSecs);
}
// 제한 초과 여부 확인
return {
limited: count > limit,
remaining: Math.max(0, limit - count),
};
}세션 스토리지
async function createSession(userId, data) {
const sessionId = crypto.randomUUID();
const key = `session:${sessionId}`;
// 만료와 함께 세션 저장
await redis.hmset(key, ["userId", userId.toString(), "created", Date.now().toString(), "data", JSON.stringify(data)]);
await redis.expire(key, 86400); // 24 시간
return sessionId;
}
async function getSession(sessionId) {
const key = `session:${sessionId}`;
// 세션 데이터 가져오기
const exists = await redis.exists(key);
if (!exists) return null;
const [userId, created, data] = await redis.hmget(key, ["userId", "created", "data"]);
return {
userId: Number(userId),
created: Number(created),
data: JSON.parse(data),
};
}구현 노트
Bun 의 Redis 클라이언트는 Zig 로 구현되었으며 Redis Serialization Protocol (RESP3) 을 사용합니다. 연결을 효율적으로 관리하며 지수 백오프로 자동 재연결을 제공합니다.
클라이언트는 명령 파이프라이닝을 지원하므로 이전 명령의 응답을 기다리지 않고 여러 명령을 보낼 수 있습니다. 이는 연속으로 여러 명령을 보낼 때 성능을 크게 향상시킵니다.
제한 사항 및 향후 계획
향후 버전에서 해결할 계획인 Redis 클라이언트의 현재 제한 사항:
- 트랜잭션 (MULTI/EXEC) 은 현재 원시 명령을 통해 수행해야 함
지원되지 않는 기능:
- Redis Sentinel
- Redis Cluster