HoverGallery
호버 시 이미지를 순서대로 전환하며 보여주는 갤러리 컴포넌트
목차
마우스를 올리면 호버 위치에 따라 이미지가 전환되어 표시되는 갤러리 컴포넌트입니다. 제품 목록, 포트폴리오 카드 등에서 미리 보기 효과를 줄 때 활용합니다.
기본 사용법 (Basic Usage)#
// URL 리스트로 간편 생성
HoverGallery(
imageUrls: [
'https://example.com/product-1.jpg',
'https://example.com/product-2.jpg',
'https://example.com/product-3.jpg',
],
)
// 커스텀 위젯 빌더 (색상 컨테이너, SVG 등)
final scheme = Theme.of(context).colorScheme;
final fills = [scheme.primary, scheme.secondary, scheme.tertiary];
HoverGallery(
itemCount: 3,
itemBuilder: (index) => ColoredBox(color: fills[index].toValue()),
)
// URL 리스트로 간편 생성
HoverGallery(
imageUrls: [
'https://example.com/product-1.jpg',
'https://example.com/product-2.jpg',
'https://example.com/product-3.jpg',
],
)
// 커스텀 컴포넌트 빌더
HoverGallery(
itemCount: 3,
itemBuilder: (index) => div(
[],
styles: Styles(raw: {
'background': ['blue', 'green', 'orange'][index],
'width': '100%',
'height': '100%',
}),
),
)
빠른 오버라이드 (Chain)#
이미 만든 HoverGallery 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.radius24처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius24 ==
CoreRadius.radius24) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.
class HoverGalleryChainExample extends StatefulWidget {
const HoverGalleryChainExample({super.key});
@override
State<HoverGalleryChainExample> createState() => _HoverGalleryChainExampleState();
}
class _HoverGalleryChainExampleState extends State<HoverGalleryChainExample> {
@override
Widget build(BuildContext context) {
return HoverGallery(
imageUrls: [
'https://picsum.photos/seed/a/400/300',
'https://picsum.photos/seed/b/400/300',
'https://picsum.photos/seed/c/400/300',
],
)
.withStyle(
const CoreHoverGalleryStyle(
height: CoreSpace.space256,
animationDuration: Duration(milliseconds: CoreDuration.fast),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
),
)
.radius16;
}
}
class HoverGalleryChainExample extends StatefulComponent {
const HoverGalleryChainExample({super.key});
@override
State<HoverGalleryChainExample> createState() => _HoverGalleryChainExampleState();
}
class _HoverGalleryChainExampleState extends State<HoverGalleryChainExample> {
@override
Component build(BuildContext context) {
return HoverGallery(
imageUrls: [
'https://picsum.photos/seed/a/400/300',
'https://picsum.photos/seed/b/400/300',
'https://picsum.photos/seed/c/400/300',
],
)
.withStyle(
const CoreHoverGalleryStyle(
height: CoreSpace.space256,
animationDuration: Duration(milliseconds: CoreDuration.fast),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
),
)
.radius16;
}
}
Live Preview#
코드 보기
class HoverGalleryDefaultExample extends StatefulComponent {
const HoverGalleryDefaultExample({super.key});
@override
State<HoverGalleryDefaultExample> createState() =>
_HoverGalleryDefaultExampleState();
}
class _HoverGalleryDefaultExampleState
extends State<HoverGalleryDefaultExample> {
@override
Component build(BuildContext context) {
return HoverGallery(
imageUrls: [
'https://picsum.photos/seed/a/400/300',
'https://picsum.photos/seed/b/400/300',
'https://picsum.photos/seed/c/400/300',
],
);
}
}
class HoverGalleryDefaultExample extends StatefulWidget {
const HoverGalleryDefaultExample({super.key});
@override
State<HoverGalleryDefaultExample> createState() =>
_HoverGalleryDefaultExampleState();
}
class _HoverGalleryDefaultExampleState
extends State<HoverGalleryDefaultExample> {
@override
Widget build(BuildContext context) {
return HoverGallery(
imageUrls: [
'https://picsum.photos/seed/a/400/300',
'https://picsum.photos/seed/b/400/300',
'https://picsum.photos/seed/c/400/300',
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 이커머스 제품 목록에서 호버 시 여러 각도의 제품 이미지를 미리 볼 때
- 포트폴리오 그리드에서 프로젝트의 스크린샷을 순서대로 보여줄 때
- 갤러리나 카탈로그에서 클릭 없이 빠른 이미지 미리 보기를 제공할 때
대신 다른 컴포넌트를 사용하세요:
- Carousel: 포인터 위치 대신 이전/다음·인디케이터·자동 재생 탐색이 필요할 때
- CardImage: 여러 이미지 위치 전환 없이 이미지 하나와 제목 슬롯·hover 확대가 필요할 때
Props / Parameters#
W 는 Flutter의 Widget / Web의 Jaspr Component입니다. itemBuilder는 같은 플랫폼 콘텐츠를 반환합니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
imageUrls |
List<String>? |
null |
표시할 이미지 URL 목록 |
itemBuilder |
W Function(int)? |
null |
커스텀 콘텐츠 빌더 |
itemCount |
int? |
null |
빌더 모드 시 아이템 수 |
fit |
BoxFit (Flutter) / String? (Web) |
BoxFit.cover / null → 'cover' |
이미지 채우기 방식 (imageUrls 모드 전용). Web 은 CSS object-fit 값 문자열 |
hoverGalleryStyle |
CoreHoverGalleryStyle? |
null |
높이·보더 라운드·전환 duration 단일 진입점 |
imageUrls와itemBuilder+itemCount중 하나는 반드시 넘겨야 합니다. Flutter 는 생성자assert로 debug 빌드에서 즉시 잡아주지만 Web 에는assert가 없고, 둘 다 생략하면 양 플랫폼 모두 예외 없이 빈 컨테이너만 렌더합니다 (height만큼의 빈 영역 — Flutter 도 release 빌드에서는 같습니다).
스타일 시스템 (Style System)#
모든 chrome / 치수 / 애니메이션 override 는 hoverGalleryStyle 슬롯 하나로 흐릅니다.
CoreHoverGalleryStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
height |
double? |
Container height override (logical px, pre-scaling). |
borderRadius |
CoreBorderRadius? |
Container border radius override (pre-scaling). |
animationDuration |
Duration? |
Image transition animation duration override. |
Resolve chain#
CoreHoverGalleryStyle.defaultX (static const)
→ CoreHoverGalleryTheme.style // 프로젝트 공통
→ widget.hoverGalleryStyle // 인스턴스별
동작 스펙 (Behavior)#
인터랙션#
- 호버 이동: 마우스 X 좌표에 따라 갤러리 영역을 이미지 수만큼 분할, 해당 영역의 이미지를 표시
- 클릭/탭: 탭한 위치의 이미지로 전환
- 초기 상태: 첫 번째 이미지 표시. 포인터가 떠났다고 첫 이미지로 초기화하지 않고 마지막 활성 위치를 유지
- 콜백·외부 활성 인덱스·비활성 플래그는 없습니다. 표시 전환만 하며 이미지 URL 모드가 빌더보다 우선합니다. 아이템 개수가 0이면 높이만 있는 빈 영역을 그립니다.
애니메이션#
- Flutter:
AnimatedSwitcher로 crossfade (300ms) - Web: CSS
opacitytransition (300ms)
사용 가이드라인 (Usage Guidelines)#
Do#
동일한 비율의 이미지 사용
HoverGallery(
imageUrls: product.images, // 모두 동일 비율 권장
hoverGalleryStyle: const CoreHoverGalleryStyle(height: CoreSpace.space200),
)
이미지 비율이 다르면 전환 시 시각적으로 불안정합니다. 동일한 비율의 이미지를 사용하세요.
Don't#
너무 많은 이미지 사용 금지
// 20개의 이미지 — 호버로 탐색하기 어려움
HoverGallery(
imageUrls: product.allImages, // 20장 이상
)
3~6개를 권장합니다.
접근성 (Accessibility)#
역할#
어느 플랫폼도 이미지 묶음임을 알리는 역할을 emit 하지 않습니다.
-
Flutter: 구현에
Semantics가 없습니다 —MouseRegion+GestureDetector+Container+AnimatedSwitcher조합이라 갤러리 컨테이너는 시맨틱 노드를 만들지 않고, 이미지만 자기 라벨을 갖습니다(아래 스크린 리더 참조). -
Web: 루트
<div>에configureAttributesoverride 가 없어 강제하는role/aria-*가 없고,attributes: component.componentAttributes로 호출자 속성이 그대로 통과합니다.HoverGallery(attributes: {'role': 'group', 'aria-label': '...'})로 직접 실을 수 있습니다.
키보드#
| 키 | 동작 |
|---|---|
| 없음 | 키보드로 이미지를 전환하는 경로가 없습니다 |
이미지 전환은 포인터 입력(호버 이동 · 클릭/탭)으로만 동작합니다.
-
Flutter:
Focus/FocusNode/onKeyEvent가 없어 traversal 정지점이 되지 않습니다. -
Web: 루트에
tabindex도keydown도 emit 하지 않고, 등록하는 이벤트는mousemove와click둘뿐입니다.
스크린 리더#
imageUrls 모드에서는 양 플랫폼 모두 활성 로케일의 galleryImageLabel(n) 을 이미지 이름으로 씁니다 — Flutter 는
Image.network(semanticLabel:), Web 은 img(alt:) 로, 같은 문자열(예: 갤러리 이미지 1)이 나갑니다.
itemBuilder 모드에서는 호출자가 직접 이름을 넣어야 합니다.
상태 통보는 없습니다 — live region 이 없어 활성 이미지가 바뀌어도 통보되지 않습니다.
노출되는 이미지 개수가 플랫폼마다 다릅니다. Flutter 는 활성 이미지 하나만 트리에 올리므로 스크린 리더에 이미지가 하나만 보입니다. Web 은 이미지 N 개를 모두 DOM 에 렌더하고 비활성 레이어를
opacity: 0 으로만 감추는데, opacity: 0 은 접근성 트리에서 제외하지 않으므로 갤러리 이미지 1
부터 N 까지 전부 읽힙니다.
포커스 관리#
오버레이가 아니므로 진입 · 이탈 · 트랩이 모두 없습니다. 갤러리 루트가 traversal 정지점이 아니라 포커스는 갤러리를 그냥 통과하고, 안에 포커스 가능한 자식(itemBuilder
모드)을 넣었다면 그 자식이 순서대로 포커스를 받습니다.
알려진 제약#
키보드 사용자는 이미지를 바꿀 수 없습니다. 활성 인덱스는 컴포넌트 내부 state 이고, CoreHoverGalleryContract
에는 그것을 주입하거나 알리는 필드가 없습니다(imageUrls · itemCount · hoverGalleryStyle
셋뿐). Web 에서 호출자가 attributes: {'tabindex': '0'} 와 onKeyDown 을 얹어 키 이벤트를 받더라도 바꿀 상태 진입점이 없습니다.
대안: HoverGallery 는 보조적인 미리 보기로만 쓰고, 전체 이미지에 도달하는 경로를 함께 두세요 — 카드를 눌러 Carousel 이 있는 상세 화면으로 보내는 식입니다.
Web 에서 호출자의 onClick 은 무시됩니다. build 가 component.events 를 먼저 펼친 뒤
'click' 키를 자기 핸들러로 덮어쓰기 때문입니다. 같은 이유로 eventHandlers 에 'mousemove'
를 넘겨도 덮입니다. onKeyDown · onKeyUp · onMouseEnter 처럼 키가 겹치지 않는 이벤트는 살아남습니다.
대안: 클릭을 가로채야 하면 HoverGallery 를 감싸는 바깥 요소에 핸들러를 두세요.
'동작 줄이기'가 Flutter 에서는 crossfade 를 멈추지 못합니다. Web 은 전환 duration 을 var(--coui-duration-300)
로 내보내 @media (prefers-reduced-motion:reduce) 가 0 으로 덮지만, Flutter resolveHoverGallery
는 resolveMotionDuration 을 거치지 않고 Duration 을 그대로 AnimatedSwitcher
에 넘깁니다. 그래서 OS 설정을 켜도 Flutter 쪽 crossfade 는 그대로 재생됩니다.
대안: 호출부에서
hoverGalleryStyle: CoreHoverGalleryStyle(animationDuration: Duration.zero)로 끕니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | HoverGallery | HoverGallery |
| 호버 감지 | MouseRegion + LayoutBuilder |
mousemove 이벤트 |
| 이미지 렌더링 | Image.network(fit: BoxFit.cover) |
img + inline object-fit: cover |
| 전환 애니메이션 | AnimatedSwitcher crossfade |
CSS opacity transition |
| 렌더하는 이미지 | 현재 활성 항목 | 모든 항목 레이어를 유지하고 opacity로 선택 |
관련 컴포넌트 (Related Components)#
- Carousel: 명시적 탐색 컨트롤이 있는 이미지 슬라이드
- Card: HoverGallery를 감싸 카드 레이아웃 구성에 사용
- DotIndicator: HoverGallery 아래에 현재 이미지 위치 표시