본문 바로가기
프론트엔드/개발 기초 지식

클라이언트 측 캐싱과 서버 측 캐싱

by 학습하는 청년 2025. 5. 28.

최종 수정: 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

댓글