Skip to content

Conversation

@GAMZAMANDU
Copy link
Contributor

@GAMZAMANDU GAMZAMANDU commented Dec 2, 2025

I25-410 reload 대신 낙관적 업데이트 도입

💡 개요

모달에서 데이터 저장 후 router.refresh() 또는 window.location.reload()를 사용하여 전체 페이지를 새로고침하던 방식을 개선하여, React Query의 낙관적 업데이트(Optimistic Update)를 도입했습니다. 이를 통해 사용자 경험을 개선하고 불필요한 페이지 리로드를 제거했습니다.

📃 작업내용

1. useFormConfigData 훅에 낙관적 업데이트 통합

useFormConfigData 훅에 React Query의 useMutation을 통합하고 낙관적 업데이트 로직을 추가했습니다.

graph TD
    A[사용자가 폼 제출] --> B[onMutate: 이전 데이터 저장]
    B --> C{쿼리 키 제공?}
    C -->|Yes| D[관련 쿼리 취소 및 이전 데이터 백업]
    C -->|No| E[낙관적 업데이트 건너뜀]
    D --> F[mutationFn 실행]
    E --> F
    F --> G{성공?}
    G -->|Yes| H[onSuccess: 쿼리 무효화]
    G -->|No| I[onError: 이전 데이터로 롤백]
    H --> J[최신 데이터 자동 refetch]
    I --> K[에러 메시지 표시]
    J --> L[UI 즉시 업데이트]
    K --> M[사용자에게 알림]
Loading

2. 모달 컴포넌트에서 reload 제거

모든 모달 컴포넌트에서 router.refresh()window.location.reload() 호출을 제거하고, 낙관적 업데이트로 대체했습니다.

graph LR
    A[기존 방식] --> B[데이터 저장]
    B --> C[router.refresh]
    C --> D[전체 페이지 리로드]
    D --> E[서버에서 데이터 재조회]
    E --> F[UI 업데이트]
    
    G[개선된 방식] --> H[데이터 저장]
    H --> I[낙관적 업데이트]
    I --> J[쿼리 캐시 즉시 업데이트]
    J --> K[UI 즉시 반영]
    K --> L[백그라운드에서 최신 데이터 refetch]
Loading

3. 주요 구현 내용

  1. useFormConfigData 훅 개선

    • React Query의 useMutation 통합
    • onMutate: 쿼리 키가 제공된 경우 이전 데이터 저장 (롤백용)
    • onError: 에러 발생 시 이전 데이터로 자동 롤백
    • onSuccess: 성공 시 관련 쿼리 무효화 및 자동 refetch
    • onSettled: 최종 정리 작업
  2. 옵션 추가

    • invalidateQueries?: string[][]: 무효화할 쿼리 키들 (제공되지 않으면 낙관적 업데이트 건너뜀)
    • shouldRefresh?: boolean: 서버 컴포넌트 사용 시 router.refresh() 호출 여부 (기본값: false)
  3. 모달 컴포넌트 수정

    • ProfileEditModal: window.location.reload()router.refresh() 제거
    • ProjectEditModal: router.refresh() 제거
    • ContentEditor: router.refresh() 제거, 로컬 상태로 즉시 반영
  4. 안전한 처리

    • 쿼리 키가 제공되지 않으면 낙관적 업데이트를 건너뛰고 mutation만 실행
    • 에러 발생 시 자동 롤백으로 데이터 일관성 유지

🔀 변경사항

추가 개선사항

  1. 쿼리 키 처리 개선

    • 기존 getQueryKey 함수 제거 (실제 쿼리 키와 불일치 가능성)
    • 쿼리 키를 옵션으로 명시적으로 전달하도록 변경
    • 쿼리 키가 없어도 정상 동작하도록 안전하게 처리
  2. 에러 처리 강화

    • 낙관적 업데이트 실패 시 자동 롤백
    • 에러 메시지 표시 및 사용자 알림
  3. 성능 개선

    • 불필요한 페이지 리로드 제거
    • 쿼리 캐시를 활용한 즉시 UI 업데이트
    • 백그라운드에서 최신 데이터만 refetch

주요 파일 변경

  • src/utils/hook/useFormConfigData.ts - 낙관적 업데이트 로직 추가
  • src/app/components/ui/profile/portfolio/ProfileEditModal.tsx - reload 제거
  • src/app/components/feature/project/components/ProjectEditModal.tsx - refresh 제거
  • src/app/components/feature/editor/ContentEditor.tsx - refresh 제거

📸 스크린샷

Copilot AI review requested due to automatic review settings December 2, 2025 16:00
@insertjenkins
Copy link

insertjenkins bot commented Dec 2, 2025

🚀 배포 준비중

고정포트 : 4261 (PR #261 전용)

@insertjenkins
Copy link

insertjenkins bot commented Dec 2, 2025

🚀 배포 완료!

✨ 개발서버 프리뷰: http://10.59.0.106:4261
📌 고정포트: 4261 (PR #261 전용)

Cloudflare WARP VPN을 통한 내부망 접근 필수, Google One Tab Login 사용 불가능

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

이 PR은 모달에서 데이터 저장 후 전체 페이지를 새로고침하던 방식(router.refresh(), window.location.reload())을 제거하고, React Query의 mutation과 쿼리 무효화를 통해 UI를 자동으로 업데이트하도록 개선하려는 시도입니다. 하지만 현재 구현에는 몇 가지 심각한 문제가 있습니다.

주요 변경 사항:

  • useFormConfigData 훅에 React Query의 useMutation 통합 및 쿼리 무효화 로직 추가
  • ProfileEditModal, ProjectEditModal, ContentEditor에서 router.refresh()window.location.reload() 호출 제거
  • 새로운 옵션 추가: invalidateQueries, onSuccess, onError, shouldRefresh

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
src/utils/hook/useFormConfigData.ts React Query mutation 통합, 쿼리 무효화 로직 추가. 하지만 실제 낙관적 업데이트는 구현되지 않았으며, 중복 무효화 및 누락된 캐시 업데이트 이슈 존재
src/app/components/ui/profile/portfolio/ProfileEditModal.tsx window.location.reload()router.refresh() 제거. 하지만 invalidateQueries 옵션이 전달되지 않아 실제로는 UI가 업데이트되지 않음
src/app/components/feature/project/components/ProjectEditModal.tsx router.refresh() 제거. 하지만 invalidateQueries 옵션이 전달되지 않아 실제로는 UI가 업데이트되지 않음
src/app/components/feature/editor/ContentEditor.tsx router.refresh() 제거 및 로컬 상태로 콘텐츠 업데이트. React Query 캐시와 불일치 가능성 존재

} else {
router.refresh();
}
// 낙관적 업데이트가 적용되었으므로 refresh 불필요
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

router.refresh() 호출을 제거했지만, invalidateQueries 옵션이 useFormConfigData 훅에 전달되지 않아 낙관적 업데이트가 실제로 작동하지 않습니다. 이로 인해 프로젝트 생성/수정 후 UI가 자동으로 업데이트되지 않습니다.

이 문제는 useFormConfigData 호출 시 invalidateQueries 옵션을 추가하여 해결해야 하지만, 해당 코드는 변경 범위 밖에 있습니다. 또는 router.refresh()를 유지해야 합니다.

Copilot uses AI. Check for mistakes.
} else {
router.refresh();
}
// 낙관적 업데이트가 적용되었으므로 refresh 불필요
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

router.refresh() 호출을 제거했지만, invalidateQueries 옵션이 useFormConfigData 훅에 전달되지 않아 낙관적 업데이트가 실제로 작동하지 않습니다. 이로 인해 프로젝트 삭제 후 UI가 자동으로 업데이트되지 않습니다.

이 문제는 useFormConfigData 호출 시 invalidateQueries 옵션을 추가하거나, 삭제 핸들러에서 수동으로 쿼리를 무효화하거나 router.refresh()를 유지해야 합니다.

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +195
// 쿼리 키가 제공된 경우에만 무효화
const invalidateKeys = context?.invalidateKeys;
if (invalidateKeys && invalidateKeys.length > 0) {
invalidateKeys.forEach((key) => {
queryClient.invalidateQueries({ queryKey: key });
});
}
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onSuccessonSettled 모두에서 동일한 쿼리 무효화가 중복 실행됩니다. 이는 불필요한 중복 작업이며 성능에 영향을 줄 수 있습니다.

해결 방법: onSettled에서의 무효화를 제거하고 onSuccess에서만 수행하거나, onSettled에서만 수행하도록 변경하세요. React Query 공식 문서에서는 일반적으로 onSettled에서 무효화를 권장하지만, 성공 시에만 무효화가 필요한 경우 onSuccess만 사용해도 됩니다.

// 옵션 1: onSuccess에서만 무효화 (현재 로직 유지)
onSuccess: (result, _variables, context) => {
  const invalidateKeys = context?.invalidateKeys;
  if (invalidateKeys && invalidateKeys.length > 0) {
    invalidateKeys.forEach((key) => {
      queryClient.invalidateQueries({ queryKey: key });
    });
  }
  // ... 나머지 코드
},
// onSettled 제거 또는 다른 정리 작업만 수행

// 옵션 2: onSettled에서만 무효화 (성공/실패 모두)
onSettled: (_data, _error, _variables, context) => {
  const invalidateKeys = context?.invalidateKeys;
  if (invalidateKeys && invalidateKeys.length > 0) {
    invalidateKeys.forEach((key) => {
      queryClient.invalidateQueries({ queryKey: key });
    });
  }
},
Suggested change
// 쿼리 키가 제공된 경우에만 무효화
const invalidateKeys = context?.invalidateKeys;
if (invalidateKeys && invalidateKeys.length > 0) {
invalidateKeys.forEach((key) => {
queryClient.invalidateQueries({ queryKey: key });
});
}
// 기타 정리 작업이 필요한 경우 여기에 추가

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +97
// 낙관적 업데이트: 저장된 콘텐츠를 즉시 반영
setEditedContentJson(data.json);
setIsEditing(false);
setEditedContentJson(null);
router.refresh();
// 서버 컴포넌트가 있는 경우에만 refresh (선택적)
// 낙관적 업데이트로 대부분의 경우 불필요하지만,
// 서버 컴포넌트에서 데이터를 가져오는 경우를 위해 주석 처리
// router.refresh();
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ContentEditor는 useFormConfigData 훅을 사용하지 않고 직접 update 함수를 호출하므로, router.refresh() 제거 시 별도의 낙관적 업데이트 메커니즘이 필요합니다.

현재 구현은 setEditedContentJson(data.json)으로 로컬 상태만 업데이트하고 있지만, 이 컴포넌트가 React Query를 사용하는 다른 컴포넌트와 데이터를 공유하는 경우 캐시 불일치가 발생할 수 있습니다.

확인 사항:

  1. 이 컴포넌트가 서버 컴포넌트로부터 initialHtmlContent를 받는 경우, router.refresh()를 유지하거나
  2. React Query를 사용하여 콘텐츠를 가져오는 다른 컴포넌트가 있다면, 해당 쿼리도 무효화해야 합니다.

로컬 상태만 사용하는 경우라면 현재 구현이 적절합니다.

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +155
onMutate: async ({ formData, additionalVariables }) => {
// 쿼리 키가 제공되지 않으면 낙관적 업데이트를 건너뜀
const invalidateKeys = options?.invalidateQueries;

if (!invalidateKeys || invalidateKeys.length === 0) {
return { previousDataMap: null, invalidateKeys: [] };
}

// 낙관적 업데이트: 관련 쿼리들의 이전 데이터를 저장하여 롤백 가능하게 함
const previousDataMap = new Map<string, unknown>();

for (const key of invalidateKeys) {
await queryClient.cancelQueries({ queryKey: key });
const previousData = queryClient.getQueryData(key);
if (previousData) {
previousDataMap.set(JSON.stringify(key), previousData);
}
}

return { previousDataMap, invalidateKeys };
},
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

낙관적 업데이트가 실제로 캐시를 업데이트하지 않습니다. onMutate에서 이전 데이터를 저장만 하고 새로운 데이터로 캐시를 업데이트하지 않아, "낙관적 업데이트"의 핵심 기능(mutation 완료 전 UI 즉시 업데이트)이 작동하지 않습니다.

해결 방법: onMutate에서 쿼리 캐시를 새 데이터로 업데이트해야 합니다:

onMutate: async ({ formData, additionalVariables }) => {
  const invalidateKeys = options?.invalidateQueries;
  
  if (!invalidateKeys || invalidateKeys.length === 0) {
    return { previousDataMap: null, invalidateKeys: [] };
  }
  
  const previousDataMap = new Map<string, unknown>();
  
  for (const key of invalidateKeys) {
    await queryClient.cancelQueries({ queryKey: key });
    const previousData = queryClient.getQueryData(key);
    if (previousData) {
      previousDataMap.set(JSON.stringify(key), previousData);
    }
    
    // 낙관적 업데이트: 캐시를 새 데이터로 즉시 업데이트
    // 데이터 구조에 맞게 업데이트 로직 구현 필요
    // 예: queryClient.setQueryData(key, (old) => ({ ...old, ...formData }));
  }

  return { previousDataMap, invalidateKeys };
},

또는 현재 구현이 의도한 것이라면, "낙관적 업데이트" 대신 "자동 쿼리 무효화"라고 명명하는 것이 더 정확합니다.

Copilot uses AI. Check for mistakes.

/**
* 데이터 저장
* Mutation을 사용한 데이터 저장 (낙관적 업데이트 포함)
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR 설명과 코드 주석에서 "낙관적 업데이트(Optimistic Update)"라고 설명하고 있지만, 실제 구현은 mutation 완료 후 쿼리를 무효화하는 방식입니다. 이는 낙관적 업데이트가 아니라 **자동 쿼리 무효화(Automatic Query Invalidation)**입니다.

낙관적 업데이트는 서버 응답을 기다리지 않고 즉시 UI를 업데이트하는 것이고, 쿼리 무효화는 mutation 완료 후 서버에서 최신 데이터를 다시 가져오는 것입니다.

현재 구현은 후자이므로, 사용자는 여전히 mutation이 완료될 때까지 기다려야 하며 UX 개선 효과가 제한적입니다. PR 제목과 설명을 실제 구현에 맞게 수정하거나, 진정한 낙관적 업데이트를 구현해야 합니다.

Suggested change
* Mutation을 사용한 데이터 저장 (낙관적 업데이트 포함)
* Mutation을 사용한 데이터 저장 (자동 쿼리 무효화 방식)

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: LEE JEONGHYEOK <157395300+GAMZAMANDU@users.noreply.github.com>
@insertjenkins
Copy link

insertjenkins bot commented Dec 3, 2025

🚀 배포 준비중

고정포트 : 4261 (PR #261 전용)

@insertjenkins
Copy link

insertjenkins bot commented Dec 3, 2025

🚀 배포 완료!

✨ 개발서버 프리뷰: http://10.59.0.106:4261
📌 고정포트: 4261 (PR #261 전용)

Cloudflare WARP VPN을 통한 내부망 접근 필수, Google One Tab Login 사용 불가능

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants