Skip to content

refactor: 이미지 컴포넌트 리뉴얼#443

Merged
WonJuneKim merged 26 commits into
devfrom
refactor/431-image-renewal
Jun 4, 2026
Merged

refactor: 이미지 컴포넌트 리뉴얼#443
WonJuneKim merged 26 commits into
devfrom
refactor/431-image-renewal

Conversation

@WonJuneKim

@WonJuneKim WonJuneKim commented May 19, 2026

Copy link
Copy Markdown
Contributor

💡 작업 내용

  • Image 컴포넌트 제거
  • Thumbnail 컴포넌트 생성

💡 자세한 설명

✅ Thumbnail

  • Image 컴포넌트 및 디자인 에셋은 더 이상 사용하지 않는 에셋입니다.
  • 또한 Thumbnail 컴포넌트 자체가 기본값으로 onClick 등의 인터렉션을 가지지 않고 특정 태그로 사용할 때만 인터렉션이 발생하도록 하였습니다.

✅ discriminated union

cornerStyle="rounded" ⇔ ratio="1:1" 에서만
orientation="landscape" ⇔ ratio≠"1:1" 에서만
children ⇔ asChild=true 에서만 (asChild면 children 필수 = Slot clone 대상)

✅ 스타일 토큰 관련

  • 일부 opacity가 들어간 스타일 및 tokenbuilder와 호환되지 않는 색상 코드등이 발견되었습니다. 해당 부분은 주석으로 처리했습니다.
  • storybook에서 fallback 기본원형은 이전 스타일 (마이그레이션)으로 처리하였습니다.

📗 참고 자료 (선택)

📢 리뷰 요구 사항 (선택)

✅ 셀프 체크리스트

  • 머지할 브랜치 확인했나요?
  • 이슈는 close 했나요?
  • Reviewers, Labels, Projects를 등록했나요?
  • 기능이 잘 동작하나요?
  • 불필요한 코드는 제거했나요?

closes #431

Summary by CodeRabbit

  • 새로운 기능

    • Thumbnail 컴포넌트 추가: 다양한 비율·방향·모서리·외관을 지원하고, 로드 실패 시 기본/커스텀 폴백을 표시합니다.
    • asChild 지원으로 버튼/링크와 자연스럽게 결합합니다.
  • 지원 중단

    • Image 컴포넌트는 더 이상 권장되지 않으며 Thumbnail 사용이 안내됩니다.
  • 변경 사항

    • 카드의 세로형 이미지 비율 기본값이 2:3 → 3:4로 조정되었습니다.
    • 배너·카드·메뉴 항목의 이미지 렌더링이 Thumbnail 기반으로 전환되었습니다.

WonJuneKim added 18 commits May 17, 2026 01:13
- default는 thumbnail 컴포넌트 내 인터페이스로 소유하도록 변경
- 중요도가 높은 컴포넌트 일 경우에만 eager로 선언
@vercel

vercel Bot commented May 19, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
ject-official-web-site-client-web Ready Ready Preview, Comment Jun 4, 2026 4:12pm

@WonJuneKim WonJuneKim self-assigned this May 19, 2026
@WonJuneKim WonJuneKim changed the title Refactor/431 image renewal(작성중) refactor: 이미지 컴포넌트 리뉴얼 May 19, 2026

@itwillbeoptimal itwillbeoptimal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

전체적으로 완성도 높게 잘 구현되어 있는 것 같습니다
단순 제안 정도로만 코멘트 남겨두었습니다! 고생하셨습니다 🙂

Comment on lines +59 to +67
<Comp
ref={forwardedRef as Ref<HTMLDivElement>}
{...restProps}
data-part='root'
className={clsx(
thumbnailStyles.root({ ratio, orientation, cornerStyle, appearance }),
className,
)}
>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

폴백 대체 시 alt가 붙지 않아서 isFallbackVisible 분기에 따라 role, aria-label 같은 접근성 속성을 추가해도 좋을 것 같아요


return (
<Comp
ref={forwardedRef as Ref<HTMLDivElement>}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(참고) 동작 상 문제되지 않는 부분인데, 코드를 보면서 공개 타입은 HTMLElement인데 내부에서 HTMLDivElement로 좁혀져 있는 것이 의아해서 as Ref<HTMLElement>로 변경해 보니 TS 에러가 발생하더라구요!

지금처럼 캐스트를 유지하거나 아래처럼 분기해서 처리하는 방법도 있을 것 같아용

const sharedProps = {
  "data-part": "root" as const,
  className: clsx(
    thumbnailStyles.root({ ratio, orientation, cornerStyle, appearance }),
    className,
  ),
  ...restProps,
};

if (asChild) {
  return (
    <Slot.Root ref={forwardedRef} {...sharedProps}>
      <Slot.Slottable>{children}</Slot.Slottable>
      {content}
    </Slot.Root>
  );
}

return (
  <div ref={forwardedRef as Ref<HTMLDivElement>} {...sharedProps}>
    {content}
  </div>
);

This comment was marked as off-topic.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

위에서 작성된 타입을 아래처럼 수정하면 될 것 같은데, HTMLElement가 아니라 HTMLDivElement로 변경하는 건 어떤가요?

export const Thumbnail = forwardRef<HTMLDivElement, ThumbnailProps>(

asChild = false만 고려하면 HTMLDivElement로 두는 것도 괜찮지만, asChild = true인 경우에는 실제로 전달한 자식 요소(button, a 등)를 ref가 가리키게 될 수 있어서, HTMLDivElement로 타입을 지정하면 타입 정보와 실제 값이 달라질 여지가 있을 것 같아요! 그래서 HTMLElement가 조금 더 적절한 타입이지 않을까 생각합니다

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.

아하 이해했습니다. 설명 감사해요.

- 폴백 표시 시 root에 role="img", aria-label을 부여해 스크린리더 대응
- asChild(Slot)/div 경로를 분리해 잘못된 ref 캐스트를 제거

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Image를 대체하는 새 Thumbnail 컴포넌트를 추가(타입·스타일·핵심 구현·폴백·스토리)하고, Banner/Card/MenuItem 등에서 Image 사용을 Thumbnail로 마이그레이션하며 Image에는 deprecated 표시를 추가합니다.

Changes

Thumbnail 도입 및 Image 마이그레이션

Layer / File(s) Summary
Thumbnail 타입 및 계약 정의
packages/jds/src/components/Thumbnail/thumbnail.types.ts
비율/방향/코너 스타일/외관 옵션과 ThumbnailShapeProps/ThumbnailProps 타입 및 이미지 렌더 콜백 계약을 정의합니다.
Thumbnail vanilla-extract 스타일 레시피
packages/jds/src/components/Thumbnail/thumbnail.css.ts
root/image/fallback 슬롯과 ratio·orientation·cornerStyle·appearance 변형, 포커스 링 및 hover/active dim을 vanilla-extract로 구현합니다.
Thumbnail 컴포넌트 핵심 구현
packages/jds/src/components/Thumbnail/Thumbnail.tsx, packages/jds/src/components/Thumbnail/ThumbnailFallback.tsx
useImageStatus 훅으로 폴백 노출을 관리하고 asChild 폴리모픽 지원, 이미지/폴백 조건부 렌더링 및 접근성 속성 주입을 구현합니다.
Thumbnail Storybook 스토리
packages/jds/src/components/Thumbnail/Thumbnail.stories.tsx
기본, asChild(button/anchor), 기본/커스텀 폴백, renderImage 주입, ratio/corner/appearance 매트릭스 스토리를 추가합니다.
Thumbnail 모듈 인덱스 및 패키지 인덱스 업데이트
packages/jds/src/components/Thumbnail/index.ts, packages/jds/src/components/index.ts
Thumbnail, ThumbnailFallback, 옵션 상수 및 타입을 재노출하고 패키지 컴포넌트 인덱스에 export * from "./Thumbnail"를 추가했습니다.
Banner → Thumbnail 마이그레이션
packages/jds/src/components/Banner/Banner.tsx, packages/jds/src/components/Banner/banner.types.ts
Banner.ImageThumbnail로 교체하고 BannerImagePropsThumbnailProps 기반으로 변경, 내부 padding/border 관련 style을 제거했습니다.
Card → Thumbnail 마이그레이션
packages/jds/src/components/Card/Card.types.ts, packages/jds/src/components/Card/compound/CardImage.tsx, packages/jds/src/components/Card/compound/compound.styles.ts, packages/jds/src/components/Card/presets/PlateWithLabel.tsx, packages/jds/src/components/Card/presets/PlateWithTitle.tsx
CardImageThumbnail로 교체하고 fallback/ratio/orientation 타입과 렌더링을 업데이트했습니다. 1:1 비율은 portrait로 처리되고 plate.vertical 기본 비율이 2:33:4로 변경되며 이미지 컨테이너에 position: relative가 추가됩니다.
MenuItem → Thumbnail 마이그레이션
packages/jds/src/components/Menu/MenuItem/MenuItem.tsx, packages/jds/src/components/Menu/MenuItem/menuItem.styles.ts
MenuItem의 썸네일 경로에서 StyledImage 기반을 Thumbnail로 변경하고 cornerStyle='angular'을 추가합니다.
Image 컴포넌트 deprecated 표시 및 Storybook 제거
packages/jds/src/components/Image/Image.tsx
Image@deprecated JSDoc을 추가하고 기존 Image Storybook 메타/스토리를 제거했습니다.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

♻refactor

Suggested reviewers

  • ccconac
  • areumH
  • itwillbeoptimal

🐰 옛 Image를 뒤로하고,
Thumbnail 새 옷으로 갈아입네!
바닐라 추출 선명하게, 폴백도 슬기롭게,
배너와 카드가 나란히 춤추네 ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 '이미지 컴포넌트 리뉴얼'은 주요 변경 사항(Image 컴포넌트 제거 및 Thumbnail 컴포넌트 생성)을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 작업 내용, 상세 설명, 체크리스트 템플릿을 포함하고 있으나, 머지 브랜치/이슈 close/기능 동작 확인 등 셀프 체크리스트 항목들이 미체크 상태입니다.
Linked Issues check ✅ Passed PR은 #431 이슈의 'Image 컴포넌트 스타일링 vanilla-extract 변경 및 구조 개선' 목표를 충족합니다. Thumbnail 신규 컴포넌트 구현, vanilla-extract 기반 스타일링, 구조 개선이 모두 포함되어 있습니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 #431 이슈의 범위 내입니다. Thumbnail 컴포넌트 신규 생성, Image 컴포넌트 deprecated 마킹, Card/Banner/MenuItem 등 관련 컴포넌트의 업데이트는 모두 이미지 컴포넌트 리뉴얼 목표와 일관성 있게 진행되었습니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/431-image-renewal

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
packages/jds/src/components/Banner/Banner.tsx (1)

62-76: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

style 스프레드 순서로 인한 포지셔닝 손실 가능성

{...imgProps}가 명시적 style보다 뒤에 전개되어, 소비자가 style을 전달하면 absolute 포지셔닝 전체가 덮어써집니다. BannerImagePropsstyle을 제외하지 않으므로 타입상 전달 가능하며, 이 경우 배너 오버레이 레이아웃이 깨질 수 있습니다.

🛠️ 제안 수정 (style 병합)
       <Thumbnail
         ratio='9:16'
         orientation='landscape'
         appearance='hollow'
         alt={title}
+        {...imgProps}
         style={{
           position: "absolute",
           top: 0,
           left: 0,
           width: "100%",
           height: "100%",
           zIndex: 0,
+          ...imgProps.style,
         }}
-        {...imgProps}
       />
🤖 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 `@packages/jds/src/components/Banner/Banner.tsx` around lines 62 - 76, The
Thumbnail call currently spreads {...imgProps} after a hardcoded style, so any
style in BannerImageProps/imgProps will overwrite required positioning; fix by
merging styles instead of letting imgProps.style win — create a mergedStyle
(e.g. combine imgProps.style with the absolute positioning defaults, ensuring
defaults take precedence) and pass that mergedStyle to Thumbnail while still
spreading imgProps (or spread imgProps first and then pass mergedStyle),
referencing the Thumbnail component, the imgProps/BannerImageProps object, and
the style prop so the absolute top/left/width/height/zIndex aren’t lost.
♻️ Duplicate comments (1)
packages/jds/src/components/Thumbnail/Thumbnail.tsx (1)

64-69: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

asChild 경로의 fallback 상태에서는 이름 없는 버튼/링크가 될 수 있습니다.

이전 피드백과 같은 맥락인데, 현재 보완은 div 분기에만 들어가 있어서 asChild에서는 이미지가 깨지는 순간 <img alt>가 사라지고 아이콘만 남습니다. 자식이 별도 라벨을 주지 않은 경우 접근 가능한 이름이 없어지니, fallback 노출 시에는 이 경로에도 최소한 aria-label={alt}를 같이 전달하는 게 좋겠습니다.

제안 수정
+    const fallbackA11yProps = isFallbackVisible ? { "aria-label": alt } : {};
+
     if (asChild) {
       return (
-        <Slot.Root ref={forwardedRef} {...sharedProps} {...restProps}>
+        <Slot.Root ref={forwardedRef} {...sharedProps} {...fallbackA11yProps} {...restProps}>
           <Slot.Slottable>{children}</Slot.Slottable>
           {content}
         </Slot.Root>
       );
     }
🤖 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 `@packages/jds/src/components/Thumbnail/Thumbnail.tsx` around lines 64 - 69,
When rendering the asChild branch inside Thumbnail (the Slot.Root wrapper used
with forwardedRef, sharedProps, restProps, children and content), ensure the
Slot.Root receives an accessible name by adding aria-label={alt} (or
aria-label={props.alt}) so that if children don’t provide a label or the image
falls back, the control remains accessible; implement this in the asChild return
path of the Thumbnail component (where Slot.Root is created) so Slot.Root gets
aria-label when alt is present.
🤖 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 `@packages/jds/src/components/Card/compound/CardImage.tsx`:
- Around line 71-91: The badge span in CardImage uses aria-hidden='true' which
hides meaningful text (badgeLabel) from assistive tech; locate the badge
rendering in CardImage (references: badgeVisible, badgeLabel) and either remove
aria-hidden or provide an accessible alternative (e.g., keep the visual span but
add an offscreen/readable element or aria-label on the container that exposes
badgeLabel) so screen readers receive the badge text while preserving visual
styling.
- Around line 74-87: The badge uses absolute positioning relative to the wrong
ancestor and hard-coded styles; update StyledCardImageContainer to include
position: "relative" so the badge anchors to the image container (rather than
StyledCardRoot), and replace the hard-coded badge values in CardImage.tsx
(backgroundColor, color, top, left, padding, fontSize, lineHeight, borderRadius)
with the appropriate card CSS variables/tokens (e.g., --card-caption-color,
--card-label-color or new --card-badge-bg / --card-badge-color /
--card-badge-padding / --card-badge-font-size) so spacing and colors follow the
card tokens and theme.

In `@packages/jds/src/components/Thumbnail/thumbnail.css.ts`:
- Around line 75-79: 현재 thumbnail.css.ts의 base 스타일(현재 display block에 설정된
overflow: hidden)과 다른 블록(약 96-104, 132-135 범위)에서 포커스 링을 ::before의 밖쪽 box-shadow로
그리고 있어, overflow: hidden 때문에 키보드 포커스 링이 잘려 보이지 않습니다; asChild로 전달되는 인터랙티브 경로에서
접근성 차단이 되므로, base 스타일에서 overflow: hidden을 제거하거나 포커스 링을 외부 그림자 대신 내부(inset)로 바꿔
::before/::after 또는 focus-visible 상태에서 inset box-shadow 또는 내부 outline으로 렌더링하도록
수정하세요; 구체적으로는 thumbnail.css.ts의 base 블록에서 overflow 속성 조정하고, ::before 기반 외부
box-shadow 코드를 찾아(또는 관련 focus selector를 찾아) 이를 inset box-shadow 또는 focus-visible
{ outline: 2px solid ...; outline-offset: -2px } 형태로 바꾸어 포커스 링이 요소 내부에 표시되게 하세요.

In `@packages/jds/src/components/Thumbnail/thumbnail.types.ts`:
- Around line 36-38: The ThumbnailContentProps union should narrow the children
type to a single ReactElement when asChild is true to match
Slot.Slottable/Slot.cloneElement contract; update the type in thumbnail.types.ts
so the branch { asChild: true; children: ReactElement } (import ReactElement
from react) replaces ReactNode, keep the { asChild?: false; children?: never }
branch as-is, and verify Thumbnail.tsx (the component that wraps children in
<Slot.Slottable>) compiles with the tightened type.

---

Outside diff comments:
In `@packages/jds/src/components/Banner/Banner.tsx`:
- Around line 62-76: The Thumbnail call currently spreads {...imgProps} after a
hardcoded style, so any style in BannerImageProps/imgProps will overwrite
required positioning; fix by merging styles instead of letting imgProps.style
win — create a mergedStyle (e.g. combine imgProps.style with the absolute
positioning defaults, ensuring defaults take precedence) and pass that
mergedStyle to Thumbnail while still spreading imgProps (or spread imgProps
first and then pass mergedStyle), referencing the Thumbnail component, the
imgProps/BannerImageProps object, and the style prop so the absolute
top/left/width/height/zIndex aren’t lost.

---

Duplicate comments:
In `@packages/jds/src/components/Thumbnail/Thumbnail.tsx`:
- Around line 64-69: When rendering the asChild branch inside Thumbnail (the
Slot.Root wrapper used with forwardedRef, sharedProps, restProps, children and
content), ensure the Slot.Root receives an accessible name by adding
aria-label={alt} (or aria-label={props.alt}) so that if children don’t provide a
label or the image falls back, the control remains accessible; implement this in
the asChild return path of the Thumbnail component (where Slot.Root is created)
so Slot.Root gets aria-label when alt is present.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 516c2b8a-6076-4995-9a9f-1d85285e9548

📥 Commits

Reviewing files that changed from the base of the PR and between 9f572bf and d97a9e8.

📒 Files selected for processing (19)
  • packages/jds/src/components/Banner/Banner.tsx
  • packages/jds/src/components/Banner/banner.types.ts
  • packages/jds/src/components/Card/Card.types.ts
  • packages/jds/src/components/Card/compound/CardImage.tsx
  • packages/jds/src/components/Card/presets/PlateWithLabel.tsx
  • packages/jds/src/components/Card/presets/PlateWithTitle.tsx
  • packages/jds/src/components/Image/Image.stories.tsx
  • packages/jds/src/components/Image/Image.style.ts
  • packages/jds/src/components/Image/Image.tsx
  • packages/jds/src/components/Image/index.ts
  • packages/jds/src/components/Menu/MenuItem/MenuItem.tsx
  • packages/jds/src/components/Menu/MenuItem/menuItem.styles.ts
  • packages/jds/src/components/Thumbnail/Thumbnail.stories.tsx
  • packages/jds/src/components/Thumbnail/Thumbnail.tsx
  • packages/jds/src/components/Thumbnail/ThumbnailFallback.tsx
  • packages/jds/src/components/Thumbnail/index.ts
  • packages/jds/src/components/Thumbnail/thumbnail.css.ts
  • packages/jds/src/components/Thumbnail/thumbnail.types.ts
  • packages/jds/src/components/index.ts
💤 Files with no reviewable changes (4)
  • packages/jds/src/components/Image/index.ts
  • packages/jds/src/components/Image/Image.stories.tsx
  • packages/jds/src/components/Image/Image.style.ts
  • packages/jds/src/components/Image/Image.tsx

Comment thread packages/jds/src/components/Card/compound/CardImage.tsx
Comment thread packages/jds/src/components/Card/compound/CardImage.tsx
Comment thread packages/jds/src/components/Thumbnail/thumbnail.css.ts
Comment thread packages/jds/src/components/Thumbnail/thumbnail.types.ts Outdated
- 포커스 링을 inset box-shadow로 변경해 overflow:hidden에 잘리지 않도록 수정
- asChild children 타입을 ReactElement로 좁혀 Slot clone 계약과 일치
- Card 뱃지의 불필요한 aria-hidden 제거 및 컨테이너 position:relative 기준 추가

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@Zero-1016 Zero-1016 left a comment

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.

Image 컴포넌트 deprecated 처리 및, Thumbnail 컴포넌트 추가 확인했어요.

처음 리뷰를 하는 만큼 아무래도 구조라든지 코드 작성 방법에 대한 질문과 간단한 개선사항이 좀 있어요. 가볍게 봐주세요.

@@ -41,32 +41,53 @@ export const CardImage = forwardRef<HTMLDivElement, CardImageProps>(
};

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.

객체의 value 마다 as const를 붙이신 이유가 있을까요? 다음 처럼 처리할 수 있어보여요.

const defaultRatioMap = {
  plate: {
    vertical: "2:3",
    vertical: "3:4",
    horizontal: "1:1",
  },
  post: {
    vertical: "1:2",
    horizontal: "1:1",
  },
} as const;

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.

추가로 속성 값을 가져오는 로직이면, 순수 함수 작성하여 컴포넌트 외부로 분리해도 좋을 것 같아요.

컴포넌트 코드에는 그려지는 로직만 있어서 가독성이 높아질 것 같아요.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

맞아요 요 부분은 레거시 쪽이라 Card쪽이 compound 쪽으로 되어있는거 같아서 같이 수정된 부분인데 요 부분은 신경 안쓰셔도 될 거같아요! (@deprecated 처리만 해놓겠습니당)

Comment on lines +28 to +31
interface ThumbnailBaseProps extends ComponentPropsWithoutRef<"div"> {
src?: string;
alt: string;
loading?: "lazy" | "eager";

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.

Thumbnail이면 <img /> 태그가 더 어울리지 않을까요? 그럼 src나, alt, loading의 경우 지금과 다르게 타입이 관리가 될 수 있을 것 같고.

추후에도 사용할 때 img 태그를 생각하면서 사용할 수 있을 것 같은데요. div 태그를 base로 잡으신 이유가 있으실까요?

Suggested change
interface ThumbnailBaseProps extends ComponentPropsWithoutRef<"div"> {
src?: string;
alt: string;
loading?: "lazy" | "eager";
interface ThumbnailBaseProps extends ComponentPropsWithoutRef<"img"> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

img가 의미상 더 자연스럽다는 점은 동의합니다~

다만 이 컴포넌트는 이미지 로드 실패 시 폴백을 렌더링하는 기능을 제공하는데, 이를 위해 폴백을 자식으로 렌더링할 수 있는 컨테이너 엘리먼트가 필요합니다. img는 void element라 자식을 가질 수 없어서 현재와 같은 방식으로 폴백을 제공하기는 어려워 보여요

그리고 Storybook 문서를 보면 caller가 root element의 의미, 동작, a11y를 결정하고 Thumbnail은 시각적 표현만 책임진다는 방향으로 설계된 것 같습니다. 이런 관점에서 div는 의미를 갖지 않는 컨테이너 역할만 수행하고, 다양한 컨텍스트에서 소비자가 필요한 시맨틱을 직접 선택할 수 있을 것 같습니다

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

해당 부분이 가장 많이 고민했던 부분인데 의미론적으로 <img> 가 맞다고 생각해요. (기존 Image도 이미지 태그 베이스로 구성되어있었어요)

<div/> 를 사용하게 된 계기가 기존에 polymorphic 유틸리티를 제거하고 slot형태로 처리하면서 발생하게된 side effect인데 Thumbnail을 anchor나 div(순수 display) 로 제공해야할 때에도 img 태그 base로 가지는게 적절하다고 생각하시는지 질문드려요!

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.

아하, 두 분 얘기 들으니 완벽하게 이해했습니다.

API가 기존 <img />와 워낙 유사하다 보니, div prop 타입 위에 src·alt 같은 img 속성을 얹어가며 작성하는 게 왜 필요한지에서 시작된 의문이었어요.

Thumbnail을 anchor나 div(순수 display)로 제공해야 할 때에도 img 태그 base로 가지는 게 적절한지

asChild가 있으니 지금 이대로 유지해도 될 것 같아요. <a><img/></a>, <div><img/></div> 전부 가능하니까요. img를 root가 아니라 content에 두는 구조라고 이해하면 깔끔하네요.

Comment on lines +73 to +86
style={{
position: "absolute",
top: "8px",
left: "8px",
zIndex: 1,
minWidth: "18px",
padding: "0 6px",
borderRadius: "2px",
backgroundColor: "rgba(0, 0, 0, 0.6)",
color: "#fff",
fontSize: "12px",
lineHeight: "18px",
textAlign: "center",
}}

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.

이 코드에 직접적인 질문은 아닌데요.

혹시 인라인 스타일, 또는 **.style.ts or **.css.ts 에 정의하는 기준이 있을까요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

이 부분은 추후에 .claude 와 같은 정책서가 있으면 좋을 거 같아요. (공통적으로 스타일을 맞추기 위함)

해당 부분 역시 레거시인데 현재 스타일링 이관 작업이기 때문에 내부 inline, 혹은 style.ts 로 표현된 부분들을 모두 외부 css.ts로 표현 중인 작업을 진행중입니다.

++)CardImage 역시 레거시인데 충돌 방지로 임시 수정된 부분임을 노티드려요.

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.

아하, 혹시 기존에 관리되던 작성 규칙 같은 파일이 있을까요? 아니면 각자 센스 것..? 작업을 했는지.


dev 브랜치에 생각보다 많은 것들이 한번에 이뤄지고 있는 것 같아서, 나중에 한번에 다운로드 해볼게요~! 감사합니다.

@ccconac ccconac Jun 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

작성 규칙에 관해서라면 FE Archive 컨벤션 문서가 있긴 한데요. 초기에 작성된 거라 원준 님이 말씀해 주신 것처럼 정책서를 새롭게 생성해야 할 것으로 보여요. (.AGENTS, .claude)

2팀이 생성되고 작업이 넘어오면서, 디자인 시스템을 관리하던 인원의 변동이 있었다 보니 네이밍이나 스타일 면에서 다른 것들이 다수 존재하는 상황이에요. 질문 주신 것처럼 정의하는 기준이 별도로 존재하지 않고, 눈치껏 작업하던 걸 현재와 같이 리뷰로 보완하던 방식이 맞습니다.


return (
<Comp
ref={forwardedRef as Ref<HTMLDivElement>}

This comment was marked as off-topic.

export * from "./Footer";
export * from "./Hero";
export * from "./Icon";
/** @deprecated `Image`는 `Thumbnail`로 대체되었습니다. 하위 호환을 위해 한시적으로 유지합니다. */

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.

Image 컴포넌트에 deprecated가 잘 들어가 있어서 제거해도 될 듯 합니다!

Suggested change
/** @deprecated `Image`는 `Thumbnail`로 대체되었습니다. 하위 호환을 위해 한시적으로 유지합니다. */

@itwillbeoptimal itwillbeoptimal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

갑자기 생각난 부분이라 같이 남겨봅니다!

현재 Thumbnail이 내부에서 <img>를 직접 렌더링하고 있어서, 소비 측 앱에서 next/image로 교체할 방법이 없는 상태로 보입니다

JDS 마이그레이션 목적 중 하나가 추후 Next.js 환경에서도 동일하게 활용할 수 있도록 하는 것이라면, 이미지 렌더링 방식을 소비자가 제어할 수 있는 구조를 열어두는 것도 고려할 수 있을 것 같습니다

renderImage?: (props: {
  src: string;
  alt: string;
  className: string;
  onError: () => void;
  loading: "lazy" | "eager";
}) => ReactNode;

이런 식으로 제공하면 기본 구현은 <img>를 유지하면서 Next.js 환경에서는 next/image를 주입해 사용할 수 있을 것 같아요~

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/jds/src/components/Thumbnail/Thumbnail.tsx (1)

18-70: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

asChild 경로와 ref 공개 타입이 불일치합니다.

asChild=true에서는 실제 ref 대상이 button/a가 될 수 있는데, 현재 공개 타입이 HTMLDivElement라서 타입 계약이 깨집니다. forwardRef<HTMLElement, ThumbnailProps>로 되돌리거나(asChild 분기 유지), 폴리모픽 ref 타입으로 분리하는 편이 안전합니다.

제안 수정안
-import { forwardRef, useState } from "react";
+import { forwardRef, useState, type Ref } from "react";

-export const Thumbnail = forwardRef<HTMLDivElement, ThumbnailProps>(
+export const Thumbnail = forwardRef<HTMLElement, ThumbnailProps>(
...
-    return (
-      <div
-        ref={forwardedRef}
+    return (
+      <div
+        ref={forwardedRef as Ref<HTMLDivElement>}
         {...sharedProps}
         {...(isFallbackVisible && { role: "img", "aria-label": alt })}
         {...restProps}
       >
#!/bin/bash
# asChild 사용 위치에서 실제 자식 엘리먼트 타입(button/a/div 등) 확인
rg -n -C2 '<Thumbnail[^>]*asChild|<Slot\.Slottable>|<button|<a ' --type=tsx

# Thumbnail ref 타입 선언 및 전달 경로 확인
rg -n -C3 'forwardRef<|ref=\{forwardedRef\}|Slot\.Root' packages/jds/src/components/Thumbnail/Thumbnail.tsx
🤖 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 `@packages/jds/src/components/Thumbnail/Thumbnail.tsx` around lines 18 - 70,
The public ref type for Thumbnail is incorrect: Thumbnail is declared with
forwardRef<HTMLDivElement, ThumbnailProps> but when asChild is true the
forwardedRef actually points to the child element (Slot.Root → whatever child
like button/a), breaking the type contract; change the forwardRef generic to a
broader type (e.g., forwardRef<HTMLElement, ThumbnailProps>) or implement a
polymorphic ref type so forwardedRef matches actual DOM element types, and
ensure all uses of forwardedRef (Slot.Root, the div branch) accept the updated
ref type; update Thumbnail's declaration (forwardRef), ThumbnailProps if
necessary, and verify references to Slot.Root and forwardedRef compile with the
new type.
🤖 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.

Duplicate comments:
In `@packages/jds/src/components/Thumbnail/Thumbnail.tsx`:
- Around line 18-70: The public ref type for Thumbnail is incorrect: Thumbnail
is declared with forwardRef<HTMLDivElement, ThumbnailProps> but when asChild is
true the forwardedRef actually points to the child element (Slot.Root → whatever
child like button/a), breaking the type contract; change the forwardRef generic
to a broader type (e.g., forwardRef<HTMLElement, ThumbnailProps>) or implement a
polymorphic ref type so forwardedRef matches actual DOM element types, and
ensure all uses of forwardedRef (Slot.Root, the div branch) accept the updated
ref type; update Thumbnail's declaration (forwardRef), ThumbnailProps if
necessary, and verify references to Slot.Root and forwardedRef compile with the
new type.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7b728d5e-4f3b-43a4-b4bc-4826058fb93d

📥 Commits

Reviewing files that changed from the base of the PR and between 2003388 and 73e441d.

📒 Files selected for processing (3)
  • packages/jds/src/components/Card/compound/CardImage.tsx
  • packages/jds/src/components/Thumbnail/Thumbnail.tsx
  • packages/jds/src/components/Thumbnail/thumbnail.css.ts
💤 Files with no reviewable changes (1)
  • packages/jds/src/components/Thumbnail/thumbnail.css.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/jds/src/components/Card/compound/CardImage.tsx

WonJuneKim and others added 2 commits June 4, 2026 01:28
Card 컴파운드 재구성 시 제거 예정인 CardImage에 @deprecated를 명시하고
Thumbnail 사용을 안내합니다.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Zero-1016

Copy link
Copy Markdown
Contributor

갑자기 생각난 부분이라 같이 남겨봅니다!

현재 Thumbnail이 내부에서 <img>를 직접 렌더링하고 있어서, 소비 측 앱에서 next/image로 교체할 방법이 없는 상태로 보입니다

JDS 마이그레이션 목적 중 하나가 추후 Next.js 환경에서도 동일하게 활용할 수 있도록 하는 것이라면, 이미지 렌더링 방식을 소비자가 제어할 수 있는 구조를 열어두는 것도 고려할 수 있을 것 같습니다

renderImage?: (props: {
  src: string;
  alt: string;
  className: string;
  onError: () => void;
  loading: "lazy" | "eager";
}) => ReactNode;

이런 식으로 제공하면 기본 구현은 <img>를 유지하면서 Next.js 환경에서는 next/image를 주입해 사용할 수 있을 것 같아요~

renderImage 형태도 좋아 보이는데, render prop 대신 children을 콜백(children-as-function)으로 받는 방식은 어떨까요?

사실 저는 render* 같은 패턴을 선호하지 않는 편인데, 이름은 다 비슷한데(renderImage, renderItem, renderHeader…) 정작 넘겨야 하는 인자나 사용법은 제각각이라 매번 시그니처를 확인해야 하는 점이 아쉽더라고요. children 콜백으로 통일하면 “함수를 넘긴다”는 사용법이 일관되게 유지되는 장점도 있을 것 같아요.

그리고 children을 넘기지 않았을 때는 기본 로 렌더링되도록 default를 잡아두면, 별도 설정 없이도 바로 쓸 수 있고 필요한 경우에만 렌더링을 커스텀할 수 있어서 좋을 것 같아요.

@Zero-1016

Copy link
Copy Markdown
Contributor
// 기본 - children 안 넘기면 <img> 사용
<Thumbnail src="/photo.jpg" alt="사진" />

// 커스텀 - Next.js 환경에서 next/image 주입
<Thumbnail src="/photo.jpg" alt="사진">
  {({ src, alt, className, onError, loading }) => (
    <Image
      src={src}
      alt={alt}
      className={className}
      onError={onError}
      loading={loading}
      width={200}
      height={200}
    />
  )}
</Thumbnail>

@WonJuneKim

Copy link
Copy Markdown
Contributor Author

@Zero-1016 저도 수정해보면서 느낀점이 renderImage로 계약을 체결하게되면 부가적으로 알아야할 정보가 늘어나는 느낌이라 불편할 수 있겠다는 생각이 들었어요.
추가로 Slot 형태도 고려해봤는데 직전에 논의되었던 <img/>를 사용하지 않고 div prop 타입 위에 img 관련 속성을 올리는 구조라 깔끔하게 대체할 수 없는 구조로 판단되었어요.

말씀해주신 children을 콜백하는 방식이 renderProps 방식보다 사용처에서 매끄럽게 사용될 수 있을 거 같네요 :) 좋은 의견 감사합니다!

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
순수 <img> 기본값은 유지하고, 이미지 렌더링에 대한 DS의 대응 범위는
추후 논의가 필요하다는 점을 TODO로 표기합니다.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@WonJuneKim

Copy link
Copy Markdown
Contributor Author

@Zero-1016 @itwillbeoptimal 두분이 주신 의견들을 취합해본 결과 renderProps를 제거하고 기존의 순수한 image 컴포넌트로 롤백처리했어요. 이유는 다음과 같아요.

@Zero-1016 님이 제안해주신 방법은 소비자 입장에서 간편하게 사용할 수 있다는 장점이 있지만 children이 function이 되는 순간 현재 asChild가 true일 때 가르키는 children 이 되는 element에 추가적인 콜백이 생겨요 따라서 asChild로 추론되는 children의 원형이 함수인지 순수 element인지 판단할 수 있는 일관성이 깨지게 되요.

@itwillbeoptimal 말씀해주신 전제인 "Next.js 환경에서도 문제 없이 사용할 수 있는 DS를 만들자" 는 것이 이번 마이그레이션의 주요 목표였어요. 다만 말씀해주신 next/image의 경우 해당 컴포넌트 자체가 복합적인 책임을 가지고 있는 컴포넌트라고 생각해요. 자체 최적화 기능이나 특정 프레임 워크(Next)에 결합되어있는 것이 그 이유라고 생각합니다.

따라서 DS가 오히려 범용적이고 순수한 단위의 컴포넌트로써 보여져야한다고 생각하기 때문에 다시 기존의 presentation 역할만을 수행하는 API만 남겨놓았습니다. Next까지 고려한다면 Provider 설정을 추가할 수도 있었지만 이 부분 역시 소비자에게 불편함을 주는 부수효과라고 생각합니다.

이 부분은 TODO로만 남겨놓을게요. DS가 어디까지 범위를 보장해줄 것인가 해석하기에 따라 구현이 달라질 수 있는 부분인 거 같아요.

…ewal

# Conflicts:
#	packages/jds/src/components/Menu/MenuItem/menuItem.styles.ts
@WonJuneKim WonJuneKim merged commit 005be0e into dev Jun 4, 2026
5 of 6 checks passed
@WonJuneKim WonJuneKim deleted the refactor/431-image-renewal branch June 4, 2026 16:12
@ccconac ccconac mentioned this pull request Jun 22, 2026
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

refactor: (JDS) Image 컴포넌트 리펙토링

4 participants