cache

김동현·2026년 3월 17일

cache

RSC (React Server Components)

cacheReact Server Components에서만 사용할 수 있어요.

소개

cache를 사용하면 데이터 페칭이나 계산의 결과를 캐시할 수 있어요.

const cachedFn = cache(fn);

목차


레퍼런스

cache(fn)

컴포넌트 바깥에서 cache를 호출해서 캐싱이 적용된 버전의 함수를 만들 수 있어요.

// 파일명 예시
import {cache} from 'react';
import calculateMetrics from 'lib/metrics';

const getMetrics = cache(calculateMetrics);

function Chart({data}) {
  const report = getMetrics(data);
  // ...
}

getMetricsdata를 가지고 처음 호출되면, getMetricscalculateMetrics(data)를 호출하고 그 결과를 캐시에 저장해요. 만약 동일한 datagetMetrics가 다시 호출되면, calculateMetrics(data)를 다시 호출하는 대신 캐시된 결과를 반환해요.

아래에서 더 많은 예시를 확인해보세요.

매개변수

  • fn: 결과를 캐시하고 싶은 함수예요. fn은 어떤 인자든 받을 수 있고, 어떤 값이든 반환할 수 있어요.

반환값

cache는 동일한 타입 시그니처를 가진 fn의 캐시된 버전을 반환해요. 이 과정에서 fn을 호출하지는 않아요.

주어진 인자로 cachedFn을 호출하면, 먼저 캐시에 캐시된 결과가 있는지 확인해요. 캐시된 결과가 있으면 그 결과를 반환하고요. 없으면 해당 인자로 fn을 호출하고, 결과를 캐시에 저장한 다음, 그 결과를 반환해요. fn이 호출되는 유일한 경우는 캐시 미스(cache miss)가 발생했을 때예요.

참고

입력값에 기반해서 반환값을 캐싱하는 최적화 기법을 메모이제이션(memoization)이라고 해요. cache에서 반환된 함수를 메모이제이션된 함수라고 부르죠.

주의사항

  • React는 각 서버 요청마다 모든 메모이제이션된 함수의 캐시를 무효화해요. 즉, 서버 요청이 새로 들어오면 캐시가 리셋된다는 의미예요.
  • cache를 호출할 때마다 새로운 함수가 생성돼요. 즉, 동일한 함수로 cache를 여러 번 호출하면 같은 캐시를 공유하지 않는 서로 다른 메모이제이션된 함수들이 반환돼요.
  • cachedFn은 에러도 캐시해요. 만약 fn이 특정 인자에 대해 에러를 던지면, 그 에러가 캐시되고, 같은 인자로 cachedFn을 호출하면 동일한 에러가 다시 던져져요.
  • cacheServer Components에서만 사용할 수 있어요.

사용법

비용이 많이 드는 계산 캐싱하기

중복 작업을 건너뛰려면 cache를 사용하세요.

// 파일명 예시
import {cache} from 'react';
import calculateUserMetrics from 'lib/user';

const getUserMetrics = cache(calculateUserMetrics);

function Profile({user}) {
  const metrics = getUserMetrics(user);
  // ...
}

function TeamReport({users}) {
  for (let user in users) {
    const metrics = getUserMetrics(user);
    // ...
  }
  // ...
}

동일한 user 객체가 ProfileTeamReport 둘 다에서 렌더링되면, 두 컴포넌트는 작업을 공유하고 해당 user에 대해 calculateUserMetrics를 한 번만 호출할 수 있어요.

Profile이 먼저 렌더링된다고 가정해볼게요. getUserMetrics를 호출하고, 캐시된 결과가 있는지 확인할 거예요. 해당 usergetUserMetrics가 처음 호출되는 것이기 때문에 캐시 미스가 발생할 거예요. 그러면 getUserMetrics는 해당 usercalculateUserMetrics를 호출하고 결과를 캐시에 저장해요.

TeamReportusers 목록을 렌더링하다가 동일한 user 객체에 도달하면, getUserMetrics를 호출하고 캐시에서 결과를 읽어올 거예요.

만약 calculateUserMetricsAbortSignal을 전달받아 취소될 수 있다면, React가 렌더링을 마쳤을 때 cacheSignal()을 사용해서 비용이 많이 드는 계산을 취소할 수 있어요. calculateUserMetrics가 내부적으로 cacheSignal을 직접 사용해서 취소를 이미 처리하고 있을 수도 있고요.

⚠️ 주의: 서로 다른 메모이제이션된 함수를 호출하면 서로 다른 캐시에서 읽어요.

동일한 캐시에 접근하려면, 컴포넌트들이 동일한 메모이제이션된 함수를 호출해야 해요.

// Temperature.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

export function Temperature({cityData}) {
  // 🚩 잘못된 방법: 컴포넌트 안에서 `cache`를 호출하면 매 렌더링마다 새로운 `getWeekReport`가 생성돼요
  const getWeekReport = cache(calculateWeekReport);
  const report = getWeekReport(cityData);
  // ...
}
// Precipitation.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

// 🚩 잘못된 방법: `getWeekReport`는 `Precipitation` 컴포넌트에서만 접근 가능해요.
const getWeekReport = cache(calculateWeekReport);

export function Precipitation({cityData}) {
  const report = getWeekReport(cityData);
  // ...
}

위 예시에서 PrecipitationTemperature는 각각 cache를 호출해서 자체 캐시 조회를 가진 새로운 메모이제이션된 함수를 만들고 있어요. 두 컴포넌트가 동일한 cityData로 렌더링되면, calculateWeekReport를 호출하는 중복 작업을 하게 돼요.

게다가 Temperature는 컴포넌트가 렌더링될 때마다 새로운 메모이제이션된 함수를 생성해서 캐시 공유가 전혀 되지 않아요.

캐시 히트를 최대화하고 작업을 줄이려면, 두 컴포넌트가 동일한 메모이제이션된 함수를 호출해서 동일한 캐시에 접근해야 해요. 대신, 컴포넌트들 사이에서 import할 수 있는 전용 모듈에 메모이제이션된 함수를 정의하세요.

// getWeekReport.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

export default cache(calculateWeekReport);
// Temperature.js
import getWeekReport from './getWeekReport';

export default function Temperature({cityData}) {
	const report = getWeekReport(cityData);
  // ...
}
// Precipitation.js
import getWeekReport from './getWeekReport';

export default function Precipitation({cityData}) {
  const report = getWeekReport(cityData);
  // ...
}

여기서 두 컴포넌트가 ./getWeekReport.js에서 내보낸 동일한 메모이제이션된 함수를 호출해서 동일한 캐시를 읽고 쓰게 돼요.

데이터 스냅샷 공유하기

컴포넌트들 사이에서 데이터 스냅샷을 공유하려면, fetch 같은 데이터 페칭 함수로 cache를 호출하세요. 여러 컴포넌트가 동일한 데이터 페치를 하면, 단 하나의 요청만 만들어지고 반환된 데이터가 캐시되어 컴포넌트들 사이에서 공유돼요. 모든 컴포넌트가 서버 렌더링 전체에 걸쳐 동일한 데이터 스냅샷을 참조하게 되는 거죠.

// 파일명 예시
import {cache} from 'react';
import {fetchTemperature} from './api.js';

const getTemperature = cache(async (city) => {
	return await fetchTemperature(city);
});

async function AnimatedWeatherCard({city}) {
	const temperature = await getTemperature(city);
	// ...
}

async function MinimalWeatherCard({city}) {
	const temperature = await getTemperature(city);
	// ...
}

AnimatedWeatherCardMinimalWeatherCard가 둘 다 동일한 city로 렌더링되면, 메모이제이션된 함수에서 동일한 데이터 스냅샷을 받게 돼요.

AnimatedWeatherCardMinimalWeatherCardgetTemperature에 서로 다른 city 인자를 전달하면, fetchTemperature가 두 번 호출되고 각 호출 지점은 서로 다른 데이터를 받게 돼요.

city가 캐시 키 역할을 하는 거예요.

참고

비동기 렌더링은 Server Components에서만 지원돼요.

async function AnimatedWeatherCard({city}) {
	const temperature = await getTemperature(city);
	// ...
}

Client Components에서 비동기 데이터를 사용하는 컴포넌트를 렌더링하려면, use() 문서를 참고하세요.

데이터 미리 로드하기

오래 걸리는 데이터 페치를 캐싱하면, 컴포넌트를 렌더링하기 전에 비동기 작업을 시작할 수 있어요.

// 파일명 예시
const getUser = cache(async (id) => {
  return await db.user.query(id);
});

async function Profile({id}) {
  const user = await getUser(id);
  return (
    <section>
      <img src={user.profilePic} />
      <h2>{user.name}</h2>
    </section>
  );
}

function Page({id}) {
  // ✅ 좋아요: 사용자 데이터 페칭을 시작해요
  getUser(id);
  // ... 다른 계산 작업
  return (
    <>
      <Profile id={id} />
    </>
  );
}

Page를 렌더링할 때, 컴포넌트가 getUser를 호출하지만 반환된 데이터를 사용하지는 않는다는 걸 주목하세요. 이 초기 getUser 호출이 비동기 데이터베이스 쿼리를 시작하고, 이 쿼리는 Page가 다른 계산 작업을 하고 자식들을 렌더링하는 동안 진행돼요.

Profile을 렌더링할 때 getUser를 다시 호출해요. 초기 getUser 호출이 이미 반환되어 사용자 데이터를 캐시했다면, Profile이 이 데이터를 요청하고 기다릴 때 또 다른 원격 프로시저 호출 없이 캐시에서 바로 읽을 수 있어요. 초기 데이터 요청이 아직 완료되지 않았더라도, 이 패턴으로 데이터를 미리 로드하면 데이터 페칭의 지연을 줄일 수 있어요.

**심층 탐구: 비동기 작업 캐싱하기**

비동기 함수를 평가하면, 해당 작업에 대한 Promise를 받게 돼요. 프로미스는 해당 작업의 상태(pending, fulfilled, failed)와 최종적으로 확정된 결과를 담고 있어요.

이 예시에서 비동기 함수 fetchDatafetch를 기다리는 프로미스를 반환해요.

async function fetchData() {
  return await fetch(`https://...`);
}

const getData = cache(fetchData);

async function MyComponent() {
  getData();
  // ... 다른 계산 작업
  await getData();
  // ...
}

getData를 처음 호출할 때, fetchData에서 반환된 프로미스가 캐시돼요. 이후의 조회에서는 동일한 프로미스를 반환하게 되죠.

첫 번째 getData 호출에서는 await를 하지 않지만 두 번째에서는 한다는 걸 주목하세요. await는 프로미스의 확정된 결과를 기다렸다가 반환하는 JavaScript 연산자예요. 첫 번째 getData 호출은 단순히 fetch를 시작해서 두 번째 getData가 조회할 수 있도록 프로미스를 캐시하는 거예요.

두 번째 호출 시점에 프로미스가 아직 pending 상태라면, await가 결과를 기다리며 일시 정지할 거예요. 최적화 포인트는 fetch를 기다리는 동안 React가 계산 작업을 계속할 수 있어서 두 번째 호출의 대기 시간을 줄인다는 거예요.

프로미스가 이미 확정되었다면, 에러든 fulfilled 결과든 await가 그 값을 즉시 반환해요. 두 경우 모두 성능적인 이점이 있어요.

⚠️ 주의: 컴포넌트 바깥에서 메모이제이션된 함수를 호출하면 캐시를 사용하지 않아요.

// 파일명 예시
import {cache} from 'react';

const getUser = cache(async (userId) => {
  return await db.user.query(userId);
});

// 🚩 잘못된 방법: 컴포넌트 바깥에서 메모이제이션된 함수를 호출하면 메모이제이션되지 않아요.
getUser('demo-id');

async function DemoProfile() {
  // ✅ 좋아요: `getUser`가 메모이제이션돼요.
  const user = await getUser('demo-id');
  return <Profile user={user} />;
}

React는 컴포넌트 안에서만 메모이제이션된 함수에 대한 캐시 접근을 제공해요. 컴포넌트 바깥에서 getUser를 호출하면, 함수는 여전히 평가되지만 캐시를 읽거나 업데이트하지는 않아요.

이건 캐시 접근이 context를 통해 제공되기 때문인데, context는 컴포넌트에서만 접근 가능하거든요.

**심층 탐구: `cache`, [`memo`](https://react.dev/reference/react/memo), [`useMemo`](https://react.dev/reference/react/useMemo) 중 언제 무엇을 사용해야 할까요?**

언급된 모든 API들이 메모이제이션을 제공하지만, 차이점은 무엇을 메모이제이션하도록 의도되었는지, 누가 캐시에 접근할 수 있는지, 그리고 캐시가 언제 무효화되는지예요.

useMemo

일반적으로 useMemo는 Client Component에서 렌더링 간에 비용이 많이 드는 계산을 캐싱하는 데 사용해야 해요. 예를 들어, 컴포넌트 내에서 데이터 변환을 메모이제이션할 때요.

// 파일명 예시
'use client';

function WeatherReport({record}) {
  const avgTemp = useMemo(() => calculateAvg(record), record);
  // ...
}

function App() {
  const record = getRecord();
  return (
    <>
      <WeatherReport record={record} />
      <WeatherReport record={record} />
    </>
  );
}

이 예시에서 App이 동일한 record로 두 개의 WeatherReport를 렌더링해요. 두 컴포넌트가 동일한 작업을 하더라도, 작업을 공유할 수 없어요. useMemo의 캐시는 해당 컴포넌트에 로컬이니까요.

하지만 useMemoApp이 리렌더링되고 record 객체가 변하지 않으면, 각 컴포넌트 인스턴스가 작업을 건너뛰고 메모이제이션된 avgTemp 값을 사용하도록 보장해요. useMemo는 주어진 의존성으로 마지막 계산만 캐시해요.

cache

일반적으로 Server Components에서 컴포넌트들 간에 공유될 수 있는 작업을 메모이제이션하려면 cache를 사용해야 해요.

// 파일명 예시
const cachedFetchReport = cache(fetchReport);

function WeatherReport({city}) {
  const report = cachedFetchReport(city);
  // ...
}

function App() {
  const city = "Los Angeles";
  return (
    <>
      <WeatherReport city={city} />
      <WeatherReport city={city} />
    </>
  );
}

이전 예시를 cache를 사용하도록 다시 작성하면, 이 경우 두 번째 WeatherReport 인스턴스가 중복 작업을 건너뛰고 첫 번째 WeatherReport와 동일한 캐시에서 읽을 수 있어요. 이전 예시와 또 다른 차이점은 cache가 데이터 페칭을 메모이제이션하는 데도 권장된다는 거예요. useMemo는 계산에만 사용해야 하는 것과 달리요.

현재 cache는 Server Components에서만 사용해야 하고, 캐시는 서버 요청마다 무효화돼요.

memo

props가 변하지 않으면 컴포넌트가 리렌더링되는 것을 막으려면 memo를 사용해야 해요.

// 파일명 예시
'use client';

function WeatherReport({record}) {
  const avgTemp = calculateAvg(record);
  // ...
}

const MemoWeatherReport = memo(WeatherReport);

function App() {
  const record = getRecord();
  return (
    <>
      <MemoWeatherReport record={record} />
      <MemoWeatherReport record={record} />
    </>
  );
}

이 예시에서 두 MemoWeatherReport 컴포넌트 모두 처음 렌더링될 때 calculateAvg를 호출할 거예요. 하지만 App이 리렌더링되고 record에 변경이 없으면, 어떤 props도 변하지 않았으므로 MemoWeatherReport가 리렌더링되지 않아요.

useMemo와 비교하면, memo는 특정 계산이 아닌 props에 기반해서 컴포넌트 렌더링을 메모이제이션해요. useMemo와 마찬가지로, 메모이제이션된 컴포넌트는 마지막 prop 값으로의 마지막 렌더링만 캐시해요. props가 변하면 캐시가 무효화되고 컴포넌트가 리렌더링돼요.


문제 해결

같은 인자로 호출했는데도 메모이제이션된 함수가 여전히 실행돼요

앞서 언급한 주의사항들을 확인해보세요:

위의 어느 것도 해당되지 않는다면, React가 캐시에 무언가가 존재하는지 확인하는 방식에 문제가 있을 수 있어요.

인자가 원시 타입이 아니라면(예: 객체, 함수, 배열), 동일한 객체 참조를 전달하고 있는지 확인하세요.

메모이제이션된 함수를 호출할 때, React는 입력 인자를 조회해서 결과가 이미 캐시되어 있는지 확인해요. React는 캐시 히트가 있는지 판단하기 위해 인자의 얕은 동등성 비교(shallow equality)를 사용해요.

// 파일명 예시
import {cache} from 'react';

const calculateNorm = cache((vector) => {
  // ...
});

function MapMarker(props) {
  // 🚩 잘못된 방법: props는 매 렌더링마다 변하는 객체예요.
  const length = calculateNorm(props);
  // ...
}

function App() {
  return (
    <>
      <MapMarker x={10} y={10} z={10} />
      <MapMarker x={10} y={10} z={10} />
    </>
  );
}

이 경우 두 MapMarker가 같은 작업을 하고 {x: 10, y: 10, z:10}이라는 동일한 값으로 calculateNorm을 호출하는 것처럼 보여요. 객체들이 동일한 값을 담고 있더라도, 각 컴포넌트가 자체 props 객체를 생성하기 때문에 같은 객체 참조가 아니에요.

React는 캐시 히트가 있는지 확인하기 위해 입력에 대해 Object.is를 호출해요.

// 파일명 예시
import {cache} from 'react';

const calculateNorm = cache((x, y, z) => {
  // ...
});

function MapMarker(props) {
  // ✅ 좋아요: 메모이제이션된 함수에 원시 타입을 전달해요
  const length = calculateNorm(props.x, props.y, props.z);
  // ...
}

function App() {
  return (
    <>
      <MapMarker x={10} y={10} z={10} />
      <MapMarker x={10} y={10} z={10} />
    </>
  );
}

이 문제를 해결하는 한 가지 방법은 벡터 차원들을 calculateNorm에 전달하는 거예요. 차원들 자체가 원시 타입이기 때문에 동작해요.

또 다른 해결책은 벡터 객체 자체를 컴포넌트에 prop으로 전달하는 거예요. 두 컴포넌트 인스턴스에 동일한 객체를 전달해야 해요.

// 파일명 예시
import {cache} from 'react';

const calculateNorm = cache((vector) => {
  // ...
});

function MapMarker(props) {
  // ✅ 좋아요: 동일한 `vector` 객체를 전달해요
  const length = calculateNorm(props.vector);
  // ...
}

function App() {
  const vector = [10, 10, 10];
  return (
    <>
      <MapMarker vector={vector} />
      <MapMarker vector={vector} />
    </>
  );
}

사이트맵

모든 문서 페이지 개요

profile
프론트에_가까운_풀스택_개발자

0개의 댓글