cacheSignal

2026. 9. 15. 02:04·리액트/레퍼런스 (react 19ver)

좋습니다. 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과 맞춘다.

이해도 테스트

정답은 바로 알려주지 않겠습니다.

  1. cache()와 cacheSignal()의 역할 차이를 각각 한 문장으로 설명해보세요.
  2. cacheSignal()에서 얻은 signal을 fetch에 전달해야 하는 이유는 무엇인가요?
  3. React가 cached 작업을 더 이상 필요로 하지 않게 되었는데 signal을 사용하지 않았다면 어떤 낭비가 생길 수 있을까요?
  4. Client Component의 useEffect + AbortController와 Server Component의 cacheSignal은 어떤 공통된 설계 철학을 가지고 있나요?
  5. 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
'리액트/레퍼런스 (react 19ver)' 카테고리의 다른 글
  • cache
  • addTransitionType
  • act
  • <ViewTransition>
배불뚱이
배불뚱이
  • 배불뚱이
    FE 개발자의 개발 기록
    배불뚱이
  • 전체
    오늘
    어제
    • 분류 전체보기 (24)
      • 리액트 (24)
        • 공식문서 강의 (19ver) (17)
        • 레퍼런스 (react 19ver) (7)
        • 레퍼런스 (react-dom 19ver) (0)
  • 공지사항

  • 인기 글

  • 태그

    레퍼런스
    component
    공식문서
    react
    Hook
  • hELLO· Designed By정상우.v4.10.6
배불뚱이
cacheSignal
상단으로

티스토리툴바