Conversation
WalkthroughReact Query 의존성을 추가하고 여러 컴포넌트에 낙관적 UI 업데이트 패턴을 구현합니다. 새로운 훅(usePreventDoubleClick, useDebouncedCallback)을 추가하여 좋아요, 저장, 팔로우, 투표 액션에 이중 클릭 방지, 즉시 UI 피드백, 300ms 지연 후 API 호출, 실패 시 상태 롤백 기능을 적용합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as UI Component
participant Hook as usePreventDoubleClick
participant State as Local State
participant API as Server API
User->>UI: 액션 버튼 클릭 (좋아요/저장/팔로우 등)
UI->>Hook: run(asyncFn) 호출
Hook->>Hook: 잠금 설정 (중복 실행 방지)
Hook->>State: isLoading = true
UI->>UI: 투명도 감소로 시각적 피드백
State->>State: 낙관적 상태 업데이트<br/>(liked/saved/followed 즉시 변경)
Note over Hook,API: 300ms 지연
Hook->>API: API 요청 (좋아요/저장/팔로우)
alt 성공 응답
API-->>Hook: 성공 데이터 반환
Hook->>State: 서버 상태와 동기화
Hook->>UI: 성공 메시지 (선택적)
else 실패 응답
API-->>Hook: 오류 응답
Hook->>State: 낙관적 업데이트 롤백
Hook->>UI: 오류 메시지 표시
end
Hook->>Hook: 잠금 해제
Hook->>State: isLoading = false
UI->>UI: 투명도 복구
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (10)
src/components/common/Post/PostFooter.tsx (3)
62-62: 300ms 인위적 지연의 목적이 불분명합니다.
usePreventDoubleClick의 lock이 이미 동시 실행을 차단하고 있으므로,await new Promise(resolve => setTimeout(resolve, 300))은 모든 요청에 불필요한 300ms 지연만 추가합니다. 이 패턴이handleSave(Line 90)와 다른 컴포넌트(Reply, SubReply, Profile, SearchBook 등)에도 동일하게 적용되어 있습니다.진정한 디바운스가 필요하다면 이 PR에 추가된
useDebouncedCallback을 활용하거나, lock만으로 충분하다면 이 지연을 제거하는 것이 좋겠습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/PostFooter.tsx` at line 62, The 300ms artificial delay (await new Promise(resolve => setTimeout(resolve, 300))) in PostFooter's async handlers (e.g., handleSave) is unnecessary because usePreventDoubleClick already prevents concurrent execution; remove that await to eliminate the constant delay and either replace it with the provided useDebouncedCallback if true debouncing is required or rely solely on the existing lock logic from usePreventDoubleClick; update other similar components (Reply, SubReply, Profile, SearchBook) to follow the same pattern so handlers only debounce when intended and do not always sleep for 300ms.
55-81: 롤백 로직 중복 — 헬퍼 함수로 추출을 권장합니다.
handleLike의!response.isSuccess분기(Lines 66-71)와catch분기(Lines 73-78)의 롤백 코드가 동일합니다.handleSave도 마찬가지(Lines 94-98 vs 101-105)입니다. 이를 내부 헬퍼로 추출하면 코드 중복을 줄이고 유지보수가 쉬워집니다.♻️ handleLike 예시
const handleLike = () => { runLike(async () => { const nextLiked = !likedRef.current; likedRef.current = nextLiked; setLiked(nextLiked); setLikeCount(prev => (nextLiked ? prev + 1 : prev - 1)); + const rollback = () => { + if (likedRef.current !== nextLiked) return; + likedRef.current = !nextLiked; + setLiked(!nextLiked); + setLikeCount(prev => (nextLiked ? prev - 1 : prev + 1)); + }; + await new Promise(resolve => setTimeout(resolve, 300)); try { const response = await postFeedLike(feedId, nextLiked); - if (!response.isSuccess && likedRef.current === nextLiked) { - const rollbackState = !nextLiked; - likedRef.current = rollbackState; - setLiked(rollbackState); - setLikeCount(prev => (nextLiked ? prev - 1 : prev + 1)); - } - } catch { - if (likedRef.current === nextLiked) { - const rollbackState = !nextLiked; - likedRef.current = rollbackState; - setLiked(rollbackState); - setLikeCount(prev => (nextLiked ? prev - 1 : prev + 1)); - } + if (!response.isSuccess) rollback(); + } catch { + rollback(); } }); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/PostFooter.tsx` around lines 55 - 81, The rollback logic duplicated in handleLike (the response.isSuccess false branch and the catch block) should be extracted into a shared helper (e.g., rollbackLike) that accepts the intended nextLiked value and performs the state rollback (updating likedRef.current, setLiked, and setLikeCount) so both branches call that helper instead of repeating code; do the same extraction for the identical logic in handleSave so both handleLike and handleSave call their respective rollback helper rather than duplicating the rollback steps used around postFeedLike and the save API call.
120-120: 접근성: 클릭 가능한<img>요소에 키보드 접근성이 없습니다.좋아요/저장 아이콘이
<img onClick={...}>로 구현되어 있어 키보드 사용자가 접근할 수 없습니다.<button>요소로 래핑하거나,role="button",tabIndex={0},onKeyDown핸들러를 추가하는 것을 권장합니다. 로딩 중에는aria-disabled도 고려해 보세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/PostFooter.tsx` at line 120, The like icon image (rendered as <img src={liked ? activeLike : like} onClick={handleLike} style={{ opacity: isLikeLoading ? 0.6 : 1 }} />) is not keyboard-accessible; update PostFooter to make it operable via keyboard by either replacing the <img> with a <button> (preferred) or wrapping it in an element with role="button", tabIndex={0}, and an onKeyDown handler that invokes handleLike on Enter/Space; also reflect loading state with aria-disabled={isLikeLoading} and preserve the visual opacity logic tied to isLikeLoading so screen-reader and keyboard users get the same feedback. Ensure you update the element that uses liked, activeLike, like, handleLike, and isLikeLoading so styling and click behavior remain identical.src/pages/searchBook/SearchBook.tsx (1)
125-127: 피드 로딩 오류가 완전히 무시됩니다.
loadFirstFeeds와loadMore의 catch 블록이// no-op으로 모든 오류를 삼키고 있습니다. 네트워크 장애 등으로 피드 로딩이 실패해도 사용자에게 아무런 피드백이 없으며, 빈 상태만 표시됩니다. 최소한 console.error 로깅이나 사용자에게 재시도 안내를 제공하는 것이 좋겠습니다.Also applies to: 146-148
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/searchBook/SearchBook.tsx` around lines 125 - 127, The catch blocks in loadFirstFeeds and loadMore currently swallow all errors ("// no-op"); update these catches to at minimum log the error (e.g., console.error or processLogger.error) and set a component error state (e.g., setFeedError or reuse existing state) so the UI can show an error message and retry action; ensure loadFirstFeeds and loadMore both call the same error handler/state updater and that finally blocks still run to clear loading flags.src/components/common/Post/SubReply.tsx (1)
47-79: Reply.tsx와 거의 동일한 좋아요 로직 — 공통 훅 추출을 고려해 보세요.
handleLike의 낙관적 업데이트/롤백/스낵바 패턴이Reply.tsx와 사실상 동일합니다. 이 로직을useOptimisticLike(commentId)같은 커스텀 훅으로 추출하면 두 컴포넌트의 중복을 제거할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/SubReply.tsx` around lines 47 - 79, The like handling in handleLike duplicates logic in Reply.tsx; extract this optimistic-update/rollback/snackbar pattern into a reusable custom hook (e.g. useOptimisticLike) that encapsulates runLike, postLike call, optimistic state updates (liked, currentLikeCount) and rollback, and snackbar calls; update SubReply.tsx to call the hook (providing commentId and callbacks) and replace handleLike with the hook's trigger, ensuring you preserve semantics of setLiked/setCurrentLikeCount, previousLiked/previousLikeCount rollback, the 300ms delay, and error snackbar behavior (openSnackbar) so both Reply.tsx and SubReply.tsx share the same implementation.src/components/common/Post/Reply.tsx (1)
41-73: PostFooter와 롤백 추적 방식이 다릅니다 — 동작은 정상이나 통일을 권장합니다.PostFooter는
ref기반으로 현재 상태를 추적하고 prop 변경을 동기화하는 반면, Reply는 클로저 캡처 방식(previousLiked,previousLikeCount)을 사용합니다. lock이 동시 실행을 방지하므로 현재 두 방식 모두 정상 동작하지만, PR 전체의 일관성을 위해 하나의 패턴으로 통일하면 유지보수가 쉬워집니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/Post/Reply.tsx` around lines 41 - 73, Summary: Reply's handleLike uses closure-captured previousLiked/previousLikeCount for rollback, which differs from PostFooter's ref-based state tracking; make Reply follow the ref-based pattern for consistency. Fix: introduce refs (e.g., previousLikedRef, previousLikeCountRef) that are updated/synced with props like PostFooter does, set the refs before performing the optimistic update in handleLike (the function using runLike), and use those refs to restore state on failure instead of closure variables; ensure any prop-driven updates to liked/likeCount are mirrored into the refs (via useEffect) so rollback always uses the latest committed state; keep using postLike for the network call and setLiked/setLikeCount for UI updates.src/components/feed/UserProfileItem.tsx (3)
36-67:usePreventDoubleClick잠금과 300ms 지연의 조합이 불필요한 지연을 유발합니다.
usePreventDoubleClick의run은 이미lockRef로 재진입을 차단하므로, 실행 중에는 연타가 불가능합니다. 따라서 Line 44의setTimeout(300ms)은 디바운스 효과 없이 매 클릭마다 순수 지연만 추가합니다.두 가지 접근 중 하나를 선택하는 것이 맞습니다:
- Lock 방식 (현재
usePreventDoubleClick): 300ms 지연 제거 → 즉시 API 호출- Debounce 방식: lock 제거 → 300ms 내 재클릭 시 이전 호출 취소 후 마지막 클릭만 실행
현재는 두 패턴을 동시에 적용하여, lock으로 인해 debounce가 실질적으로 작동하지 않으면서 300ms 지연만 남게 됩니다. 이 패턴은 PR 내 다른 컴포넌트(PostFooter, Reply, SubReply 등)에도 동일하게 적용되어 있으므로 일괄 검토가 필요합니다.
Lock 방식 유지 시 지연 제거 예시
runFollow(async () => { const nextFollowed = !followedRef.current; followedRef.current = nextFollowed; setFollowed(nextFollowed); - await new Promise(resolve => setTimeout(resolve, 300)); - try { const response = await postFollow(userId, nextFollowed);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/feed/UserProfileItem.tsx` around lines 36 - 67, The click handler toggleFollow is using both the lock-based runFollow (usePreventDoubleClick) and an unnecessary 300ms artificial delay (the await new Promise with setTimeout) which causes pure latency; remove that delay so runFollow can immediately perform the API call. Specifically, inside toggleFollow (and similarly in other components using runFollow), delete the await new Promise(resolve => setTimeout(resolve, 300)) line, rely on followedRef/current and setFollowed for optimistic UI, keep the try/catch with postFollow(userId, nextFollowed) and rollback logic intact, and ensure runFollow remains the locking wrapper to prevent reentrancy.
46-53: Lock 기반에서 stale-check 가드가 도달 불가능(dead code)합니다.Line 48의
if (followedRef.current !== nextFollowed) return;은followedRef가 이 함수 내부에서만 변경되고,usePreventDoubleClick의 lock이 동시 실행을 차단하므로 항상false입니다. Line 61의 동일한 가드도 마찬가지입니다.현재 로직에서는
followedRef를 외부에서 변경하는 경로(예: prop syncuseEffect)가 없기 때문에 이 조건은 절대true가 되지 않습니다. 향후 debounce 방식으로 전환할 경우에만 의미가 생깁니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/feed/UserProfileItem.tsx` around lines 46 - 53, The stale-check guard using followedRef.current !== nextFollowed is dead code because followedRef is only mutated inside this locked handler and usePreventDoubleClick prevents concurrent runs; remove both occurrences of that if-guard in the follow/unfollow handler (the checks surrounding nextFollowed before and after the postFollow call) so the flow relies on the lock and the response update via postFollow/response.data.isFollowing and setFollowed; if you intend to support debounce/external updates later, replace the guards with a clear external sync (e.g., compare to a prop or an external ref updated by useEffect) instead of the current no-op check.
23-24:isFollowingprop 변경 시 로컬 상태와 동기화되지 않습니다.
useState(!!isFollowing)과useRef(!!isFollowing)는 마운트 시점에만 초기화됩니다. 부모 컴포넌트에서 데이터를 리패치하여isFollowingprop이 변경되어도followed상태와followedRef는 갱신되지 않아 UI가 서버 상태와 불일치할 수 있습니다.PollRecord 컴포넌트(Line 72-75)에서는
useEffect로 prop 변경을 동기화하고 있으므로, 동일한 패턴 적용을 권장합니다.prop 동기화 useEffect 추가 예시
const { isLoading: isFollowLoading, run: runFollow } = usePreventDoubleClick(); + useEffect(() => { + setFollowed(!!isFollowing); + followedRef.current = !!isFollowing; + }, [isFollowing]); + const handleProfileClick = () => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/feed/UserProfileItem.tsx` around lines 23 - 24, UserProfileItem's local state (followed) and ref (followedRef) are only initialized from the isFollowing prop and won't update when isFollowing changes; add a useEffect in the UserProfileItem component that watches isFollowing and updates both setFollowed(!!isFollowing) and followedRef.current = !!isFollowing (same pattern used in PollRecord) so the UI/ref stay in sync with prop changes from the parent.src/components/memory/RecordItem/PollRecord.tsx (1)
105-105: UserProfileItem과 동일하게 300ms 지연이 lock과 중복됩니다.
usePreventDoubleClick의 lock이 재진입을 차단하므로 이 지연은 debounce 효과 없이 순수 지연만 추가합니다. UserProfileItem에 대한 코멘트와 동일한 사항이며, PR 전체적으로 일괄 검토가 필요합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/memory/RecordItem/PollRecord.tsx` at line 105, PollRecord.tsx contains an explicit 300ms setTimeout delay that duplicates behavior already enforced by usePreventDoubleClick’s lock (same issue noted for UserProfileItem); remove the await new Promise(resolve => setTimeout(resolve, 300)) from the handler in PollRecord (and remove the same pattern across the PR) and either rely on usePreventDoubleClick’s reentrancy lock or, if a debounce is actually required, implement the debounce inside usePreventDoubleClick (or a shared debounce util) and call that instead so the lock and timing behavior are not duplicated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Line 19: package.json contains an unused dependency "@tanstack/react-query";
remove the dependency line for "@tanstack/react-query" from package.json (or
alternatively add the actual imports/usages where you intend to use it), run
yarn/npm install to update lockfile, and ensure no code references
"@tanstack/react-query" (search for imports of react-query or
useQuery/useMutation) before merging; if you plan to add optimistic update logic
later, move the dependency change into a separate PR.
In `@src/components/memory/RecordItem/PollRecord.tsx`:
- Around line 72-75: The useEffect that syncs pollOptions (the block calling
setCurrentOptions and optionsRef.current = pollOptions) can overwrite an ongoing
optimistic update; add a guard at the start of that effect to skip prop
synchronization when a vote/lock is in-flight (e.g., check an existing in-flight
flag like isVotingRef, isLocked, or pendingRequestRef used by your voting
logic), so that when a request is pending you do not overwrite
currentOptions/optionsRef.current; keep the existing sync behavior when no
request is pending and ensure the in-flight flag is set/cleared by the voting
functions so the stale-check logic later still works.
In `@src/hooks/useDebouncedCallback.ts`:
- Around line 3-6: The hook signature uses `any` for callback args; change it to
use `unknown[]` for type-safety by updating the generic constraint and rest-arg
types — e.g. change `T extends (...args: any[]) => void` to `T extends (...args:
unknown[]) => void` and replace any usages of `any[]` or `...args: any[]` inside
`useDebouncedCallback` with `unknown[]` or `...args: unknown[]` so the callback
parameter types and invocations remain consistent while satisfying
`@typescript-eslint/no-explicit-any`.
---
Nitpick comments:
In `@src/components/common/Post/PostFooter.tsx`:
- Line 62: The 300ms artificial delay (await new Promise(resolve =>
setTimeout(resolve, 300))) in PostFooter's async handlers (e.g., handleSave) is
unnecessary because usePreventDoubleClick already prevents concurrent execution;
remove that await to eliminate the constant delay and either replace it with the
provided useDebouncedCallback if true debouncing is required or rely solely on
the existing lock logic from usePreventDoubleClick; update other similar
components (Reply, SubReply, Profile, SearchBook) to follow the same pattern so
handlers only debounce when intended and do not always sleep for 300ms.
- Around line 55-81: The rollback logic duplicated in handleLike (the
response.isSuccess false branch and the catch block) should be extracted into a
shared helper (e.g., rollbackLike) that accepts the intended nextLiked value and
performs the state rollback (updating likedRef.current, setLiked, and
setLikeCount) so both branches call that helper instead of repeating code; do
the same extraction for the identical logic in handleSave so both handleLike and
handleSave call their respective rollback helper rather than duplicating the
rollback steps used around postFeedLike and the save API call.
- Line 120: The like icon image (rendered as <img src={liked ? activeLike :
like} onClick={handleLike} style={{ opacity: isLikeLoading ? 0.6 : 1 }} />) is
not keyboard-accessible; update PostFooter to make it operable via keyboard by
either replacing the <img> with a <button> (preferred) or wrapping it in an
element with role="button", tabIndex={0}, and an onKeyDown handler that invokes
handleLike on Enter/Space; also reflect loading state with
aria-disabled={isLikeLoading} and preserve the visual opacity logic tied to
isLikeLoading so screen-reader and keyboard users get the same feedback. Ensure
you update the element that uses liked, activeLike, like, handleLike, and
isLikeLoading so styling and click behavior remain identical.
In `@src/components/common/Post/Reply.tsx`:
- Around line 41-73: Summary: Reply's handleLike uses closure-captured
previousLiked/previousLikeCount for rollback, which differs from PostFooter's
ref-based state tracking; make Reply follow the ref-based pattern for
consistency. Fix: introduce refs (e.g., previousLikedRef, previousLikeCountRef)
that are updated/synced with props like PostFooter does, set the refs before
performing the optimistic update in handleLike (the function using runLike), and
use those refs to restore state on failure instead of closure variables; ensure
any prop-driven updates to liked/likeCount are mirrored into the refs (via
useEffect) so rollback always uses the latest committed state; keep using
postLike for the network call and setLiked/setLikeCount for UI updates.
In `@src/components/common/Post/SubReply.tsx`:
- Around line 47-79: The like handling in handleLike duplicates logic in
Reply.tsx; extract this optimistic-update/rollback/snackbar pattern into a
reusable custom hook (e.g. useOptimisticLike) that encapsulates runLike,
postLike call, optimistic state updates (liked, currentLikeCount) and rollback,
and snackbar calls; update SubReply.tsx to call the hook (providing commentId
and callbacks) and replace handleLike with the hook's trigger, ensuring you
preserve semantics of setLiked/setCurrentLikeCount,
previousLiked/previousLikeCount rollback, the 300ms delay, and error snackbar
behavior (openSnackbar) so both Reply.tsx and SubReply.tsx share the same
implementation.
In `@src/components/feed/UserProfileItem.tsx`:
- Around line 36-67: The click handler toggleFollow is using both the lock-based
runFollow (usePreventDoubleClick) and an unnecessary 300ms artificial delay (the
await new Promise with setTimeout) which causes pure latency; remove that delay
so runFollow can immediately perform the API call. Specifically, inside
toggleFollow (and similarly in other components using runFollow), delete the
await new Promise(resolve => setTimeout(resolve, 300)) line, rely on
followedRef/current and setFollowed for optimistic UI, keep the try/catch with
postFollow(userId, nextFollowed) and rollback logic intact, and ensure runFollow
remains the locking wrapper to prevent reentrancy.
- Around line 46-53: The stale-check guard using followedRef.current !==
nextFollowed is dead code because followedRef is only mutated inside this locked
handler and usePreventDoubleClick prevents concurrent runs; remove both
occurrences of that if-guard in the follow/unfollow handler (the checks
surrounding nextFollowed before and after the postFollow call) so the flow
relies on the lock and the response update via
postFollow/response.data.isFollowing and setFollowed; if you intend to support
debounce/external updates later, replace the guards with a clear external sync
(e.g., compare to a prop or an external ref updated by useEffect) instead of the
current no-op check.
- Around line 23-24: UserProfileItem's local state (followed) and ref
(followedRef) are only initialized from the isFollowing prop and won't update
when isFollowing changes; add a useEffect in the UserProfileItem component that
watches isFollowing and updates both setFollowed(!!isFollowing) and
followedRef.current = !!isFollowing (same pattern used in PollRecord) so the
UI/ref stay in sync with prop changes from the parent.
In `@src/components/memory/RecordItem/PollRecord.tsx`:
- Line 105: PollRecord.tsx contains an explicit 300ms setTimeout delay that
duplicates behavior already enforced by usePreventDoubleClick’s lock (same issue
noted for UserProfileItem); remove the await new Promise(resolve =>
setTimeout(resolve, 300)) from the handler in PollRecord (and remove the same
pattern across the PR) and either rely on usePreventDoubleClick’s reentrancy
lock or, if a debounce is actually required, implement the debounce inside
usePreventDoubleClick (or a shared debounce util) and call that instead so the
lock and timing behavior are not duplicated.
In `@src/pages/searchBook/SearchBook.tsx`:
- Around line 125-127: The catch blocks in loadFirstFeeds and loadMore currently
swallow all errors ("// no-op"); update these catches to at minimum log the
error (e.g., console.error or processLogger.error) and set a component error
state (e.g., setFeedError or reuse existing state) so the UI can show an error
message and retry action; ensure loadFirstFeeds and loadMore both call the same
error handler/state updater and that finally blocks still run to clear loading
flags.
| "@emotion/css": "^11.13.5", | ||
| "@emotion/react": "^11.14.0", | ||
| "@emotion/styled": "^11.14.0", | ||
| "@tanstack/react-query": "^5.90.21", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if `@tanstack/react-query` is imported anywhere in the codebase
rg '@tanstack/react-query' --type ts --type tsx -g '!node_modules' -g '!package.json' -g '!package-lock.json'Repository: THIP-TextHip/THIP-Web
Length of output: 92
🏁 Script executed:
#!/bin/bash
# Check if `@tanstack/react-query` is imported anywhere in the codebase
rg '@tanstack/react-query' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' -g '!node_modules'Repository: THIP-TextHip/THIP-Web
Length of output: 47
@tanstack/react-query는 코드베이스에서 사용되지 않습니다.
이 패키지는 package.json에 추가되었지만 어떤 파일에서도 import되거나 사용되지 않습니다. 낙관적 업데이트 패턴이 모두 수동 상태 관리(useState/useRef)로 구현되어 있으므로, 이 의존성을 제거하거나 실제로 사용하는 코드를 추가해야 합니다. 향후 사용 예정인 경우 별도 PR에서 처리하는 것을 권장합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@package.json` at line 19, package.json contains an unused dependency
"@tanstack/react-query"; remove the dependency line for "@tanstack/react-query"
from package.json (or alternatively add the actual imports/usages where you
intend to use it), run yarn/npm install to update lockfile, and ensure no code
references "@tanstack/react-query" (search for imports of react-query or
useQuery/useMutation) before merging; if you plan to add optimistic update logic
later, move the dependency change into a separate PR.
| useEffect(() => { | ||
| setCurrentOptions(pollOptions); | ||
| optionsRef.current = pollOptions; | ||
| }, [pollOptions]); |
There was a problem hiding this comment.
prop 동기화 useEffect가 진행 중인 낙관적 업데이트를 덮어쓸 수 있습니다.
투표 중(lock 획득 상태)에 부모가 pollOptions를 새 값으로 전달하면, 이 useEffect가 optionsRef.current와 currentOptions를 prop 값으로 덮어써서 낙관적 UI가 즉시 되돌아갑니다. 이후 API 응답이 도착하면 Line 113의 stale-check로 응답이 무시되거나, 다시 상태가 변경되어 시각적 깜빡임이 발생할 수 있습니다.
진행 중인 요청이 있을 때는 prop 동기화를 건너뛰는 가드를 추가하는 것이 좋습니다.
가드 추가 예시
useEffect(() => {
+ if (isVoting) return;
setCurrentOptions(pollOptions);
optionsRef.current = pollOptions;
}, [pollOptions]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/memory/RecordItem/PollRecord.tsx` around lines 72 - 75, The
useEffect that syncs pollOptions (the block calling setCurrentOptions and
optionsRef.current = pollOptions) can overwrite an ongoing optimistic update;
add a guard at the start of that effect to skip prop synchronization when a
vote/lock is in-flight (e.g., check an existing in-flight flag like isVotingRef,
isLocked, or pendingRequestRef used by your voting logic), so that when a
request is pending you do not overwrite currentOptions/optionsRef.current; keep
the existing sync behavior when no request is pending and ensure the in-flight
flag is set/cleared by the voting functions so the stale-check logic later still
works.
| export const useDebouncedCallback = <T extends (...args: any[]) => void>( | ||
| callback: T, | ||
| delay: number, | ||
| ) => { |
There was a problem hiding this comment.
ESLint 오류: any 타입 사용을 수정하세요.
정적 분석에서 @typescript-eslint/no-explicit-any 오류가 보고되었습니다. any 대신 unknown[]를 사용하면 타입 안전성을 유지하면서 범용성을 확보할 수 있습니다.
🔧 수정 제안
-export const useDebouncedCallback = <T extends (...args: any[]) => void>(
+export const useDebouncedCallback = <T extends (...args: unknown[]) => void>(🧰 Tools
🪛 ESLint
[error] 3-3: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useDebouncedCallback.ts` around lines 3 - 6, The hook signature
uses `any` for callback args; change it to use `unknown[]` for type-safety by
updating the generic constraint and rest-arg types — e.g. change `T extends
(...args: any[]) => void` to `T extends (...args: unknown[]) => void` and
replace any usages of `any[]` or `...args: any[]` inside `useDebouncedCallback`
with `unknown[]` or `...args: unknown[]` so the callback parameter types and
invocations remain consistent while satisfying
`@typescript-eslint/no-explicit-any`.
📝작업 내용
상태 토글 액션(좋아요/저장/팔로우/투표/댓글 좋아요)에 중복 실행 방지 패턴을 공통 적용했습니다.
변경 내용
각 버튼에 동일 패턴 적용
적용범위
적용파일
기대 효과
위키
자세한 수정사항은 위키에서 참조부탁드립니다.
Summary by CodeRabbit
새로운 기능
의존성