Next.js 메타데이터 중복 문제 해결: revalidatePath에서 Optimistic Updates로의 전환

Next.js메타데이터revalidatePathOptimistic Updates서버 상태 동기화generateMetadata성능 최적화사용자 경험캐시 관리
읽는데 약 6분 정도 소요
처음 쓰여진 날: 2025-09-25
마지막으로 고쳐진 날: 2025-09-25
이 글을 보러온 횟수: 30

요약

Next.js에서 서버 상태 동기화 방식의 한계와 generateMetadata 중복 실행 문제를 해결하는 방법을 다룹니다. revalidatePath 사용 시 발생하는 메타데이터 중복 생성 문제와 Optimistic Updates 우선 사용의 필요성에 대해 설명합니다.

Next.js 메타데이터 중복 문제 해결: revalidatePath에서 Optimistic Updates로의 전환

문제 상황

Next.js 앱에서 사용자 상호작용(좋아요, 댓글, 조회수 증가)이 발생할 때마다 revalidatePath를 사용하여 페이지 캐시를 무효화하고 있었습니다. 하지만 이 방식으로 인해 메타데이터 중복 생성 문제가 발생했습니다.

중복 메타데이터 예시

html
<head>
  <title>SQL 정보처리기사 실기 문제 모음 | 정처기 감자</title>
  <title>SQL 정보처리기사 실기 문제 모음 | 정처기 감자</title>
  <meta name="description" content="..." />
  <meta name="description" content="..." />
  <!-- 동일한 메타데이터가 두 번 생성됨 -->
</head>

원인 분석

기존 플로우

  1. 사용자가 페이지 접속하면서 generateMetadata 실행(title, meta description 태그 생성)
  2. 사용자가 좋아요/댓글/조회수 액션 수행
  3. Server Action에서 revalidatePath 호출
  4. 페이지 캐시 무효화
  5. 다음 요청 시 generateMetadata 재실행
  6. 새로운 메타데이터가 기존 메타데이터와 중복되어 생성

문제의 핵심

  • revalidatePath로 인한 캐시 무효화가 메타데이터 생성 로직을 다시 실행
  • 서버와 클라이언트 간의 메타데이터 동기화 이슈
  • SSR과 CSR 간의 메타데이터 처리 충돌

Next.js에서 서버 상태 동기화의 한계

서버 상태 동기화 방식의 문제점

Next.js에서 서버 상태 동기화(Server State Synchronization) 방식을 사용할 경우, 필연적으로 revalidatePath를 사용해야 합니다. 하지만 이는 다음과 같은 심각한 문제를 야기합니다:

1. generateMetadata 중복 실행 문제

typescript
// 서버 상태 동기화 방식
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를 우선으로 고려해야 합니다.

이유:

  1. 메타데이터 중복 방지: revalidatePath 사용을 피할 수 있음
  2. 성능 최적화: 불필요한 페이지 재렌더링 방지
  3. 사용자 경험 향상: 즉각적인 UI 반응
  4. 캐시 효율성: 페이지 캐시를 유지하면서 데이터만 동기화

서버 상태 동기화가 필요한 경우:

  • 데이터 일관성이 매우 중요한 경우 (금융, 결제 시스템)
  • 실시간 동기화가 필수인 경우 (협업 도구, 채팅)
  • 복잡한 비즈니스 로직이 서버에 있는 경우

하지만 일반적인 웹 애플리케이션에서는 Optimistic Updates가 더 나은 선택입니다.

해결 방안: Optimistic Updates 도입

1. revalidatePath 완전 제거

Before:

typescript
// 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:

typescript
// 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 구현

좋아요 버튼

typescript
// 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);
  }
};

댓글 시스템

typescript
// 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));
  }
};

소프트 삭제 처리

typescript
// 서버: 소프트 삭제
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로 모든 상태 변경 타입 체크
  • 에러 처리: 각 단계별 에러 상황 대응

핵심 인사이트

  1. 메타데이터 중복은 캐시 관리 문제: revalidatePath의 부적절한 사용이 원인
  2. Optimistic Updates의 힘: 사용자 경험과 성능을 동시에 개선
  3. 서버와 클라이언트 역할 분리: 서버는 데이터 처리, 클라이언트는 UI 상태 관리
  4. 소프트 삭제의 일관성: 서버와 클라이언트 모두 동일한 삭제 전략 사용

결론

revalidatePath에서 Optimistic Updates로의 전환은 단순한 기술적 변경이 아닌, 사용자 경험의 근본적 개선이었습니다. 메타데이터 중복 문제를 해결하면서 동시에 더 빠르고 반응적인 인터페이스를 구현할 수 있었습니다.

이 경험을 통해 캐시 전략의 중요성클라이언트 사이드 상태 관리의 가치를 다시 한번 확인할 수 있었습니다.


기술 스택: Next.js 15, TypeScript, Supabase, Tailwind CSS
적용 범위: 좋아요, 댓글, 조회수, 다운로드 카운트 시스템
성과: 메타데이터 중복 0%, UI 반응성 100% 개선