--- title: cache canary: true --- `cache`는 오직 [React 서버 컴포넌트](/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components)와 함께 사용합니다. `cache`를 통해 가져온 데이터나 연산의 결과를 캐싱합니다. ```js const cachedFn = cache(fn); ``` --- ## 레퍼런스 {/*reference*/} ### `cache(fn)` {/*cache*/} 컴포넌트 외부에서 `cache`를 호출해 캐싱 기능을 가진 함수의 한 버전을 만들 수 있습니다. ```js {4,7} import {cache} from 'react'; import calculateMetrics from 'lib/metrics'; const getMetrics = cache(calculateMetrics); function Chart({data}) { const report = getMetrics(data); // ... } ``` `getMetrics`가 처음 `data`를 호출할 때, `getMetrics`는 `calculateMetrics(data)`를 호출하고 캐시에 결과를 저장합니다. `getMetrics`가 같은 `data`와 함께 다시 호출되면, `calculateMetrics(data)`를 다시 호출하는 대신에 캐싱된 결과를 반환합니다. [아래 예시를 참고하세요.](#usage) #### 매개변수 {/*parameters*/} - `fn`: 결과를 저장하고 싶은 함수. `fn`은 어떤 인수도 받을 수 있고 어떠한 결과도 반환할 수 있습니다. #### 반환값 {/*returns*/} `cache`는 같은 타입 시그니처를 가진 `fn`의 캐싱된 버전을 반환합니다. 이 과정에서 `fn`을 호출하지 않습니다. 주어진 인수와 함께 `cachedFn`을 호출할 때, 캐시에 캐싱된 데이터가 있는지 먼저 확인합니다. 만약 캐싱된 데이터가 있다면, 그 결과를 반환합니다. 만약 없다면, 매개변수와 함께 `fn`을 호출하고 결과를 캐시에 저장하고 값을 반환합니다. `fn`이 유일하게 호출되는 경우는 캐싱된 데이터가 없는 경우입니다. 입력을 기반으로 반환 값 캐싱을 최적화하는 것을 [_메모이제이션_](https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98)이라고 합니다. `cache`에서 반환되는 함수를 메모화된 함수라고 합니다. #### 주의 사항 {/*caveats*/} - React는 서버 요청마다 모든 메모화된 함수들을 위해 캐시를 무효화합니다. - `cache`를 호출할 때마다 새 함수가 생성됩니다. 즉, 동일한 함수로 `cache`를 여러 번 호출하면 동일한 캐시를 공유하지 않는 다른 메모화된 함수가 반환됩니다. - `cachedFn` 또한 캐시 오류를 잡아냅니다. `fn`이 특정 인수에 대해 오류를 던지면 캐싱되고, 동일한 인수로 `cachedFn`를 호출하면 동일한 오류가 다시 발생합니다. - `cache`는 [서버 컴포넌트](/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components)에서만 사용가능합니다. --- ## 사용법 {/*usage*/} ### 고비용 연산 캐싱하기 {/*cache-expensive-computation*/} 반복 작업을 피하기 위해 `cache`를 사용하세요. ```js [[1, 7, "getUserMetrics(user)"],[2, 13, "getUserMetrics(user)"]] 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` 객체가 `Profile`과 `TeamReport`에서 렌더링될 때, 두 컴포넌트는 작업을 공유하고, `user`를 위한 `calculateUserMetrics`를 한 번만 호출합니다. `Profile`이 먼저 렌더링된다고 가정합시다. `Profile`은 `getUserMetrics`를 호출하고, 캐싱된 결과가 있는지 확인합니다. `user`와 함께 `getUserMetrics`를 처음 호출하기 때문에, 현재 저장된 캐시는 없습니다. `getUserMetrics`는 `user`와 함께 `calculateUserMetrics`를 호출하고 캐시에 결과값을 저장합니다. `TeamReport`가 `users` 목록과 함께 렌더링될 때 같은 `user` 객체를 사용하게 되고, 이는 `getUserMetrics`를 호출해 캐시에서 결과값을 읽어옵니다. ##### 다른 메모화된 함수를 호출하면 다른 캐시에서 읽습니다. {/*pitfall-different-memoized-functions*/} 같은 캐시에 접근하기 위해선, 컴포넌트는 반드시 같은 메모화된 함수를 호출해야 합니다. ```js [[1, 7, "getWeekReport"], [1, 7, "cache(calculateWeekReport)"], [1, 8, "getWeekReport"]] // 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); // ... } ``` ```js [[2, 6, "getWeekReport"], [2, 6, "cache(calculateWeekReport)"], [2, 9, "getWeekReport"]] // 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); // ... } ``` 위의 예시에서, `Precipitation``Temperature`는 각각 `cache`를 호출하여 자체 캐시 조회를 통해 새로운 메모화된 함수를 만들어 냅니다. 두 컴포넌트가 같은 `cityData`를 렌더링한다면, `calculateWeekReport`를 호출하는 반복 작업을 하게 됩니다. 게다가, `Temperature`는 컴포넌트가 렌더링될 때마다 어떤 캐시 공유도 허용하지 않는 새로운 메모화된 함수를 생성하게 됩니다. 캐시 사용을 늘리고 작업을 줄이기 위해서 두 컴포넌트는 같은 캐시에 접근하는 같은 메모화된 함수를 호출해야 합니다. 대신, 컴포넌트끼리 [`import` 할 수 있는](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) 전용 모듈에 메모화된 함수를 정의하세요. ```js [[3, 5, "export default cache(calculateWeekReport)"]] // getWeekReport.js import {cache} from 'react'; import {calculateWeekReport} from './report'; export default cache(calculateWeekReport); ``` ```js [[3, 2, "getWeekReport", 0], [3, 5, "getWeekReport"]] // Temperature.js import getWeekReport from './getWeekReport'; export default function Temperature({cityData}) { const report = getWeekReport(cityData); // ... } ``` ```js [[3, 2, "getWeekReport", 0], [3, 5, "getWeekReport"]] // Precipitation.js import getWeekReport from './getWeekReport'; export default function Precipitation({cityData}) { const report = getWeekReport(cityData); // ... } ``` 여기 두 컴포넌트가 같은 캐시를 읽고 쓰기 위해 `./getWeekReport.js`로 부터 `export`해 온 같은 메모화된 함수를 호출했습니다. ### 데이터의 스냅샷 공유하기 {/*take-and-share-snapshot-of-data*/} 컴포넌트끼리 데이터의 스냅샷을 공유하기 위해선 `fetch`와 같이 데이터를 가져오는 함수와 함께 `cache`를 사용해야 합니다. 여러 컴포넌트가 같은 데이터를 받아올 때, 요청이 한 번만 발생하고 받아온 데이터는 캐싱되며 컴포넌트끼리 공유됩니다. 모든 컴포넌트는 서버 렌더링 전반에 걸쳐 동일한 데이터 스냅샷을 참조합니다. ```js [[1, 4, "city"], [1, 5, "fetchTemperature(city)"], [2, 4, "getTemperature"], [2, 9, "getTemperature"], [1, 9, "city"], [2, 14, "getTemperature"], [1, 14, "city"]] 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); // ... } ``` `AnimatedWeatherCard`와 `MinimalWeatherCard`가 같은 city를 렌더링할 때, 메모화된 함수로 부터 같은 데이터의 스냅샷을 받게 됩니다. `AnimatedWeatherCard`와 `MinimalWeatherCard`가 다른 city`getTemperature`의 인수로 받게 된다면, `fetchTemperature`는 두 번 호출되고 호출마다 다른 데이터를 받게됩니다. city가 캐시 키Key처럼 동작하게 됩니다. 비동기 렌더링은 [서버 컴포넌트](/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components)에서만 지원됩니다. ```js [[3, 1, "async"], [3, 2, "await"]] async function AnimatedWeatherCard({city}) { const temperature = await getTemperature(city); // ... } ``` 클라이언트 컴포넌트에서 비동기 데이터를 사용하는 컴포넌트를 렌더링하고 싶다면 [`use`](/reference/react/use) 문서를 참고하세요. ### 사전에 데이터 받아두기 {/*preload-data*/} 긴 실행 시간이 소요되는 데이터 가져오기를 캐싱하면, 컴포넌트를 렌더링하기 전에 비동기 작업을 시작할 수 있습니다. ```jsx [[2, 6, "await getUser(id)"], [1, 17, "getUser(id)"]] const getUser = cache(async (id) => { return await db.user.query(id); }); async function Profile({id}) { const user = await getUser(id); return (

{user.name}

); } function Page({id}) { // ✅ Good: 사용자 데이터 가져오기를 시작합니다. getUser(id); // ... 몇몇의 계산 작업들 return ( <> ); } ``` `Page`를 렌더링할 때, 컴포넌트는 `getUser`를 호출하지만, 반환된 데이터를 사용하지 않는다는 점에 유의하세요. 이 초기 `getUser` 호출은 페이지가 다른 계산 작업을 수행하고 자식을 렌더링하는 동안 발생하는, 비동기 데이터베이스 쿼리를 시작합니다. `Profile`을 렌더링할 때, `getUser`를 다시 호출합니다. 초기 `getUser` 호출이 이미 사용자 데이터에 반환되고 캐싱되었다면, `Profile`이 해당 데이터를 요청하고 기다릴 때, 다른 원격 프로시저 호출 없이 쉽게 캐시에서 읽어올 수 있습니다. 초기 데이터 요청이 완료되지 않은 경우, 이 패턴으로 데이터를 미리 로드하면 데이터를 받아올 때 생기는 지연이 줄어듭니다. #### 비동기 작업 캐싱하기 {/*caching-asynchronous-work*/} [비동기 함수](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/async_function)의 결과를 보면, [Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise)를 받습니다. 이 Promise는 작업에 대한 상태(_대기_, _완료_, _실패_)와 최종적으로 확정된 결과를 가지고 있습니다. 이 예시에서, 비동기 함수 `fetchData`는 `fetch`를 기다리는 Promise를 반환합니다. ```js [[1, 1, "fetchData()"], [2, 8, "getData()"], [3, 10, "getData()"]] 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`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/await)은 자바스크립트 연산자로, 기다렸다가 확정된 Promise의 결과를 반환합니다. 첫 번째 `getData`는 단순히 조회할 두 번째 `getData`에 대한 Promise를 캐싱하기 위해 `fetch`를 실행합니다. 두 번째 호출에서 Promise가 여전히 대기 중이면, 결과를 기다리는 동안 `await`이 일시 중지됩니다. 이 최적화는 데이터 불러오기를 기다리는 동안 React가 계산 작업을 계속할 수 있게 해 두 번째 호출에 대한 대기 시간을 줄일 수 있게 합니다. _완료된_ 결과나 오류에 대한 Promise가 이미 정해진 경우, `await`는 즉시 값을 반환합니다. 두 결과 모두 성능상의 이점이 있습니다. ##### 컴포넌트 외부에서 메모화된 함수를 사용하면 캐시가 사용되지 않습니다. {/*pitfall-memoized-call-outside-component*/} ```jsx [[1, 3, "getUser"]] 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 ; } ``` React는 컴포넌트에서 메모화된 함수의 캐시 접근만 제공합니다. 컴포넌트 외부에서 `getUser`를 호출하면 여전히 함수를 실행하지만, 캐시를 읽거나 업데이트하지는 않습니다. 이는 컴포넌트에서만 접근할 수 있는 [Context](/learn/passing-data-deeply-with-context)를 통해 캐시 접근이 제공되기 때문입니다. #### `cache`, [`memo`](/reference/react/memo), [`useMemo`](/reference/react/useMemo) 중 언제 어떤 걸 사용해야 하나요? {/*cache-memo-usememo*/} 언급된 모든 API들은 메모이제이션을 제공하지만, 메모화 대상, 캐시 접근 권한, 캐시 무효화 시점에 차이가 있습니다. #### `useMemo` {/*deep-dive-use-memo*/} 일반적으로 [`useMemo`](/reference/react/useMemo)는 클라이언트 컴포넌트에서 렌더링에 걸쳐 고비용의 계산을 캐싱할 때 사용합니다. 예를 들어 컴포넌트 내에서 데이터의 변환을 메모화할 수 있습니다. ```jsx {4} 'use client'; function WeatherReport({record}) { const avgTemp = useMemo(() => calculateAvg(record), record); // ... } function App() { const record = getRecord(); return ( <> ); } ``` 이 예시에서 `App`은 두 개의 `WeatherReport`를 같은 데이터와 함께 렌더링했습니다. 두 컴포넌트가 같은 작업을 수행했음에도 불구하고 서로 작업을 공유하지 않습니다. `useMemo`의 캐시는 해당 컴포넌트 내부에만 있습니다. 하지만 `useMemo`는 `App`이 다시 렌더링되고 `record` 객체가 변경되지 않는 경우, 각 컴포넌트 인스턴스가 작업을 건너뛰고 메모화된 `avgTemp`의 값을 사용합니다. `useMemo`는 주어진 종속성을 가진 `avgTemp`의 마지막 계산만 캐싱합니다. #### `cache` {/*deep-dive-cache*/} 일반적으로 `cache`는 서버 컴포넌트에서 컴포넌트 간에 공유할 수 있는 작업을 메모화하기 위해 사용합니다. ```js [[1, 12, ""], [3, 13, ""], [2, 1, "cache(fetchReport)"]] const cachedFetchReport = cache(fetchReport); function WeatherReport({city}) { const report = cachedFetchReport(city); // ... } function App() { const city = "Los Angeles"; return ( <> ); } ``` 이전 예시를 `cache`를 이용해 재작성하면, 이 경우에 `WeatherReport`의 두 번째 인스턴스는 중복 작업을 생략하고 첫 번째 `WeatherReport`와 같은 캐시를 읽게 됩니다. 이전 예시와 다른 점은 계산에만 사용되는 `useMemo`와 달리 `cache`는 데이터 가져오기를 메모화하는 데도 권장된다는 점입니다. 이때, `cache`는 서버 컴포넌트에서만 사용해야 하며 캐시는 서버 요청 전체에서 무효화가 됩니다. #### `memo` {/*deep-dive-memo*/} [`memo`](reference/react/memo)는 프로퍼티가 변경되지 않았을 때 컴포넌트가 다시 렌더링되는 것을 막기 위해 사용합니다. ```js 'use client'; function WeatherReport({record}) { const avgTemp = calculateAvg(record); // ... } const MemoWeatherReport = memo(WeatherReport); function App() { const record = getRecord(); return ( <> ); } ``` 예시에서 `MemoWeatherReport` 컴포넌트 모두 첫 번째 렌더링에서 `calculateAvg`를 호출합니다. 하지만 `App`이 다시 렌더링될 때 `record`의 변경이 없다면 프로퍼티의 변경이 없기 때문에 `MemoWeatherReport`가 다시 렌더링되지 않습니다. `useMemo`와 비교하면 `memo`는 프로퍼티와 특정 계산을 기반으로 컴포넌트 렌더링을 메모화합니다. `useMemo`와 유사하게, 메모화된 컴포넌트는 마지막 프로퍼티 값에 대한 마지막 렌더링을 캐싱합니다. 프로퍼티가 변경되면, 캐시는 무효화되고 컴포넌트는 다시 렌더링됩니다. --- ## 문제 해결 {/*troubleshooting*/} ### 동일한 인수로 함수를 호출해도 메모된 함수가 계속 실행됩니다 {/*memoized-function-still-runs*/} 앞서 언급된 주의 사항들을 확인하세요. * [다른 메모화된 함수를 호출하면 다른 캐시에서 읽습니다.](#pitfall-different-memoized-functions) * [컴포넌트 외부에서 메모화된 함수를 사용하면 캐시가 사용되지 않습니다.](#pitfall-memoized-call-outside-component) 위의 어느 것도 해당하지 않는다면, React가 캐시에 무엇이 존재하는지 확인하는 방식에 문제가 있을 수 있습니다. 인자가 [원시 값](https://developer.mozilla.org/ko/docs/Glossary/Primitive)(객체, 함수, 배열 등)이 아니라면, 같은 객체 참조를 넘겼는지 확인하세요. 메모화된 함수 호출 시, React는 입력된 인자값을 조회해 결과가 이미 캐싱되어 있는지 확인합니다. React는 인수들의 얕은 동등성을 사용해 캐시 히트가 있는지를 결정합니다. ```js 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}`와 함께 `calculateNorm`를 호출하는 듯 보입니다. 객체에 동일한 값이 포함되어 있더라도 각 컴포넌트가 자체 프로퍼티 객체를 생성하므로, 동일한 객체 참조가 아닙니다. React는 입력에서 [`Object.is`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is)를 호출해 캐시 히트가 있는지 확인합니다. ```js {3,9} 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 ( <> ); } ``` 이 문제를 해결하는 한 가지 방법은 벡터 차원을 `calculateNorm`에 전달하는 것입니다. 차원 자체가 원시 값이기 때문에 가능합니다. 다른 방법은 벡터 객체를 컴포넌트의 프로퍼티로 전달하는 방법입니다. 두 컴포넌트 인스턴스에 동일한 객체를 전달해야 합니다. ```js {3,9,14} 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 ( <> ); } ```