캐싱 전략 (Caching Strategies)
자바스크립트에서 성능을 끌어올리려면 캐싱이 큰 역할을 한다. 반복적인 연산이나 데이터 요청을 줄여서 더 빠르고 효율적인 코드를 만들 수 있다. 이번에는 캐싱 전략을 기본부터 심화까지 코드와 함께 자세히 풀어보려고 한다.
캐싱을 잘 활용하면 리소스 낭비를 줄이고 응답 속도를 높일 수 있다. 어떤 방법들이 있는지 하나씩 살펴보자.
메모리 캐싱 기본
가장 간단한 캐싱은 변수에 값을 저장하는 것이다:
let cachedValue;
function getData() {
if (cachedValue) {
console.log("캐시에서 가져옴");
return cachedValue;
}
cachedValue = Math.random();
console.log("새로 계산함");
return cachedValue;
}
console.log(getData());
console.log(getData());
// "새로 계산함"
// 0.12345...
// "캐시에서 가져옴"
// 0.12345...
한 번 계산한 값을 저장해두니 두 번째 호출부터는 새로 계산하지 않았다. 간단하면서도 효과적이다.
1. 메모이제이션 캐싱
입력값에 따라 결과를 저장하면 중복 연산을 피할 수 있다:
function memoize(fn) {
const cache = {};
return function(n) {
if (n in cache) return cache[n];
return (cache[n] = fn(n));
};
}
const slowSquare = memoize(n => {
console.log("계산 중:", n);
return n * n;
});
console.log(slowSquare(5));
console.log(slowSquare(5));
// "계산 중: 5"
// 25
// 25 (캐시 사용)
같은 입력값으로 호출하면 캐시에서 바로 가져오니 계산 부담이 줄었다.
2. 브라우저 캐싱 활용
HTTP 요청을 줄이려면 localStorage
를 사용해보자:
async function fetchWithCache(url) {
const cachedData = localStorage.getItem(url);
if (cachedData) {
console.log("캐시에서 로드");
return JSON.parse(cachedData);
}
const response = await fetch(url);
const data = await response.json();
localStorage.setItem(url, JSON.stringify(data));
console.log("서버에서 로드");
return data;
}
fetchWithCache("https://jsonplaceholder.typicode.com/posts/1")
.then(data => console.log(data.title));
// "서버에서 로드"
// "sunt aut facere..."
// 두 번째 호출: "캐시에서 로드"
localStorage
에 저장해두니 네트워크 요청이 줄어들었다.
3. TTL 기반 캐싱
캐시에 유효 기간을 추가하면 데이터 신선도를 관리할 수 있다:
const cache = new Map();
function setWithTTL(key, value, ttl) {
const expiry = Date.now() + ttl;
cache.set(key, { value, expiry });
}
function getWithTTL(key) {
const item = cache.get(key);
if (!item || Date.now() > item.expiry) {
cache.delete(key);
return null;
}
return item.value;
}
setWithTTL("data", "중요 데이터", 2000);
console.log(getWithTTL("data"));
setTimeout(() => console.log(getWithTTL("data")), 3000);
// "중요 데이터"
// null (2초 후 만료)
TTL(Time To Live)을 설정하니 오래된 데이터가 자동으로 제거됐다.
4. LRU 캐싱
최근 사용된 항목을 우선 유지하는 LRU(Least Recently Used) 캐싱을 구현해보자:
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return null;
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
set(key, value) {
if (this.cache.has(key)) this.cache.delete(key);
else if (this.cache.size >= this.capacity) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}
const cache = new LRUCache(2);
cache.set("a", 1);
cache.set("b", 2);
cache.set("c", 3);
console.log(cache.get("a"));
console.log(cache.get("b"));
// null ("a"가 제거됨)
// 2
용량을 초과하면 가장 오래된 항목이 제거되니 메모리 효율이 좋아졌다.
5. 프록시로 캐싱
객체 접근을 가로채서 캐싱할 수 있다:
const handler = {
get(target, prop) {
if (!target.cache) target.cache = {};
if (prop in target.cache) return target.cache[prop];
const value = target[prop]();
target.cache[prop] = value;
return value;
}
};
const target = {
getRandom: () => Math.random()
};
const cachedObj = new Proxy(target, handler);
console.log(cachedObj.getRandom);
console.log(cachedObj.getRandom);
// 0.456...
// 0.456... (캐시 사용)
Proxy
로 접근을 감지하고 캐싱하니 함수 호출 결과를 재사용할 수 있었다.
6. 서비스 워커로 네트워크 캐싱
서비스 워커를 통해 오프라인 캐싱을 구현해보자:
// sw.js
const CACHE_NAME = "my-cache-v1";
self.addEventListener("install", event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.add("/data.json");
})
);
});
self.addEventListener("fetch", event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
// main.js
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}
서비스 워커로 리소스를 캐싱하니 오프라인에서도 빠르게 로드됐다.
7. 함수 결과 캐싱
비용이 큰 함수의 결과를 캐싱해보자:
const cache = new WeakMap();
function processHeavy(obj) {
if (cache.has(obj)) return cache.get(obj);
const result = Array.from({ length: 1000 }, () => obj.value);
cache.set(obj, result);
return result;
}
const data = { value: 5 };
console.time("첫 번째");
processHeavy(data);
console.timeEnd("첫 번째");
console.time("두 번째");
processHeavy(data);
console.timeEnd("두 번째");
// 첫 번째: 0.5ms
// 두 번째: 0.01ms
WeakMap
으로 캐싱하니 두 번째 호출이 훨씬 빨라졌다.
8. 동적 캐싱 전략
상황에 따라 캐싱 방식을 동적으로 바꿀 수 있다:
function dynamicCache(strategy) {
const cache = strategy === "memory" ? {} : localStorage;
return {
get: key => cache[key] || cache.getItem(key),
set: (key, value) => strategy === "memory"
? (cache[key] = value)
: cache.setItem(key, value)
};
}
const memoryCache = dynamicCache("memory");
memoryCache.set("key", "값");
console.log(memoryCache.get("key"));
// "값"
메모리와 로컬 스토리지를 상황에 맞게 선택하니 유연성이 높아졌다.
9. 캐시 무효화 관리
데이터가 바뀌면 캐시를 갱신해야 한다:
const cache = new Map();
let version = 1;
function getData(key, currentVersion) {
const cached = cache.get(key);
if (cached && cached.version === currentVersion) {
return cached.value;
}
const value = Math.random();
cache.set(key, { value, version: currentVersion });
return value;
}
console.log(getData("test", version));
version++;
console.log(getData("test", version));
// 0.789... (첫 계산)
// 0.123... (버전 변경 후 새 계산)
버전을 통해 캐시를 무효화하니 최신 데이터를 유지할 수 있었다.
10. 조건부 캐싱
특정 조건에서만 캐싱하도록 설정할 수 있다:
const cache = {};
function conditionalCache(key, condition, fn) {
if (condition && key in cache) return cache[key];
const result = fn();
if (condition) cache[key] = result;
return result;
}
const shouldCache = true;
console.log(conditionalCache("rand", shouldCache, () => Math.random()));
console.log(conditionalCache("rand", shouldCache, () => Math.random()));
// 0.234... (첫 계산)
// 0.234... (캐시 사용)
조건에 따라 캐싱 여부를 결정하니 더 유연하게 관리할 수 있었다.
마무리
캐싱 전략은 성능을 높이고 자원을 아끼는 데 큰 도움을 준다. 메모리 캐싱, 브라우저 저장소, TTL, LRU, 서비스 워커까지 상황에 맞는 방법을 선택하면 효율적인 코드를 만들 수 있다.
'코딩 공부 > 자바스크립트' 카테고리의 다른 글
85. 자바스크립트 메모리 관리 (Memory Management) (1) | 2025.03.28 |
---|---|
84. 자바스크립트 지연 로딩과 비동기 로딩 (Lazy Loading and Async Loading) (1) | 2025.03.28 |
82. 자바스크립트 코드 최적화 기법 (Code Optimization Techniques) (1) | 2025.03.27 |
81. 자바스크립트 디버깅 팁 (1) | 2025.03.27 |
80. 자바스크립트 테스트 커버리지 (Test Coverage) (1) | 2025.03.27 |