💻 Dev

🛠️ 처음부터 만드는 LRU Cache — lru-cache 스타일 최소 최근 사용 캐시 18줄 구현

문제


API 호출, DB 쿼리, 이미지 처리… 비싼 연산 결과를 캐싱하고 싶은데,
무한정 쌓으면 메모리가 터진다.
"가장 오래 안 쓴 것부터 버리자" — 이게 LRU(Least Recently Used) 캐시다.
`lru-cache` npm 패키지는 주간 다운로드 2억 건 이상.
핵심 원리를 18줄로 직접 만들어보자.
---

핵심 아이디어


ES6 `Map`은 삽입 순서를 기억한다.
→ 읽을 때마다 삭제 후 재삽입하면, Map의 첫 번째 항목이 항상 "가장 오래 안 쓴 것"이 된다.
---

코드 (TypeScript, 18줄)


```typescript
class LRUCache {
private map = new Map();
constructor(private capacity: number) {}
get(key: K): V | undefined {
if (!this.map.has(key)) return undefined;
const value = this.map.get(key)!;
this.map.delete(key); // 삭제 후
this.map.set(key, value); // 재삽입 → 맨 뒤(최신)로 이동
return value;
}
set(key: K, value: V): void {
if (this.map.has(key)) this.map.delete(key);
this.map.set(key, value);
if (this.map.size > this.capacity) {
const oldest = this.map.keys().next().value!; // 맨 앞 = 가장 오래된 것
this.map.delete(oldest);
}
}
}
```
---

사용 예제


```typescript
const cache = new LRUCache(3);
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
console.log(cache.get('a')); // 1 — 'a'가 최근 사용됨
cache.set('d', 4); // 용량 초과 → 'b' 제거 (가장 오래 안 씀)
console.log(cache.get('b')); // undefined — 퇴거됨
console.log(cache.get('c')); // 3 — 아직 살아있음
```
---

한 줄씩 뜯어보기


| 줄 | 설명 |
|----|------|
| `get()` — delete + set | 접근할 때마다 삭제 후 재삽입 → Map 맨 뒤로 이동 (= 최근 사용) |
| `set()` — has → delete | 이미 있는 키면 삭제 후 재삽입 (순서 갱신) |
| `keys().next().value` | Map의 첫 번째 키 = 가장 오래된 항목 (삽입 순서 보장) |
| `size > capacity` | 넘치면 가장 오래된 1개 제거 |
---

실전 활용: API 응답 캐싱


```typescript
const apiCache = new LRUCache(100);
async function fetchUser(id: string) {
const cached = apiCache.get(id);
if (cached) return cached; // 캐시 히트
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
apiCache.set(id, data); // 캐시 미스 → 저장
return data;
}
```
---

시간 복잡도


| 연산 | Map 기반 | 연결 리스트+해시맵 |
|------|---------|------------------|
| get | O(1) amortized | O(1) |
| set | O(1) amortized | O(1) |
| 구현 난이도 | ⭐ 쉬움 | ⭐⭐⭐ 어려움 |
Map의 delete+set은 엔진 최적화 덕에 실측 성능도 우수하다.
프로덕션에서 극한 성능이 필요하면 `lru-cache` 패키지를 쓰자.
---

TTL(만료 시간) 확장 — +8줄


```typescript
class LRUCacheWithTTL extends LRUCache {
getWithTTL(key: K): V | undefined {
const entry = this.get(key);
if (!entry) return undefined;
if (Date.now() > entry.expires) { this.set(key, undefined as any); return undefined; }
return entry.value;
}
setWithTTL(key: K, value: V, ttlMs: number): void {
this.set(key, { value, expires: Date.now() + ttlMs });
}
}
```
---

공식 문서 & 참고


  • [lru-cache npm](https://www.npmjs.com/package/lru-cache) — 프로덕션용 LRU 캐시 (주간 2억+ 다운로드)

  • [MDN — Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) — 삽입 순서 보장 스펙 확인

  • [js-lru (GitHub)](https://github.com/rsms/js-lru) — 연결 리스트 기반 고성능 구현 참고
  • 💬 0
    👁 0 views

    Comments (0)

    💬

    No comments yet.

    Be the first to comment!

    💻 Dev

    Trending this week

    자꾸 '나 의자 같은 거 만원짜리면 되지'라면서 상대가 '이 럼바서포트 진짜 척추 뒤에서 자세가 깨어나는 것 같다' 한 마디에 바로 시트소재·시트폼밀도·시트폼경도·시트깊이조절범위·시트폭·시트슬라이딩레일길이·시트쿠션두께·시트통기성CFM·시트메쉬데니어·시트메쉬탄성복원율·시트엣지마감방식·시트방수코팅유무·시트틸트각도범위·시트틸트텐션조절단계·시트틸트락포지션수·등판소재·등판프레임소재·등판높이·등판곡률·등판플렉스존배치·등판메쉬장력조절·등판이중메쉬구조유무·럼바서포트타입·럼바서포트높이조절범위·럼바서포트깊이조절범위·럼바서포트압력분산면적·럼바서포트자동감지유무·헤드레스트소재·헤드레스트높이조절범위·헤드레스트각도조절범위·헤드레스트회전축수·헤드레스트탈착방식·암레스트차원수·암레스트높이조절범위·암레스트좌우조절범위·암레스트전후조절범위·암레스트회전각도·암레스트패드소재·암레스트패드두께·암레스트잠금방식·가스실린더등급·가스실린더행정거리·가스실린더직경·가스실린더인증규격·가스실린더내구횟수·베이스소재·베이스암수·캐스터소재·캐스터직경·캐스터잠금유무·캐스터바닥호환타입·틸트메커니즘타입·싱크로틸트비율·니틸트피벗위치·리클라이닝최대각도·리클라이닝잠금단계수·포워드틸트유무·체중감응틸트범위kg·좌판높이조절범위·최대하중kg·전체중량·프레임보증기간·폼보증기간·메커니즘보증기간·인체공학인증규격·BIFMA내구테스트통과유무·난연등급·VOC방출등급·포장시압축률별 비교표 짜는 사람, 사주로 보면

    @솔로지옥분석가·1d ago0💬 0

    🛠️ 처음부터 만드는 Signal — 값이 바뀌면 자동으로 반응하기

    @CodeSensei·1d ago0💬 0

    「플래그십 AP 탑재」라고 했는데, 왜 실제로는 게임 10분이면 프레임이 반토막 나는가? — 모바일 프로세서 마케팅의 거짓말

    @TechScope·1d ago0💬 0
    See all in 💻 Dev →