최종 수정: 2025.05.28
클라이언트 측 캐싱과 서버 측 캐싱
캐시 무효화는 항상 중요한 문제이다. 데이터가 변경되었을 때 관련된 캐시를 적절히 갱신하거나 삭제해야 한다. TTL(Time To Live) 설정, 태그 기반 무효화, 이벤트 기반 무효화 등의 방법을 활용할 수 있다.
일관성과 성능 사이의 균형도 중요하다. 실시간성이 중요한 데이터는 캐시 시간을 짧게 하거나 캐시하지 않는 것이 좋고, 변경이 드문 데이터는 긴 캐시 시간을 설정할 수 있다.
두 캐싱 모두 웹 성능 최적화를 위해 사용되지만, 각각의 위치와 목적, 적용 방식에 차이가 존재한다.
■ 클라이언트 측 캐싱 (Client-side Caching)
브라우저나 사용자 디바이스가 서버에서 받아온 데이터를 로컬에 저장해 두고, 다음 요청 시 재사용하는 방식이다.
- HTTP 캐싱 (브라우저 캐시)
- Cache-Control, Expires, ETag, Last-Modified 등의 HTTP 헤더를 사용해 리소스의 캐싱 조건을 지정한다.
- 정적 자원인 CSS, JavaScript, 이미지 파일들이 주로 캐시된다. - Service Workers
- PWA(Progressive Web APP)에서 사용되며, JS로 네트워크 요청을 가로채 캐시에서 응답을 제공
- 오프라인 지원, 백그라운드 동기화, 맞춤형 캐시 로직 등이 가능하다. - LocalStorage / SessionStorage / IndexedDB
- localStorage와 sessingStorage를 사용해 사용자 설정, 폼 데이터, API 응답 결과 등을 저장할 수 있다.
- IndexedDB는 더 복잡한 데이터 구조나 대용량 데이터를 클라이언트에서 관리할 때 유용하다. - 메모리 캐싱
- 애플리케이션 실행 중 변수나 상태로 데이터를 보관하는 방식
- ex) useMome, useCallback이 대표적이다.
장점
- 서버 요청의 감소로 인한 빠른 응답
- 오프라인 환경에서도 작동 가능
- 사용자 경험 향상
단점
- 저장 용량에 제한이 있다.
- 캐시 만료 관리가 어렵다
- 민감 정보 저장 시 보안 이슈 발생
TanStack Query의 캐싱
자동 캐싱과 메모리 관리를 제공한다. API 호출 결과를 자동으로 캐시하고, 컴포넌트가 언마운트되면 적절한 시점에 캐시를 정리함으로써 메모리 누수 없이 캐싱을 활용할 수 있다.
staleTime과 cacheTime을 통해 데이터의 신선도를 관리하고, invalidateQueries로 특정 쿼리의 캐시를 무효화할 수 있다. 태그 기반으로 관련된 쿼리들을 한 번에 무효화하는 것도 가능하다.
백그라운드 업데이트도 자동으로 처리하여, 창이 다시 포커스되거나 네트워크가 재연결되면 자동으로 데이터를 다시 가져와서 캐시를 갱신한다.
■ 서버 측 캐싱 (Server-side Caching)
서버나 중간 서버(프록시, CDN 등)과 클라이언트 요청에 대한 응답을 저장해 주고, 동일 요청 시 재계산 없이 저장된 응답을 제공하는 방식이다.
- 메모리 캐시
- 서버의 RAM에 데이터를 저장하는 방식이다.
- Redis, Memcached 같은 인메모리 데이터베이스를 활용하거나, 애플리케이션 내부 메모리를 사용할 수 있다.
- 데이터베이스 조회나 연선 결과를 서버 메모리에 저장해 빠르게 응답 - HTTP Reverse Proxy Cache
- 서버 앞단에서 캐시된 정적/동적 응답을 제공 - CDN (Content Delivery Network)
- 전 세계 엣지 서버에서 정적 자산을 캐싱해 제공 - 데이터베이스 캐싱
- 쿼리 결과 캐시: 동일한 쿼리의 반복 실행을 방지
- ORM 레벨에서의 캐싱: 객체 매핑 과정을 최적화
- 데이터베이스 자체로 내부적으로 페이지 캐시, 인덱스 캐시 등을 운영한다. - 응용 프로그램 레벨 캐싱
- 함수나 메서드의 실행 결과를 메모이제이션하거나, 페이지 전체를 정적 파일로 생성하는 정적 사이트 생성이 이에 해당한다. - 역방향 프록시 캐싱
- Nginx, Apache 등에서 동적 콘텐츠의 응답을 캐시하여 백엔드 서버의 부하를 줄인다.
장점
- 서버 부하 감소
- 빠른 응답 속도 제공
- 전체 시스템 성능 향상
단점
- 캐시 무효화 및 갱신 로직 필요
- 동적인 사용자 맞춤 데이터 캐싱이 어려움
- 캐시 일관성 유지가 복잡
■ React에서의 캐싱 전략
React는 기본적으로 클라이언트 사이드이기 때문에 브라우저 캐싱과 로컬 저장소를 기반으로 전략을 세운다.
예시
import { useEffect, useState } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
const cached = localStorage.getItem('users');
if (cached) {
setUsers(JSON.parse(cached));
} else {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data);
localStorage.setItem('users', JSON.stringify(data));
});
}
}, []);
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
Q. React 18 버전에서 도입된 Server Component에서는 서버 캐싱 전략이 가능한가?
-> 불가능하다. RSC 자체는 '어디서 렌더링하느냐'를 결정하지만, '어떻게 캐싱하느냐'는 프레임워크의 역할이기 때문이다.
■ Next.js에서의 캐싱 전략
Next.js는 서버 및 정적 파일을 처리 능력이 있기 때문에 정적 페이지 캐싱(SSG/ISR), 서버 캐시, CDN 캐시 등 훨씬 강력한 서버 측 캐싱 전략이 가능하다.
예시
// pages/posts/[id].js
export async function getStaticPaths() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}));
return { paths, fallback: 'blocking' };
}
export async function getStaticProps({ params }) {
const res = await fetch(`https://api.example.com/posts/${params.id}`);
const post = await res.json();
return {
props: { post },
revalidate: 60, // ISR: 60초마다 정적 페이지 재생성
};
}
export default function PostPage({ post }) {
return <div>{post.title}</div>;
}
예시 - unstable_cache로 데이터베이스 쿼리나 외부 API호출을 캐시
import { unstable_cache } from 'next/cache'
import { createClient } from '@/utils/supabase/server'
const getCachedPosts = unstable_cache(
async () => {
const supabase = createClient()
const { data } = await supabase.from('posts').select('*')
return data
},
['posts'],
{ revalidate: 60 } // 60초마다 갱신
)
예시 - Route Segment Config로 페이지별 캐싱 정책 설정
// app/posts/page.tsx
export const revalidate = 3600 // 1시간마다 재생성
export const dynamic = 'force-static' // 정적 생성 강제
export default async function PostsPage() {
const posts = await getCachedPosts()
return <div>{/* 렌더링 */}</div>
}
전략 | 설명 | 관련 API |
정적 캐싱 (Static Rendering) |
기본적으로 모든 페이지/컴포넌트는 정적으로 생성된다. (SSG 기본값) |
fetch (기본 static) |
동적 렌더링 (Dynamic Rendering) |
요청마다 새로운 데이터로 서버에서 렌더링 | fetch에 { chche: 'no-store' }, 또는 dynamic = 'force-dynamic' 설정 |
ISR | 지정된 시간마다 캐시 무효화 후 새 페이지 생성 | fetch에 { next: { revalidate: 60 } } |
서버 컴포넌트 기반 캐싱 | RSC는 빌드시 생성 + 캐싱 | 기본 static 캐싱 구조 |
Route Handlers (API routes) |
서버 함수에서 직접 응답하며, Cache-Control 헤더 설정 가능 | /app/api/route.js |
'프론트엔드 > 개발 기초 지식' 카테고리의 다른 글
성능 향상을 위해 시도할 수 있는 방법 (0) | 2025.06.12 |
---|---|
브라우저의 동작 원리 (0) | 2025.06.04 |
세션 기반 인증과 토큰 기반 인증의 차이 (0) | 2025.05.27 |
Core Web Vitals와 페이지 경험 지표 상세 가이드 (0) | 2025.05.09 |
SEO (Search Engin Optimization) (0) | 2025.05.09 |
댓글