Next.js 메타데이터 중복 문제 해결: revalidatePath에서 Optimistic Updates로의 전환
요약
Next.js에서 서버 상태 동기화 방식의 한계와 generateMetadata 중복 실행 문제를 해결하는 방법을 다룹니다. revalidatePath 사용 시 발생하는 메타데이터 중복 생성 문제와 Optimistic Updates 우선 사용의 필요성에 대해 설명합니다.
Next.js 메타데이터 중복 문제 해결: revalidatePath에서 Optimistic Updates로의 전환
문제 상황
Next.js 앱에서 사용자 상호작용(좋아요, 댓글, 조회수 증가)이 발생할 때마다 revalidatePath
를 사용하여 페이지 캐시를 무효화하고 있었습니다. 하지만 이 방식으로 인해 메타데이터 중복 생성 문제가 발생했습니다.
중복 메타데이터 예시
<head>
<title>SQL 정보처리기사 실기 문제 모음 | 정처기 감자</title>
<title>SQL 정보처리기사 실기 문제 모음 | 정처기 감자</title>
<meta name="description" content="..." />
<meta name="description" content="..." />
<!-- 동일한 메타데이터가 두 번 생성됨 -->
</head>
원인 분석
기존 플로우
- 사용자가 페이지 접속하면서 generateMetadata 실행(title, meta description 태그 생성)
- 사용자가 좋아요/댓글/조회수 액션 수행
- Server Action에서
revalidatePath
호출 - 페이지 캐시 무효화
- 다음 요청 시
generateMetadata
재실행 - 새로운 메타데이터가 기존 메타데이터와 중복되어 생성
문제의 핵심
revalidatePath
로 인한 캐시 무효화가 메타데이터 생성 로직을 다시 실행- 서버와 클라이언트 간의 메타데이터 동기화 이슈
- SSR과 CSR 간의 메타데이터 처리 충돌
Next.js에서 서버 상태 동기화의 한계
서버 상태 동기화 방식의 문제점
Next.js에서 서버 상태 동기화(Server State Synchronization) 방식을 사용할 경우, 필연적으로 revalidatePath
를 사용해야 합니다. 하지만 이는 다음과 같은 심각한 문제를 야기합니다:
1. generateMetadata 중복 실행 문제
// 서버 상태 동기화 방식
export async function toggleLike(slug: string) {
const result = await togglePostLike(slug, userSession);
if (result.success) {
revalidatePath(`/exam-registration/${slug}`); // 캐시 무효화
// → generateMetadata가 다시 실행되어 메타데이터 중복 생성
return { success: true, liked: result.liked };
}
}
2. 성능 오버헤드
- 불필요한 페이지 재렌더링: 전체 페이지가 다시 생성됨
- 메타데이터 재계산:
generateMetadata
함수가 매번 실행 - 캐시 효율성 저하: 페이지 캐시가 계속 무효화됨
Next.js에서의 권장사항
Next.js에서는 서버 상태 동기화 방식보다 항상 Optimistic Updates를 우선으로 고려해야 합니다.
이유:
- 메타데이터 중복 방지:
revalidatePath
사용을 피할 수 있음 - 성능 최적화: 불필요한 페이지 재렌더링 방지
- 사용자 경험 향상: 즉각적인 UI 반응
- 캐시 효율성: 페이지 캐시를 유지하면서 데이터만 동기화
서버 상태 동기화가 필요한 경우:
- 데이터 일관성이 매우 중요한 경우 (금융, 결제 시스템)
- 실시간 동기화가 필수인 경우 (협업 도구, 채팅)
- 복잡한 비즈니스 로직이 서버에 있는 경우
하지만 일반적인 웹 애플리케이션에서는 Optimistic Updates가 더 나은 선택입니다.
해결 방안: Optimistic Updates 도입
1. revalidatePath 완전 제거
Before:
// actions.ts
export async function toggleLike(slug: string) {
const result = await togglePostLike(slug, userSession);
if (result.success) {
revalidatePath(`/exam-registration/${slug}`); // 캐시 무효화
return { success: true, liked: result.liked };
}
}
After:
// actions.ts
export async function toggleLike(slug: string) {
const result = await togglePostLike(slug, userSession);
if (result.success) {
// revalidatePath 제거 - 데이터만 반환
return { success: true, liked: result.liked };
}
}
2. 클라이언트 사이드 Optimistic Updates 구현
좋아요 버튼
// LikeButtonSupabase.tsx
const handleClick = async () => {
// 1. 낙관적 업데이트: 즉시 UI 반영
const newLikedState = !isLiked;
const newCount = newLikedState ? likeCount + 1 : likeCount - 1;
setIsLiked(newLikedState);
setLikeCount(Math.max(0, newCount));
// 2. 서버 업데이트
const result = await toggleLike(slug);
if (result.success) {
// 3. 서버 결과로 동기화
setIsLiked(result.liked ?? false);
setLikeCount(result.likeCount ?? 0);
} else {
// 4. 실패 시 롤백
setIsLiked(!newLikedState);
setLikeCount(likeCount);
}
};
댓글 시스템
// CommentSection.tsx
const handleAddComment = async (data: CommentFormData) => {
// 1. 임시 댓글 생성
const tempId = `temp_${Date.now()}`;
const tempComment: Comment = {
id: tempId,
content: data.content,
author: data.author,
// ...
};
// 2. 낙관적 업데이트: 즉시 UI에 반영
setComments((prev) => [...prev, tempComment]);
// 3. 서버 업데이트
const result = await createComment(slug, data);
if (result.success && result.comment) {
// 4. 성공 시 임시 댓글을 실제 댓글로 교체
setComments((prev) =>
prev.map((comment) =>
comment.id === tempId ? { ...result.comment!, replies: [] } : comment
)
);
} else {
// 5. 실패 시 임시 댓글 제거
setComments((prev) => prev.filter((comment) => comment.id !== tempId));
}
};
소프트 삭제 처리
// 서버: 소프트 삭제
const { error } = await supabase
.from("comments")
.update({ author: null, content: null })
.eq("id", commentId);
// 클라이언트: 낙관적 소프트 삭제
setComments((prev) =>
prev.map((comment) =>
comment.id === commentId
? { ...comment, author: null, content: null, isDeleted: true }
: comment
)
);
결과 및 개선사항
✅ 문제 해결
- 메타데이터 중복 완전 제거:
revalidatePath
제거로 불필요한 캐시 무효화 방지 - 즉각적인 UI 반응: 서버 응답을 기다리지 않고 즉시 화면 업데이트
- 일관된 사용자 경험: 네트워크 지연과 무관하게 빠른 인터랙션
📈 성능 개선
- 네트워크 요청 최적화: 페이지 재검증 없이 데이터만 동기화
- 캐시 효율성: 불필요한 캐시 무효화 제거로 성능 향상
- SEO 안정성: 메타데이터 중복으로 인한 SEO 이슈 해결
🛡️ 안정성 확보
- 롤백 메커니즘: 서버 요청 실패 시 UI 상태 복구
- 타입 안전성: TypeScript로 모든 상태 변경 타입 체크
- 에러 처리: 각 단계별 에러 상황 대응
핵심 인사이트
- 메타데이터 중복은 캐시 관리 문제:
revalidatePath
의 부적절한 사용이 원인 - Optimistic Updates의 힘: 사용자 경험과 성능을 동시에 개선
- 서버와 클라이언트 역할 분리: 서버는 데이터 처리, 클라이언트는 UI 상태 관리
- 소프트 삭제의 일관성: 서버와 클라이언트 모두 동일한 삭제 전략 사용
결론
revalidatePath
에서 Optimistic Updates로의 전환은 단순한 기술적 변경이 아닌, 사용자 경험의 근본적 개선이었습니다. 메타데이터 중복 문제를 해결하면서 동시에 더 빠르고 반응적인 인터페이스를 구현할 수 있었습니다.
이 경험을 통해 캐시 전략의 중요성과 클라이언트 사이드 상태 관리의 가치를 다시 한번 확인할 수 있었습니다.
기술 스택: Next.js 15, TypeScript, Supabase, Tailwind CSS
적용 범위: 좋아요, 댓글, 조회수, 다운로드 카운트 시스템
성과: 메타데이터 중복 0%, UI 반응성 100% 개선