5편. Redis List 실습: 최근 기록과 간단한 버퍼 처리
📚 목차
1. 최근 본 게시글 목록 만들기
2. 최근 검색어 저장하기
3. 간단한 작업 큐 구현하기
4. 로그 버퍼 만들기

Redis List는 순서가 있는 문자열 목록을 저장하는 자료구조입니다.
List는 왼쪽과 오른쪽 양쪽에서 데이터를 넣고 뺄 수 있습니다.
왼쪽 <---- Redis List ----> 오른쪽
LPUSH RPUSH
LPOP RPOP
- 최근 본 게시글
- 최근 검색어
- 간단한 작업 큐
- 최근 로그 버퍼
📂 [GitHub 코드 보러가기] : https://github.com/cericube/nodejs-practice-lab/tree/main/redis-examples
1. 최근 본 게시글 목록 만들기
최근 본 게시글은 Redis List를 사용하기 좋은 대표적인 예입니다.
사용자가 게시글을 볼 때마다 게시글 ID를 List의 앞쪽에 추가하면 됩니다.
사용자 1번이 게시글을 조회
LPUSH list:user:1:recent-posts 10
LPUSH list:user:1:recent-posts 15
LPUSH list:user:1:recent-posts 20
결과:
[20, 15, 10]
최근 본 게시글 목록에서는 보통 다음 조건이 필요합니다.
1. 최신 게시글이 앞에 와야 합니다.
2. 같은 게시글이 중복으로 쌓이면 안 됩니다.
3. 최근 N개까지만 유지해야 합니다.
4. 실제 게시글 정보는 DB에서 조회해야 합니다.
Redis List는 순서를 유지할 수 있고, LTRIM으로 최대 개수를 제한할 수 있기 때문에 이 요구사항에 잘 맞습니다.
✔️ 사용하는 Redis Key
RedisKey.list.postRecentViews(userId)
list:user:1:recent-posts
list:user:2:recent-posts
✔️ 최근 본 게시글 추가
await redis.lRem(key, 0, value);
await redis.lPush(key, value);
await redis.lTrim(key, 0, limit - 1);
// src/services/post-list.service.ts
/**
* 사용자가 최근 본 게시글을 Redis List에 기록합니다.
*
* 1. 사용자별 최근 본 게시글 List key를 만듭니다.
* 2. 같은 게시글 ID가 이미 있으면 먼저 제거해서 중복을 방지합니다.
* 3. 새로 본 게시글 ID를 List 앞쪽에 넣어 최신순을 유지합니다.
* 4. 지정한 개수만 남기고 오래된 기록은 잘라냅니다.
*
* 실습 포인트:
* Redis List는 입력 순서를 유지하므로 최근 본 글, 최근 검색어처럼 순서가 중요한 기록에 적합합니다.
*
* 참고:
* 게시글 상세 데이터 전체를 Redis에 저장하지 않고 ID만 저장하면 캐시 무효화 부담을 줄일 수 있습니다.
*/
async addRecentViewedPost(userId: number, postId: number, limit = 10): Promise<void> {
const key = RedisKey.list.postRecentViews(userId);
const value = String(postId);
// LREM은 List 안에서 지정한 값을 제거합니다. count 0은 방향과 개수 제한 없이 모든 일치 값을 지운다는 뜻입니다.
await redis.lRem(key, 0, value);
// LPUSH는 값을 List 왼쪽에 추가합니다. 여기서는 왼쪽을 최신 위치로 사용합니다.
await redis.lPush(key, value);
// LTRIM은 지정한 인덱스 범위만 남깁니다. 0부터 limit - 1까지만 유지해 List 길이를 제한합니다.
await redis.lTrim(key, 0, limit - 1);
}
✔️ 최근 본 게시글 ID 목록을 조회
/**
* Redis List에서 최근 본 게시글 ID 목록을 조회합니다.
*
* 1. 사용자별 최근 본 게시글 List key를 만듭니다.
* 2. Redis List의 앞쪽부터 limit개만 읽습니다.
* 3. Redis에 문자열로 저장된 게시글 ID를 number로 변환합니다.
*
* 실습 포인트:
* LRANGE는 List의 일부 구간을 조회할 때 사용합니다.
*/
async getRecentViewedPostIds(userId: number, limit = 10): Promise<number[]> {
const key = RedisKey.list.postRecentViews(userId);
// LRANGE는 시작 인덱스부터 끝 인덱스까지의 값을 가져옵니다. 0부터 읽으면 최신 기록부터 조회됩니다.
const values = await redis.lRange(key, 0, limit - 1);
return values.map(Number);
}
2. 최근 검색어 저장하기
요구사항은 최근 본 게시글과 비슷합니다.
1. 최신 검색어가 앞에 와야 합니다.
2. 같은 검색어는 중복 저장하지 않습니다.
3. 사용자별로 최근 N개까지만 유지합니다.
4. 검색어 삭제 기능을 제공할 수 있습니다.
✔️ 사용하는 Redis Key
RedisKey.list.searchRecent(userId)
list:user:1:recent-searches
list:user:2:recent-searches
✔️ 최근 검색어를 Redis List에 기록
await redis.lRem(key, 0, normalizedKeyword);
await redis.lPush(key, normalizedKeyword);
await redis.lTrim(key, 0, limit - 1);
이렇게 하면 같은 검색어를 다시 검색했을 때 중복 저장하지 않고 맨 앞으로 이동시킬 수 있습니다
/**
* 사용자의 최근 검색어를 Redis List에 기록합니다.
*
* 1. 검색어 앞뒤 공백을 제거합니다.
* 2. 빈 검색어는 저장하지 않습니다.
* 3. 같은 검색어가 이미 있으면 먼저 제거해서 중복을 방지합니다.
* 4. 새 검색어를 List 앞쪽에 넣어 최신순을 유지합니다.
* 5. 지정한 개수만 남기고 오래된 검색어는 잘라냅니다.
*
* 실습 포인트:
* Redis List는 입력 순서를 유지하므로 최근 검색어처럼 순서가 중요한 기록에 적합합니다.
*
* 참고:
* 같은 검색어를 다시 검색하면 기존 위치의 값을 제거한 뒤 맨 앞으로 옮기는 방식으로 최신 기록을 갱신합니다.
*/
async addRecentSearchKeyword(userId: number, keyword: string, limit = 10): Promise<void> {
const normalizedKeyword = normalizeKeyword(keyword);
if (!normalizedKeyword) {
return;
}
const key = RedisKey.list.searchRecent(userId);
// LREM은 List 안에서 지정한 값을 제거합니다. count 0은 모든 위치의 일치 값을 지운다는 뜻입니다.
await redis.lRem(key, 0, normalizedKeyword);
// LPUSH는 값을 List 왼쪽에 추가합니다. 여기서는 왼쪽을 최신 검색어 위치로 사용합니다.
await redis.lPush(key, normalizedKeyword);
// LTRIM은 지정한 인덱스 범위만 남깁니다. 0부터 limit - 1까지만 유지해 List 길이를 제한합니다.
await redis.lTrim(key, 0, limit - 1);
}
✔️ 최근 검색어 목록을 최신순으로 조회
/**
* Redis List에서 최근 검색어 목록을 최신순으로 조회합니다.
*
* 1. 사용자별 최근 검색어 List key를 만듭니다.
* 2. Redis List의 앞쪽부터 limit개만 읽습니다.
* 3. 조회한 문자열 목록을 응답 객체 배열로 변환합니다.
*
* 실습 포인트:
* LRANGE는 List의 일부 구간을 조회할 때 사용합니다.
*
* 참고:
* Redis List가 이미 순서를 보장하므로 order 같은 별도 순번 필드는 만들지 않습니다.
*/
async getRecentSearchKeywords(userId: number, limit = 10): Promise<RecentSearchKeywordOutput[]> {
const key = RedisKey.list.searchRecent(userId);
// LRANGE는 시작 인덱스부터 끝 인덱스까지의 값을 가져옵니다. 0부터 읽으면 최신 검색어부터 조회됩니다.
const keywords = await redis.lRange(key, 0, limit - 1);
return keywords.map((keyword) => ({
keyword,
}));
}
3. 간단한 작업 큐 구현하기
간단한 Queue 용도로도 사용할 수 있습니다.
예를 들어 다음과 같은 작업을 임시로 쌓아둘 수 있습니다
- 이메일 발송 작업
- 알림 발송 작업
- 이미지 리사이즈 요청
- 간단한 비동기 처리 요청
List를 Queue처럼 사용할 때는 보통 한쪽에서 넣고 반대쪽에서 꺼냅니다.
Producer
↓ LPUSH
Redis List
↓ RPOP
Consumer
즉, LPUSH로 작업을 넣고 RPOP으로 오래된 작업부터 꺼내면 FIFO Queue처럼 동작합니다.
다만 실무에서 안정적인 작업 큐가 필요하다면 Redis List만으로는 부족할 수 있습니다.
재시도, 실패 처리, 처리 중 상태 추적이 필요하다면 Redis Stream 또는 BullMQ를 사용하는 것이 더 적합합니다.
✔️ 사용하는 Redis Key
RedisKey.list.simpleJobQueue()
list:simple-job-queue
✔️ 새 작업을 Redis List 큐에 추가
// src/services/job-list.service.ts
/**
* 새 작업을 Redis List 큐에 추가합니다.
*
* 1. 작업 큐에 사용할 Redis key를 만듭니다.
* 2. 작업 ID와 생성 시각을 포함한 SimpleJob 객체를 만듭니다.
* 3. 작업 객체를 JSON 문자열로 변환합니다.
* 4. Redis List 왼쪽에 작업을 넣습니다.
*
* 실습 포인트:
* Redis List는 문자열 목록이므로 객체를 저장하려면 JSON.stringify()로 직렬화해야 합니다.
*
* 참고:
* 이 서비스는 LPUSH로 왼쪽에 넣고 RPOP으로 오른쪽에서 꺼내 FIFO 큐처럼 사용합니다.
*/
async enqueueJob(input: EnqueueJobInput): Promise<SimpleJob> {
const key = RedisKey.list.simpleJobQueue();
const job: SimpleJob = {
id: createJobId(),
type: input.type,
payload: input.payload,
createdAt: new Date().toISOString(),
};
// LPUSH는 값을 List 왼쪽에 추가합니다. 새 작업은 왼쪽에 쌓이고, 오래된 작업은 오른쪽으로 밀립니다.
await redis.lPush(key, JSON.stringify(job));
return job;
}
Redis List는 문자열 목록입니다.
따라서 객체 형태의 작업 데이터는 JSON 문자열로 변환해 저장합니다.
저장되는 값은 다음과 비슷합니다.
{
"id": "job_1780000000000_x1a2b3c4",
"type": "SEND_EMAIL",
"payload": {
"email": "kim@example.com",
"message": "회원가입을 환영합니다."
},
"createdAt": "2026-06-22T00:00:00.000Z"
}
✔️ 큐에서 처리할 작업을 하나 꺼내기
/**
* 큐에서 처리할 작업을 하나 꺼냅니다.
*
* 1. 작업 큐에 사용할 Redis key를 만듭니다.
* 2. Redis List 오른쪽에서 작업 문자열을 하나 제거하면서 가져옵니다.
* 3. 큐가 비어 있으면 null을 반환합니다.
* 4. JSON 문자열을 SimpleJob 객체로 변환해 반환합니다.
*
* 실습 포인트:
* LPUSH로 넣은 작업을 RPOP으로 꺼내면 먼저 들어온 작업이 오른쪽 끝에 있으므로 먼저 처리됩니다.
*
* 참고:
* RPOP은 값을 조회만 하는 명령이 아니라 List에서 제거까지 함께 수행합니다.
*/
async dequeueJob(): Promise<SimpleJob | null> {
const key = RedisKey.list.simpleJobQueue();
// RPOP은 List 오른쪽 끝 값을 꺼내면서 삭제합니다. 큐가 비어 있으면 null을 반환합니다.
const value = await redis.rPop(key);
if (!value) {
return null;
}
return parseSimpleJob(value);
}
4. 로그 버퍼 만들기
로그 버퍼는 최근 로그 몇 개만 Redis에 보관하는 구조입니다.
예를 들어 개발 환경이나 관리자 화면에서 최근 에러 로그, 최근 요청 로그를 빠르게 확인하고 싶을 때 사용할 수 있습니다.
최근 요청 로그 100개
최근 에러 로그 100개
최근 작업 처리 로그 100개
Redis List와 LTRIM을 사용하면 최근 N개 로그만 유지하는 버퍼를 쉽게 만들 수 있습니다.
await redis.lPush(key, JSON.stringify(entry));
await redis.lTrim(key, 0, limit - 1);
LPUSH는 새 로그를 List 앞쪽에 추가합니다.
새 로그 추가 전:
[log3, log2, log1]
새 로그 log4 추가:
[log4, log3, log2, log1]
LTRIM은 최근 N개만 남깁니다.
limit = 3
LTRIM 0 2 실행 후:
[log4, log3, log2]
즉 오래된 로그는 자동으로 제거됩니다.
다만 Redis List 로그 버퍼는 정식 로그 저장소를 대체하지 않습니다.
운영 환경에서는 로그를 파일, Elasticsearch, OpenSearch 같은 별도 저장소로 보내는 것이 일반적입니다.
Redis List 로그 버퍼는 최근 상태를 빠르게 확인하기 위한 보조 도구로 보는 것이 좋습니다.
✔️ 사용하는 Redis Key
RedisKey.list.logBuffer()
list:log-buffer
✔️ 새 로그를 Redis List 버퍼에 추가
/**
* 새 로그를 Redis List 버퍼에 추가합니다.
*
* 1. 로그 버퍼에 사용할 Redis key를 만듭니다.
* 2. 로그 레벨, 메시지, context, 생성 시각을 포함한 로그 객체를 만듭니다.
* 3. 로그 객체를 JSON 문자열로 변환합니다.
* 4. Redis List 왼쪽에 로그를 넣어 최신 로그가 앞쪽에 오게 합니다.
* 5. LTRIM으로 최근 limit개만 남기고 오래된 로그를 제거합니다.
*
* 실습 포인트:
* Redis List는 최근 N개의 로그만 유지하는 버퍼로 활용할 수 있습니다.
*
* 참고:
* 이 예제는 로그 원본 저장소가 아니라 최근 로그 확인용 짧은 버퍼를 Redis에 두는 방식입니다.
*/
async addLog(input: AddLogInput, limit = 100): Promise<LogBufferEntry> {
const key = RedisKey.list.logBuffer();
const entry: LogBufferEntry = {
level: input.level,
message: input.message,
context: input.context,
createdAt: new Date().toISOString(),
};
// LPUSH는 값을 List 왼쪽에 추가합니다. 여기서는 왼쪽을 최신 로그 위치로 사용합니다.
await redis.lPush(key, JSON.stringify(entry));
// LTRIM은 지정한 인덱스 범위만 남깁니다. 0부터 limit - 1까지만 유지해 오래된 로그를 잘라냅니다.
await redis.lTrim(key, 0, limit - 1);
return entry;
}
✔️ 최근 로그 목록 조회
/**
* Redis List에서 최근 로그 목록을 최신순으로 조회합니다.
*
* 1. 로그 버퍼에 사용할 Redis key를 만듭니다.
* 2. Redis List의 앞쪽부터 limit개만 읽습니다.
* 3. JSON 문자열 목록을 로그 객체 목록으로 변환합니다.
* 4. JSON 파싱에 실패한 값은 결과에서 제외합니다.
*
* 실습 포인트:
* LRANGE는 List 데이터를 삭제하지 않고 지정한 구간만 조회합니다.
*
* 참고:
* LPUSH로 최신 로그를 왼쪽에 넣었으므로 0번 인덱스부터 읽으면 최신 로그부터 조회됩니다.
*/
async getRecentLogs(limit = 100): Promise<LogBufferEntry[]> {
const key = RedisKey.list.logBuffer();
// LRANGE는 시작 인덱스부터 끝 인덱스까지 값을 읽습니다. 0부터 읽으면 최신 로그부터 조회됩니다.
const values = await redis.lRange(key, 0, limit - 1);
return values.map(parseLogEntry).filter((entry): entry is LogBufferEntry => entry !== null);
}
/**
* 최근 로그 중 ERROR 레벨 로그만 조회합니다.
*
* 1. getRecentLogs로 최근 로그 목록을 가져옵니다.
* 2. level 값이 ERROR인 로그만 남깁니다.
*
* 실습 포인트:
* Redis List에서는 최근 로그 순서를 관리하고, 레벨 필터링은 애플리케이션 코드에서 처리합니다.
*/
async getRecentErrorLogs(limit = 100): Promise<LogBufferEntry[]> {
const logs = await this.getRecentLogs(limit);
return logs.filter((log) => log.level === 'ERROR');
}
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'1.시스템&인프라 > redis' 카테고리의 다른 글
| 6편. Redis Set 실습: 중복 제거와 상태 관리 (0) | 2026.07.08 |
|---|---|
| 4편. Redis Hash 실습: 프로필, 세션, 재고, 사용자 설정 (0) | 2026.06.26 |
| 3편. Redis String 실습: 캐시, 인증 코드, 카운터, Rate Limiting (0) | 2026.06.23 |
| 2편. Redis 실습 프로젝트 구성하기: Prisma, SQLite, Redis, Vitest (0) | 2026.06.22 |
| 1편. Redis 설치 및 기본 명령어 정리 (1) | 2026.06.15 |