Skip to content

fix&refactor: 로딩 상태 스켈레톤 ui#130

Merged
skyblue1232 merged 1 commit intodevelopfrom
fix/#119/qa-refactor
Nov 15, 2025
Merged

fix&refactor: 로딩 상태 스켈레톤 ui#130
skyblue1232 merged 1 commit intodevelopfrom
fix/#119/qa-refactor

Conversation

@jjangminii
Copy link
Copy Markdown
Contributor

@jjangminii jjangminii commented Nov 15, 2025

🔥 작업 내용

  • 노드페이지 로딩 상태 스켈레톤 ui
    • 1초 이상 로딩일 때 전체 페이지 스켈레톤 뜨게했고
    • 이미지만 늦게 도착하는거같길래 이미지만 따로 추가적으로 스켈레톤ui 처리 해둿습니다

🤔 추후 작업 사항

  • ex) 소셜 로그인 연동 (카카오, 구글)

🔗 이슈

  • close #

PR Point (To Reviewer)

  • ex) 로그인 입력 검증 로직 적절한지 확인 부탁드립니다.

📸 피그마 스크린샷 or 기능 GIF

(작업 내역 스크린샷)
image

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 이미지 로딩 중 스켈레톤 플레이스홀더 추가로 더 나은 로딩 경험 제공
    • 1초 지연 후 스켈레톤 표시로 깜빡임 방지
    • 이미지 로딩 완료 시 페이드 트랜지션 효과 적용
    • 이미지 로드 완료 후에만 스탬프 버튼 표시
  • Bug Fixes

    • 미로그인 사용자의 스탬프 기능 접근 제한

@jjangminii jjangminii self-assigned this Nov 15, 2025
@jjangminii jjangminii linked an issue Nov 15, 2025 that may be closed by this pull request
@jjangminii jjangminii requested a review from KongMezu as a code owner November 15, 2025 03:43
@vercel
Copy link
Copy Markdown

vercel bot commented Nov 15, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
af-fe Ready Ready Preview Comment Nov 15, 2025 3:43am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Nov 15, 2025

Walkthrough

이미지 로딩 UI/UX를 개선하기 위해 스켈레톤 로더 컴포넌트를 추가하고, 1초 지연 후 스켈레톤을 표시하며, 이미지 로딩 상태를 관리합니다. 또한 스탬프 클릭 핸들러에 로그인 가드를 추가하고 디버그 로그를 제거합니다.

Changes

Cohort / File(s) 요약
스켈레톤 컴포넌트
src/shared/components/skeleton/Skeleton.tsx
새로운 스켈레톤 로더 컴포넌트 추가. className prop을 받아 animate-pulse bg-gray-200 rounded-md 스타일을 적용하며, cn 유틸리티로 클래스 병합
장소 상세 페이지 이미지 로딩 개선
src/pages/main/node/[placeId].tsx
imageLoaded, showSkeleton 상태 추가; 1초 지연 후 스켈레톤 표시하는 useEffect 추가; 이미지 페이드 전환 구현; 스탐프 버튼을 이미지 로드 완료 후에만 렌더링; 스탬프 클릭 핸들러에 로그인 가드 추가; 디버그 로그 제거

Sequence Diagram

sequenceDiagram
    participant User
    participant UI
    participant Image
    
    User->>UI: 페이지 방문
    activate UI
    UI->>UI: isLoading = true 설정
    Note over UI: 0초: 아무것도 표시 안 함
    
    par 1초 대기
        UI->>UI: setTimeout 1000ms
    end
    
    Note over UI: 1초 경과<br/>showSkeleton = true
    UI->>UI: 스켈레톤 표시
    
    UI->>Image: 이미지 로드 시작
    Image-->>UI: 이미지 로드 중...
    
    Image-->>UI: onLoadingComplete 콜백
    activate UI
    UI->>UI: imageLoaded = true
    UI->>UI: 스켈레톤 숨김 (opacity 전환)
    UI->>UI: 이미지 표시
    UI->>UI: 스탐프 버튼 활성화
    deactivate UI
    deactivate UI
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

  • 확인 필요 항목:
    • useEffect 타이머 정리(cleanup) 로직이 올바르게 구현되었는지 확인
    • 스켈레톤 지연 로직이 의도한 대로 작동하는지 확인 (1초 지연 후에만 표시)
    • 이미지 페이드 트랜지션 CSS/Tailwind 클래스 검증
    • 로그인 가드 이후의 스탬프 클릭 흐름이 다른 PR(FE#122)과 올바르게 통합되었는지 확인

Possibly related PRs

  • FE#122: 같은 파일에서 스탐프 클릭 플로우 수정 (API 호출, 로그인 팝업, 네비게이션 구현) - 본 PR의 로그인 가드와 연관

Suggested labels

fix

Suggested reviewers

  • KongMezu

Poem

🐰✨ 스켈레톤 춤을 추며,
이미지 나타나길 기다리네요.
1초 지연은 사용자 경험의 우아함,
로그인 가드로 안전을 더하고,
페이드 전환과 함께 모든 것이 완벽해져요! 🎨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive PR 설명이 대부분 작성되었으나 일부 필수 섹션이 미완성 상태입니다. 이슈 번호를 기입하고, PR Point 섹션에 구체적인 검토 요청 사항을 추가해주시기 바랍니다.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Pull request 제목이 변경 사항의 주요 내용을 명확하게 반영하고 있습니다. 로딩 상태에 대한 스켈레톤 UI 추가라는 핵심 변경 사항을 정확하게 설명합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/#119/qa-refactor

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added fix 버그 및 자잘한 오류 수정 refactor 프로덕션 코드 리팩토링업, QA 반영 labels Nov 15, 2025
@github-actions
Copy link
Copy Markdown

🏷️ Labeler has automatically applied labels based on your PR title, branch name, or commit message.
Please verify that they are correct before merging.

1 similar comment
@github-actions
Copy link
Copy Markdown

🏷️ Labeler has automatically applied labels based on your PR title, branch name, or commit message.
Please verify that they are correct before merging.

@github-actions github-actions bot added the comment 필요한 주석 추가 및 변경 label Nov 15, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/shared/components/skeleton/Skeleton.tsx (1)

1-8: 스켈레톤 컴포넌트 구현은 단순·명확해서 좋습니다.

  • cn으로 기본 스타일과 className을 합치는 구조가 재사용하기에도 적절해 보입니다.
  • 타입은 인라인 객체 대신 아래처럼 SkeletonProps 인터페이스로 분리하면 추후 확장 시 가독성이 조금 더 좋아질 수 있습니다 (선택 사항입니다).
-export const Skeleton = ({ className }: { className?: string }) => {
+interface SkeletonProps {
+  className?: string;
+}
+
+export const Skeleton = ({ className }: SkeletonProps) => {
  • 향후 필요하다면 스켈레톤이 완전히 장식용일 경우 aria-hidden='true' 같은 접근성 속성을 추가하는 것도 고려해 볼 만합니다.
src/pages/main/node/[placeId].tsx (2)

1-4: Pages Router에서는 'use client' 지시어가 불필요합니다.

이 프로젝트는 Next.js Pages Router를 사용하고 있어서, src/pages 아래 컴포넌트는 기본적으로 클라이언트 컴포넌트로 동작합니다. 따라서 Line 1의 'use client'; 지시어는 의미가 없고 스타일/린트 규칙에 따라 오히려 혼동을 줄 수 있어 제거하는 편이 좋습니다.

-'use client';
-
-import { useState, useEffect } from 'react';
+import { useState, useEffect } from 'react';

Based on learnings


123-162: 이미지 스켈레톤 + 페이드인 처리, 그리고 스탬프 버튼 조건부 렌더링이 UI/UX 관점에서 잘 구성되어 있습니다.

  • imageLoaded 상태로 이미지 스켈레톤을 제어하고, onLoadingComplete에서 페이드인(opacity + transition-opacity) 처리하는 흐름은 의도대로 동작할 것으로 보입니다.
  • fill + relative w-full h-[256px] 구조도 Next/Image 레이아웃 요구사항에 맞게 잘 짜여 있습니다.
  • 이미지를 다 받은 뒤에만 스탬프 버튼을 렌더링하도록 imageLoaded && (...)로 감싼 것도 UX 상 자연스럽습니다.

다만, 아주 사소한 리팩토링 여지는 있습니다.

  1. imageLoaded 중복 조건
    버튼 자체가 imageLoaded && 조건으로 감싸져 있어서, 클래스 안의 삼항은 사실상 항상 imageLoaded === true입니다.

    {imageLoaded && (
      <button
        className={cn(
          'absolute bottom-0 right-0',
          isCompleted && 'p-[2.5rem]',
          imageLoaded ? 'opacity-100' : 'opacity-0 h-0',
        )}
        ...
      >

    여기서는 단순히 'opacity-100'만 남겨도 동작은 동일합니다.

  •    cn(
    
  •      'absolute bottom-0 right-0',
    
  •      isCompleted && 'p-[2.5rem]',
    
  •      imageLoaded ? 'opacity-100' : 'opacity-0 h-0',
    
  •    )}
    
  •    cn(
    
  •      'absolute bottom-0 right-0 opacity-100',
    
  •      isCompleted && 'p-[2.5rem]',
    
  •    )}
    
    
    
  1. 스켈레톤 클래스 중복(선택 사항)
    Skeleton 컴포넌트 자체가 이미 animate-pulse bg-gray-200 rounded-md를 가지고 있어서, 이미지 위 스켈레톤에 넘기는 클래스 중 일부는 살짝 중복이 될 수 있습니다.

    <Skeleton className='absolute inset-0 w-full h-full rounded-[16px] animate-pulse bg-gradient-to-br from-gray-200 to-gray-100' />

    실제 렌더링에는 문제가 없으니 그대로 두어도 되지만, 필요하다면 rounded-[16px]/bg-gradient-to-br 쪽만 덮어쓰는 형태로 정리하면 클래스가 좀 더 깔끔해질 수 있습니다.

전반적으로 이미지 로딩 상태와 연계된 스켈레톤/버튼 UI는 잘 구성된 것 같고, 위 내용은 전부 선택적인 정리 수준입니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ed6aa0 and bb5d85b.

📒 Files selected for processing (2)
  • src/pages/main/node/[placeId].tsx (7 hunks)
  • src/shared/components/skeleton/Skeleton.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-31T17:34:04.352Z
Learnt from: jjangminii
Repo: geulDa/FE PR: 64
File: src/pages/map/result/components/ResultMap.tsx:3-3
Timestamp: 2025-10-31T17:34:04.352Z
Learning: The geulDa/FE project uses Next.js Pages Router (not App Router), so components should import useRouter from 'next/router' and should not use the 'use client' directive.

Applied to files:

  • src/pages/main/node/[placeId].tsx
🧬 Code graph analysis (2)
src/shared/components/skeleton/Skeleton.tsx (1)
src/shared/lib/utils.ts (1)
  • cn (71-73)
src/pages/main/node/[placeId].tsx (2)
src/shared/components/skeleton/Skeleton.tsx (1)
  • Skeleton (4-8)
src/shared/lib/utils.ts (1)
  • cn (71-73)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-deploy
🔇 Additional comments (2)
src/pages/main/node/[placeId].tsx (2)

69-112: 스탬프 클릭 시 로그인 가드 추가는 좋지만, 하드코딩 좌표는 릴리즈 전에 확인이 필요합니다.

  • !isLoggedIn일 때 바로 로그인 팝업을 띄우고 return하는 가드는 플로우 상 자연스럽고, 불필요한 API 호출도 막아줘서 좋습니다.

  • 다만 body에 들어가는 좌표가 아직 고정값으로 남아 있습니다:

    const body = {
      latitude: 37.48585193654532,
      longitude: 126.80355242431538,
      // 실제 위치 사용 시:
      // latitude: pos.coords.latitude,
      // longitude: pos.coords.longitude,
    };

    QA 용도라면 괜찮지만, 운영 배포 전에는 실제 위치를 사용할 것인지, 혹은 특정 기준 좌표를 쓸 것인지 확정하는 편이 좋습니다. 실제 위치를 쓸 계획이라면, 아래처럼 주석 부분을 되살리고 하드코딩 값을 제거하는 방향을 추천합니다.

  •    const body = {
    
  •      latitude: 37.48585193654532,
    
  •      longitude: 126.80355242431538,
    
  •      // 실제 위치 사용 시:
    
  •      // latitude: pos.coords.latitude,
    
  •      // longitude: pos.coords.longitude,
    
  •    };
    
  •    const body = {
    
  •      latitude: pos.coords.latitude,
    
  •      longitude: pos.coords.longitude,
    
  •    };
    
    
    
  • 에러 처리(onError, 위치 정보 실패 콜백)도 잘 연결돼 있어서, 추가로 로그 수준/로깅 방식만 팀 규칙에 맞는지만 확인해 보시면 될 것 같습니다.

174-190: 로그인/에러 팝업 플로우는 기존 UX를 해치지 않으면서 명확합니다.

  • 로그인 팝업에서 바로 /auth로 보내는 흐름과, 위치 확인 실패 시 별도의 에러 팝업을 띄워주는 구조가 명확하고 단순합니다.
  • 주석으로 “팝업 영역”을 명시해 둔 것도 추후 유지보수 시 영역 파악에 도움이 될 것 같습니다.

추가로, 팝업이 열렸을 때 백그라운드 스크롤 잠금이나 포커스 트랩 같은 접근성 요구사항이 팀 수준에서 있다면, 공통 PopupSet 컴포넌트 차원에서만 잘 처리되어 있는지 한 번만 확인해 보시면 좋겠습니다.

Comment on lines +27 to +32
// 이미지 로딩 상태
const [imageLoaded, setImageLoaded] = useState(false);

// 스켈레톤 표시 여부 (로딩이 1초 이상일 때만 true)
const [showSkeleton, setShowSkeleton] = useState(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

로딩 딜레이 + 에러 처리 로직에 버그가 있고, 타이머 타입도 안전하지 않습니다.

  1. 로딩 중 에러 메시지가 먼저 떠버리는 문제 (크리티컬)
    현재 흐름:

    if (isLoading && showSkeleton) { /* 스켈레톤 */ }
    
    if (isError || !data)
      return <p>데이터를 불러오지 못했습니다 😢</p>;

    초기 요청 시 보통 isLoading === true, data === undefined, showSkeleton === false입니다.
    이 상태에서는 스켈레톤은 안 나오고, !data 조건 때문에 곧바로 “데이터를 불러오지 못했습니다 😢” 에러 문구가 노출됩니다. 로딩이 정상적으로 진행되는 동안에도 잠깐 에러 화면이 깜빡일 수 있는 버그입니다.

    제안 수정: 로딩이 끝난 이후에만 !data를 에러로 취급하도록 !isLoading을 조건에 추가하는 편이 안전합니다.

  • if (isLoading && showSkeleton) {
  • if (isLoading && showSkeleton) {
    return (
    {/* 스켈레톤 UI */}
    );
    }
  • if (isError || !data)
  • if (!isLoading && (isError || !data))
    return

    데이터를 불러오지 못했습니다 😢

    ;
    
    
  1. timer 타입 및 초기화 문제 (TS 컴파일 에러 가능성)
    useEffect 안에서:

    let timer: NodeJS.Timeout;
    if (isLoading) {
      timer = setTimeout(...);
    }
    return () => clearTimeout(timer);

    와 같이 작성되면, TypeScript에서는 isLoading === false인 코드 경로에서 timer가 할당되지 않은 채 clearTimeout(timer)가 호출될 수 있다고 판단해 “변수가 할당되기 전에 사용될 수 있다”는 컴파일 에러가 날 수 있습니다.

    브라우저 환경에서는 setTimeout 리턴 타입이 number이기도 해서, NodeJS.Timeout 타입을 강제하는 것보다는 ReturnType<typeof setTimeout>를 사용하는 편이 더 일반적입니다.

    제안 수정:

  • useEffect(() => {
  • let timer: NodeJS.Timeout;
  • if (isLoading) {
  •  timer = setTimeout(() => setShowSkeleton(true), 1000);
    
  • } else {
  •  setShowSkeleton(false);
    
  • }
  • return () => clearTimeout(timer);
  • }, [isLoading]);
  • useEffect(() => {
  • let timer: ReturnType | null = null;
  • if (isLoading) {
  •  timer = setTimeout(() => setShowSkeleton(true), 1000);
    
  • } else {
  •  setShowSkeleton(false);
    
  • }
  • return () => {
  •  if (timer) {
    
  •    clearTimeout(timer);
    
  •  }
    
  • };
  • }, [isLoading]);
    
    

이 두 부분을 함께 고치면, 1초 지연 스켈레톤 UI 의도는 그대로 유지하면서도 초기 로딩 시 잘못된 에러 화면 노출과 TS 타입 이슈를 동시에 방지할 수 있습니다.

Also applies to: 41-49, 51-65

🤖 Prompt for AI Agents
In src/pages/main/node/[placeId].tsx around lines 27-32 (and also apply same
changes to 41-49, 51-65): the error display logic shows the "데이터를 불러오지 못했습니다"
message during initial loading because it treats !data as an error even when
isLoading is true, and the timer variable is typed/used unsafely causing
possible TS errors; change the error check to only treat missing data as an
error after loading finishes (e.g., require !isLoading && (isError || !data)),
and update the useEffect timer handling to use a safe type like let timer:
ReturnType<typeof setTimeout> | undefined (or initialize to undefined), only
assign timer when setting the timeout, and guard the cleanup with if (timer)
clearTimeout(timer) so clearTimeout is never called with an uninitialized value.

Copy link
Copy Markdown
Contributor

@skyblue1232 skyblue1232 left a comment

Choose a reason for hiding this comment

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

로딩 관련해서 ux 적으로도 안정감을 줄 수 있을 것 같아서 좋은 것 같네요:) 수고하셨습니다!

@skyblue1232 skyblue1232 merged commit 4989c72 into develop Nov 15, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comment 필요한 주석 추가 및 변경 fix 버그 및 자잘한 오류 수정 refactor 프로덕션 코드 리팩토링업, QA 반영

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[fix&refactor] 자잘자잘한 qa

2 participants