#75 feat: Suspense 기반 로딩 처리 임시 도입 및 문제 선택 UX 개선#86
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbit릴리스 노트
Walkthrough데이터 페칭 훅들을 Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
짧은 칭찬: Suspense 전환 깔끔해요 — 로딩 UX 일관성은 좋습니다. 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/features/assignment/filter-assignment/lib/useAssignmentList.ts (1)
9-20:⚠️ Potential issue | 🟡 Minor타입 시그니처를 실제 구현과 일치시키세요.
useCourseFilter에서 반환하는selectedCourseId는number | null인데,useAssignmentList는number로 선언하고 있습니다. 호출부에서selectedCourseId!로 non-null assertion을 사용하면서 이 불일치를 무시하고 있으나, 구현 내부에서는enabled: !!selectedCourseId와selectedCourseId ? ... : ...로 실제로는 nullable 로직을 다루고 있습니다.mock 데이터 단계라 하더라도, 타입 정의가 실제 런타임 동작을 반영해야 TypeScript의 타입 안정성이 의미가 있습니다. 시그니처를
selectedCourseId: number | null로 수정하고, 쿼리 설정 시enabled: selectedCourseId !== null로 명확하게 처리해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/assignment/filter-assignment/lib/useAssignmentList.ts` around lines 9 - 20, The function signature of useAssignmentList should accept nullable course IDs: change the parameter type on useAssignmentList to selectedCourseId: number | null and keep the return type TAssignment[]; update the query enablement to use enabled: selectedCourseId !== null (instead of !!selectedCourseId) and ensure the selection logic still uses selectedCourseId ? (assignments ?? []) : (allAssignments ?? []) so the implementation and types align with the nullable behavior.
🧹 Nitpick comments (1)
src/shared/ui/Layout.tsx (1)
4-4: 여기만 상대 경로라서 규칙이 살짝 깨집니다.이 파일은
src/**규칙상 절대 경로 임포트를 쓰는 편이 일관됩니다.경로 정리 예시
-import Loading from './Loading'; +import Loading from '@/shared/ui/Loading';As per coding guidelines,
src/**: 절대 경로 임포트(@/...) 사용🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/ui/Layout.tsx` at line 4, The import in Layout.tsx uses a relative path for the Loading component which breaks the project convention; update the import of Loading in src/shared/ui/Layout.tsx to use the project absolute alias (e.g., "@/shared/ui/Loading") so it follows the `src/**` absolute-import rule and keeps imports consistent across the codebase.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/shared/ui/Layout.tsx`:
- Around line 8-20: Layout currently wraps children with Suspense (Suspense,
Loading, Outlet, Header) but has no ErrorBoundary, so errors from
useSuspenseQuery will crash the UI; fix by adding an error boundary around the
Suspense content: implement or import an ErrorBoundary component and wrap the
existing Suspense (or the entire Layout return) with it, or alternatively add an
errorElement for the route that renders <Layout /> in App.tsx; ensure the
ErrorBoundary renders a fallback UI and logs/handles the thrown error from
queries so useSuspenseQuery errors are caught.
In `@src/shared/ui/Loading.tsx`:
- Around line 5-10: The Loading component currently uses fixed inset-0 which
forces a full-viewport overlay; change Loading (in src/shared/ui/Loading.tsx) to
accept a boolean prop like fullscreen (default true) and conditionally apply the
positioning classes: when fullscreen is true keep 'fixed inset-0 flex-center',
when false render a non-fixed inline container (e.g. only 'flex-center' or allow
parent sizing) so Suspense fallbacks can be localized; ensure the prop is typed
in the component signature and that LoadingImg and the inner span styling remain
unchanged, and update usages (or add separate PageLoading / SectionLoading
wrappers) to pass fullscreen appropriately.
---
Outside diff comments:
In `@src/features/assignment/filter-assignment/lib/useAssignmentList.ts`:
- Around line 9-20: The function signature of useAssignmentList should accept
nullable course IDs: change the parameter type on useAssignmentList to
selectedCourseId: number | null and keep the return type TAssignment[]; update
the query enablement to use enabled: selectedCourseId !== null (instead of
!!selectedCourseId) and ensure the selection logic still uses selectedCourseId ?
(assignments ?? []) : (allAssignments ?? []) so the implementation and types
align with the nullable behavior.
---
Nitpick comments:
In `@src/shared/ui/Layout.tsx`:
- Line 4: The import in Layout.tsx uses a relative path for the Loading
component which breaks the project convention; update the import of Loading in
src/shared/ui/Layout.tsx to use the project absolute alias (e.g.,
"@/shared/ui/Loading") so it follows the `src/**` absolute-import rule and keeps
imports consistent across the codebase.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ce69dc35-4d8f-445f-9675-f0d996a2fa6d
📒 Files selected for processing (8)
src/features/assignment/filter-assignment/lib/useAssignmentList.tssrc/pages/admin/units/UnitLayout.tsxsrc/pages/admin/units/ui/UnitFormLayout.tsxsrc/pages/dashboard/Dashboard.tsxsrc/pages/select-assignment/AssignmentSelectPage.tsxsrc/pages/submit-assignment/AssignmentSubmitLayout.tsxsrc/shared/ui/Layout.tsxsrc/shared/ui/Loading.tsx
| <Suspense fallback={<Loading />}> | ||
| <div className='min-h-screen overflow-x-hidden flex justify-center'> | ||
| <div className='min-w-289 px-4 py-12'> | ||
| <div className='w-289 pb-8 mx-auto relative z-10'> | ||
| <Header /> | ||
| </div> | ||
|
|
||
| <main> | ||
| <Outlet /> | ||
| </main> | ||
| <main> | ||
| <Outlet /> | ||
| </main> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </Suspense> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find src -type f -name "*.tsx" -o -name "*.ts" | head -20Repository: 2025-snowCode/snowCode-Client
Length of output: 811
🏁 Script executed:
cat -n src/shared/ui/Layout.tsxRepository: 2025-snowCode/snowCode-Client
Length of output: 818
🏁 Script executed:
cat -n src/App.tsxRepository: 2025-snowCode/snowCode-Client
Length of output: 5157
🏁 Script executed:
rg "ErrorBoundary|errorElement" --type tsx --type tsRepository: 2025-snowCode/snowCode-Client
Length of output: 100
🏁 Script executed:
rg "useSuspenseQuery" --type tsx --type tsRepository: 2025-snowCode/snowCode-Client
Length of output: 100
🏁 Script executed:
fd "error" --type f -i | grep -E "\.(tsx?|jsx?)$" | head -20Repository: 2025-snowCode/snowCode-Client
Length of output: 105
🏁 Script executed:
rg "ErrorBoundary" -g "*.tsx" -g "*.ts"Repository: 2025-snowCode/snowCode-Client
Length of output: 55
🏁 Script executed:
rg "errorElement" -g "*.tsx" -g "*.ts"Repository: 2025-snowCode/snowCode-Client
Length of output: 55
🏁 Script executed:
rg "useSuspenseQuery" -g "*.tsx" -g "*.ts"Repository: 2025-snowCode/snowCode-Client
Length of output: 1090
🏁 Script executed:
cat -n src/shared/lib/handleApiError.tsRepository: 2025-snowCode/snowCode-Client
Length of output: 1222
🏁 Script executed:
find src -name "main.tsx" -o -name "index.tsx" -o -name "index.ts" | grep -v node_modulesRepository: 2025-snowCode/snowCode-Client
Length of output: 395
🏁 Script executed:
cat -n src/widgets/private-route/ui/PrivateRoute.tsxRepository: 2025-snowCode/snowCode-Client
Length of output: 817
🏁 Script executed:
cat -n src/main.tsxRepository: 2025-snowCode/snowCode-Client
Length of output: 591
에러 경계(Error Boundary) 추가 필요합니다.
현재 Suspense는 로딩 상태만 처리하는데, 하위의 모든 useSuspenseQuery가 던지는 에러는 잡지 못합니다. 코드베이스에 에러 경계가 없으므로, 쿼리 실패 시 레이아웃 전체가 깨질 가능성이 있습니다.
개선 방법:
- Layout을 ErrorBoundary로 감싸거나, App.tsx의
<Route element={<Layout />}>에errorElement속성 추가 - 또는 React Query의
throwOnError설정과 함께 에러 처리 전략 수립
React Suspense 공식 문서와 TanStack Query Suspense 가이드를 참고하여 에러 처리 구조를 정립하면 좋겠습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/shared/ui/Layout.tsx` around lines 8 - 20, Layout currently wraps
children with Suspense (Suspense, Loading, Outlet, Header) but has no
ErrorBoundary, so errors from useSuspenseQuery will crash the UI; fix by adding
an error boundary around the Suspense content: implement or import an
ErrorBoundary component and wrap the existing Suspense (or the entire Layout
return) with it, or alternatively add an errorElement for the route that renders
<Layout /> in App.tsx; ensure the ErrorBoundary renders a fallback UI and
logs/handles the thrown error from queries so useSuspenseQuery errors are
caught.
| <div className='inset-0 fixed flex-center flex-col gap'> | ||
| <LoadingImg className='w-50 h-50 animate-bounce' /> | ||
| <span className='-mt-8 ml-3.5 text-base text-primary-black font-semibold animate-pulse'> | ||
| 로딩 중... | ||
| </span> | ||
| </div> |
There was a problem hiding this comment.
재사용 로딩 컴포넌트가 전면 오버레이로 고정돼 있습니다.
fixed inset-0 때문에 부분 Suspense fallback에서도 항상 뷰포트 전체를 덮습니다. PR 설명대로 문제 목록 영역에도 Suspense를 붙였다면, 리스트만 기다릴 때도 페이지 전체가 가려지는 회귀가 납니다. 전역용 PageLoading과 영역용 SectionLoading을 분리하거나 fullscreen prop으로 위치 전략을 나눠 주세요. React Suspense fallback 패턴 공식 문서도 같이 확인해 두면 좋겠습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/shared/ui/Loading.tsx` around lines 5 - 10, The Loading component
currently uses fixed inset-0 which forces a full-viewport overlay; change
Loading (in src/shared/ui/Loading.tsx) to accept a boolean prop like fullscreen
(default true) and conditionally apply the positioning classes: when fullscreen
is true keep 'fixed inset-0 flex-center', when false render a non-fixed inline
container (e.g. only 'flex-center' or allow parent sizing) so Suspense fallbacks
can be localized; ensure the prop is typed in the component signature and that
LoadingImg and the inner span styling remain unchanged, and update usages (or
add separate PageLoading / SectionLoading wrappers) to pass fullscreen
appropriately.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/pages/manage-assignment/AssignmentManagePage.tsx`:
- Line 32: The hook signature mismatch: callers (AssignmentManagePage and
AssignmentSelectPage) are using non-null assertions on selectedCourseId from
useCourseFilter (number | null) to call useAssignmentList(number), so change
useAssignmentList to accept selectedCourseId: number | null and keep its
internal enabled: !!selectedCourseId guard; then remove the non-null assertions
at the call sites (AssignmentManagePage and AssignmentSelectPage) so types align
and the enabled flag clearly controls query execution.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: fdcf62e3-abcf-47f0-960e-895b21a00cf6
📒 Files selected for processing (1)
src/pages/manage-assignment/AssignmentManagePage.tsx
| selectedCourseLabel, | ||
| } = useCourseFilter(courses); | ||
| const assignmentList = useAssignmentList(selectedCourseId); | ||
| const assignmentList = useAssignmentList(selectedCourseId!); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 목적: selectedCourseId nullability와 useAssignmentList 시그니처/호출부 정합성 확인 (read-only)
set -e
echo "1) useAssignmentList 선언 확인"
rg -n --type=ts --type=tsx -C3 'export const useAssignmentList\s*=\s*\(selectedCourseId:\s*'
echo
echo "2) useCourseFilter의 selectedCourseId 타입 확인"
rg -n --type=ts --type=tsx -C4 'useState<number \| null>\(null\)|selectedCourseId'
echo
echo "3) non-null assertion 호출부 확인"
rg -n --type=ts --type=tsx -C2 'useAssignmentList\s*\(\s*selectedCourseId!\s*\)'
echo
echo "4) 전체 호출부 패턴 확인"
rg -n --type=ts --type=tsx -C2 '\buseAssignmentList\s*\('Repository: 2025-snowCode/snowCode-Client
Length of output: 144
🏁 Script executed:
#!/bin/bash
# 수정된 스크립트: tsx 타입 제외하고 ts 파일만 검색, 또는 rg에서 타입 옵션 제거
echo "=== 1) useAssignmentList 선언 확인 ==="
rg -n 'export const useAssignmentList' -A 3
echo
echo "=== 2) useCourseFilter 반환 타입 확인 ==="
rg -n 'useCourseFilter' -B 2 -A 5 src/features/course/
echo
echo "=== 3) selectedCourseId! 호출부 확인 (AssignmentManagePage) ==="
rg -n 'selectedCourseId!' src/pages/manage-assignment/
echo
echo "=== 4) useAssignmentList 호출 패턴 전체 ==="
rg -n 'useAssignmentList\s*\(' -B 1 -A 1
echo
echo "=== 5) useAssignmentList 내부 enabled 가드 확인 ==="
rg -n 'enabled:' src/features/assignment/ -B 1 -A 1Repository: 2025-snowCode/snowCode-Client
Length of output: 2758
selectedCourseId! 단언 대신 훅 시그니처로 타입 경계를 맞춰 주세요.
useCourseFilter는 number | null을 반환하지만, useAssignmentList는 number 타입만 받습니다. 현재는 호출부(AssignmentManagePage.tsx:32, AssignmentSelectPage.tsx:33)에서 non-null assertion으로 타입 체크를 무시하고 있고, 내부의 enabled: !!selectedCourseId 가드가 우연히 런타임 에러를 방지하고 있습니다.
더 나은 방법은 useAssignmentList의 시그니처를 selectedCourseId: number | null로 수정하는 것입니다. 그러면:
- 타입-런타임 불일치가 해소되어 회귀 위험이 줄어듭니다
- 호출부의 단언을 제거할 수 있습니다
- 훅 내부의
enabled가드가 더 명확한 의도를 드러냅니다
🔧 제안 diff
--- a/src/features/assignment/filter-assignment/lib/useAssignmentList.ts
+++ b/src/features/assignment/filter-assignment/lib/useAssignmentList.ts
@@ -6,7 +6,7 @@ import {assignmentQueries} from '@/features/assignment/query-keys';
import {TAssignment} from '@/types/assignment.types';
-export const useAssignmentList = (selectedCourseId: number): TAssignment[] => {
+export const useAssignmentList = (selectedCourseId: number | null): TAssignment[] => {
const {data: allAssignments} = useSuspenseQuery(
assignmentQueries.getAllAssignments()
);--- a/src/pages/manage-assignment/AssignmentManagePage.tsx
+++ b/src/pages/manage-assignment/AssignmentManagePage.tsx
@@ -29,7 +29,7 @@ export const AssignmentManagePage: React.FC = () => {
courses,
} = useCourseFilter(courses);
- const assignmentList = useAssignmentList(selectedCourseId!);
+ const assignmentList = useAssignmentList(selectedCourseId);(동일하게 AssignmentSelectPage.tsx:33도 수정)
참고 자료:
- TypeScript non-null assertion: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#non-null-assertion-operator-postfix-
- TanStack Query enabled 옵션: https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/manage-assignment/AssignmentManagePage.tsx` at line 32, The hook
signature mismatch: callers (AssignmentManagePage and AssignmentSelectPage) are
using non-null assertions on selectedCourseId from useCourseFilter (number |
null) to call useAssignmentList(number), so change useAssignmentList to accept
selectedCourseId: number | null and keep its internal enabled:
!!selectedCourseId guard; then remove the non-null assertions at the call sites
(AssignmentManagePage and AssignmentSelectPage) so types align and the enabled
flag clearly controls query execution.
⚙️ Related ISSUE Number
📄 Work Description
Layout,AssignmentSubmitLayout에 Suspense 경계 적용useQuery→useSuspenseQuery전환 (useAssignmentList,UnitLayout)저장 중...) 표시📷 Screenshot
💬 To Reviewers
🔗 Reference