-
Notifications
You must be signed in to change notification settings - Fork 0
[I25-410] feat : reload 대신 낙관적 업데이트 도입 #261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release/v2
Are you sure you want to change the base?
The head ref may contain hidden characters: "I25-410-reload-\uB300\uC2E0-\uB099\uAD00\uC801-\uC5C5\uB370\uC774\uD2B8"
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,12 +67,9 @@ const ProjectEditModal = ({ | |
| ); | ||
| if (redirectPath) { | ||
| router.push(redirectPath); | ||
| } else { | ||
| router.refresh(); | ||
| } | ||
| } else { | ||
| router.refresh(); | ||
| } | ||
| // 낙관적 업데이트가 적용되었으므로 refresh 불필요 | ||
|
||
| } else { | ||
| const errorMessage = formatErrorMessage(result.message, 'project'); | ||
| showToast(errorMessage, 'error', 2000, '오류'); | ||
|
|
@@ -99,9 +96,8 @@ const ProjectEditModal = ({ | |
| if (config.redirect?.deletePath) { | ||
| const redirectPath = config.redirect.deletePath(variables); | ||
| router.push(redirectPath); | ||
| } else { | ||
| router.refresh(); | ||
| } | ||
| // 낙관적 업데이트가 적용되었으므로 refresh 불필요 | ||
|
||
| } catch (err) { | ||
| const errorMessage = err instanceof Error ? err.message : '삭제 중 오류가 발생했습니다.'; | ||
| showToast(errorMessage, 'error', 3000, '오류'); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,8 @@ | ||||||||||||||||||
| 'use client'; | ||||||||||||||||||
|
|
||||||||||||||||||
| import { useState, useEffect, useCallback } from 'react'; | ||||||||||||||||||
| import { useMutation, useQueryClient } from '@tanstack/react-query'; | ||||||||||||||||||
| import { useRouter } from 'next/navigation'; | ||||||||||||||||||
| import { FormConfig } from '@/app/components/ui/input/types/inputTypes'; | ||||||||||||||||||
| import { MultiInputItem } from '@/utils/hook/useInputList'; | ||||||||||||||||||
| import { | ||||||||||||||||||
|
|
@@ -22,15 +24,29 @@ export function useFormConfigData( | |||||||||||||||||
| autoLoad?: boolean; // 자동으로 데이터 로드 (기본: true) | ||||||||||||||||||
| mode?: FormMode; // 모달의 동작 모드 (기본: 'create') | ||||||||||||||||||
| isUpdate?: boolean; // 업데이트 모드 여부 (deprecated, mode 사용 권장) | ||||||||||||||||||
| onSuccess?: (result: Result) => void; // 성공 시 콜백 | ||||||||||||||||||
| onError?: (error: Error) => void; // 에러 시 콜백 | ||||||||||||||||||
| /** | ||||||||||||||||||
| * 무효화할 쿼리 키들 | ||||||||||||||||||
| * 제공되지 않으면 낙관적 업데이트를 건너뜀 | ||||||||||||||||||
| * 예: [['portfolios'], ['projects', profileName]] | ||||||||||||||||||
| */ | ||||||||||||||||||
| invalidateQueries?: string[][]; | ||||||||||||||||||
| /** | ||||||||||||||||||
| * 서버 컴포넌트를 사용하는 경우 refresh 여부 | ||||||||||||||||||
| * 기본값: false (낙관적 업데이트로 충분) | ||||||||||||||||||
| */ | ||||||||||||||||||
| shouldRefresh?: boolean; | ||||||||||||||||||
| }, | ||||||||||||||||||
| ) { | ||||||||||||||||||
| const [initialValues, setInitialValues] = useState< | ||||||||||||||||||
| Record<string, MultiInputItem[][] | string[] | boolean | File | string | null> | ||||||||||||||||||
| >({}); | ||||||||||||||||||
| const [isLoading, setIsLoading] = useState(false); | ||||||||||||||||||
| const [isSaving, setIsSaving] = useState(false); | ||||||||||||||||||
| const [error, setError] = useState<string | null>(null); | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||
| const router = useRouter(); | ||||||||||||||||||
| const dataService = getGraphQLDataService(); | ||||||||||||||||||
| const autoLoad = options?.autoLoad !== false; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -69,68 +85,144 @@ export function useFormConfigData( | |||||||||||||||||
| }, [formConfig, variables, dataService, shouldLoadData]); | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * 데이터 저장 | ||||||||||||||||||
| * Mutation을 사용한 데이터 저장 (낙관적 업데이트 포함) | ||||||||||||||||||
|
||||||||||||||||||
| * Mutation을 사용한 데이터 저장 (낙관적 업데이트 포함) | |
| * Mutation을 사용한 데이터 저장 (자동 쿼리 무효화 방식) |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
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
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onSuccess와 onSettled 모두에서 동일한 쿼리 무효화가 중복 실행됩니다. 이는 불필요한 중복 작업이며 성능에 영향을 줄 수 있습니다.
해결 방법: 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 });
});
}
},| // 쿼리 키가 제공된 경우에만 무효화 | |
| const invalidateKeys = context?.invalidateKeys; | |
| if (invalidateKeys && invalidateKeys.length > 0) { | |
| invalidateKeys.forEach((key) => { | |
| queryClient.invalidateQueries({ queryKey: key }); | |
| }); | |
| } | |
| // 기타 정리 작업이 필요한 경우 여기에 추가 |
There was a problem hiding this comment.
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를 사용하는 다른 컴포넌트와 데이터를 공유하는 경우 캐시 불일치가 발생할 수 있습니다.확인 사항:
initialHtmlContent를 받는 경우,router.refresh()를 유지하거나로컬 상태만 사용하는 경우라면 현재 구현이 적절합니다.