다음은 NextJS 프레임워크를 풀스택이 아닌 프론트엔드로 개발시 최적화 방법론이다.
1. 목적
백엔드 서버에서 새로운 리뷰 데이터가 생길 때마다
Next.js 15 서버 캐시를 무효화 (revalidateTag())
클라이언트 페이지 이동 시 router.refresh()로 최신 데이터 반영
이렇게 하면 SEO에 강하면서도 실시간성이 있는 구조를 만들 수 있음
2.방법
1)백엔드 서버에서 웹훅으로 변경된 데이터 전송
2)프론엔드 넥스트프레임워크에서 웹훅 데이터를 받고, 해당 fetch 태그를 revalidateTag 로 무효화 시킨다.
3)페이지 이동간에 항상 router.refresh 를 추가해 준다.
소스 : https://github.com/braverokmc79/nextjs-review
1. revalidateTag를 사용하는 방법 (Next.js 13~15 서버 컴포넌트 방식)
import { revalidateTag } from "next/cache";
✅ 장점
Next.js 서버 캐시 최적화에 딱 맞는 방식입니다.
ISR(Incremental Static Regeneration)처럼 페이지를 정적으로 생성하되, 필요할 때만 invalidate 할 수 있어서 성능에 아주 유리합니다.
서버 컴포넌트와 fetch 함수에서 캐시 정책을 tags, revalidate 단위로 세밀하게 관리 가능.
Spring → Next.js 웹훅 호출로 변경 사항을 선제적으로 감지하여 invalidate 가능.
❌ 단점
클라이언트 상태 업데이트(로딩, 토스트 등)**를 직접적으로 연결하긴 어려움 → router.refresh() 같은 강제 새로고침 필요.
SSR/SSG 기반 프로젝트에 더 적합, CSR 기반이면 활용도가 떨어짐.
2. React Query로 CSR 방식 데이터 캐싱
✅ 장점
useQuery, useMutation, queryClient.invalidateQueries 같은 기능을 통해 클라이언트에서 아주 유연하게 캐시 관리 가능.
폼 제출 후 toast, 로딩, 에러 처리, 리다이렉션, 새로고침 없이 데이터 갱신 등 모든 UI 흐름을 부드럽게 컨트롤 가능.
WebSocket이나 SSE 연동 시에도 훨씬 다루기 쉬움.
❌ 단점
CSR 방식이므로 초기 페이지 로딩 속도는 느릴 수 있음 (특히 SEO 필요한 경우 불리).
데이터가 많아지면 클라이언트 메모리 부담 커질 수 있음.
서버 캐시가 아닌 브라우저 메모리 기반 캐시 → 서버 재시작 시 무의미.

추천
콘텐츠 중심의 정적인 페이지 (예: 리뷰, 블로그)
→ revalidateTag 방식이 좋습니다. Next.js 15에서 SSR/ISR에 최적화되어 있어요.사용자 인터랙션이 많고 실시간 데이터 연동이 필요한 경우 (예: 게시판, 채팅)
→ React Query를 추천합니다. UX 측면에서 훨씬 유연하고 반응성이 뛰어납니다.
결론
"정적 데이터는 revalidateTag, 사용자 입력이나 상호작용이 잦은 동적 데이터는 React Query."
이 두 가지를 혼용해서 사용하는 것도 아주 흔한 구조이다.
예를 들어:
리뷰 목록 페이지는 revalidateTag로 빠르게 렌더링하고,
리뷰 등록/수정/삭제는 React Query + mutation으로 처리하는 식.
✅ 왜 React Query 없이도 괜찮은가?
현재 구성 요약:

➡️ 이 구조는 이미 Next.js가 React Query의 주요 기능을 자체적으로 다 처리하고 있다는 의미예요.
하지만 넥스트프레임워크를 풀스택으로 개발시에는 백엔드에서 웹훅 전송이 없기 때문에 React Query 필요하다.
✅ React Query가 필요한 상황은 언제일까?
React Query는 일반적으로 아래 상황에서 빛나요:
필요설명
CSR : 중심클라이언트에서 데이터를 자주 갱신하고 조작해야 할 때
실시간 UI : 조작데이터가 자주 변경되며, 화면에서 곧바로 반영해야 할 때
수동 캐시 조작 : invalidateQueries, setQueryData 등으로 수동으로 다뤄야 할 때
오프라인 지원 : 브라우저 캐시와 상태 동기화 등
하지만 백엔드 프론트 엔드로 분리 개발해서 백엔드에서 웹훅으로 변경된 데이터를 전송하고, 넥스트 프레임워크를 프론트엔드 전용으로
사용시에는
서버 컴포넌트 기반 (RSC)
태그 캐시 + Webhook + router.refresh() 조합
페이지 단위로 데이터 패치
이런 경우엔 React Query 없이 더 깔끔하고 빠른 구조가 가능합니다.
✅ 결론
❌ React Query는 지금 구조에선 “필수가 아님”
오히려 사용하면 중복 관리, 복잡도 증가, 캐시 충돌 가능성까지 생길 수 있음
⭕revalidateTag 캐시 사용하는 방법 웹훅 개발 방법
1.백엔드 : 스프링부트
✅1. Spring Boot 웹훅 시스템 만들기
// WebhookNotifier.java
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@Component
public class WebhookNotifier {
private final RestTemplate restTemplate = new RestTemplate();
private final String WEBHOOK_URL = "http://localhost:3000/api/webhooks/event"; // Next.js 서버 주소
public void notifyChange(String modelName, String action) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("x-webhook-secret", "your-secret-token"); // 보안 인증용
Map<String, String> payload = Map.of(
"model", modelName,
"action", action // 예: create / update / delete
);
HttpEntity<Map<String, String>> request = new HttpEntity<>(payload, headers);
try {
restTemplate.postForEntity(WEBHOOK_URL, request, Void.class);
System.out.println("✅ Webhook 전송 완료: " + payload);
} catch (Exception e) {
System.err.println("❌ Webhook 전송 실패: " + e.getMessage());
}
}
}
2 다음과 같이 각 서비스 로직에서 호출 해야 하지만 ✅ AOP로 Webhook 자동 전송 처리하게
만들수 있다.
// 예: ReviewService.java
public void createReview(ReviewDto dto) {
// DB에 저장
reviewRepository.save(dto.toEntity());
// 웹훅 알림 전송
webhookNotifier.notifyChange("review", "create");
}
✅2. AOP로 Webhook 자동 전송 처리
1)WebhookAction.java (enum)
public enum WebhookAction {
CREATE, UPDATE, DELETE
}
2) ✅ 커스텀 어노테이션 정의 WebhookEntityEvent.java (custom annotation)
// ✅ WebhookEntityEvent.java (custom annotation)
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebhookEntityEvent {
String model();
}
3) WebhookEntityListener.java (JPA EntityListener)
import jakarta.persistence.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@Component
public class WebhookEntityListener {
private static WebhookNotifier notifier;
@Autowired
public void init(WebhookNotifier webhookNotifier) {
WebhookEntityListener.notifier = webhookNotifier;
}
@PostPersist
public void postPersist(Object entity) {
sendWebhookIfAnnotated(entity, WebhookAction.CREATE);
}
@PostUpdate
public void postUpdate(Object entity) {
sendWebhookIfAnnotated(entity, WebhookAction.UPDATE);
}
@PostRemove
public void postRemove(Object entity) {
sendWebhookIfAnnotated(entity, WebhookAction.DELETE);
}
private void sendWebhookIfAnnotated(Object entity, WebhookAction action) {
Class<?> clazz = entity.getClass();
if (clazz.isAnnotationPresent(WebhookEntityEvent.class)) {
WebhookEntityEvent annotation = clazz.getAnnotation(WebhookEntityEvent.class);
String modelName = annotation.model();
// 트랜잭션 성공 후 실행
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
notifier.notifyChange(modelName, action.name().toLowerCase());
}
});
} else {
notifier.notifyChange(modelName, action.name().toLowerCase());
}
}
}
}
4) 엔티티에 적용 예
// ✅ Review.java (예시 Entity)
import jakarta.persistence.*;
@Entity
@EntityListeners(WebhookEntityListener.class)
@WebhookEntityEvent(model = "review")
public class Review {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
// ... 기타 필드와 getter/setter
}
⭕✅위 코드는 완전 자동 감지형으로 구성한 JPA 기반 웹훅 시스템을 구현한 코드이다.
@WebhookEntityEvent: 엔티티에 model명을 지정해주는 커스텀 어노테이션
WebhookEntityListener: JPA 엔티티 이벤트 감지 후 트랜잭션 커밋 시점에만 웹훅 전송
WebhookNotifier: Next.js 등 외부 시스템으로 알림 전송
enum WebhookAction: 타입 안정성을 위한 액션 정의
이 구조면 각 서비스에서 직접 호출하지 않고도 자동으로 변경사항을 외부로 전파할 수 있어요.
2.백엔드 : strapi
Strapi CMS에서 Next.js 프론트엔드와 연동하여 웹훅을 통해 캐시를 무효화하고 싶을 때, 다음과 같은 방식으로 설정할 수 있습니다.
목표
Strapi에서 리뷰(review)가 생성/수정/삭제될 때마다
Next.js 서버의 /api/webhooks/event 엔드포인트로 요청을 보내
해당 태그(reviews)의 캐시를 무효화 (revalidateTag("reviews"))
✔️ 1. Strapi 관리자 페이지 접속
http://localhost:1337/admin 으로 접속
✔️ 2. 웹훅 설정 메뉴 이동
좌측 사이드바 > "Settings (설정)" 클릭
아래로 내려서 "Webhooks" 선택
✔️ 3. 새 웹훅 생성
➕ "Create new webhook" 클릭
설정 항목:
Name: Next.js 캐시 리프레시
URL: http://localhost:3000/api/webhooks/event
Events:
Entry create
Entry update
Entry delete
(선택적으로 Publish, Unpublish 도 추가 가능)
Trigger on:
Content type: review만 선택
⚡ 이 설정은 리뷰에 대한 변경사항만 캐시 무효화하도록 제한합니다.
✔ "Save" 클릭
✔️ 4. Next.js API Route 작성 예시 (/api/webhooks/event)
// app/api/webhooks/event/route.ts
import { revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const payload = await request.json();
// review 모델에 대해서만 캐시 무효화 수행
if (payload && payload.model === "review") {
revalidateTag("reviews");
console.log("Revalidated cache for reviews");
}
return new Response(null, { status: 200 });
}
✅ 결과
이제 Strapi에서 리뷰를 작성/수정/삭제하면
Next.js가 해당 태그 캐시("reviews")를 무효화하여
/reviews 목록 페이지가 최신 상태로 유지됩니다.
추가 팁
실서버 배포 시에는 URL을 http://localhost:3000 대신 실제 도메인으로 바꿔야 합니다.
인증 토큰이 필요하면 Webhook 요청 헤더에 설정할 수 있으며, API에서도 검증 로직 추가 가능.
3.프론트: NextJS
1) Next.js 15(App Router 기준)의 데이터 캐싱과 관련된 동작 원리
개발시에는 새로고침을 통해 새롭게 갱신되나, 빌드후 배포시에는 다음과 같은 작동 된다.
✅ 기본 동작: 정적 캐싱(Default Static Rendering)
Next.js 15에서는 서버 컴포넌트(Server Components)에서 fetch()를 사용할 경우, 기본적으로 정적으로 캐싱됩니다.
이 말은 페이지를 처음 빌드할 때 가져온 데이터를 캐시에 저장하고, 그 이후부터는 같은 요청에 대해 캐시된 응답을 재사용한다는 뜻입니다.
그래서 브라우저를 새로고침하거나 닫았다가 다시 열어도 새 데이터를 가져오지 않고 기존 캐시된 데이터를 계속 사용합니다.
✅ 해결 방법
1. revalidate 설정하기 (ISR, Incremental Static Regeneration)
export const revalidate = 30; // 30초마다 백그라운드에서 캐시 갱신
컴포넌트나 라우트 레벨에서 설정 가능
일정 시간(예: 30초)마다 백그라운드에서 캐시된 데이터를 갱신함
2. dynamic = 'force-dynamic' 설정하기 (완전한 서버사이드 요청)
export const dynamic = 'force-dynamic';
해당 페이지는 매 요청마다 서버에서 새 데이터를 fetch합니다 (SSR처럼 동작)
캐시를 사용하지 않기 때문에 항상 최신 데이터를 보여줌
3. fetch()에서 next 옵션으로 직접 설정
await fetch("https://api.example.com/data", {
next: {
revalidate: 30, // ISR
// 또는
// cache: "no-store", // 매번 요청 (force-dynamic과 유사)
}
});
???? 아무것도 설정하지 않은 경우?
fetch()는 기본적으로 build 시점에 실행되고 캐싱됨 (즉, 정적 데이터)
브라우저 새로고침해도 빌드 당시 캐시된 데이터를 그대로 사용하게 됨
✅ 결론
“Next.js 15에서 별도로 revalidate나 force-dynamic을 설정하지 않으면 새 데이터를 가져오지 못한다
단, fetch에 cache: 'no-store'나 revalidate, force-dynamic 등을 명시하면 동작을 변경할 수 있다
따라서, 여기에서는 백엔드에서 웹훅 전송을 받으면 해당 태그를 revalidate 처리하는 방법이다.
2)백엔드에서 전송된 웹훅을 받아와서 fetch 태그를 revalidateTag 처리하기
1) src/app/api/webhooks/event
import { CACHE_TAG_REVIEWS } from "@/lib/reviews";
import { revalidateTag } from "next/cache";
export async function POST( request: Request) {
const payload = await request.json();
if(payload&& payload.model==="review"){
revalidateTag(CACHE_TAG_REVIEWS);
console.log("서버에서 갱신 요청 옴 :revalidated: ",payload.model);
}
return new Response(null, {status: 200,});
}
cms 백엔드 관리자에서 직접 데이터를 넣을 경우 변경이 안되기 때문에, fetch 이 revalidate 를 추가 해준다.
또는, 넥스트 프레임워크에서 버튼을 만들어서 캐시를 삭제해 주는 기능을 구현해야 한다.
revalidate: 60*5, // 5분
export async function fetchReviews(parameters:object) {
const url = `${CMS_URL}/api/reviews?`
+ qs.stringify(parameters, { encodeValuesOnly: true });
//console.log(" [fetchReviews] url: ", url);
const response = await fetch(url,
{
next:{
tags: [CACHE_TAG_REVIEWS],
}
}
);
if (!response.ok) {
throw new Error(`CMS returned ${response.status} for ${url} `);
}
return await response.json();
}
2)페이지 이동시 router.refresh() 추가해야 한다.
1. smartPush 기능을 가진 커스텀 Link 컴포넌트 만들기
"use client"
import { useRouter } from "next/navigation"
import { startTransition } from "react"
export function SmartLink({ href, children }: { href: string, children: React.ReactNode }) {
const router = useRouter()
const handleClick = (e: React.MouseEvent) => {
e.preventDefault() // 기본 링크 동작을 방지
router.push(href)
startTransition(() => {
router.refresh() // 페이지 새로 고침
})
}
return (
<a href={href} onClick={handleClick}>
{children}
</a>
)
}
2)app/lib/navigation.ts 파일에 아래처럼 작성:
"use client"
import { useRouter } from "next/navigation"
import { startTransition } from "react"
export function useSmartRouter() {
const router = useRouter()
const smartPush = (href: string) => {
router.push(href)
startTransition(() => {
router.refresh() // 새로고침하여 최신 데이터 반영
})
}
const smartReplace = (href: string) => {
router.replace(href) // URL은 변경되지만, 히스토리에 기록은 안 남음
startTransition(() => {
router.refresh(); // 페이지를 새로고침하여 최신 데이터 반영
})
}
return { ...router, smartPush, smartReplace }
}
✅ 사용 예시:
1)SmartLink
<SmartLink href={`/reviews/${review.slug}`}>
<button>Review Detail</button>
</SmartLink>
2)smartPush : 새로고침하여 최신 데이터 반영
"use client"
import { useSmartRouter } from "@/lib/navigation"
export default function Button() {
const { smartPush} = useSmartRouter()
return (
<button onClick={() => smartPush("/reviews")}>
리뷰 페이지 이동 & 데이터 최신화
</button>
)
}
3)smartReplace : URL은 변경되지만, 히스토리에 기록은 안 남음
"use client"
import { useSmartRouter } from "@/lib/navigation"
export default function Button() {
const { smartReplace} = useSmartRouter()
return (
<button onClick={() => smartReplace("/reviews")}>
리뷰 페이지로 이동 (히스토리 기록 안 남음)
</button>
)
}
3. Link와 smartPush를 혼합한 방식 (SSR과 클라이언트 사이드 동작 모두 지원)
만약, <Link>와 smartPush를 함께 사용하려면, 클라이언트와 서버에서 동작을 분리해야 하기 때문에, 클라이언트에서만 smartPush를 사용하도록 해야 합니다.
import Link from "next/link"
export default function ReviewLink({ review }) {
const isClient = typeof window !== "undefined"
const handleSmartPush = () => {
if (isClient) {
smartPush(`/reviews/${review.slug}`)
}
}
return (
<Link href={`/reviews/${review.slug}`} onClick={handleSmartPush}>
<button>Review Detail</button>
</Link>
)
}
✅ 요약 정리 :Next.js 15 + Webhook 기반 캐시 무효화 및 실시간 데이터 갱신 전략
1. 목적
백엔드 서버에서 새로운 리뷰 데이터가 생길 때마다
Next.js 15 서버 캐시를 무효화 (revalidateTag())
클라이언트 페이지 이동 시 router.refresh()로 최신 데이터 반영
이렇게 하면 SEO에 강하면서도 실시간성이 있는 구조를 만들 수 있음
2. 서버 데이터 fetch (태그 기반 캐시 적용)
export async function fetchReviews(parameters: object) {
const url = `${CMS_URL}/api/reviews?` +
qs.stringify(parameters, { encodeValuesOnly: true });
const response = await fetch(url, {
next: {
tags: [CACHE_TAG_REVIEWS],
},
});
if (!response.ok) {
throw new Error(`CMS returned ${response.status} for ${url}`);
}
return await response.json();
}
3. 백엔드 서버에서 보낸 Webhook 수신 처리 (캐시 무효화)
import { CACHE_TAG_REVIEWS } from "@/lib/reviews";
import { revalidateTag } from "next/cache";
export async function POST(request: Request) {
const payload = await request.json();
if (payload && payload.model === "review") {
revalidateTag(CACHE_TAG_REVIEWS); // 서버 캐시 무효화
console.log("서버에서 갱신 요청 옴 :revalidated: ", payload.model);
}
return new Response(null, { status: 200 });
}
4. 클라이언트에서 페이지 이동 시 최신화 (UI 자동 반영)
"use client"
import { useSmartRouter } from "@/lib/navigation"
export default function NavigateButton() {
const { smartPush } = useSmartRouter()
const handleMove = () => {
smartPush("/somewhere")
}
return <button onClick={handleMove}>이동하기</button>
}
✅ 정리
fetch(..., { next: { tags: [...] } }): 태그 기반 캐시 적용 (기본은 유지)
revalidateTag(...): 서버에서 해당 태그 캐시 무효화
router.refresh(): 클라이언트에서 이동 시 서버 컴포넌트 최신화
✔️ 장점
변경 없을 땐 캐시로 빠르게 응답 → 성능 향상 & SEO 최적화
변경 발생 시만 최소 갱신 → 실시간성 유지
필요 시 router.refresh()를 특정 액션(등록, 수정 등)에만 적용하거나, WebSocket 기반 실시간 갱신으로도 확장 가능
















댓글 ( 0)
댓글 남기기