[FEAT] 에러/로딩/온보딩 상태 페이지 Lottie 애니메이션 적용#131
Conversation
- 상태 페이지 애니메이션 렌더링을 위해 lottie-react 패키지를 추가했습니다
- 공통 LottiePlayer, StatusScreen 컴포넌트를 추가했습니다 - global-error, 500, 404 페이지에 상태별 Lottie 애니메이션과 안내 문구를 적용했습니다 - loading 페이지를 로띠 애니메이션만 중앙에 노출하도록 구성했습니다 - 404 응답을 유도하기 위한 [...rest] catch-all 라우트를 추가했습니다 - 500/404/전역 에러 문구가 각각 올바른 번역 네임스페이스를 참조하도록 수정했습니다
- 온보딩 페이지에 LottiePlayer로 온보딩 애니메이션을 노출했습니다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough전역 에러, 500 에러, 404, 로딩, 온보딩, 캐치올 라우트에 공통 Changes상태 페이지 Lottie 적용
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant ErrorPage as "ErrorPage / GlobalErrorContent"
participant Sentry
participant StatusScreen
participant LottiePlayer
User->>ErrorPage: 라우트에서 에러 발생
ErrorPage->>Sentry: captureException(error)
ErrorPage->>StatusScreen: title/description/action 전달
StatusScreen->>LottiePlayer: lottieSrc, ariaLabel 전달
LottiePlayer->>LottiePlayer: fetch(src)로 애니메이션 로드
LottiePlayer-->>StatusScreen: animationData 렌더링
StatusScreen-->>User: 상태 화면 표시 (홈 버튼 포함)
Possibly related PRs
Suggested labels: Suggested reviewers: 리뷰 한마디: Lottie 애니메이션까지 챙기신 센스, 에러 화면도 이제 시각적으로 우아하게 실패하네요 ✨ 다만 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 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 |
Timo Performance ReportBundle Size — timo-web
Lighthouse — timo-web
Image Optimization — timo-web
측정 커밋: |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/timo-web/app/`[locale]/loading.tsx:
- Around line 5-7: `LottiePlayer` is rendered without an accessible name, so
update the loading UI in the `loading.tsx` component to pass a meaningful
`ariaLabel` alongside the existing `src` and `className` props. Use the
`LottiePlayer` usage in this screen to ensure it still renders as `role="img"`
but now has a screen-reader-friendly label that describes the loading state.
In `@apps/timo-web/app/`[locale]/onboarding/page.tsx:
- Around line 5-9: The onboarding page animation in the LottiePlayer usage is
hard-coded for desktop and overflows on small screens, and it also lacks an
accessibility decision. Update the LottiePlayer configuration to use responsive
positioning/sizing in the onboarding page component (for example by adjusting
the current absolute placement across breakpoints or making the desktop-only
intent explicit), and add an appropriate ariaLabel if the animation conveys
meaning or hide it from assistive tech if it is decorative.
In `@apps/timo-web/app/global-error.tsx`:
- Around line 50-58: 중복된 상태 화면 액션 버튼(Link) 구현을 공통 컴포넌트로 추출하세요. global-error.tsx의
action, error.tsx의 action, not-found.tsx의 버튼이 동일한 스타일과 거의 같은 구조를 반복하므로,
StatusActionButton 같은 재사용 컴포넌트를 만들어 href와 label만 받도록 정리하고 세 파일에서 이를 사용하세요. 또한
global-error.tsx와 error.tsx의 wrapping div와 not-found.tsx의 단일 Link 구조 차이를 함께 정리해,
각 화면의 action 렌더링 형태가 일관되게 맞도록 수정하세요.
In `@apps/timo-web/components/lottie/LottiePlayer.tsx`:
- Around line 22-33: In LottiePlayer’s fetch effect, the current response
handling passes every resolved fetch directly to response.json(), which allows
404/500 responses to be treated as valid animation data. Update the useEffect
flow to check response.ok before parsing, and throw an error when it is false so
the failure is routed into the existing catch path. Keep the fix localized to
the fetch chain in LottiePlayer so only valid animation JSON reaches
setAnimationData.
In `@apps/timo-web/messages/ko.json`:
- Around line 44-57: Fix the Korean localization text in ko.json by correcting
the typos in Error.description (“잠시후” to “잠시 후” and “주새요” to “주세요”) and making
punctuation consistent by adding a period to NotFound.description; update the
affected entries in the Error and NotFound message objects so the wording and
sentence endings match the rest of the translations.
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: 802cb16c-4a85-4edd-b90e-0e30c119f8d0
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (16)
apps/timo-web/app/[locale]/[...rest]/page.tsxapps/timo-web/app/[locale]/error.tsxapps/timo-web/app/[locale]/loading.tsxapps/timo-web/app/[locale]/not-found.tsxapps/timo-web/app/[locale]/onboarding/page.tsxapps/timo-web/app/global-error.tsxapps/timo-web/components/lottie/LottiePlayer.tsxapps/timo-web/components/status-screen/StatusScreen.tsxapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.jsonapps/timo-web/package.jsonapps/timo-web/public/lottie/404.jsonapps/timo-web/public/lottie/500.jsonapps/timo-web/public/lottie/error.jsonapps/timo-web/public/lottie/loading.jsonapps/timo-web/public/lottie/onboarding.json
| <div className="relative h-screen w-full"> | ||
| <LottiePlayer | ||
| src="/lottie/onboarding.json" | ||
| className="absolute top-1/2 left-[197px] w-[500px] -translate-y-1/2" | ||
| /> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 온보딩 페이지가 데스크톱 전용인지, 반응형 대응이 필요한지 확인합니다.
# 1. 미디어 쿼리나 반응형 클래스 사용 여부 검색
rg -n 'md:|lg:|sm:|xl:' apps/timo-web/app/[locale]/onboarding/
# 2. 다른 페이지의 반응형 패턴 확인
rg -n 'left-\[|w-\[500' apps/timo-web/app/ --type=ts
# 3. Viewport meta 태그 또는 반응형 설정 확인
rg -n 'viewport|max-width|min-width' apps/timo-web/app/[locale]/layout.tsxRepository: Team-Timo/Timo-client
Length of output: 285
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- onboarding page ---'
sed -n '1,200p' apps/timo-web/app/[locale]/onboarding/page.tsx | cat -n
echo
echo '--- find LottiePlayer definition/usages ---'
fd -a 'LottiePlayer' apps/timo-web components | sed -n '1,120p' || true
rg -n "export .*LottiePlayer|function LottiePlayer|const LottiePlayer|ariaLabel|aria-label" apps/timo-web components -g '!**/node_modules/**' | sed -n '1,200p' || true
echo
echo '--- list lottie component files ---'
fd -a '' components/lottie apps/timo-web/components/lottie 2>/dev/null | sed -n '1,200p' || trueRepository: Team-Timo/Timo-client
Length of output: 1763
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- onboarding-related files ---'
fd -a 'onboarding' apps/timo-web | sed -n '1,200p'
echo
echo '--- onboarding page references ---'
rg -n "onboarding" apps/timo-web -g '!**/node_modules/**' | sed -n '1,200p'
echo
echo '--- LottiePlayer definition ---'
sed -n '1,220p' apps/timo-web/components/lottie/LottiePlayer.tsx | cat -n
echo
echo '--- relevant layout/page sizing hints ---'
sed -n '1,220p' apps/timo-web/app/[locale]/layout.tsx | cat -nRepository: Team-Timo/Timo-client
Length of output: 3587
온보딩 애니메이션에 반응형/접근성 레일을 깔아주세요
레이아웃 뼈대는 깔끔합니다. 다만 left-[197px] + w-[500px] 조합은 375px 폭에서 오른쪽으로 크게 넘쳐 보입니다. sm/md에서 위치·크기를 조정하거나, 정말 데스크톱 전용이면 의도를 코드에 드러내 주세요.
또한 LottiePlayer는 ariaLabel을 지원하므로, 의미 있는 안내라면 라벨을 넣고 장식용이라면 숨김 처리하는 쪽이 맞습니다.
참고: MDN ARIA, Next.js Accessibility
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/timo-web/app/`[locale]/onboarding/page.tsx around lines 5 - 9, The
onboarding page animation in the LottiePlayer usage is hard-coded for desktop
and overflows on small screens, and it also lacks an accessibility decision.
Update the LottiePlayer configuration to use responsive positioning/sizing in
the onboarding page component (for example by adjusting the current absolute
placement across breakpoints or making the desktop-only intent explicit), and
add an appropriate ariaLabel if the animation conveys meaning or hide it from
assistive tech if it is decorative.
| action={ | ||
| <div className="flex items-center gap-3"> | ||
| <Link | ||
| href="/" | ||
| className="typo-headline-m-16 rounded-[8px] border border-gray-500 px-4 py-2.5 text-gray-900" | ||
| > | ||
| {t("homeButton")} | ||
| </Link> | ||
| </div> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
액션 버튼 코드 중복 — 공통 컴포넌트 추출을 제안합니다.
global-error.tsx, error.tsx(26-34행), not-found.tsx(14-21행) 세 파일에서 동일한 Link 버튼(typo-headline-m-16 rounded-[8px] border border-gray-500 px-4 py-2.5 text-gray-900)이 반복됩니다. not-found.tsx는 wrapping <div> 없이 Link만 전달하고, 나머지 두 파일은 <div className="flex items-center gap-3">으로 감싸고 있어 구조 불일치도 있습니다.
공통 StatusActionButton 컴포넌트를 추출하면 중복 제거와 일관성 확보를 동시에 잡을 수 있습니다.
♻️ 공통 StatusActionButton 컴포넌트 추출 제안
// components/status-screen/StatusActionButton.tsx
import { Link } from "`@/i18n/navigation`";
interface StatusActionButtonProps {
label: string;
href?: string;
}
export const StatusActionButton = ({
label,
href = "/",
}: StatusActionButtonProps) => (
<Link
href={href}
className="typo-headline-m-16 rounded-[8px] border border-gray-500 px-4 py-2.5 text-gray-900"
>
{label}
</Link>
);각 파일에서의 사용:
- action={
- <div className="flex items-center gap-3">
- <Link
- href="/"
- className="typo-headline-m-16 rounded-[8px] border border-gray-500 px-4 py-2.5 text-gray-900"
- >
- {t("homeButton")}
- </Link>
- </div>
- }
+ action={<StatusActionButton label={t("homeButton")} />}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/timo-web/app/global-error.tsx` around lines 50 - 58, 중복된 상태 화면 액션
버튼(Link) 구현을 공통 컴포넌트로 추출하세요. global-error.tsx의 action, error.tsx의 action,
not-found.tsx의 버튼이 동일한 스타일과 거의 같은 구조를 반복하므로, StatusActionButton 같은 재사용 컴포넌트를 만들어
href와 label만 받도록 정리하고 세 파일에서 이를 사용하세요. 또한 global-error.tsx와 error.tsx의 wrapping
div와 not-found.tsx의 단일 Link 구조 차이를 함께 정리해, 각 화면의 action 렌더링 형태가 일관되게 맞도록 수정하세요.
| useEffect(() => { | ||
| const controller = new AbortController(); | ||
|
|
||
| fetch(src, { signal: controller.signal }) | ||
| .then((response) => response.json()) | ||
| .then(setAnimationData) | ||
| .catch(() => { | ||
| // 애니메이션 로드 실패 시 컨테이너만 비운 상태로 유지 (레이아웃 시프트 방지) | ||
| }); | ||
|
|
||
| return () => controller.abort(); | ||
| }, [src]); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# lottie-react v2.4.1의 Lottie 컴포넌트가 animationData, loop, role, aria-label props를 지원하는지 확인
rg -n "animationData|aria-label|role" node_modules/lottie-react/dist/*.d.ts 2>/dev/null || echo "Type definitions not found at expected path"
fd "lottie-react" node_modules --type d --max-depth 3Repository: Team-Timo/Timo-client
Length of output: 203
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file =="
sed -n '1,220p' apps/timo-web/components/lottie/LottiePlayer.tsx
echo
echo "== lottie-react references =="
rg -n "from 'lottie-react'|from \"lottie-react\"|<Lottie" apps/timo-web -S
echo
echo "== package refs =="
rg -n "\"lottie-react\"|lottie-react" package.json apps/timo-web/package.json pnpm-lock.yaml package-lock.json yarn.lock 2>/dev/null || trueRepository: Team-Timo/Timo-client
Length of output: 1949
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== LottiePlayer.tsx =="
cat -n apps/timo-web/components/lottie/LottiePlayer.tsx | sed -n '1,220p'
echo
echo "== search for response.ok around fetches =="
rg -n "response\.ok|fetch\(" apps/timo-web -SRepository: Team-Timo/Timo-client
Length of output: 1680
🌐 Web query:
lottie-react animationData prop invalid object behavior response.ok fetch JSON 404
💡 Result:
In lottie-react, the animationData prop expects a parsed JSON object [1][2]. It does not support passing a URL string directly [3][1]. If you are fetching your animation data, you must perform the fetch operation manually and pass the resulting JSON object to the component [1]. When fetching animation data, it is critical to handle HTTP errors correctly because the Fetch API does not automatically throw an error for status codes like 404 or 500 [4]. Behavior with invalid objects or 404s: 1. Fetch API 404 Behavior: If a request results in a 404 status, the fetch promise will still resolve as successful [4]. If you blindly call response.json on a 404 response, the parser will likely fail with a SyntaxError because it will attempt to parse the server's HTML error page (which usually starts with <) as JSON [4]. 2. Invalid animationData: If you pass an invalid object (e.g., malformed JSON, an empty object, or a string instead of an object) to the animationData prop, the Lottie player will typically fail to initialize the animation, often resulting in no output or silent failures [1][5]. Recommended Pattern: Always check the response.ok property before attempting to parse the response [4][6]. const fetchAnimation = async (url) => { const response = await fetch(url); if (!response.ok) { throw new Error(Failed to load animation: ${response.status} ${response.statusText}); } const data = await response.json; return data; }; In your React component, use state to handle the asynchronous loading of this data, and conditionally render the Lottie component only after the data has been successfully fetched and parsed [1]. If the fetch fails, you should catch the error and handle it in your state to avoid passing undefined or invalid data to the Lottie component [1].
Citations:
- 1: https://www.sanity.io/answers/lottie-animations-error-please-update-the-following-components-lottie
- 2: https://gamote.github.io/lottie-react
- 3: Issue with next js 13 Gamote/lottie-react#91
- 4: https://jsmonkey.net/fetch-api-youre-probably-using-it-wrong/
- 5: Animation not loading via JSON fetch airbnb/lottie-web#2353
- 6: https://stackoverflow.com/questions/60839491/how-do-i-catch-and-parse-errors-properly-in-my-react-form-submission-handler
response.ok를 먼저 확인하세요.
fetch()는 404/500에서도 성공으로 resolve되므로, 비정상 응답까지 json()으로 넘기면 잘못된 animationData가 <Lottie>에 들어갈 수 있습니다. response.ok가 아니면 예외를 던져 현재 catch로 보내는 게 안전합니다.
MDN: Response.ok, fetch()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/timo-web/components/lottie/LottiePlayer.tsx` around lines 22 - 33, In
LottiePlayer’s fetch effect, the current response handling passes every resolved
fetch directly to response.json(), which allows 404/500 responses to be treated
as valid animation data. Update the useEffect flow to check response.ok before
parsing, and throw an error when it is false so the failure is routed into the
existing catch path. Keep the fix localized to the fetch chain in LottiePlayer
so only valid animation JSON reaches setAnimationData.
ehye1
left a comment
There was a problem hiding this comment.
로띠 파일 어떻게 넣는지 궁금했는데 pr설명이랑 코드 보고 배워갑니다💥
수고하셨습니다!!!💨🫡
| useEffect(() => { | ||
| const controller = new AbortController(); | ||
|
|
||
| fetch(src, { signal: controller.signal }) |
There was a problem hiding this comment.
lottie json 파일을 어떻게 가져오나 궁금했는데 fetch로 가져오게 되면 필요할 때만 가져올 수 있어서 번들 크기를 줄일 수 있겠네요!
| .then((response) => response.json()) | ||
| .then(setAnimationData) | ||
| .catch(() => { | ||
| // 애니메이션 로드 실패 시 컨테이너만 비운 상태로 유지 (레이아웃 시프트 방지) |
There was a problem hiding this comment.
lottie 파일이 렌더링 되기 전까지 CLS가 발생하는 것 같은데 나중에 높이 고정해놓거나 placeholder 설정해두면 좋을 것 같아요!
jjangminii
left a comment
There was a problem hiding this comment.
이제 500,404에러도 이쁘겠네요~
간단한 코멘트라 어푸 먼저 해드릴게요-! 코멘트 내용 확인해주세요!
| }, | ||
| "Error": { | ||
| "title": "잠시 문제가 발생했어요.", | ||
| "description": "더 나은 경험을 위해 잠시 점검 중입니다. 잠시후 다시 이용해 주새요.", |
| export default function Loading() { | ||
| return ( | ||
| <div className="flex min-h-screen w-full items-center justify-center"> | ||
| <LottiePlayer src="/lottie/loading.json" className="w-[143px]" /> |
| fetch(src, { signal: controller.signal }) | ||
| .then((response) => response.json()) | ||
| .then(setAnimationData) | ||
| .catch(() => { | ||
| // 애니메이션 로드 실패 시 컨테이너만 비운 상태로 유지 (레이아웃 시프트 방지) |
There was a problem hiding this comment.
| fetch(src, { signal: controller.signal }) | |
| .then((response) => response.json()) | |
| .then(setAnimationData) | |
| .catch(() => { | |
| // 애니메이션 로드 실패 시 컨테이너만 비운 상태로 유지 (레이아웃 시프트 방지) | |
| fetch(src, { signal: controller.signal }) | |
| .then((response) => { | |
| if (!response.ok) throw new Error(`Failed to load: ${response.status}`); | |
| return response.json(); | |
| }) | |
| .then(setAnimationData) | |
| .catch(() => { | |
| // 애니메이션 로드 실패 시 컨테이너만 비운 상태로 유지 (레이아웃 시프트 방지) | |
| }); |
fetch는 404/500에서도 성공으로 resolve되기 때문에, response.ok 체크 없이 바로 .json()을 호출하면 에러 페이지의 HTML이 animationData로 들어가서 Lottie가 깨질 수 있을 것 같아요!
response.ok가 false일 때 에러를 throw하면 기존 .catch(() => {}) 블록으로 떨어져서 animationData가 null인 상태로 유지되고, Lottie 컴포넌트가 렌더링되지 않아요. 기존 catch 로직을 재활용하면서 최소한의 변경으로 방어할 수 있을 것 같아요!
민아님은 어떻게 생각하시나요?
ISSUE 🔗
close #130
What is this PR? 🔍
전역 에러(500), 404, 로딩, 온보딩 화면에 공통
StatusScreen/LottiePlayer컴포넌트를 도입해 Lottie 애니메이션 기반 상태 화면으로 정비했습니다.배경
global-error.tsx만 최소한으로 존재했고, 404·500 전용 페이지와 로딩 화면은 없었으며 상태별 안내 문구도 별도로 관리되지 않았습니다.StatusScreen)와 Lottie 렌더러(LottiePlayer)를 만들고, 각 상태(500/404/전역 에러/로딩/온보딩)마다 해당 Lottie 애니메이션과 i18n 문구를 매핑했습니다.LottiePlayer / StatusScreen 공통 컴포넌트
lottie-react기반LottiePlayer와, 타이틀·설명·액션 버튼을 배치하는StatusScreen을 새로 추가했습니다.LottiePlayer는src로 받은 경로를fetch해 JSON 애니메이션 데이터를 상태로 들고 있다가 로드되면Lottie컴포넌트를 렌더링합니다. 로드 실패 시에는 컨테이너만 비워 두어 레이아웃 시프트를 방지합니다.StatusScreen은lottieSrc/title/description/action을 props로 받아 중앙 정렬 레이아웃을 구성합니다.상태 페이지 (500 / 404 / 전역 에러 / 로딩)
[locale]/error.tsx(500),[locale]/not-found.tsx(404),global-error.tsx(전역 크래시),[locale]/loading.tsx를 새로 추가/정비하고, 404 유도를 위한[locale]/[...rest]catch-all 라우트를 추가했습니다.StatusScreen에 상태별lottieSrc(/lottie/500.json,404.json,error.json)와 홈으로 이동하는action을 전달합니다.loading.tsx는 텍스트 없이LottiePlayer만 화면 중앙에 배치했습니다.global-error.tsx는<html>/<body>를 직접 렌더링해야 하는 Next.js 제약 때문에NextIntlClientProvider로 메시지를 직접 주입하고, 최초 렌더 시 URL 세그먼트에서 locale을 추출해 폰트·언어를 맞췄습니다. 또한 500/404/전역 에러 화면이 각각 별도로 정의된Error/NotFound/GlobalError번역 네임스페이스 대신 전부Error를 참조하고 있던 것을 발견해, 상태별로 올바른 네임스페이스를 참조하도록 수정했습니다.global-error.tsx는 클라이언트 컴포넌트로 유지되며Sentry.captureException으로 에러를 보고합니다.온보딩 페이지
LottiePlayer로 온보딩 애니메이션(/lottie/onboarding.json)을 노출했습니다.top-1/2 left-[197px])로 애니메이션을 배치했습니다.To Reviewers
global-error.tsx는 Next.js 특성상<html>/<body>를 직접 감싸고 locale을 클라이언트에서 뒤늦게 결정하는 구조라 최초 렌더에는 기본 locale(routing.defaultLocale)로 잠깐 보였다가 바뀝니다. 이 전제가 맞는지 확인 부탁드립니다.Screenshot 📷
Test Checklist ✔
pnpm check-types통과pnpm lint통과