좋습니다. 이번 페이지의 cache는 React Server Components에서 여러 컴포넌트가 같은 데이터 요청이나 비싼 계산 결과를 공유하도록 만드는 메모이제이션 API입니다.
가장 먼저 중요한 제한부터 잡겠습니다.
cache는 React Server Components 전용입니다. 클라이언트 컴포넌트에서 일반적인 메모이제이션 용도로 쓰는 API가 아닙니다. (React)
1. 기본 형태
import { cache } from 'react';
const cachedFn = cache(fn);
예를 들어:
import { cache } from 'react';
const getMetrics = cache(calculateMetrics);
이제:
getMetrics(data);
를 처음 호출하면:
cache miss
→ calculateMetrics(data) 실행
→ 결과 저장
같은 인수로 다시 호출하면:
cache hit
→ calculateMetrics 실행 안 함
→ 이전 결과 반환
입니다. (React)
즉 전형적인 memoization입니다.
2. 그런데 useMemo와 뭐가 다른가?
이게 이번 페이지의 가장 중요한 질문입니다.
둘 다 계산 결과를 재사용합니다.
하지만 캐시의 범위가 다릅니다.
useMemo:
function Component({ data }) {
const result = useMemo(
() => expensiveCalculation(data),
[data]
);
return ...
}
캐시는 해당 컴포넌트 인스턴스 내부에 있습니다.
즉:
Component A
→ 자기 useMemo cache
Component B
→ 자기 useMemo cache
이기 때문에 두 컴포넌트가 같은 계산을 해도 결과를 공유하지 않습니다.
반면 cache:
const getResult = cache(expensiveCalculation);
를 여러 Server Component가 같은 함수로 호출하면:
Component A ─┐
├─ 같은 cache
Component B ─┘
를 사용합니다. (React)
이 차이가 핵심입니다.
3. 비싼 계산 공유하기
공식 문서의 대표 사례입니다.
import { cache } from 'react';
const getUserMetrics = cache(calculateUserMetrics);
function Profile({ user }) {
const metrics = getUserMetrics(user);
// ...
}
function TeamReport({ users }) {
for (const user of users) {
const metrics = getUserMetrics(user);
// ...
}
}
같은 user가 Profile과 TeamReport에서 사용된다고 합시다.
먼저 Profile이:
getUserMetrics(user);
를 호출합니다.
cache miss
→ calculateUserMetrics(user)
→ 결과 저장
그 후 TeamReport가 같은 user로:
getUserMetrics(user);
를 호출합니다.
이번에는:
cache hit
→ 이전 결과 재사용
합니다. (React)
따라서 한 번의 서버 렌더 중 여러 컴포넌트가 중복 작업을 하지 않아도 됩니다.
4. 가장 중요한 원칙: 같은 cached function을 호출해야 한다
여기서 흔한 실수가 있습니다.
다음 두 코드를 봅시다.
// Temperature.js
const getWeekReport = cache(calculateWeekReport);
그리고:
// Precipitation.js
const getWeekReport = cache(calculateWeekReport);
둘 다:
cache(calculateWeekReport)
를 호출했으니까 같은 cache를 사용할 것 같죠?
아닙니다.
cache()를 호출할 때마다 새로운 memoized function과 새로운 cache가 만들어집니다. (React)
즉:
cache(fn) #1
→ cache A
cache(fn) #2
→ cache B
입니다.
따라서 두 컴포넌트는 결과를 공유하지 못합니다.
5. 해결 방법: cached function을 모듈에서 한 번만 만든다
이 패턴을 기억하면 됩니다.
// getWeekReport.js
import { cache } from 'react';
import { calculateWeekReport } from './report';
export default cache(calculateWeekReport);
그리고 여러 컴포넌트에서 같은 함수를 import합니다.
// Temperature.js
import getWeekReport from './getWeekReport';
export function Temperature({ cityData }) {
const report = getWeekReport(cityData);
// ...
}
// Precipitation.js
import getWeekReport from './getWeekReport';
export function Precipitation({ cityData }) {
const report = getWeekReport(cityData);
// ...
}
이제:
Temperature ─┐
├─ same getWeekReport
Precipitation┘
↓
same cache
입니다. (React)
실무에서는 이 구조가 굉장히 중요합니다.
6. 컴포넌트 안에서 cache()를 호출하면 안 된다
이 코드는 잘못된 패턴입니다.
function Temperature({ cityData }) {
const getWeekReport = cache(calculateWeekReport);
const report = getWeekReport(cityData);
// ...
}
왜냐하면 컴포넌트가 렌더링될 때마다:
cache(calculateWeekReport)
가 다시 호출됩니다.
그러면 매 렌더마다:
new cached function
→ new cache
가 만들어집니다.
캐시를 만든 의미가 거의 없어집니다. (React)
그래서 기본 규칙은:
cache(fn)
→ 컴포넌트 바깥
→ 보통 모듈 최상단
입니다.
7. 데이터 fetching에도 사용할 수 있다
cache는 단순한 CPU 계산만을 위한 API가 아닙니다.
서버에서 데이터 요청 결과도 캐싱할 수 있습니다.
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);
// ...
}
두 컴포넌트가 같은:
getTemperature('Seoul')
을 호출하면 서버 렌더 중 실제 데이터 요청은 한 번만 일어날 수 있습니다.
두 번째 컴포넌트는 첫 번째 요청의 결과를 공유합니다. (React)
8. 정확히 말하면 Promise도 캐싱한다
비동기 함수:
async function fetchData() {
return await fetch(...);
}
를 호출하면 바로 결과가 나오는 게 아니라:
Promise
를 반환하죠.
그래서:
const getData = cache(fetchData);
라면 첫 호출에서 Promise 자체를 cache에 저장합니다. (React)
예를 들어:
getData();
첫 번째 호출:
fetch 시작
Promise P 생성
cache에 P 저장
아직 완료되지 않았는데 두 번째:
await getData();
를 호출합니다.
그러면:
새 fetch 시작 ❌
cache의 동일한 Promise P 반환 ✅
입니다.
그래서 여러 컴포넌트가 진행 중인 요청도 공유할 수 있습니다.
9. 이걸 이용하면 preload가 가능하다
공식 문서에서 꽤 중요한 사용법입니다.
const getUser = cache(async id => {
return await db.user.query(id);
});
그리고:
function Page({ id }) {
getUser(id);
// 다른 계산
return <Profile id={id} />;
}
여기서:
getUser(id);
를 호출했지만 await하지 않았습니다.
이 호출의 목적은 결과를 사용하는 것이 아니라:
요청을 미리 시작하는 것
입니다. (React)
그 사이 React가 다른 작업을 합니다.
getUser(id)
→ DB 요청 시작
동시에
→ Page의 다른 계산
→ 자식 렌더링
나중에:
async function Profile({ id }) {
const user = await getUser(id);
// ...
}
가 실행됩니다.
이미 요청이 끝났다면 즉시 캐시 결과를 받고,
아직 끝나지 않았다면 이미 진행 중인 Promise를 기다립니다.
10. waterfall을 줄이는 패턴이다
이것을 시간 흐름으로 보면 이해하기 쉽습니다.
preload하지 않은 경우
Page 계산
██████
그 후 Profile 렌더
↓
fetch
█████████
총 시간
███████████████
preload한 경우
fetch
█████████
Page 계산
██████
Profile에서 await
거의 끝남
총 시간
█████████
즉:
비동기 작업과 다른 렌더링 작업을 겹쳐 실행해서 waterfall을 줄인다.
는 전략입니다.
우리가 Suspense에서 배운 “fetch-on-render보다 가능한 일찍 작업을 시작한다”는 사고방식과 연결됩니다.
11. 데이터 snapshot을 공유한다
cache의 또 다른 중요한 역할입니다.
서버 렌더 도중 두 컴포넌트가:
getTemperature('Seoul');
을 호출하면 둘 다 같은 캐시 결과를 사용합니다.
따라서 한 서버 렌더 안에서는 같은 데이터의 동일한 snapshot을 보게 됩니다. (React)
예를 들어 API 값이 아주 빠르게 변한다고 해도:
Component A
→ 20°C
Component B
→ 갑자기 21°C
가 아니라 같은 cached call이라면:
A → 20°C
B → 20°C
처럼 같은 결과를 공유할 수 있습니다.
12. 하지만 요청 간에는 cache가 유지되지 않는다
여기서 매우 중요한 caveat입니다.
React는 각 서버 요청마다 cache를 invalidate합니다. (React)
즉:
HTTP Request A
→ cache 생성
→ 여러 Server Component가 공유
→ 요청 끝
HTTP Request B
→ 새로운 cache
입니다.
따라서 cache는:
글로벌 영구 캐시
가 아닙니다.
이건 반드시 기억하세요.
13. 그래서 cache는 DB 캐시나 CDN 캐시와 다르다
보충 설명
실무에서는 “cache”라는 단어 때문에 혼동하기 쉽습니다.
React cache:
Server Component rendering 중
중복 계산/요청 제거
요청 단위 cache
Redis 같은 외부 cache:
여러 요청/서버 인스턴스에 걸쳐
오래 데이터 유지 가능
HTTP/CDN cache:
네트워크 response 자체 캐싱
완전히 다른 레이어입니다.
따라서:
“React
cache를 썼으니 DB 요청 결과가 1시간 캐싱된다.”
라고 생각하면 틀립니다.
14. 인수가 cache key 역할을 한다
다음 함수가 있다고 합시다.
const getTemperature = cache(async city => {
return fetchTemperature(city);
});
그러면:
getTemperature('Seoul');
getTemperature('Seoul');
은 같은 cache entry를 사용합니다.
하지만:
getTemperature('Seoul');
getTemperature('Busan');
은 다른 인수이므로 각각 별도로 실행됩니다. (React)
개념적으로:
cache
"Seoul" → 25
"Busan" → 27
같은 구조입니다.
15. 객체 인수에서는 reference가 중요하다
이게 가장 흔한 함정 중 하나입니다.
const calculateNorm = cache(vector => {
// ...
});
그리고:
<Marker x={10} y={10} z={10} />
<Marker x={10} y={10} z={10} />
내부에서:
calculateNorm(props);
를 호출한다고 합시다.
겉보기에는 두 props 모두:
{
x: 10,
y: 10,
z: 10
}
입니다.
하지만 JS에서는:
{ x: 10 } === { x: 10 }
// false
죠.
서로 다른 object reference입니다.
React는 cache key를 비교할 때 인수에 대해 Object.is 기반 비교를 사용하므로 cache miss가 발생합니다. (React)
16. 그래서 primitive 인수가 유리하다
이렇게 바꾸면:
const calculateNorm = cache((x, y, z) => {
// ...
});
컴포넌트에서:
calculateNorm(
props.x,
props.y,
props.z
);
라고 호출할 수 있습니다.
둘 다:
10
10
10
이라는 primitive 값이므로 cache hit가 가능합니다. (React)
보충 설명
React Hook dependency에서 우리가 계속 봤던:
객체의 값이 같다
≠ 객체 identity가 같다
가 여기에도 그대로 적용됩니다.
17. 같은 객체 reference를 전달하는 것도 가능하다
반드시 primitive로 바꿔야 하는 것은 아닙니다.
const vector = {
x: 10,
y: 10,
z: 10
};
<MapMarker vector={vector} />
<MapMarker vector={vector} />
처럼 정확히 같은 객체를 두 컴포넌트에 전달한다면:
calculateNorm(vector);
도 cache hit가 가능합니다. (React)
중요한 것은:
값의 구조가 같음
이 아니라:
같은 reference
입니다.
18. error도 cache된다
이것도 꽤 중요한 caveat입니다.
const getUser = cache(id => {
if (id === 'invalid') {
throw new Error('Invalid user');
}
return findUser(id);
});
처음:
getUser('invalid');
에서 에러가 throw되면 그 에러도 cache됩니다. (React)
같은 인수로 다시 호출하면:
getUser('invalid');
함수를 다시 실행하는 것이 아니라 같은 cached error를 다시 throw합니다.
개념적으로:
cache key: "invalid"
result:
Error("Invalid user")
입니다.
19. cached function을 컴포넌트 밖에서 호출하면 cache를 사용하지 않는다
이 caveat는 조금 의외입니다.
const getUser = cache(async userId => {
return db.user.query(userId);
});
// 🚩 컴포넌트 밖
getUser('123');
이 호출은 함수 자체는 실행되지만 React cache를 읽거나 쓰지 않습니다. (React)
왜냐하면 React의 cache 접근은 Server Component rendering context를 통해 제공되기 때문입니다.
공식 문서는 이를 React context를 통해 접근할 수 있기 때문이라고 설명합니다. (React)
따라서:
async function Profile() {
const user = await getUser('123');
// ...
}
처럼 컴포넌트 렌더링 흐름 안에서 호출해야 memoization을 사용합니다.
20. 이것도 헷갈리기 쉬운 포인트다
다음 두 규칙을 동시에 기억해야 합니다.
cache(fn)을 만드는 위치
→ 컴포넌트 밖
cachedFn을 호출하는 위치
→ Server Component 렌더링 안
즉:
// ✅ module scope
const getUser = cache(fetchUser);
async function Profile() {
// ✅ Server Component rendering
const user = await getUser();
// ...
}
입니다.
21. cache vs useMemo vs memo
공식 문서가 이 셋을 직접 비교합니다. (React)
이렇게 정리하면 됩니다.
| API | 무엇을 기억? | 범위 | 주 사용처 |
|---|---|---|---|
cache |
함수 호출 결과 | 여러 Server Components가 공유 | 서버 데이터/계산 |
useMemo |
계산 결과 | 해당 컴포넌트 인스턴스 | Client Component 계산 |
memo |
컴포넌트 렌더링 결과 | 해당 컴포넌트 | props 같을 때 리렌더 건너뛰기 |
조금 더 풀어보겠습니다.
22. useMemo
'use client';
function WeatherReport({ record }) {
const avgTemp = useMemo(
() => calculateAvg(record),
[record]
);
// ...
}
목적:
같은 컴포넌트가 다시 렌더링될 때 계산을 반복하지 않기
입니다.
하지만:
<WeatherReport record={record} />
<WeatherReport record={record} />
두 인스턴스는 서로 cache를 공유하지 않습니다. (React)
23. cache
const getReport = cache(fetchReport);
그리고:
<WeatherReport city="Seoul" />
<WeatherReport city="Seoul" />
두 Server Component가 같은 cached function을 호출하면 서로 결과를 공유할 수 있습니다. (React)
그래서:
useMemo
→ component-local memoization
cache
→ server-render-wide shared memoization
이라고 잡으면 좋습니다.
24. memo
const MemoWeatherReport = memo(WeatherReport);
memo는 계산 결과를 캐싱하는 API가 아닙니다.
props가 같으면
→ 컴포넌트 자체의 rerender를 건너뛰자
가 목적입니다. (React)
즉 질문 자체가 다릅니다.
cache:
같은 함수 호출을 또 해야 하나?
useMemo:
이 컴포넌트에서 같은 계산을 또 해야 하나?
memo:
이 컴포넌트를 아예 다시 렌더링해야 하나?
이렇게 구분하세요.
25. cache와 use는 어떻게 연결되는가?
보충 설명
Server Component에서는:
async function Profile() {
const user = await getUser(id);
}
처럼 직접 await할 수 있습니다.
하지만 Client Component에서는 component 자체를 일반적으로 async로 만들어 같은 방식으로 기다리는 게 아니라, React의 use를 통해 Promise를 읽는 패턴을 사용할 수 있습니다.
공식 문서도 Client Component에서 비동기 데이터를 소비하려면 use() 문서를 참고하라고 안내합니다. (React)
즉 우리가 Suspense에서 배운 흐름과 연결하면:
cache
→ Promise 공유 / 중복 요청 제거
use
→ Promise 결과 읽기
Suspense
→ Promise가 아직 pending이면 fallback 처리
라고 볼 수 있습니다.
다만 cache 자체는 Server Components 전용이라는 점은 그대로입니다.
26. cacheSignal()과도 연결된다
현재 문서에서는 비싼 계산이나 요청이 취소 가능하다면 cacheSignal()을 사용할 수 있다고 안내합니다. (React)
예를 들어 개념적으로:
const signal = cacheSignal();
expensiveOperation({ signal });
React가 해당 서버 렌더의 cache lifecycle을 끝냈을 때 불필요한 비동기 작업을 취소하는 데 사용할 수 있습니다.
이 API는 뒤에서 별도 페이지로 공부할 수 있으므로 지금은:
cache의 수명과 연동되는 AbortSignal도 있다.
정도로만 기억하면 충분합니다.
27. 실무에서 어디에 쓰나?
대표적인 경우를 생각해보면:
DB 조회
const getUser = cache(id =>
db.user.findUnique({ where: { id } })
);
페이지 곳곳에서 같은 사용자가 필요해도 중복 조회를 줄일 수 있습니다.
권한 계산
const getPermissions = cache(user =>
calculatePermissions(user)
);
여러 Server Component가 같은 권한 결과를 공유할 수 있습니다.
비싼 서버 계산
const getReport = cache(data =>
generateComplexReport(data)
);
preload
getUser(id); // 미리 시작
return <Profile id={id} />;
같은 패턴입니다.
28. 언제 쓰지 않아야 하나?
첫째, Client Component입니다.
'use client';
const fn = cache(...); // 목적에 맞지 않음
이런 용도가 아닙니다.
둘째, 장기간 데이터 캐싱을 원하는 경우입니다.
"이 결과를 1시간 저장하고 싶다"
React cache의 역할이 아닙니다.
셋째, 동일 함수 호출이 거의 반복되지 않는 단순 계산이라면 굳이 쓸 필요가 없습니다.
캐싱도 복잡성을 추가하므로 실제 중복 작업이 있는 곳에서 사용하는 게 좋습니다.
29. 가장 흔한 실수 정리
실수 1
컴포넌트 안에서:
const getData = cache(fetchData);
→ 매번 다른 cache를 만듭니다.
실수 2
각 파일에서:
cache(fetchData)
를 따로 호출합니다.
→ 서로 cache를 공유하지 않습니다.
실수 3
cachedFn({ id: 1 });
cachedFn({ id: 1 });
이면 cache hit일 거라 생각합니다.
→ 객체 reference가 다르므로 miss일 수 있습니다.
실수 4
React cache를:
permanent server cache
라고 생각합니다.
→ 서버 요청마다 invalidate됩니다. (React)
실수 5
컴포넌트 바깥에서:
getUser(id);
를 호출해서 cache를 미리 채우려고 합니다.
→ React render context 밖에서는 cache를 사용하지 않습니다. (React)
30. 이번 페이지의 핵심 mental model
이 구조로 기억하면 됩니다.
Server Request
│
▼
React cache scope
│
┌─────────────┼─────────────┐
▼ ▼ ▼
Server Comp A Server Comp B Server Comp C
│ │ │
└──── same cachedFn ─────────┘
│
same arguments?
/ \
yes no
│ │
cached result fn 실행
그리고 요청이 끝나면:
cache invalidate
입니다.
핵심 정리
cache를 한 문장으로 설명하면:
cache(fn)은 React Server Components에서 같은 인수로 호출된 함수의 결과를 한 서버 요청 동안 여러 컴포넌트가 공유하도록 만드는 memoization API입니다. (React)
가장 중요한 규칙은 다음입니다.
- Server Components 전용입니다.
cache(fn)은 컴포넌트 밖에서 한 번 만들고, 같은 memoized function을 여러 컴포넌트가 공유해야 합니다.- 같은 인수로 호출하면 결과뿐 아니라 비동기 함수의 Promise도 공유할 수 있습니다.
- 이를 이용해 데이터 요청을 미리 시작하는 preload 패턴을 만들 수 있습니다. (React)
- cache는 서버 요청마다 초기화되므로 장기 저장소가 아닙니다.
- 객체/배열/함수 인수는 내용이 같아도 reference가 다르면 cache miss가 발생합니다.
- 함수가 throw한 error 역시 cache됩니다.
cache는 여러 Server Component 간 작업 공유,useMemo는 한 컴포넌트의 계산 재사용,memo는 컴포넌트 rerender 자체를 건너뛰는 용도입니다. (React)
이해도 테스트
정답은 바로 알려주지 않겠습니다.
- 다음 코드에서
UserCard와Header가 같은id로getUser를 호출한다면 DB query는 몇 번 실행될 가능성이 높을까요? 이유도 설명해보세요.
const getUser = cache(fetchUser);
async function Header({ id }) {
const user = await getUser(id);
}
async function UserCard({ id }) {
const user = await getUser(id);
}
- 다음 두 함수가 cache를 공유할까요?
const a = cache(fetchUser);
const b = cache(fetchUser);
- 왜 아래 코드는 cache hit가 되지 않을 수 있을까요?
getData({ id: 1 });
getData({ id: 1 });
cache와useMemo의 가장 중요한 범위(scope) 차이는 무엇인가요?getUser(id)를 먼저await없이 호출한 뒤, 나중에 다시await getUser(id)하는 preload 패턴이 waterfall을 줄일 수 있는 이유를 설명해보세요.
'리액트 > 레퍼런스 (react 19ver)' 카테고리의 다른 글
| cacheSignal (0) | 2026.09.15 |
|---|---|
| addTransitionType (0) | 2026.09.15 |
| act (0) | 2026.09.15 |
| <ViewTransition> (0) | 2026.09.15 |
| <Activity> (0) | 2026.09.14 |