좋습니다. cacheSignal은 바로 직전에 본 cache의 “수명”과 연결되는 API입니다.
한 문장으로 먼저 잡으면:
cacheSignal()은 현재 React cache가 더 이상 필요하지 않을 때 abort되는 AbortSignal을 제공합니다.
즉 cache()로 시작한 비동기 작업을 React cache의 생명주기에 맞춰 취소할 수 있게 하는 API입니다.
1. 가장 기본적인 형태
import { cache, cacheSignal } from 'react';
const getData = cache(async () => {
const signal = cacheSignal();
const response = await fetch('/api/data', {
signal,
});
return response.json();
});
여기서 중요한 부분은:
const signal = cacheSignal();
입니다.
이 signal을:
fetch(url, { signal })
에 넘기면 React가 해당 cache 작업을 더 이상 필요로 하지 않을 때 요청을 abort할 수 있습니다.
2. 왜 이런 API가 필요한가?
직전 cache 페이지에서 이런 패턴을 봤습니다.
const getUser = cache(async id => {
return fetchUser(id);
});
여러 Server Component가:
getUser(id);
를 공유하면 중복 요청을 피할 수 있었습니다.
그런데 한 가지 문제가 있습니다.
서버 렌더링 도중 React가:
“이 렌더 결과는 이제 필요 없어.”
라고 판단했다고 해봅시다.
예를 들어:
렌더 A 시작
↓
데이터 요청 시작
↓
렌더 A 폐기
↓
새로운 렌더 B 시작
이런 상황입니다.
그런데 기존 데이터 요청이 취소되지 않는다면:
렌더 A는 이미 필요 없음
하지만 fetch A는 계속 실행 중
이 됩니다.
CPU, DB connection, 네트워크 같은 자원을 불필요하게 계속 사용할 수 있죠.
cacheSignal()은 바로 이 문제를 해결합니다.
3. cacheSignal()이 의미하는 것
개념적으로:
React cache entry
│
├── 작업이 필요함
│ signal.aborted = false
│
└── 더 이상 필요 없음
signal abort
입니다.
따라서:
const signal = cacheSignal();
은:
“현재 이 cached 작업의 lifetime을 나타내는 AbortSignal을 줘.”
라는 뜻으로 이해하면 됩니다.
4. AbortSignal 복습
JavaScript에서 AbortSignal은 비동기 작업 취소에 사용하는 표준 API입니다.
가장 대표적인 형태는:
const controller = new AbortController();
fetch('/api/data', {
signal: controller.signal,
});
controller.abort();
입니다.
흐름은:
AbortController
│
└── signal
↓
fetch
controller.abort()
↓
signal.aborted = true
↓
fetch 취소
입니다.
cacheSignal()도 같은 AbortSignal을 반환합니다.
차이는 우리가 직접:
controller.abort()
를 호출하는 게 아니라 React가 cache lifecycle에 맞춰 abort한다는 점입니다.
5. cacheSignal + cache
가장 자연스러운 사용법입니다.
const getUser = cache(async id => {
const signal = cacheSignal();
const response = await fetch(
`/api/users/${id}`,
{ signal }
);
return response.json();
});
여러 Server Component가:
await getUser('123');
을 호출한다고 합시다.
첫 호출:
cache miss
↓
fetch 시작
↓
AbortSignal도 해당 cache lifecycle에 연결
두 번째 호출:
cache hit
↓
같은 Promise 공유
그리고 React가 해당 cache entry를 더 이상 필요로 하지 않는다면:
signal abort
↓
진행 중인 fetch 취소 가능
입니다.
6. 왜 cache 함수 안에서 쓰는가?
cacheSignal()은 일반적인 글로벌 AbortSignal API가 아닙니다.
현재 React cache context의 lifetime을 알아야 하기 때문에 cache 작업과 연결해서 사용하는 것이 핵심입니다.
즉 이런 mental model이 좋습니다.
cache(fn)
→ 작업 결과의 lifetime 관리
cacheSignal()
→ 그 lifetime 종료를 async 작업에 전달
둘이 한 세트에 가깝습니다.
7. 서버 요청이 끝났다는 것과 정확히 같은가?
완전히 같은 개념으로만 보면 안 됩니다.
cacheSignal()은 단순히:
HTTP request 종료
→ abort
라는 타이머가 아닙니다.
더 정확히는:
React가 해당 cache된 작업이 더 이상 필요하지 않다고 판단하는 lifecycle에 연결됩니다.
예를 들어 React 서버 렌더링은 작업을 시작했다가 중단하거나 버릴 수 있습니다.
render attempt
↓
cached async 작업 시작
↓
렌더 폐기
↓
해당 작업 더 이상 불필요
↓
signal abort
같은 상황도 포함됩니다.
8. fetch에 가장 자연스럽게 연결된다
AbortSignal을 기본 지원하는 대표 API가 fetch입니다.
const getWeather = cache(async city => {
const signal = cacheSignal();
const res = await fetch(
`/weather?city=${city}`,
{ signal }
);
return res.json();
});
이렇게 하면 React cache가 무효해질 때 불필요한 HTTP 요청을 취소할 수 있습니다.
9. 직접 만든 비동기 함수에도 사용할 수 있다
반드시 fetch만 가능한 것은 아닙니다.
예를 들어:
async function expensiveOperation(signal) {
while (!signal.aborted) {
// expensive work
}
if (signal.aborted) {
throw new DOMException(
'Aborted',
'AbortError'
);
}
}
그리고:
const getResult = cache(async () => {
const signal = cacheSignal();
return expensiveOperation(signal);
});
처럼 사용할 수 있습니다.
핵심은 비동기 작업이 AbortSignal을 이해하도록 설계되어 있어야 한다는 겁니다.
10. DB 쿼리에도 가능할까?
가능할 수 있지만 DB 라이브러리가 AbortSignal 또는 이에 준하는 cancellation API를 지원해야 합니다.
예를 들어 어떤 DB 클라이언트가:
db.query(sql, { signal })
을 지원한다면:
const getUsers = cache(async () => {
const signal = cacheSignal();
return db.query(
'SELECT * FROM users',
{ signal }
);
});
처럼 연결할 수 있습니다.
보충 설명
모든 DB 라이브러리가 AbortSignal을 직접 지원하는 것은 아닙니다.
이 경우:
React signal abort
→ DB driver cancel API 호출
같은 adapter를 직접 만들 수도 있습니다.
즉 cacheSignal은 cancellation을 발생시키는 신호이지, 모든 비동기 시스템을 자동으로 취소시키는 API는 아닙니다.
11. signal.aborted를 직접 확인할 수도 있다
AbortSignal이므로:
const signal = cacheSignal();
if (signal.aborted) {
// 이미 취소된 상태
}
처럼 확인할 수 있습니다.
또:
signal.addEventListener('abort', () => {
// cleanup
});
도 가능합니다.
예를 들어:
const getReport = cache(async () => {
const signal = cacheSignal();
signal.addEventListener('abort', () => {
console.log('report generation cancelled');
});
return generateReport(signal);
});
입니다.
12. 그런데 보통은 fetch(..., { signal })처럼 넘기는 게 좋다
직접:
signal.addEventListener(...)
를 쓰기보다 대상 API가 signal 옵션을 지원한다면 그대로 전달하는 편이 가장 좋습니다.
fetch(url, { signal });
왜냐하면 cancellation lifecycle을 그 API가 올바르게 처리해주기 때문입니다.
13. cache가 hit됐을 때 signal은 어떻게 생각하면 될까?
여기서 조금 중요한 개념이 있습니다.
const getData = cache(async () => {
const signal = cacheSignal();
return fetchData(signal);
});
첫 호출:
getData()
↓
함수 본문 실행
↓
signal 생성
↓
Promise cache
두 번째 동일 호출:
getData()
↓
cache hit
↓
함수 본문 다시 실행 ❌
이므로 새로운 cacheSignal()도 만들지 않습니다.
첫 번째 실행에서 시작된 같은 작업/Promise를 공유합니다.
즉:
하나의 cached computation
→ 하나의 lifetime
→ 그 lifetime과 연결된 signal
이라고 보면 됩니다.
14. cacheSignal을 호출했다고 결과가 캐싱되는 것은 아니다
이건 역할을 분리해서 봐야 합니다.
cacheSignal()
자체는 memoization API가 아닙니다.
cache()
→ 함수 결과 캐싱
cacheSignal()
→ cache lifecycle cancellation signal
입니다.
따라서:
const fn = async () => {
const signal = cacheSignal();
};
만으로 함수 호출 결과가 자동 캐시되는 것은 아닙니다.
15. 왜 React가 이런 API를 제공할까?
React Server Components는 서버 렌더링을 단순히:
컴포넌트 호출
→ HTML 생성
처럼 한 번에 끝내지 않습니다.
Suspense를 포함하면:
렌더 시작
↓
async resource 대기
↓
일부 stream
↓
다른 subtree 렌더
↓
렌더 취소/재시도 가능
같은 복잡한 lifecycle이 생깁니다.
그래서 React가:
“이 작업은 현재 render/cache에 필요하다.”
와:
“이제 더 이상 필요 없다.”
를 알고 있습니다.
cacheSignal()은 React가 알고 있는 그 lifetime 정보를 외부 async system에도 전달하는 API입니다.
16. Suspense와 연결하면 이해가 쉽다
예를 들어:
const getUser = cache(async id => {
const signal = cacheSignal();
return fetchUser(id, { signal });
});
그리고:
async function Profile({ id }) {
const user = await getUser(id);
return <h1>{user.name}</h1>;
}
Profile이 suspend할 수 있습니다.
Profile render
↓
getUser()
↓
Promise pending
↓
Suspense
그런데 이 렌더 자체가 취소되거나 더 이상 필요 없어지면:
cached request 필요 없음
↓
cacheSignal abort
↓
fetchUser 취소 가능
입니다.
즉:
Suspense
→ 기다리는 UI 관리
cache
→ Promise 공유
cacheSignal
→ Promise 작업의 취소 lifecycle 관리
라고 연결할 수 있습니다.
17. cache 페이지의 preload 패턴과도 연결된다
우리가 앞에서:
getUser(id); // preload
return <Profile id={id} />;
패턴을 봤습니다.
이렇게 미리 비동기 작업을 시작할 수 있죠.
그런데 이후 해당 UI가 필요 없어졌다면:
preload는 했는데
실제로 소비하지 않음
이 될 수 있습니다.
cacheSignal()이 있으면 이 preload 작업도 cache lifetime과 연결해서 취소할 수 있습니다.
미리 시작
↓
더 이상 필요 없음
↓
abort
입니다.
preload를 공격적으로 할수록 cancellation이 중요해지는 이유입니다.
18. cacheSignal()과 직접 만든 AbortController의 차이
직접 만들 수도 있습니다.
const controller = new AbortController();
하지만 문제는:
언제 controller.abort()를 호출할 것인가?
입니다.
React Server Components의 lifecycle은 React가 가장 잘 알고 있습니다.
직접 관리하면:
렌더가 언제 폐기됐는지
cache가 언제 더 이상 필요한지
를 애플리케이션 코드가 알아내기 어렵습니다.
cacheSignal()은 이 lifecycle을 React가 관리해줍니다.
직접 AbortController
→ lifecycle도 직접 관리
cacheSignal
→ React cache lifecycle에 자동 연결
입니다.
19. 일반 Client Component의 Effect cleanup과 비교
이전에 우리는 클라이언트에서 이런 패턴을 많이 봤습니다.
useEffect(() => {
const controller = new AbortController();
fetch(url, {
signal: controller.signal,
});
return () => {
controller.abort();
};
}, [url]);
여기서는:
Effect lifetime
→ AbortController lifetime
입니다.
Server Component + cache에서는:
const getData = cache(async () => {
const signal = cacheSignal();
return fetch(url, { signal });
});
이고:
React cache lifetime
→ AbortSignal lifetime
입니다.
비슷하죠.
보충 설명
이 둘의 공통 철학은:
side effect / async 작업의 lifetime을 그것을 필요로 하는 React lifetime과 일치시켜라.
입니다.
클라이언트에서는 Effect cleanup이 담당하고, 서버 cache에서는 cacheSignal이 이 역할을 합니다.
20. cacheSignal은 global cancellation signal이 아니다
다음처럼 생각하면 안 됩니다.
const signal = cacheSignal();
// 앱 전체 요청 취소 신호
아닙니다.
현재 React cache scope에 연결된 signal입니다.
즉 각 cached 작업은 각각 자신의 lifetime을 가질 수 있습니다.
21. 에러 처리에서 AbortError를 구분할 수 있다
fetch가 abort되면 일반적으로 AbortError가 발생할 수 있습니다.
예를 들어:
try {
const res = await fetch(url, { signal });
return res.json();
} catch (error) {
if (error.name === 'AbortError') {
// 취소된 작업
throw error;
}
throw error;
}
처럼 구분할 수 있습니다.
다만 cancellation을:
실패
처럼 사용자에게 별도 오류 메시지로 보여줘야 하는 상황인지 신중히 생각해야 합니다.
React가 더 이상 필요하지 않아서 정상적으로 취소한 작업이라면 일반적인 application error와 의미가 다릅니다.
22. abort를 catch해서 정상값으로 바꾸는 건 조심해야 한다
예를 들어:
const getData = cache(async () => {
const signal = cacheSignal();
try {
return await fetchData({ signal });
} catch {
return null;
}
});
처럼 모든 오류를 삼켜버리면 abort뿐 아니라 실제 네트워크 에러도 숨길 수 있습니다.
그리고 cancellation 의미가 흐려질 수 있습니다.
따라서:
catch (error) {
if (error.name === 'AbortError') {
throw error;
}
// 실제 에러 처리
}
처럼 의도를 분명히 하는 편이 좋습니다.
23. 언제 사용하면 좋은가?
대표적인 경우는 다음입니다.
긴 HTTP 요청
무거운 서버 계산
취소 가능한 DB query
스트리밍 작업
preload한 async resource
그리고 공통점은:
결과가 더 이상 필요 없는데 작업을 계속하면 비용이 크다.
는 것입니다.
24. 언제 굳이 필요하지 않을까?
예를 들어 계산이:
const getValue = cache(x => x * 2);
처럼 거의 즉시 끝난다면 cancellation signal의 이점은 없습니다.
또 대상 async API가 cancellation을 지원하지 않는다면 cacheSignal()을 받아도 실제 작업을 멈출 수 없습니다.
따라서:
취소 가능한가?
+
취소할 가치가 있는가?
를 판단해야 합니다.
25. cache, cacheSignal, Suspense 세 개를 구분하자
이 세 API를 한 번에 정리해봅시다.
cache
const getData = cache(fetchData);
질문:
같은 작업을 중복 실행해야 하는가?
답:
아니면 결과/Promise를 공유한다.
cacheSignal
const signal = cacheSignal();
질문:
이 cached 작업을 언제 중단해도 되는가?
답:
React cache lifetime이 끝나면 abort한다.
Suspense
<Suspense fallback={<Loading />}>
질문:
작업이 아직 준비되지 않았을 때 사용자에게 무엇을 보여줄 것인가?
답:
fallback/reveal을 관리한다.
26. 전체 흐름으로 보면
const getUser = cache(async id => {
const signal = cacheSignal();
const response = await fetch(
`/api/users/${id}`,
{ signal }
);
return response.json();
});
Server Component:
async function Profile({ id }) {
const user = await getUser(id);
return <ProfileView user={user} />;
}
Suspense:
<Suspense fallback={<ProfileSkeleton />}>
<Profile id={id} />
</Suspense>
전체 흐름:
Profile render
│
▼
getUser(id)
│
├── cache hit
│ └─ 기존 Promise/결과 사용
│
└── cache miss
│
▼
cacheSignal()
│
▼
fetch 시작
│
┌───────┴────────┐
│ │
Promise pending 더 이상 필요 없음
│ │
Suspense fallback ▼
│ signal abort
│ │
resolve fetch 취소
│
▼
Profile reveal
이 그림이 이번 페이지의 핵심입니다.
27. cacheSignal은 Server Components 맥락의 API다
cache와 마찬가지로 이 API도 React의 서버 cache infrastructure와 연결되어 있습니다.
따라서 일반적인 Client Component에서:
'use client';
const signal = cacheSignal();
같은 용도로 생각하면 안 됩니다.
클라이언트에서 요청 취소가 필요하다면 보통:
useEffect + AbortController
같은 패턴을 사용합니다.
28. 흔한 실수
첫 번째:
cacheSignal()이 요청을 자동으로 취소한다.
정확히는 아닙니다.
cacheSignal()은 abort되는 signal을 제공합니다.
대상 작업이 그 signal을 받아서 cancellation을 지원해야 합니다.
fetch(url, { signal });
처럼요.
두 번째:
cacheSignal()을 쓰면 결과가 캐시된다.
아닙니다.
캐싱은:
cache(...)
가 담당합니다.
세 번째:
일반 Client Component의 fetch에도 쓰는 API다.
아닙니다.
React Server Components의 cache lifecycle에 연결된 API입니다.
네 번째:
작업이 abort되면 무조건 애플리케이션 에러다.
아닙니다.
React가 결과를 더 이상 필요로 하지 않아 정상적으로 취소한 것일 수도 있습니다.
핵심 정리
cacheSignal()을 한 문장으로 설명하면:
React Server Components에서 현재 cache lifetime과 연결된 AbortSignal을 얻어, 더 이상 필요하지 않은 cached 비동기 작업을 취소할 수 있게 하는 API입니다.
가장 대표적인 형태는:
const getData = cache(async () => {
const signal = cacheSignal();
return fetch(url, { signal });
});
입니다.
그리고 세 API의 역할을 반드시 구분하세요.
cache
→ 결과/Promise 공유
cacheSignal
→ cached 작업의 cancellation lifecycle
Suspense
→ pending 상태의 UI 처리
또 클라이언트의:
useEffect cleanup + AbortController
와 서버의:
cache + cacheSignal
은 서로 다른 환경이지만 같은 철학을 공유합니다.
비동기 작업의 lifetime을 그것을 필요로 하는 React lifecycle과 맞춘다.
이해도 테스트
정답은 바로 알려주지 않겠습니다.
- cache()와 cacheSignal()의 역할 차이를 각각 한 문장으로 설명해보세요.
- cacheSignal()에서 얻은 signal을 fetch에 전달해야 하는 이유는 무엇인가요?
- React가 cached 작업을 더 이상 필요로 하지 않게 되었는데 signal을 사용하지 않았다면 어떤 낭비가 생길 수 있을까요?
- Client Component의 useEffect + AbortController와 Server Component의 cacheSignal은 어떤 공통된 설계 철학을 가지고 있나요?
- cacheSignal()을 호출하면 모든 비동기 작업이 자동으로 중단된다고 말하면 왜 틀린 설명일까요?
'리액트 > 레퍼런스 (react 19ver)' 카테고리의 다른 글
| cache (0) | 2026.09.15 |
|---|---|
| addTransitionType (0) | 2026.09.15 |
| act (0) | 2026.09.15 |
| <ViewTransition> (0) | 2026.09.15 |
| <Activity> (0) | 2026.09.14 |