Skip to content

feat(image): add image component#262

Open
minjeeki wants to merge 4 commits into
devfrom
feature/image-component-minjee
Open

feat(image): add image component#262
minjeeki wants to merge 4 commits into
devfrom
feature/image-component-minjee

Conversation

@minjeeki
Copy link
Copy Markdown

Changes

@sipe-team/image 패키지의 Image 컴포넌트 초기 구현입니다.

Image 컴포넌트 기본 동작 구현

  • src, alt 필수
  • width, height (number | string) 지원
  • fit 지원 (contain | cover | fill, 기본값 cover)
  • fill 지원
  • loading 기본값 lazy
  • placeholder 지원

useImageStatus 훅 분리

에러/대체 이미지 상태 관리를 위해 useImageStatus 훅을 추가했습니다.

  • 상태: loading | normal | fallback | error
  • fallback 1회 전환 + 무한루프 방지 (hasTriedFallback 플래그)
  • onError 콜백 호출 보장

Storybook Components/Image 추가

  • Basic, Fallbacks, WithPlaceholder, WithFill, Fits, ResponsiveWidth
  • 각 스토리 설명(docs.description.story) 추가

Image 테스트 코드 추가

총 8개 케이스를 작성했습니다 (packages/image/src/Image.test.tsx)

  • src, alt 필수 전달 시 이미지 정상 렌더링 여부 확인
  • width={320}, height="50%" 같이 number/string 혼합 입력이 스타일로 반영되는지 확인
  • 첫 에러 발생 시 fallbackSrc로 1회 전환되는지 확인
  • fallbackSrc가 없을 때 에러 상태로 전환되고 이미지가 visibility: hidden 처리되는지 확인
  • 로딩 중 placeholder 노출, load 이벤트 후 placeholder 제거되는지 확인
  • fill 사용 시 position: absolute, width/height: 100% 스타일 적용 확인
  • loading prop 미지정 시 기본값 lazy 확인
  • 에러 발생 시 사용자 onError 콜백이 항상 호출되는지 확인

Visuals

  • Basic — 기본 렌더링
스크린샷 2026-04-28 오후 5 29 25
  • Fallbacks : (fallbackSrc 있음 = fallback 전환 / 없음 = 영역 유지, 이미지 숨김)
스크린샷 2026-04-28 오후 5 30 04 스크린샷 2026-04-28 오후 5 37 33
  • Filts : object-fit 동작
스크린샷 2026-04-28 오후 5 36 42
  • ResponsiveWidth : 반응형 너비
스크린샷 2026-04-28 오후 5 36 42

Checklist

  • Have you written the functional specifications?
  • Have you written the test code?

Additional Discussion Points

디자인시스템 레퍼런스 분석 기준

구현 방향을 잡기 위해 Ant Design, Chakra UI v3, Mantine, Next.js 총 4개 디자인시스템의 Image 컴포넌트를 분석했습니다.
노션 링크 내부에 각 레퍼런스별 공식문서와 github 링크, 파악한 내용 정리한 부분을 첨부합니다.

채택한 방향

  • 상태 loading / normal / fallback / error 4단계로 분리하는 구조 : Ant Design 참고
  • 에러 시 visibility: hidden으로 공간을 유지하는 방식 : Ant Design 참고
  • fallbackSrc prop 제공 + onError 콜백은 항상 호출하는 방식 : Ant Design + Next.js 참고
  • fallback 1회 전환 후 무한루프 방지(hasTriedFallback)는 레퍼런스에 없는 방식으로 직접 추가

팀원들과 논의를 하고 싶은 부분

  • 에러 표시 정책
    현재는 visibility: hidden으로 공간을 유지하면서 내용을 숨기는 방식으로 구현했습니다. Ant Design과 동일한 방식입니다. 기본 fallback UI를 내장하는 방향으로 가면 에러 상태가 더 명확해지고 일관성도 높아질 것 같아서, 디자인팀과 UI 형태를 논의한 후 추가하는 방향을 제안합니다.
  • radius, size 등에 대한 토큰 처리 관련
    현재는 사용자가 직접 값을 입력하는 방식으로 열어뒀습니다. 토큰 관련해서 논의 후에 진행하는 것이 좋을 것 같습니다.
  • aspectRatio prop 처리 여부 관련
    현재 서비스에서 비율로 처리하는 케이스가 한 곳으로 CSS로도 대응 가능한 수준이라 제외했습니다. 실제로 쓰다가 불편하다는 피드백이 오면 그때 추가하는 방향을 고민해봤습니다.
  • next/image 관련
    현재는 기반 코어로만 구현했습니다. next/image는 자체적으로 최적화와 로딩 처리를 하기 때문에 우리 상태 관리와 충돌이 생길 수 있어서, 별도 패키지(@sipe-team/image-next)로 분리해서 작업하는 방향을 고민해봤습니다.

늦게 올려서 죄송합니다. 작업이 처음이라 부족한 부분이 많을 것 같습니다.. 피드백 주시면 최대한 열심히 찾아보겠습니다.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 28, 2026

🦋 Changeset detected

Latest commit: ac2eb99

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@sipe-team/image Minor
@sipe-team/side Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • main
  • release/v1

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9247d850-7f00-4cfb-9122-429994900049

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/image-component-minjee

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
packages/image/src/Image.css.ts 100.00% <100.00%> (ø)
packages/image/src/Image.test.tsx 100.00% <100.00%> (ø)
packages/image/src/Image.tsx 100.00% <100.00%> (ø)
packages/image/src/hooks/useImageStatus.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread packages/image/src/Image.tsx Outdated
Comment on lines +43 to +56
const sizeStyle: CSSProperties = {
width: typeof width === 'number' ? `${width}px` : width,
height: typeof height === 'number' ? `${height}px` : height,
objectFit: fit,
};

const fillStyle: CSSProperties = fill
? {
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
}
: {};
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.

다른 컴포넌트들과 마찬가지로 vanilla-extract로 스타일링하면 좋을꺼 같습니다 : )

Comment thread packages/image/src/Image.tsx Outdated
placeholder?: ReactNode;
}

export function Image({
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.

다른 컴포넌트와 마찬가지기로 forwardRef로 export하면 좋을꺼 같습니다

onError: onErrorFromProps,
}: UseImageStatusParams): UseImageStatusResult {
const [status, setStatus] = useState<ImageStatus>('loading');
const [imgSrc, setImgSrc] = useState(src);
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.

src prop가 변경되면 갱신되어야 하는 로직 고민이 피요할꺼 같아요 🤔

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

아... 민지님 이건 안됩니다...

function Gallery() {
    const [currentSrc, setCurrentSrc] = useState('/photo-1.jpg');

    return (
      <>
        <Image src={currentSrc} alt="gallery" width={400} height={300} />
        <button onClick={() => setCurrentSrc('/photo-2.jpg')}>다음 사진</button>
      </>
    );
  }

이런 src 가 부모에서 교체되는 상황이라면 이미지 교체가 안돼요.
src 가 바뀌면 imgSrc 가 업데이트 되게끔 되어야 합니다.

Comment on lines +51 to +56
"./styles.css": "./dist/index.css"
}
},
"sideEffects": [
"**/*.css"
]
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.

지금 따로 cssvanilla-extract가 없어서 여기는 불필요할꺼 같습니다 : )

css none으로 가신다면 수정 필요한 부분입니당

@Yeom-JinHo
Copy link
Copy Markdown
Member

많이 고민하신 부분이 보여서 너무 좋네요!
hasTriedFallback 도 좋은거 같습니다~~

개인적으로 ant-design 진짜 수준높게 잘 만들어진 디자인 시스템이라고 생각합니다 🐜

Copy link
Copy Markdown
Contributor

@KYBee KYBee left a comment

Choose a reason for hiding this comment

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

Image 컴포넌트 작업 잘 봤습니다. 고생 많으셨습니다 :)

[nit] side export에는 아직 추가되지 않은 게 맞을까요?

import { Image } from './Image';

describe('Image', () => {
it('renders with required src and alt props', () => {
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
Collaborator

@G-hoon G-hoon left a comment

Choose a reason for hiding this comment

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

민지님 changeset 추가 부탁드립니다.

onError: onErrorFromProps,
}: UseImageStatusParams): UseImageStatusResult {
const [status, setStatus] = useState<ImageStatus>('loading');
const [imgSrc, setImgSrc] = useState(src);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

아... 민지님 이건 안됩니다...

function Gallery() {
    const [currentSrc, setCurrentSrc] = useState('/photo-1.jpg');

    return (
      <>
        <Image src={currentSrc} alt="gallery" width={400} height={300} />
        <button onClick={() => setCurrentSrc('/photo-2.jpg')}>다음 사진</button>
      </>
    );
  }

이런 src 가 부모에서 교체되는 상황이라면 이미지 교체가 안돼요.
src 가 바뀌면 imgSrc 가 업데이트 되게끔 되어야 합니다.


if (!hasTriedFallback && fallbackSrc) {
setImgSrc(fallbackSrc);
setHasTriedFallback(true);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

제 생각엔, hasTriedFallback 상태를 제거해도 될 것 같아요.
검토 부탁드립니다.

if (status !== 'fallback' && fallbackSrc) {
        setImgSrc(fallbackSrc);
        setStatus('fallback');
        return;
      }

      setStatus('error');

Copy link
Copy Markdown
Collaborator

@G-hoon G-hoon left a comment

Choose a reason for hiding this comment

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

다른 컴포넌트는 forwardRef 쓰고 있는데,
forwardRef 적용하는 것 검토해주세요~!
ref로 DOM에 접근해야 하는 경우(IntersectionObserver 등)도 있을 것 같습니다.

Comment thread packages/image/global.d.ts Outdated
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

이 file이 혹시 필요한가요...?
필요한 지 검토 부탁드립니다.

Comment thread packages/image/src/index.ts Outdated
@@ -0,0 +1,2 @@
export * from './hooks/useImageStatus';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

이거 굳이 export 할 필요가 있을까요?
사용자 입장에서, 이 hook 을 사용할 일이 있을까 해서요

Comment thread packages/image/src/Image.tsx Outdated
type ImageFit = 'contain' | 'cover' | 'fill';

export interface ImageProps
extends Omit<ComponentPropsWithoutRef<'img'>, 'src' | 'alt' | 'width' | 'height' | 'onLoad'> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

여기 onError 도 추가해주면 좋을 것 같습니다!

Copy link
Copy Markdown
Contributor

@froggy1014 froggy1014 left a comment

Choose a reason for hiding this comment

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

어려운거하시네요 ~ !

Comment thread packages/image/global.d.ts Outdated
@@ -0,0 +1 @@
declare module '*.module.css';
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.

@minjeeki

아마 이거 사용하실일 없을거예요. 이거 Template으로 만드신 패키지같은데, vanilla extract 마이그레이션 이전의 상태인가보네요.

이건 module.css 사용할때 있었던 잔재같아요 ~

@@ -0,0 +1,57 @@
{
"name": "@sipe-team/image",
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.

@minjeeki

side 패키지에도 추가가 되어야할거같아요.

expect(img).toHaveAttribute('src', 'https://picsum.photos/400/300');
});

it('applies width and height styles for number and string values', () => {
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.

@minjeeki

이 컴포넌트를 next/image 대체로 쓰는 그림은 아니라기보다, native <img />를 감싼 디자인 시스템용 wrapper로 이해했습니다.

그래서 컴포넌트 자체보다 useImageStatus를 범용적으로 재사용할 수 있게 열어두는 쪽이 더 중요해 보여요. next/image 같은 다른 이미지 렌더러에서도 같은 loading/fallback DX를 만들 수 있도록 하고, 그 사용 시나리오를 테스트로 함께 보강하면 좋겠습니다.

minjeeki added 2 commits May 13, 2026 20:23
- Sync imgSrc and loading state when src prop changes
- Remove hasTriedFallback; use fallback status for placeholder visibility
- Migrate styles to Vanilla Extract (fill/fit/hidden, sized + inline vars)
- Use shared tsup config; remove obsolete global.d.ts
- Add forwardRef to the underlying img element
- Tighten onError typing (omit from rest, explicit handler type)
- Stop exporting useImageStatus from the main package entry
- Align @sipe-team/side aggregate exports with @sipe-team/image

BREAKING CHANGE: useImageStatus is no longer exported from @sipe-team/image main entry
@minjeeki
Copy link
Copy Markdown
Author

minjeeki commented May 13, 2026

Changes

올려주신 PR 코멘트 내용을 기반으로 코드를 수정했습니다. 상세 내용은 아래와 같습니다.
각각 코멘트별로 댓글 달 예정이라 아래 내용은 전체적인 변경지점 파악용으로만 봐주세요.

1. vanilla-extract로 스타일링 방식 통일

관련 리뷰

수정 사항

  1. 패키지 의존성 추가
    • @vanilla-extract/css
    • clsx : className 병합용 (다른 DS 컴포넌트와 동일한 패턴 유지 목적)
    • @vanilla-extract/dynamic : width, height를 위해 추가
      (사용자 입력 없이 토큰값으로만 설정할 경우 dynamic 패키지 및 관련 코드 삭제 필요)
  2. vanilla extract 빌드 파이프라인 연동 : 공유 tsup.config를 쓰도록 변경
  3. global.d.ts 파일 삭제
  4. 코드 수정
    • fill, fit, hidden 관련 스타일 수정
    • width와 height에 대해서 useSized로 조건부로 수행하도록 설정
  5. 테스트코드 추가
    • width만 / height만 지정 시, 나머지 축이 auto로 계산되는지 확인
    • fit prop에 따른 object-fit (contain, fill variant) 적용 여부 확인
    • fill + width 동시 지정 시, 레이아웃은 fill(100%)이 우선되고 width 픽셀(sized)이 끼지 않는지 확인

2. forwardRef 추가

관련 리뷰

수정사항

  1. forwardRef 추가
  2. img 태그에 ref 속성값 추가
  3. 테스트 코드 추가
    • forwardRef: ref가 실제 를 가리키는지 확인

3. useImageStatus src 동기화 문제

관련 리뷰

수정사항

  1. useEffect() 추가 : src 변경 시 setImgSrc(src), setStatus('loading') 설정
  2. 테스트코드 추가 : rerender로 src만 변경 시 가 새 URL로 바뀌는지 확인

4. hasTriedFallback

관련 리뷰

수정사항

  • useImageStatus 훅
    • hasTriedFallback를 status === 'fallback'으로 조건 대체
    • useCallback 의존성에 hasTriedFallback 제거 후 status 추가
  • Image 컴포넌트
    • showPlaceholder에 status === 'fallback' 조건 추가

5. 그 외 작업

추가 변경 가능성

  • 디자인팀과 토큰·크기 API가 정해지면, width / height 표현 방식을 비롯한 스타일 수정이 있을 수 있습니다.

@minjeeki
Copy link
Copy Markdown
Author

minjeeki commented May 13, 2026

미해결 PR 리뷰

useImageState 훅 활용 관련

관련 리뷰

문제 파악

  • 사용자가 훅 컴포넌트를 직접 사용할 경우가 적음에도 훅 역시 export로 제공하고 있음
    (= 지훈님께서 남기신 내용)

  • 이미지 컴포넌트는 현재 next/image를 사용하고 싶은 유저들에게는 제한적인 선택지임

    • 해결방안 1. img가 아닌 next/image 를 사용한 별도 패키지를 제공
      • 문제점 : 디자인시스템이 next/image에 대해서 제공하기 위해서는 의존성으로 next를 설치해야하며 이에 따라 운영 부담이 증가함
    • 해결방안 2. loading/fallback 등에 대한 동일한 동작을 보장할 수 있도록 훅만 제공
      (= 정민님께서 제안해주신 내용)

제안하는 방향

@sipe-team/image/hooks와 같이 서브패스를 제공하는 것은 어떨지 제안해봅니다.

  • image 자체에서는 훅을 제공하지 않을 수 있을 것으로 예상
  • 훅만 사용하려는 유저들에게도 이미지 컴포넌트까지 가져올 부담을 줄여줄 수 있을 것으로 예상

작업 진행 계획

좀 더 찾아보고 진행 계획 세우겠습니당...!

  1. next/image와 맞출 loading/error·완료 콜백 계약 정리(사전 조사)
  2. useImageStatus 전용 테스트 보강 (onLoadingComplete 등 시나리오)
  3. @sipe-team/image/hooks 서브패스(또는 합의된 경로) 생성

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants