use cache
use cache
디렉티브는 라우트(Route), React 컴포넌트, 또는 함수에 캐시 기능을 적용할 수 있게 해줘요. 이걸 사용하면 해당 함수나 컴포넌트의 반환 값을 캐싱해서, 불필요한 재렌더링이나 무거운 작업을 줄일 수 있답니다.
보통 파일 상단에 쓰면 그 파일 내 모든 export에 대해 캐싱을 적용하고, 함수나 컴포넌트 바로 위에 inline으로 쓰면 그 특정 함수의 결과만 캐싱할 수도 있어요.
사용법
이 기능은 현재 실험적인(Experimental) 기능이라 정식 버전에서는 조금 달라질 수 있어요. 사용하려면 next.config.ts
파일에 useCache
옵션을 추가해야 합니다.
// next.config.ts
const nextConfig = {
experimental: {
useCache: true,
},
};
export default nextConfig;
좀 더 쉽게 설명하자면?
React를 다루다 보면, 컴포넌트가 똑같은 props를 받았는데도 자꾸 재렌더링되는 경우가 있죠? 이때 use cache
를 사용하면, 컴포넌트가 이전에 렌더링해서 갖고 있던 결과를 재사용해서 성능을 끌어올릴 수 있어요.
함수에도 적용할 수 있는데, 함수가 복잡한 연산을 수행할 때 매번 그 연산을 반복하지 않고 저장해둔 값을 돌려주는 효과가 있습니다.
주의할 점!
- 지금은 실험 기능이니, 안정성이나 호환성 이슈가 있을 수 있어요.
- 캐싱된 결과는 상태(state)나 컨텍스트 변경에는 반응하지 않으니, 상태에 따라 바뀌어야 하는 값에는 적절하지 않습니다.
추가 팁
- Next.js에서 SSR(Server Side Rendering)과 캐시를 함께 사용할 때도
use cache
를 통해 일부 결과를 캐싱하면 응답 속도를 상당히 단축할 수 있어요. - 캐시는 적절히 관리해줘야 메모리 누수 같은 문제를 예방할 수 있으니, 특별히 오래된 캐시를 청소하는 로직도 함께 고려해보시는 걸 추천해요.
요즘엔 이런 캐시 기능이 점점 중요해지고 있으니, 실험 기능이 안정화되면 꼭 적극적으로 써보시길 바랍니다!
Next.js에서 use cache
옵션을 사용하는 방법을 알려드릴게요! 이 옵션은 캐시를 적극 활용해서 성능을 최적화하고, 데이터 요청을 더 빠르게 처리하는 데 도움을 줘요.
먼저, next.config.js
나 next.config.ts
파일에서 실험 기능인 useCache
를 활성화해줘야 해요. 이렇게 설정합니다:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
useCache: true,
},
}
export default nextConfig
참고할 점!
useCache
는 dynamicIO
옵션을 통해서도 사용할 수 있어요. 만약 캐시를 동적으로 제어하고 싶다면 dynamicIO
도 함께 살펴보면 좋겠죠?
use cache
사용법
use cache
를 파일, 컴포넌트, 혹은 함수 단위에서 사용할 수 있어요. 다음 예시를 참고하세요.
적용 위치 | 예시 코드 | 설명 |
---|---|---|
파일 레벨 | 'use cache' export default async function Page() { ... } | 파일 전체에 캐시 사용을 적용 |
컴포넌트 레벨 | export async function MyComponent() { 'use cache' return <></> } | 특정 컴포넌트만 캐시 사용 |
함수 레벨 | export async function getData() { 'use cache' const data = await fetch('/api/data') return data } | 특정 함수 내에서만 캐시 사용 |
추가 팁!
'use cache'
는 마치 자바스크립트의'use strict'
처럼 바로 함수(혹은 파일) 최상단에 위치해야 인식돼요.- 캐시를 잘 활용하면 SSR(서버사이드 렌더링) 환경에서 API 요청 횟수를 줄이고, 페이지 렌더링 속도를 개선할 수 있어요.
- 다만, 데이터가 자주 변경되는 상황에서는 캐시가 최신 데이터를 반영하지 않을 수도 있으니 캐시 정책을 꼼꼼히 짜는 게 중요해요.
이렇게 use cache
를 적절히 활용해 Next.js 애플리케이션의 퍼포먼스를 한층 올려보세요! 개발하면서 캐시 전략을 고민하는 것이 장기적으로 봤을 때 큰 이득이 될 거예요. 궁금한 점 있으면 언제든 물어보세요 :)
캐시 사용법
캐시 키(Cache keys)
캐시 항목의 키는 입력값을 직렬화한 버전을 사용해서 생성돼요. 여기서 입력값에는 다음이 포함됩니다:
- 빌드 ID: 빌드할 때마다 새롭게 생성되는 고유한 ID예요.
- 함수 ID: 각 함수마다 유일하게 부여된 보안 식별자입니다.
- 직렬화 가능한 함수 인자(또는 props): 함수에 전달되는 인자들이에요. 이 값들을 직렬화해서 키에 포함시킵니다.
즉, 캐시는 단순히 함수 이름만으로 구분하는 게 아니라, '어떤 빌드에서, 어떤 함수에, 어떤 인자들로 호출했는지'를 정확하게 고려해서 고유한 키를 만들고, 그 키를 통해 캐시 데이터를 관리합니다.
이런 방식을 사용하면, 같은 함수라도 인자가 다르면 다른 캐시 항목으로 인식하고, 이전에 수행한 결과를 재사용할지 여부를 정확하게 판단할 수 있어 성능 최적화에 큰 도움이 됩니다.
참고로, 이 과정에서 함수 인자를 직렬화하는 이유는 객체나 배열처럼 복잡한 데이터 타입도 문자열 형태로 변환해서 고유성을 보장하기 위함이에요. 만약 직렬화가 제대로 안 되면 캐시가 잘못된 결과를 반환하거나, 캐시 히트률이 낮아질 수 있으니 신경 쓰셔야 해요.
함수 캐싱할 때 전달되는 인자들과, 함수가 부모 스코프에서 읽는 값들은 자동으로 캐시 키의 일부가 됩니다. 즉, 입력값이 같으면 같은 캐시 항목이 재사용된다는 뜻이죠.
직렬화 불가능한 인자들
여기서 중요한 점! 만약 직렬화(serialize)할 수 없는 인자나 props, 혹은 클로저로 묶인 값들이 들어오면, 이 값들은 캐시 함수 안에서 그냥 참조로만 다뤄집니다. 즉, 캐시 함수 내부에서 이 값들을 검사하거나 변경할 수 없고, 단순히 받아서 사용할 뿐이에요. 그리고 이런 직렬화 불가능한 값들은 요청 시점에 채워지고, 캐시 키의 일부가 되지는 않습니다.
예를 들어, 캐시된 함수가 children
prop으로 JSX를 받는다고 할 때, div
안에 children
을 그냥 넣어줄 순 있지만, 실제 children
객체 내부를 들여다보지는 못해요. 이런 특성 덕분에 캐시된 컴포넌트 안에 캐싱되지 않은 내용(uncached content)을 자유롭게 중첩해서 사용할 수 있습니다.
조금 더 쉽게 풀어볼게요!
- 캐시 키란? 함수에 들어가는 인자와 외부에서 읽는 변수가 일종의 '이름표'처럼 합쳐져서 캐시 데이터가 저장되는 키가 된다는 거예요.
- 직렬화 불가능한 값은 뭐가 있냐면? 예를 들어 함수, DOM 노드, 혹은 복잡한 객체(내부에 메서드가 있거나 순환 참조가 있는 객체) 등이 있죠. 이런 값들은 그냥 ‘참조’로 넘겨지고 캐시에 포함되지 않습니다.
- 왜 이런게 중요하냐면? 함수가 내부 값을 깊게 분석해서 캐시 키를 만들면 성능이 떨어질 수 있거든요. 그래서 React 같은 라이브러리도 종종 이런 방식으로 캐시 관리를 해요.
캐싱을 잘 활용하면 불필요한 연산을 줄이고, 사용자 경험을 훨씬 부드럽게 만들 수 있으니 꼭 기억해두세요!
function CachedComponent({ children }: { children: ReactNode }) {
'use cache'
return <div>{children}</div>
}
반환 값(Return values)
캐시 가능한 함수의 반환 값은 반드시 직렬화(serializable)가 가능해야 합니다.
이렇게 해야 캐시된 데이터를 올바르게 저장하고 꺼내 쓸 수 있어요. 예를 들어, 함수나 DOM 노드 같은 직렬화 불가능한 값은 캐시 저장소에 넣으면 안 된다는 뜻입니다.
빌드 타임에 use cache 사용하기
'use cache'
는 일반적인 리액트 훅처럼 동작하는 게 아니라, 함수의 결과를 빌드 타임에 캐싱해서 성능 최적화를 도와줍니다.
정적 데이터가 자주 변하지 않는 경우라면, 빌드 타임에 캐시를 생성해두면 재랜더 시점에 불필요한 연산을 줄일 수 있죠.
추가 팁!
- 반환값이 객체라면, 반드시 JSON으로 변환 가능한 구조인지 확인하세요.
- 복잡한 상태를 캐시해야 한다면, 필요에 따라 커스텀 직렬화 방식을 구현할 수도 있습니다.
use cache
는 아직 실험적 기능일 수 있으니, 사용 시 React 버전과 호환성도 체크해 주세요!
이걸 잘 활용하면 서버 컴포넌트나 Next.js 같은 프레임워크에서 렌더링 성능을 꽤 끌어올릴 수 있으니 꼭 알아두세요!
레이아웃이나 페이지 상단에 사용하면, 해당 라우트 세그먼트가 미리 렌더링(prerendering)됩니다. 이렇게 하면 나중에 다시 검증(revalidate)할 수 있게 되죠.
하지만 여기서 중요한 점! 이런 프리렌더링을 하면 요청 시점(request-time)에서 사용하는 API들, 예를 들어 쿠키(cookies)나 헤더(headers)를 이용할 수 없다는 점 기억하세요. 즉, 이런 데이터들을 기반으로 동적인 처리를 해야 한다면 프리렌더링과는 맞지 않아요.
런타임 시점에 캐시 사용하기 (use cache at runtime)
서버에서는 각각의 컴포넌트나 함수들의 캐시 항목이 메모리 내에 저장됩니다. 그러니까 한 번 처리된 결과를 메모리에 담아두고 다시 사용할 수 있다는 뜻이죠. 이 덕분에 성능이 개선되고 불필요한 중복 연산을 피할 수 있습니다.
추가로 알아두면 좋은 점
-
프리렌더링 vs 런타임 캐시
프리렌더링은 페이지를 미리 만들어 두고 배포하는 개념이라, 사용자 요청마다 즉시 렌더링하지 않아 서버 부하가 적죠. 대신 실시간 사용자 정보 처리엔 제한적입니다.
반면 런타임 캐시는 요청 시점에 데이터를 활용하면서도, 동일한 데이터 요청에 대해서는 캐시된 결과를 활용하기 때문에 성능과 유연성을 적절히 조절할 수 있어요. -
캐시 무효화 전략
캐시는 한번 저장되면 갱신되지 않으면 오래된 데이터를 보여줄 수도 있으니, 적절한 무효화 전략도 꼭 신경써야 합니다. 필요에 따라 주기적으로 다시 검증하거나, 데이터 변경 시 캐시를 삭제하는 방식을 사용할 수 있죠.
이렇게 캐시 사용법과 특성을 이해해서 프로젝트 성격에 맞게 적용하면, 사용자 경험과 서버 성능 모두 잡을 수 있습니다!
클라이언트에서는 서버 캐시에서 반환된 모든 콘텐츠가 세션이 유지되는 동안 또는 재검증(revalidation)될 때까지 브라우저 메모리에 저장됩니다.
재검증 중에는?
기본적으로 use cache는 서버 측 재검증 기간이 15분으로 설정되어 있어요. 이 기간은 자주 업데이트가 필요 없는 콘텐츠에는 적합하지만, 개별 캐시 항목이 언제 다시 확인되어야 하는지를 더 세밀하게 조정하고 싶다면, cacheLife와 cacheTag API를 이용할 수 있어요.
- cacheLife: 캐시 항목의 수명을 설정할 수 있어요. 예를 들어, 10분, 1시간 등 원하는 기간 동안 캐시가 유효하게 만들 수 있어요.
- cacheTag: 특정 태그를 만들어서 필요할 때 해당 태그가 붙은 캐시를 선택적으로 재검증할 수 있어요. 예를 들어, 게시판 글, 뉴스 등 특정 콘텐츠 그룹만 빠르게 새로고침할 때 유용하답니다.
추가 팁!
캐시를 잘 활용하면 서버 부하를 많이 줄이고 사용자 경험도 부드럽게 만들 수 있어요. 하지만 캐시가 너무 오래 유지되면 업데이트가 반영되지 않는 문제가 생길 수 있으니, 콘텐츠 특성과 업데이트 빈도에 맞춰 캐시 정책을 적절히 조정하는 게 중요합니다. 특히 실시간 정보가 중요한 서비스라면 재검증 주기를 짧게 가져가는 걸 추천해요!
두 API 모두 클라이언트와 서버 캐싱 계층에 걸쳐 통합해서 사용할 수 있어요. 즉, 캐싱 관련 설정을 한 번만 해주면 어디서든 똑같이 적용된다는 뜻이죠.
캐싱을 더 자세히 알고 싶다면 cacheLife와 cacheTag API 문서도 꼭 참고해보세요. 여기에 캐시 유지 기간이나 태그를 지정하는 방법들이 자세히 나와 있어서요.
예시
use cache로 전체 라우트 캐싱하기
(이 부분부터는 실제 코드 예시나 사용법을 자연스럽게 이어서 작성하면 좋아요.)
전체 라우트를 미리 렌더링(prerendering)하고 싶다면, 레이아웃(layout)과 페이지(page) 파일 맨 위에 use cache
를 추가해주면 돼요. 각각의 세그먼트(segments)는 애플리케이션에서 개별 진입점(entry points)으로 간주되고, 각각 독립적으로 캐시가 적용됩니다.
'use cache'
export default function Layout({ children }: { children: ReactNode }) {
return <div>{children}</div>
}
여기서 중요한 점! 페이지 파일에 임포트해서 중첩된 컴포넌트들은 페이지가 가진 캐시 동작을 그대로 상속받아요.
'use cache'
async function Users() {
const users = await fetch('/api/users')
// users를 활용해서 렌더링 작업 수행
}
export default function Page() {
return (
<main>
<Users />
</main>
)
}
조금 더 쉽게 정리하자면, use cache
를 쓰면 해당 레이아웃이나 페이지가 요청 시마다 새로 렌더링 되는 게 아니라, 한 번 렌더링된 결과를 저장했다가 다음에 같은 요청이 들어오면 저장해둔 캐시를 바로 보여줘서 성능이 훨씬 좋아져요. 다만, 캐시가 적용된 컴포넌트 내부에서 사용하는 데이터가 자주 바뀐다면 그에 맞춰서 캐시를 재검증하거나 적절히 관리해주는 것이 중요합니다.
이 기능을 통해 SSR(Server Side Rendering)하면서도 서버 부하를 줄이고 빠른 응답을 기대할 수 있으니, 복잡한 데이터 랜더링이 필요한 앱이라면 적극 활용해보세요!
좋은 정보 한 가지 알려드릴게요!
만약 use cache를 레이아웃(layout)이나 페이지에만 추가한다면, 그 특정 경로(route) 세그먼트와 거기서 임포트된 컴포넌트들만 캐시가 됩니다. 그런데 만약 경로 내 중첩된 자식 컴포넌트 중에 Dynamic API를 사용하는 컴포넌트가 있다면, 해당 경로는 prerendering(미리 렌더링) 기능을 자동으로 포기하게 돼요.
use cache로 컴포넌트 출력값 캐싱하기
use cache 훅을 컴포넌트 단위에서 사용하면, 컴포넌트 내부에서 하는 fetch 요청이나 계산 결과를 캐싱할 수 있습니다. 이 캐시는 컴포넌트에 전달된 props가 직렬화한 결과가 동일할 때마다 재사용돼요. 덕분에 불필요한 네트워크 요청이나 리렌더링을 막아서 성능 최적화에 유리합니다.
아래는 예시 코드입니다:
export async function Bookings({ type = 'haircut' }: BookingsProps) {
async function getBookingsData() {
// type을 쿼리 파라미터에 넣어 API 호출
const data = await fetch(`/api/bookings?type=${encodeURIComponent(type)}`)
return data
}
return // 실제 렌더링할 JSX를 여기에 작성
}
interface BookingsProps {
type: string
}
여기서 캐싱을 제대로 하려면 type
값이 바뀔 때마다 fetch가 다시 호출돼야 하겠죠? 그래서 props를 정확히 전달하고, props가 바뀌지 않는 한 데이터는 재활용됩니다.
덧붙여서
- use cache는 React Server Components 환경에서 많이 쓰이고, fetch와 연계해서 서버에서 데이터를 미리 받아두는데 적합해요.
- 클라이언트 측에서도 비슷한 기능을 원한다면 SWR, React Query 같은 라이브러리를 활용할 수 있답니다.
- 또한, 캐시 무효화 전략도 꼭 고민해보세요. 데이터가 자주 바뀌는 경우 캐시가 너무 오래 남으면 오히려 사용에 혼란을 줄 수 있으니까요.
이렇게 use cache를 잘 활용하면 네트워크 요청 수를 줄이고, 페이지 로딩 속도도 훨씬 개선할 수 있으니 꼭 한 번 적용해보시길 추천합니다!
use cache로 함수 출력 값 캐싱하기
use cache
를 이용하면 비동기 함수 어디에든 캐싱 기능을 쉽게 추가할 수 있어요. 그래서 오직 컴포넌트나 라우트에만 국한되지 않고, 네트워크 요청, 데이터베이스 쿼리, 그리고 시간이 오래 걸리는 계산 결과 등도 캐시할 수 있답니다.
예를 들어, 이런 간단한 데이터 요청 함수가 있다고 할게요:
export async function getData() {
const data = await fetch('/api/data')
return data
}
여기에 use cache
를 적용하면 매번 서버에 요청하지 않고, 캐시된 결과를 바로 받을 수 있어서 성능이 확실히 좋아집니다.
실무에서는 API 요청이 많거나, DB 쿼리 결과가 자주 변하지 않을 때 이런 캐시를 적절히 써주는 게 매우 효과적이에요. 다만, 데이터가 자주 바뀐다면 캐시 만료 전략도 잘 고려해야겠죠? 예를 들면 캐시 TTL(Time To Live)을 정하거나, 상황에 따라 캐시를 강제로 갱신하는 방식이 이에요.
Interleaving (인터리빙)
(다음에 이어서 설명을 드릴게요! 만약 인터리빙이라는 개념이 생소하다면, 쉽게 말해 여러 작업을 섞어서 동시에 처리하는 방식을 의미합니다. 예를 들어, 서버에서 여러 비동기 요청을 순차적으로 기다리지 않고, 중간에 처리할 수 있는 작업을 끼워 넣어 처리 속도를 높이는 것이죠.)
더 궁금한 게 있다면 언제든 물어봐 주세요!
캐시 가능한 함수에 직렬화가 불가능한(non-serializable) 인자를 넘겨야 할 때, 이 인자들을 children으로 전달하면 좋아요. 이렇게 하면 children의 참조가 바뀌어도 캐시에는 영향을 주지 않거든요.
예를 들어, 아래 코드를 한번 살펴볼게요.
export default async function Page() {
const uncachedData = await getData()
return (
<CacheComponent>
<DynamicComponent data={uncachedData} />
</CacheComponent>
)
}
async function CacheComponent({ children }: { children: ReactNode }) {
'use cache'
const cachedData = await fetch('/api/cached-data')
return (
<div>
<PrerenderedComponent data={cachedData} />
{children}
</div>
)
}
여기서 CacheComponent
는 'use cache' 함수로 동작하면서도, children으로 전달된 DynamicComponent
의 데이터는 캐시 영향 없이 렌더링할 수 있어요. 즉, 캐시된 데이터와 캐시되지 않은 데이터를 함께 다룰 때 유용하죠!
또한, 서버 액션(Server Actions)을 캐시된 컴포넌트에 넘겨주고, 그걸 클라이언트 컴포넌트에게 전달할 수도 있어요. 이때 중요한 점은 캐시 함수 내부에서 서버 액션을 바로 호출하면 안 된다는 것! 직접 호출하지 않고, 함수 참조만 넘겨줘야 합니다.
import ClientComponent from './ClientComponent'
export default async function Page() {
const performUpdate = async () => {
'use server'
// 서버 쪽 업데이트 작업
await db.update(...)
}
return <CacheComponent performUpdate={performUpdate} />
}
async function CacheComponent({
performUpdate,
}: {
performUpdate: () => Promise<void>
}) {
'use cache'
// performUpdate를 여기서 호출하지 마세요!
return <ClientComponent action={performUpdate} />
}
이렇게 하면, 서버 액션 함수를 안전하게 클라이언트 컴포넌트에 props로 넘길 수 있고, 실제 호출은 클라이언트 쪽에서 일어나게 됩니다. React Server Components 환경에서 서버 함수와 클라이언트 함수의 역할 분리가 명확해지는 좋은 방법이죠!
요약하자면,
- children을 이용해 non-serializable 데이터를 캐시 영향 없이 전달하기
- 캐시 함수에 서버 액션을 넘길 때, 내부에서 호출하지 말고 함수 참조만 전달하기
이 두 가지를 잘 활용하면, Next.js 13+의 서버 컴포넌트와 클라이언트 컴포넌트 연동을 깔끔하고 효율적으로 할 수 있답니다!
필요하다면 여러분 프로젝트에 맞게 캐시 전략을 조금씩 조정해가면서 써보세요. 개발하다 보면 이 패턴들이 점점 익숙해질 거예요 :)
이 코드는 Next.js 13에서 "use client" 디렉티브를 사용한 클라이언트 컴포넌트를 정의하고 있어요.
간단하게 설명하자면, 이 컴포넌트는 action이라는 비동기 함수를 props로 받아서, 버튼 클릭 시 이 함수를 실행하는 역할을 합니다.
조금 더 풀어보면:
'use client'
: Next.js 13에서는 React 서버 컴포넌트가 기본이에요. 그런데 클라이언트에서 직접 상호작용을 해야 할 경우,"use client"
라는 지시자를 넣어 클라이언트 전용 코드임을 알려줘야 해요.action: () => Promise<void>
타입은, 매개변수로 아무것도 받지 않고, Promise를 반환하는 비동기 함수임을 나타내요.- 버튼 클릭하면
action
이 실행되고, 그 작업이 끝나면 아무 값도 반환하지 않는 구조입니다.
여기서 조금 더 팁을 드리자면:
onClick
핸들러에 async 함수를 바로 넘겨도 되지만, 만약 에러 처리를 하고 싶다면 내부에서 try/catch를 사용하는 게 좋아요.- 버튼 클릭 시 로딩 상태 등을 관리하는 상태를 추가하면 UX가 더 좋아집니다. 예를 들어, 버튼이 눌렸을 때 "Loading..." 상태를 보여주거나 버튼을 비활성화시킬 수 있어요.
아래는 약간 개선한 예시입니다:
'use client'
import { useState } from 'react'
export default function ClientComponent({
action,
}: {
action: () => Promise<void>
}) {
const [loading, setLoading] = useState(false)
const handleClick = async () => {
setLoading(true)
try {
await action()
} catch (error) {
console.error('업데이트 중 에러 발생:', error)
// 사용자가 알 수 있도록 UI에 에러 표시를 추가할 수도 있어요.
} finally {
setLoading(false)
}
}
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Updating...' : 'Update'}
</button>
)
}
이렇게 하면 사용자 입장에서 업데이트 중임을 알 수 있고, 여러번 중복 클릭도 막을 수 있답니다.
이 코드와 개념들을 참고해서 상황에 맞게 응용해보세요!