cache - This feature is available in the latest Canary

Canary

  • cache는 오직 React 서버 컴포넌트와 함께 사용됩니다. React 서버 컴포넌트를 지원하는 프레임워크를 확인해 보세요.

  • cacheCanary실험 채널에서만 사용할 수 있습니다. 프로덕션 환경에서 cache를 사용하기 전에 이 한계점에 대해 인지하고 있어야 합니다. React의 릴리즈 채널에 대한 자세한 내용은 여기를 참조하세요.

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);
// ...
}

getMetrics가 처음 data를 호출할 때, getMetricscalculateMetrics(data)를 호출하고 캐시에 결과를 저장합니다. getMetrics가 같은 data와 함께 다시 호출되면, calculateMetrics(data)를 다시 호출하는 대신에 캐싱 된 결과를 반환합니다.

아래에 있는 예시를 참고하세요.

매개변수

  • fn: 결과를 저장하고 싶은 함수. fn는 어떤 인자값도 받을 수 있고 어떤 결과도 반환할 수 있습니다.

반환값

cache는 같은 타입 시그니처를 가진 fn의 캐싱 된 버전을 반환합니다. 이 과정에서 fn를 호출하지 않습니다.

주어진 인자값과 함께 cachedFn를 호출할 때, 캐시에 캐싱 된 데이터가 있는지 먼저 확인합니다. 만약 캐싱 된 데이터가 있다면, 그 결과를 반환합니다. 만약 없다면, 매개변수와 함께 fn을 호출하고 결과를 캐시에 저장하고 값을 반환합니다. fn가 유일하게 호출되는 경우는 캐싱 된 데이터가 없는 경우입니다.

중요합니다!

입력을 기반으로 반환 값 캐싱을 최적화하는 것을 메모이제이션라고 합니다. cache에서 반환되는 함수를 메모화된 함수라고 합니다.

주의

  • React는 서버 요청마다 모든 메모화된 함수들을 위해 캐시를 무효화합니다.
  • cache를 호출할 때마다 새 함수가 생성됩니다. 즉, 동일한 함수로 cache를 여러 번 호출하면 동일한 캐시를 공유하지 않는 다른 메모화된 함수가 반환됩니다.
  • cachedFn 또한 캐시 에러를 잡아냅니다. fn가 특정 인수에 대해 에러를 던지면 캐싱 되고, 동일한 인수로 cachedFn를 호출하면 동일한 에러가 다시 발생합니다.
  • cache서버 컴포넌트에서만 사용가능합니다.

사용법

고비용 연산 캐싱하기

반복 작업을 피하기 위해 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이 먼저 렌더된다고 가정해 봅시다. ProfilegetUserMetrics를 호출하고, 캐싱 된 결과가 있는지 확인합니다. user와 함께 getUserMetrics를 처음 호출하기 때문에, 현재 저장된 캐시는 없습니다. getUserMetricsuser와 함께 calculateUserMetrics를 호출하고 캐시에 결괏값을 저장합니다.

TeamReportusers 목록과 함께 렌더될 때 같은 user 객체를 사용하게 되고, 이는 getUserMetrics를 호출해 캐시에서 결괏값을 읽어옵니다.

주의하세요!

다른 메모화된 함수를 호출하면 다른 캐시에서 읽습니다.

같은 캐시에 접근하기 위해선, 컴포넌트는 반드시 같은 메모화된 함수를 호출해야 합니다.

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

export function Temperature({cityData}) {
// 🚩 Wrong: 컴포넌트에서 `cache`를 호출하면 각 렌더링에 대해 `getWeekReport`가 생성됩니다.
const getWeekReport = cache(calculateWeekReport);
const report = getWeekReport(cityData);
// ...
}
// Precipitation.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

// 🚩 Wrong: `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로 부터 export해 온 같은 메모화된 함수를 호출했습니다.

데이터의 스냅샷 공유하기

컴포넌트끼리 데이터의 스냅샷을 공유하기 위해선 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가 같은 도시를 렌더링할 때, 메모화된 함수로 부터 같은 데이터의 스냅샷을 받게 됩니다.

AnimatedWeatherCardMinimalWeatherCard가 다른 도시getTemperature의 인자로 받게 된다면, fetchTemperature는 두 번 호출되고 호출마다 다른 데이터를 받게 됩니다.

도시가 캐시 키처럼 동작하게 됩니다.

중요합니다!

비동기 렌더링서버 컴포넌트에서만 지원됩니다.

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

클라이언트 컴포넌트에서 비동기 데이터를 사용하는 컴포넌트를 렌더링하고 싶다면 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}) {
// ✅ Good: 사용자 데이터 가져오기를 시작합니다.
getUser(id);
// ... 몇몇의 계산 작업들
return (
<>
<Profile id={id} />
</>
);
}

Page를 렌더링할 때, 컴포넌트는 getUser를 호출하지만, 반환된 데이터를 사용하지 않는다는 점에 유의하세요. 이 초기 getUser 호출은 페이지가 다른 계산 작업을 수행하고 자식을 렌더링하는 동안 발생하는, 비동기 데이터베이스 쿼리를 시작합니다.

Profile을 렌더링할 때, getUser를 다시 호출합니다. 초기 getUser 호출이 이미 사용자 데이터에 반환되고 캐싱 되었다면, Profile해당 데이터를 요청하고 기다릴 때, 다른 원격 프로시저 호출 없이 쉽게 캐시에서 읽어올 수 있습니다. 초기 데이터 요청이 완료되지 않은 경우, 이 패턴으로 데이터를 미리 로드하면 데이터를 받아올 때 생기는 지연이 줄어듭니다.

Deep Dive

비동기 작업 캐싱하기

비동기 함수의 결과를 보면, Promise를 받습니다. 이 Promise는 작업에 대한 상태(보류 중, 완료됨, 실패함)와 최종적으로 확정된 결과를 가지고 있습니다.

이 예시에서, 비동기 함수 fetchDatafetch를 기다리는 Promise를 반환합니다.

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

const getData = cache(fetchData);

async function MyComponent() {
getData();
// ... some computational work
await getData();
// ...
}

getData를 처음 호출할 때, fetchData에서 반환된 Promise가 캐싱 됩니다. 이후 조회 시, 같은 Promise를 반환합니다.

첫 번째 getData 호출은 기다리지 않지만(await) 두 번째는 기다립니다. await 는 자바스크립트 연산자로, 기다렸다가 확정된 Promise의 결과를 반환합니다. 첫 번째 getData은 단순히 조회할 두 번째 getData에 대한 Promise를 캐싱하기 위해 fetch를 실행합니다.

두 번째 호출에서 Promise가 여전히 _보류 중_이면, 결과를 기다리는 동안 await가 일시 중지됩니다. 이 최적화는 데이터 불러오기를 기다리는 동안 React가 계산 작업을 계속할 수 있게 해 두 번째 호출에 대한 대기 시간을 줄일 수 있게 합니다.

완료된 결과나 에러에 대한 Promise가 이미 정해진 경우, await는 즉시 값을 반환합니다. 두 결과 모두 성능상의 이점이 있습니다.

주의하세요!

컴포넌트 외부에서 메모화된 함수를 사용하면 캐시가 사용되지 않습니다.
import {cache} from 'react';

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

// 🚩 Wrong: 컴포넌트 외부에서 메모화된 함수를 호출하면 메모화하지 않습니다.
getUser('demo-id');

async function DemoProfile() {
// ✅ Good: `getUser`는 메모화 됩니다.
const user = await getUser('demo-id');
return <Profile user={user} />;
}

React는 컴포넌트에서 메모화된 함수의 캐시 접근만 제공합니다. 컴포넌트 외부에서 getUser를 호출하면 여전히 함수를 실행하지만, 캐시를 읽거나 업데이트하지는 않습니다.

이는 컴포넌트에서만 접근할 수 있는 컨텍스트를 통해 캐시 접근이 제공되기 때문입니다.

Deep Dive

cache, memo, or useMemo 중 언제 어떤 걸 사용해야 하나요?

언급된 모든 API들은 메모이제이션을 제공하지만, 메모화 대상, 캐시 접근 권한, 캐시 무효화 시점에 차이가 있습니다.

useMemo

일반적으로 useMemo는 클라이언트 컴포넌트에서 렌더링에 걸쳐 고비용의 계산을 캐싱할 때 사용합니다. 예를 들어 컴포넌트 내에서 데이터의 변환을 메모화할 수 있습니다.

'use client';

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

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

이 예시에서 App은 두 개의 WeatherReport를 같은 데이터와 함께 렌더했습니다. 두 컴포넌트가 같은 작업을 수행했음에도 불구하고 서로 작업을 공유하지 않습니다. useMemo의 캐시는 해당 컴포넌트 내부에만 있습니다.

하지만 useMemoApp이 다시 렌더링 되고 record 객체가 변경되지 않는 경우, 각 컴포넌트 인스턴스가 작업을 건너뛰고 메모화된 avgTemp의 값을 사용합니다. useMemo는 주어진 종속성을 가진 avgTemp의 마지막 계산만 캐싱합니다.

cache

일반적으로 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와 같은 캐시를 읽게 됩니다. 이전 예시와 다른 점은 계산에만 사용되는 useMemo와 달리 cache데이터 가져오기를 메모화하는 데도 권장된다는 점입니다.

이때, cache는 서버 컴포넌트에서만 사용해야 하며 캐시는 서버 요청 전체에서 무효화가 됩니다.

memo

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의 변경이 없다면 프로퍼티의 변경이 없기 때문에 MemoWeatherReport가 다시 렌더링 되지 않습니다.

useMemo와 비교하면 memo는 프로퍼티와 특정 계산을 기반으로 컴포넌트 렌더링을 메모화합니다. useMemo와 유사하게, 메모화된 컴포넌트는 마지막 프로퍼티 값에 대한 마지막 렌더링을 캐싱합니다. 프로퍼티가 변경되면, 캐쉬는 무효화되고 컴포넌트는 재 렌더링 됩니다.


문제 해결

동일한 인수로 함수를 호출해도 메모된 함수가 계속 실행됩니다.

앞서 언급된 주의 사항들을 확인하세요.

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

인자가 원시 값(객체, 함수, 배열 등) 이 아니라면, 같은 객체 참조를 넘겼는지 확인하세요.

메모화된 함수 호출 시, React는 입력된 인자값을 조회해 결과가 이미 캐싱 되어 있는지 확인합니다. React는 인수의 얕은 동등성을 사용해 캐시 히트가 있는지를 결정합니다.

import {cache} from 'react';

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

function MapMarker(props) {
// 🚩 Wrong: 인자가 매 렌더링마다 변경되는 객체입니다.
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를 호출하는 듯 보입니다. 객체에 동일한 값이 포함되어 있더라도 각 컴포넌트가 자체 프로퍼티 객체를 생성하므로, 동일한 객체 참조가 아닙니다.

React는 입력에서 Object.is를 호출해 캐시 히트가 있는지 확인합니다.

import {cache} from 'react';

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

function MapMarker(props) {
// ✅ Good: 메모화 함수에 인자로 원시값 제공하기
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에 전달하는 것입니다. 차원 자체가 원시 값이기 때문에 가능합니다.

다른 방법은 벡터 객체를 컴포넌트의 프로퍼티로 전달하는 방법입니다. 두 컴포넌트 인스턴스에 동일한 객체를 전달해야 합니다.

import {cache} from 'react';

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

function MapMarker(props) {
// ✅ Good: 동일한 `vector` 객체를 넘겨줍니다.
const length = calculateNorm(props.vector);
// ...
}

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