Skip to content

THIP-405: Skeleton UI 공통 컴포넌트 구현#305

Merged
ljh130334 merged 3 commits into
refactor-skeletonfrom
THIP-405
Feb 19, 2026
Merged

THIP-405: Skeleton UI 공통 컴포넌트 구현#305
ljh130334 merged 3 commits into
refactor-skeletonfrom
THIP-405

Conversation

@ljh130334
Copy link
Copy Markdown
Member

@ljh130334 ljh130334 commented Feb 19, 2026

📝 작업 내용

피드 페이지의 로딩 상태 UX 개선을 위해 Skeleton UI 컴포넌트를 도입했습니다. 기존 LoadingSpinner는 단순 스피너만 표시하여 사용자가 어떤 콘텐츠가 로딩되는지 예측할 수 없었지만, Skeleton UI는 실제 콘텐츠의 레이아웃과 구조를 미리 보여주어 더 나은 사용자 경험을 제공합니다.

주요 변경사항

1. Skeleton UI 공통 컴포넌트 구현 (src/shared/ui/Skeleton/)
구현된 컴포넌트:

  • Skeleton - 기본 스켈레톤 (커스텀 크기/모양)
  • Skeleton.Box - 박스 형태 스켈레톤 (카드, 이미지 등)
  • Skeleton.Circle - 원형 스켈레톤 (프로필 이미지)
  • Skeleton.Text - 텍스트 라인 스켈레톤 (다중 라인 지원)

주요 기능:

  • Shimmer 애니메이션 자동 적용 (1.5초 주기)
  • 유연한 크기 설정 (px, %, rem 등 모든 단위 지원)
  • 다크 테마 최적화 (global.ts의 색상 시스템 활용)
  • TypeScript 타입 안전성 보장

2. 피드 페이지 스켈레톤 적용
변경 파일:

  • src/pages/feed/Feed.tsx
    • LoadingSpinner → FeedPostSkeleton으로 교체
    • 최소 1초 로딩 보장 로직 추가 (깜빡임 방지)
    • 스켈레톤 3개 표시로 리스트 느낌 제공
  • src/pages/feed/Feed.styled.ts
    • SkeletonWrapper 추가 (136px 상단 여백으로 헤더/탭 아래 정확한 위치 배치)
  • src/shared/ui/Skeleton/FeedPostSkeleton.tsx
    • 피드 포스트 구조를 반영한 스켈레톤 예시 컴포넌트
    • 프로필 이미지, 텍스트, 이미지, 푸터 영역 구조화

3. 기술적 개선사항
깜빡임 방지:

const minLoadingTime = new Promise(resolve => setTimeout(resolve, 1000));
await Promise.all([loadTotalFeeds(), minLoadingTime]);

API 응답이 빠르더라도 스켈레톤을 최소 1초 표시하여 자연스러운 로딩 경험 제공

스크린샷 (선택)

2026-02-19.4.14.40.mov

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 피드 로딩 시 스켈레톤 UI가 표시됩니다.
  • 개선 사항

    • 최소 1초의 로딩 지연이 적용됩니다.

- LoadingSpinner 대신 콘텐츠 구조를 보여주는 Skeleton UI 도입
- shared/ui/Skeleton에 재사용 가능한 컴포넌트 구현 (Box/Circle/Text)
- 피드 페이지 로딩 상태에 스켈레톤 적용으로 UX 개선
@ljh130334 ljh130334 self-assigned this Feb 19, 2026
@ljh130334 ljh130334 added the ✨ Feature 기능 개발 label Feb 19, 2026
@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 19, 2026

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

Project Deployment Actions Updated (UTC)
thip Ready Ready Preview, Comment Feb 19, 2026 7:16am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 19, 2026

개요

새로운 스켈레톤 기반 로딩 UI 시스템을 도입하여 피드 페이지의 로딩 상태를 개선했습니다. LoadingSpinner를 FeedPostSkeleton 컴포넌트로 교체하고, 최소 1초의 인위적 로딩 지연을 추가했으며, 재사용 가능한 Skeleton 컴포넌트 라이브러리를 새로 구축했습니다.

변경 사항

Cohort / File(s) 요약
Feed 페이지 스타일링
src/pages/feed/Feed.styled.ts
스켈레톤 로딩 상태를 위한 전용 래퍼 컴포넌트 SkeletonWrapper 추가 (min-height: 100vh, 패딩, 배경색 포함)
Feed 페이지 로딩 로직
src/pages/feed/Feed.tsx
LoadingSpinner를 SkeletonWrapper와 FeedPostSkeleton으로 교체. Promise.all을 통해 최소 1초의 로딩 지연을 데이터 로딩과 병렬 처리
Skeleton UI 컴포넌트 라이브러리
src/shared/ui/Skeleton/Skeleton.tsx, src/shared/ui/Skeleton/Skeleton.styled.ts, src/shared/ui/Skeleton/index.ts
SkeletonBox, SkeletonCircle, SkeletonText 등 재사용 가능한 스켈레톤 기본 컴포넌트 및 스타일 정의. 시머 애니메이션과 동적 크기 조정 지원
FeedPost 스켈레톤 구현
src/shared/ui/Skeleton/FeedPostSkeleton.tsx, src/shared/ui/Skeleton/FeedPostSkeleton.styled.ts
피드 포스트 구조를 모방하는 스켈레톤 컴포넌트 (헤더, 콘텐츠, 이미지, 푸터)
Skeleton 문서
src/shared/ui/Skeleton/README.md
Skeleton UI 컴포넌트의 사용법, API 명세, 커스터마이징 가이드 및 코드 예제 포함

시퀀스 다이어그램

sequenceDiagram
    participant User
    participant Feed as Feed 컴포넌트
    participant SkeletonUI as SkeletonWrapper<br/>(FeedPostSkeleton)
    participant DataLoader as 데이터 로더
    participant Promise as Promise.all

    User->>Feed: 탭 전환/페이지 로드
    Feed->>Feed: 로딩 상태 시작
    Feed->>SkeletonUI: SkeletonWrapper와 3개의<br/>FeedPostSkeleton 렌더링
    SkeletonUI-->>User: 스켈레톤 UI 표시
    
    par 병렬 처리
        Feed->>DataLoader: 피드 데이터 요청
        Feed->>Promise: 1초 최소 지연 시작
    end
    
    DataLoader-->>Promise: 데이터 로드 완료
    Promise-->>Feed: Promise.all 완료
    Feed->>Feed: 로딩 상태 종료
    Feed-->>User: 실제 데이터로 UI 업데이트
Loading

코드 리뷰 예상 소요 시간

🎯 3 (Moderate) | ⏱️ ~25 minutes

관련 가능성 있는 PR

제안 라벨

🎨 Html&css

제안 검토자

  • heeeeyong

🐰 뼈대 그리고 기다림의 춤,
스켈레톤이 춤을 추네!
로딩 중일 때 깜빡이며,
실제 데이터가 나타날 때까지—
한 초간의 기다림, 아름다운 경험! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 주요 변경사항을 명확하게 반영합니다. Skeleton UI 공통 컴포넌트 구현이라는 핵심 목표가 잘 표현되어 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 THIP-405

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • THIP-405: Request failed with status code 403

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.

Copy link
Copy Markdown

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/feed/Feed.tsx (1)

139-160: ⚠️ Potential issue | 🟠 Major

useEffect 클린업 부재로 인한 경쟁 조건 (Race Condition).

탭을 빠르게 전환하면 이전 effect의 비동기 작업이 계속 실행되어 setTabLoading(false)가 새 effect의 로딩이 완료되기 전에 호출됩니다. minLoadingTime이 1초이므로 이 경쟁 조건의 발생 가능성이 더 높아졌습니다.

시나리오:

  1. 탭 A 로드 시작 → setTabLoading(true), Promise.all 시작
  2. 500ms 후 탭 B로 전환 → 새 effect가 setTabLoading(true) 재설정
  3. 탭 A의 Promise.all(1000ms)이 완료 → setTabLoading(false) 호출 → 스켈레톤 조기 소멸
  4. 탭 B의 데이터가 아직 준비되지 않은 상태에서 콘텐츠가 노출됨
🔒 수정 예시: `cancelled` 플래그로 클린업 처리
 useEffect(() => {
+  let cancelled = false;
+
   const loadFeedsWithToken = async () => {
     await waitForToken();
+    if (cancelled) return;

     setTabLoading(true);

     try {
       const minLoadingTime = new Promise(resolve => setTimeout(resolve, 1000));

       if (activeTab === '피드') {
         await Promise.all([loadTotalFeeds(), minLoadingTime]);
       } else if (activeTab === '내 피드') {
         await Promise.all([loadMyFeeds(), minLoadingTime]);
       }
     } finally {
-      setTabLoading(false);
-      setInitialLoading(false);
+      if (!cancelled) {
+        setTabLoading(false);
+        setInitialLoading(false);
+      }
     }
   };

   loadFeedsWithToken();
+
+  return () => {
+    cancelled = true;
+  };
 }, [activeTab, waitForToken, loadTotalFeeds, loadMyFeeds]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/Feed.tsx` around lines 139 - 160, The effect in Feed.tsx
(useEffect -> loadFeedsWithToken) lacks a cleanup and so previous async work can
call setTabLoading/setInitialLoading after a new tab starts; fix by adding a
local cancelled flag (or AbortController) inside the useEffect, check it after
each await/Promise.all and before calling setTabLoading(false) or
setInitialLoading(false), and return a cleanup function that flips cancelled =
true so earlier promises do not mutate state for a newer activeTab (apply checks
around minLoadingTime/Promise.all and after loadTotalFeeds/loadMyFeeds calls).
🧹 Nitpick comments (3)
src/pages/feed/Feed.styled.ts (1)

11-16: 136px 매직 넘버를 CSS 변수 또는 공유 상수로 추출하는 것을 권장합니다.

padding-top: 136pxMainHeaderTabBar의 합산 높이를 하드코딩한 값입니다. 둘 중 하나의 높이가 변경되면 스켈레톤 위치가 어긋나게 됩니다.

♻️ 리팩토링 예시
+// src/shared/constants/layout.ts 또는 tokens 파일
+export const FEED_CONTENT_TOP_OFFSET = 136; // MainHeader(88px) + TabBar(48px)

// Feed.styled.ts
+import { FEED_CONTENT_TOP_OFFSET } from '@/shared/constants/layout';

export const SkeletonWrapper = styled.div`
  min-height: 100vh;
-  padding-top: 136px;
+  padding-top: ${FEED_CONTENT_TOP_OFFSET}px;
  padding-bottom: 125px;
  background-color: var(--color-black-main);
`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/Feed.styled.ts` around lines 11 - 16, The hardcoded
padding-top: 136px in SkeletonWrapper should be replaced with a shared value:
extract the combined MainHeader and TabBar height into a CSS variable (or shared
constant) and use that variable (or calc using --main-header-height and
--tab-bar-height) for padding-top so the skeleton tracks header/tab height
changes; update SkeletonWrapper to reference that variable and add/update the
variable definitions where global CSS or theme constants are defined (also
update any places using the 136px magic number).
src/shared/ui/Skeleton/Skeleton.styled.ts (1)

22-23: display: inline-block → 블록 레벨 스켈레톤에 display: block 검토.

현재 모든 사용처가 flex 컨테이너 내부이므로 실제 레이아웃 문제는 없습니다. 다만 스켈레톤은 의미상 블록 수준 플레이스홀더이므로 display: block이 더 자연스러운 기본값입니다. inline-block은 flex/grid 외부 컨텍스트에서 예기치 않은 레이아웃을 유발할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Skeleton/Skeleton.styled.ts` around lines 22 - 23, The
SkeletonBase styled component currently sets display: inline-block which can
cause unexpected layout outside flex/grid contexts; update the SkeletonBase
definition (styled.div<SkeletonBaseProps>) to use display: block as the default
for true block-level skeleton placeholders, then run/adjust any layout-related
tests or snapshots that assume inline behavior and verify components using
SkeletonBase inside flex containers still render correctly.
src/pages/feed/Feed.tsx (1)

139-160: 탭 전환 시마다 데이터 재요청 + 최소 1초 지연 발생.

activeTab이 바뀔 때마다 이미 로드된 탭으로 돌아와도 항상 재요청 후 스켈레톤을 1초 이상 노출합니다. 이는 이 PR에서 추가된 minLoadingTime으로 인해 기존 대비 UX 체감이 더 떨어질 수 있습니다. 탭별 로드 완료 여부를 별도 상태(totalLoaded, myLoaded)로 관리하여, 이미 로드된 탭에는 스켈레톤을 건너뛰는 방안을 고려해 보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/feed/Feed.tsx` around lines 139 - 160, The effect always forces a
reload and the 1s minLoadingTime on every activeTab change via minLoadingTime;
add per-tab loaded flags (e.g., totalLoaded, myLoaded) and update them to true
after successful load in loadTotalFeeds and loadMyFeeds (or immediately after
those calls); then in the useEffect check these flags before triggering a
reload—only call loadTotalFeeds when activeTab === '피드' && !totalLoaded (and
similarly for '내 피드' with myLoaded), and only include minLoadingTime in the
Promise.all when performing an actual network load; make sure you still call
setTabLoading(true) for real loads and clear setTabLoading(false) and
setInitialLoading(false) in finally as before.
🤖 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/Skeleton/README.md`:
- Around line 25-50: Add a blank line before and after each Markdown table so
they are properly fenced by empty lines to satisfy MD058: specifically insert an
empty line above and below the table under the "### Skeleton (기본)" heading, the
table following "### Skeleton.Circle" (the Circle props table), and the table
under "### Skeleton.Text" so each table is separated from surrounding
paragraphs/headings by a blank line.

In `@src/shared/ui/Skeleton/Skeleton.styled.ts`:
- Around line 22-36: The styled div SkeletonBase is passing the custom prop
borderRadius through to the DOM causing React warnings; update the styled
component creation to use Emotion's shouldForwardProp to filter out borderRadius
(e.g., only forward native HTML attributes like width/height) so borderRadius is
not passed to the underlying div, and apply the same shouldForwardProp change to
the derived components SkeletonBox, SkeletonCircle, and SkeletonTextLine so none
of them forward the borderRadius prop to the DOM.

---

Outside diff comments:
In `@src/pages/feed/Feed.tsx`:
- Around line 139-160: The effect in Feed.tsx (useEffect -> loadFeedsWithToken)
lacks a cleanup and so previous async work can call
setTabLoading/setInitialLoading after a new tab starts; fix by adding a local
cancelled flag (or AbortController) inside the useEffect, check it after each
await/Promise.all and before calling setTabLoading(false) or
setInitialLoading(false), and return a cleanup function that flips cancelled =
true so earlier promises do not mutate state for a newer activeTab (apply checks
around minLoadingTime/Promise.all and after loadTotalFeeds/loadMyFeeds calls).

---

Nitpick comments:
In `@src/pages/feed/Feed.styled.ts`:
- Around line 11-16: The hardcoded padding-top: 136px in SkeletonWrapper should
be replaced with a shared value: extract the combined MainHeader and TabBar
height into a CSS variable (or shared constant) and use that variable (or calc
using --main-header-height and --tab-bar-height) for padding-top so the skeleton
tracks header/tab height changes; update SkeletonWrapper to reference that
variable and add/update the variable definitions where global CSS or theme
constants are defined (also update any places using the 136px magic number).

In `@src/pages/feed/Feed.tsx`:
- Around line 139-160: The effect always forces a reload and the 1s
minLoadingTime on every activeTab change via minLoadingTime; add per-tab loaded
flags (e.g., totalLoaded, myLoaded) and update them to true after successful
load in loadTotalFeeds and loadMyFeeds (or immediately after those calls); then
in the useEffect check these flags before triggering a reload—only call
loadTotalFeeds when activeTab === '피드' && !totalLoaded (and similarly for '내 피드'
with myLoaded), and only include minLoadingTime in the Promise.all when
performing an actual network load; make sure you still call setTabLoading(true)
for real loads and clear setTabLoading(false) and setInitialLoading(false) in
finally as before.

In `@src/shared/ui/Skeleton/Skeleton.styled.ts`:
- Around line 22-23: The SkeletonBase styled component currently sets display:
inline-block which can cause unexpected layout outside flex/grid contexts;
update the SkeletonBase definition (styled.div<SkeletonBaseProps>) to use
display: block as the default for true block-level skeleton placeholders, then
run/adjust any layout-related tests or snapshots that assume inline behavior and
verify components using SkeletonBase inside flex containers still render
correctly.

Comment on lines +25 to +50
### Skeleton (기본)
| Props | Type | Default | Description |
|-------|------|---------|-------------|
| width | string \| number | '100%' | 너비 (px 또는 문자열) |
| height | string \| number | 20 | 높이 (px 또는 문자열) |
| borderRadius | string \| number | 4 | 테두리 반경 (px 또는 문자열) |
| className | string | - | 추가 스타일을 위한 클래스명 |

### Skeleton.Box
기본 Skeleton과 동일한 props 사용

### Skeleton.Circle
| Props | Type | Default | Description |
|-------|------|---------|-------------|
| width | string \| number | 40 | 원의 크기 (width, height 동일) |
| className | string | - | 추가 스타일을 위한 클래스명 |

### Skeleton.Text
| Props | Type | Default | Description |
|-------|------|---------|-------------|
| lines | number | 1 | 텍스트 라인 개수 |
| gap | string \| number | 8 | 라인 사이 간격 (px 또는 문자열) |
| width | string \| number | '100%' | 너비 |
| height | string \| number | 16 | 각 라인의 높이 |
| lastLineWidth | string \| number | '70%' | 마지막 라인의 너비 |
| className | string | - | 추가 스타일을 위한 클래스명 |
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

테이블 앞뒤에 빈 줄을 추가해야 합니다 (MD058).

정적 분석 도구에서 26번, 37번, 43번 줄의 Markdown 테이블이 빈 줄로 둘러싸이지 않았다고 경고합니다.

📝 수정 예시
 ### Skeleton (기본)
+
 | Props | Type | Default | Description |
 |-------|------|---------|-------------|
 ...
+
 ### Skeleton.Box

 ...

 ### Skeleton.Circle
+
 | Props | Type | Default | Description |
 ...
+
 ### Skeleton.Text
+
 | Props | Type | Default | Description |
 ...
+
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Skeleton (기본)
| Props | Type | Default | Description |
|-------|------|---------|-------------|
| width | string \| number | '100%' | 너비 (px 또는 문자열) |
| height | string \| number | 20 | 높이 (px 또는 문자열) |
| borderRadius | string \| number | 4 | 테두리 반경 (px 또는 문자열) |
| className | string | - | 추가 스타일을 위한 클래스명 |
### Skeleton.Box
기본 Skeleton과 동일한 props 사용
### Skeleton.Circle
| Props | Type | Default | Description |
|-------|------|---------|-------------|
| width | string \| number | 40 | 원의 크기 (width, height 동일) |
| className | string | - | 추가 스타일을 위한 클래스명 |
### Skeleton.Text
| Props | Type | Default | Description |
|-------|------|---------|-------------|
| lines | number | 1 | 텍스트 라인 개수 |
| gap | string \| number | 8 | 라인 사이 간격 (px 또는 문자열) |
| width | string \| number | '100%' | 너비 |
| height | string \| number | 16 | 각 라인의 높이 |
| lastLineWidth | string \| number | '70%' | 마지막 라인의 너비 |
| className | string | - | 추가 스타일을 위한 클래스명 |
### Skeleton (기본)
| Props | Type | Default | Description |
|-------|------|---------|-------------|
| width | string \| number | '100%' | 너비 (px 또는 문자열) |
| height | string \| number | 20 | 높이 (px 또는 문자열) |
| borderRadius | string \| number | 4 | 테두리 반경 (px 또는 문자열) |
| className | string | - | 추가 스타일을 위한 클래스명 |
### Skeleton.Box
기본 Skeleton과 동일한 props 사용
### Skeleton.Circle
| Props | Type | Default | Description |
|-------|------|---------|-------------|
| width | string \| number | 40 | 원의 크기 (width, height 동일) |
| className | string | - | 추가 스타일을 위한 클래스명 |
### Skeleton.Text
| Props | Type | Default | Description |
|-------|------|---------|-------------|
| lines | number | 1 | 텍스트 라인 개수 |
| gap | string \| number | 8 | 라인 사이 간격 (px 또는 문자열) |
| width | string \| number | '100%' | 너비 |
| height | string \| number | 16 | 각 라인의 높이 |
| lastLineWidth | string \| number | '70%' | 마지막 라인의 너비 |
| className | string | - | 추가 스타일을 위한 클래스명 |
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 26-26: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 37-37: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 43-43: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Skeleton/README.md` around lines 25 - 50, Add a blank line
before and after each Markdown table so they are properly fenced by empty lines
to satisfy MD058: specifically insert an empty line above and below the table
under the "### Skeleton (기본)" heading, the table following "### Skeleton.Circle"
(the Circle props table), and the table under "### Skeleton.Text" so each table
is separated from surrounding paragraphs/headings by a blank line.

Comment on lines +22 to +36
export const SkeletonBase = styled.div<SkeletonBaseProps>`
display: inline-block;
width: ${({ width }) => (typeof width === 'number' ? `${width}px` : width || '100%')};
height: ${({ height }) => (typeof height === 'number' ? `${height}px` : height || '20px')};
border-radius: ${({ borderRadius }) =>
typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius || '4px'};
background: linear-gradient(
90deg,
${colors.darkgrey.main} 0%,
${colors.grey[400]} 50%,
${colors.darkgrey.main} 100%
);
background-size: 468px 100%;
animation: ${shimmer} 1.5s ease-in-out infinite;
`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

borderRadius 커스텀 prop이 DOM으로 전달되어 React 개발 모드 경고 발생.

Emotion은 기본적으로 string tag(e.g. div)에 대해 유효한 HTML 어트리뷰트인 prop만 DOM으로 전달합니다. width·height는 HTML 표준 어트리뷰트이므로 전달돼도 경고가 없지만, borderRadius는 HTML 표준 어트리뷰트가 아니므로 개발 모드에서 Warning: React does not recognize the borderRadius prop on a DOM element 경고가 출력됩니다.

shouldForwardProp을 사용하여 커스텀 prop이 DOM으로 전달되지 않도록 수정하세요.

🛡️ 수정 예시
+import isPropValid from '@emotion/is-prop-valid';

-export const SkeletonBase = styled.div<SkeletonBaseProps>`
+export const SkeletonBase = styled('div', {
+  shouldForwardProp: (prop) => isPropValid(prop),
+})<SkeletonBaseProps>`
   display: inline-block;
   width: ${({ width }) => (typeof width === 'number' ? `${width}px` : width || '100%')};
   height: ${({ height }) => (typeof height === 'number' ? `${height}px` : height || '20px')};
   border-radius: ${({ borderRadius }) =>
     typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius || '4px'};
   ...
`;

SkeletonBase를 상속하는 SkeletonBox, SkeletonCircle, SkeletonTextLine에도 동일하게 적용되어야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/Skeleton/Skeleton.styled.ts` around lines 22 - 36, The styled
div SkeletonBase is passing the custom prop borderRadius through to the DOM
causing React warnings; update the styled component creation to use Emotion's
shouldForwardProp to filter out borderRadius (e.g., only forward native HTML
attributes like width/height) so borderRadius is not passed to the underlying
div, and apply the same shouldForwardProp change to the derived components
SkeletonBox, SkeletonCircle, and SkeletonTextLine so none of them forward the
borderRadius prop to the DOM.

@ljh130334 ljh130334 merged commit 7d2a417 into refactor-skeleton Feb 19, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant