개요
ZIndex
는 자식 위젯의 레이어 순서(z-index)를 제어하는 위젯입니다. Flutter 자체에는 없는 Flitter만의 고유한 위젯으로, CSS의 z-index와 유사한 방식으로 위젯의 앞뒤 순서를 결정할 수 있습니다.
다른 위젯 위에 특정 위젯을 표시하거나, 복잡한 레이어 구조를 만들 때 사용합니다. Stack 위젯과 함께 사용하면 더욱 정교한 레이아웃을 구현할 수 있습니다.
언제 사용하나요?
- 위젯의 렌더링 순서를 명시적으로 제어해야 할 때
- 팝업이나 오버레이를 다른 위젯 위에 표시할 때
- 복잡한 레이어 구조에서 특정 위젯을 앞으로 가져와야 할 때
- 드래그 앤 드롭 시 선택된 요소를 가장 위로 올려야 할 때
- 다이어그램이나 게임에서 요소들의 깊이를 관리할 때
기본 사용법
import { ZIndex, Container, Stack } from '@meursyphus/flitter';
// 기본 z-index 적용
const LayeredWidget = Stack({
children: [
Container({
width: 200,
height: 200,
color: '#e74c3c'
}),
ZIndex({
zIndex: 10,
child: Container({
width: 100,
height: 100,
color: '#3498db'
})
})
]
});
Props
zIndex (필수)
값: number
위젯의 레이어 순서를 나타내는 숫자입니다. 높은 값일수록 앞쪽(위쪽)에 렌더링됩니다.
- 양수: 기본 레이어보다 앞쪽에 표시
- 0: 기본 레이어 (기본값)
- 음수: 기본 레이어보다 뒤쪽에 표시
// 가장 앞에 표시
zIndex: 9999
// 기본 레이어
zIndex: 0
// 가장 뒤에 표시
zIndex: -1
child (선택)
값: Widget
z-index가 적용될 자식 위젯입니다. 하나의 자식만 가질 수 있습니다.
스택킹 컨텍스트 (Stacking Context)
ZIndex는 CSS의 스택킹 컨텍스트와 유사한 시스템을 사용합니다:
기본 규칙
- 높은 z-index가 낮은 z-index보다 앞에 표시됩니다
- 같은 z-index일 때는 문서 순서(작성 순서)에 따라 결정됩니다
- 중첩된 ZIndex는 부모의 스택킹 컨텍스트에 제한됩니다
컨텍스트 격리
// 부모의 zIndex가 1이면, 자식의 zIndex 9999도
// 다른 형제의 zIndex 2보다 뒤에 표시될 수 있습니다
ZIndex({
zIndex: 1,
child: ZIndex({
zIndex: 9999,
child: Container({ color: 'red' })
})
})
실제 사용 예제
예제 1: 기본 레이어링
import { ZIndex, Container, Stack, Positioned } from '@meursyphus/flitter';
const BasicLayering = Stack({
children: [
// 배경 (z-index 없음, 기본값 0)
Container({
width: 300,
height: 300,
color: '#ecf0f1'
}),
// 중간 레이어
Positioned({
top: 50,
left: 50,
child: ZIndex({
zIndex: 1,
child: Container({
width: 200,
height: 200,
color: '#3498db',
child: Center({
child: Text('중간 레이어', {
style: { color: 'white', fontSize: 16 }
})
})
})
})
}),
// 최상위 레이어
Positioned({
top: 100,
left: 100,
child: ZIndex({
zIndex: 10,
child: Container({
width: 100,
height: 100,
color: '#e74c3c',
child: Center({
child: Text('최상위', {
style: { color: 'white', fontSize: 14 }
})
})
})
})
}),
// 배경 아래 (음수 z-index)
Positioned({
top: 25,
left: 25,
child: ZIndex({
zIndex: -1,
child: Container({
width: 50,
height: 50,
color: '#95a5a6',
child: Center({
child: Text('뒤', {
style: { color: 'white', fontSize: 12 }
})
})
})
})
})
]
});
예제 2: 드롭다운 메뉴
import { ZIndex, Container, Column, GestureDetector } from '@meursyphus/flitter';
class DropdownMenu extends StatefulWidget {
createState() {
return new DropdownMenuState();
}
}
class DropdownMenuState extends State {
isOpen = false;
toggleDropdown = () => {
this.setState(() => {
this.isOpen = !this.isOpen;
});
};
build() {
return Stack({
children: [
// 메인 버튼
GestureDetector({
onTap: this.toggleDropdown,
child: Container({
padding: EdgeInsets.symmetric({ horizontal: 16, vertical: 12 }),
decoration: new BoxDecoration({
color: '#3498db',
borderRadius: BorderRadius.circular(4)
}),
child: Row({
mainAxisSize: MainAxisSize.min,
children: [
Text('메뉴', { style: { color: 'white' } }),
SizedBox({ width: 8 }),
Icon(
this.isOpen ? Icons.arrow_drop_up : Icons.arrow_drop_down,
{ color: 'white' }
)
]
})
})
}),
// 드롭다운 메뉴 (높은 z-index로 다른 요소 위에 표시)
if (this.isOpen)
Positioned({
top: 50,
left: 0,
child: ZIndex({
zIndex: 1000, // 높은 z-index로 모든 요소 위에 표시
child: Container({
width: 150,
decoration: new BoxDecoration({
color: 'white',
borderRadius: BorderRadius.circular(4),
boxShadow: [
new BoxShadow({
color: 'rgba(0, 0, 0, 0.1)',
offset: new Offset({ x: 0, y: 2 }),
blurRadius: 8
})
]
}),
child: Column({
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildMenuItem('옵션 1'),
_buildMenuItem('옵션 2'),
_buildMenuItem('옵션 3')
]
})
})
})
})
]
});
}
_buildMenuItem(text: string) {
return GestureDetector({
onTap: () => {
console.log(`${text} 선택됨`);
this.toggleDropdown();
},
child: Container({
padding: EdgeInsets.all(12),
child: Text(text, { style: { fontSize: 14 } })
})
});
}
}
예제 3: 모달 다이얼로그
import { ZIndex, Container, Stack, Positioned, GestureDetector } from '@meursyphus/flitter';
const Modal = ({ isVisible, onClose, children }) => {
if (!isVisible) return null;
return Stack({
children: [
// 배경 오버레이 (중간 z-index)
Positioned.fill({
child: ZIndex({
zIndex: 100,
child: GestureDetector({
onTap: onClose,
child: Container({
color: 'rgba(0, 0, 0, 0.5)'
})
})
})
}),
// 모달 컨텐츠 (가장 높은 z-index)
Center({
child: ZIndex({
zIndex: 200,
child: Container({
width: 300,
padding: EdgeInsets.all(24),
decoration: new BoxDecoration({
color: 'white',
borderRadius: BorderRadius.circular(12),
boxShadow: [
new BoxShadow({
color: 'rgba(0, 0, 0, 0.2)',
offset: new Offset({ x: 0, y: 4 }),
blurRadius: 16
})
]
}),
child: children
})
})
})
]
});
};
예제 4: 드래그 가능한 카드 시스템
import { ZIndex, Container, GestureDetector } from '@meursyphus/flitter';
class DraggableCard extends StatefulWidget {
constructor(
private color: string,
private initialZIndex: number,
private title: string
) {
super();
}
createState() {
return new DraggableCardState();
}
}
class DraggableCardState extends State {
isDragging = false;
position = new Offset({ x: 0, y: 0 });
get currentZIndex() {
// 드래그 중일 때는 가장 높은 z-index 사용
return this.isDragging ? 9999 : this.widget.initialZIndex;
}
build() {
return Positioned({
left: this.position.x,
top: this.position.y,
child: ZIndex({
zIndex: this.currentZIndex,
child: GestureDetector({
onPanStart: (details) => {
this.setState(() => {
this.isDragging = true;
});
},
onPanUpdate: (details) => {
this.setState(() => {
this.position = this.position.plus(details.delta);
});
},
onPanEnd: (details) => {
this.setState(() => {
this.isDragging = false;
});
},
child: Container({
width: 120,
height: 80,
decoration: new BoxDecoration({
color: this.widget.color,
borderRadius: BorderRadius.circular(8),
boxShadow: this.isDragging ? [
new BoxShadow({
color: 'rgba(0, 0, 0, 0.3)',
offset: new Offset({ x: 0, y: 8 }),
blurRadius: 16
})
] : [
new BoxShadow({
color: 'rgba(0, 0, 0, 0.1)',
offset: new Offset({ x: 0, y: 2 }),
blurRadius: 4
})
]
}),
child: Center({
child: Text(this.widget.title, {
style: {
color: 'white',
fontWeight: 'bold',
fontSize: 14
}
})
})
})
})
})
});
}
}
// 사용 예제
const DraggableCards = Stack({
children: [
DraggableCard('#e74c3c', 1, '카드 1'),
DraggableCard('#3498db', 2, '카드 2'),
DraggableCard('#2ecc71', 3, '카드 3')
]
});
예제 5: 툴팁 시스템
import { ZIndex, Container, Positioned, GestureDetector } from '@meursyphus/flitter';
class TooltipWidget extends StatefulWidget {
constructor(
private tooltip: string,
private child: Widget
) {
super();
}
createState() {
return new TooltipWidgetState();
}
}
class TooltipWidgetState extends State {
showTooltip = false;
build() {
return Stack({
children: [
// 기본 위젯
GestureDetector({
onLongPress: () => this.setState(() => {
this.showTooltip = true;
}),
onTapDown: () => this.setState(() => {
this.showTooltip = false;
}),
child: this.widget.child
}),
// 툴팁 (매우 높은 z-index)
if (this.showTooltip)
Positioned({
bottom: 0,
left: 0,
child: ZIndex({
zIndex: 9999,
child: Container({
padding: EdgeInsets.symmetric({
horizontal: 8,
vertical: 4
}),
decoration: new BoxDecoration({
color: 'rgba(0, 0, 0, 0.8)',
borderRadius: BorderRadius.circular(4)
}),
child: Text(this.widget.tooltip, {
style: {
color: 'white',
fontSize: 12
}
})
})
})
})
]
});
}
}
예제 6: 게임 오브젝트 레이어링
import { ZIndex, Container, Stack } from '@meursyphus/flitter';
const GameScene = Stack({
children: [
// 배경 레이어 (가장 뒤)
ZIndex({
zIndex: -10,
child: Container({
width: 800,
height: 600,
decoration: new BoxDecoration({
gradient: LinearGradient({
colors: ['#87CEEB', '#98FB98']
})
})
})
}),
// 지형 레이어
ZIndex({
zIndex: 0,
child: TerrainWidget()
}),
// 게임 오브젝트 레이어
ZIndex({
zIndex: 10,
child: PlayerWidget()
}),
// 적 캐릭터 레이어
ZIndex({
zIndex: 15,
child: EnemyWidget()
}),
// 파티클 효과 레이어
ZIndex({
zIndex: 20,
child: ParticleEffectWidget()
}),
// UI 레이어 (가장 앞)
ZIndex({
zIndex: 100,
child: GameUIWidget()
})
]
});
주의사항
- 성능: 과도한 ZIndex 사용은 렌더링 성능에 영향을 줄 수 있습니다
- 복잡성: 중첩된 스택킹 컨텍스트는 예상과 다른 결과를 만들 수 있습니다
- 디버깅: z-index가 예상대로 작동하지 않을 때는 스택킹 컨텍스트 계층을 확인하세요
- 접근성: 시각적 순서와 포커스 순서가 다를 수 있으므로 접근성을 고려하세요
- 일관성: 프로젝트 전체에서 z-index 값의 체계를 일관되게 유지하세요
관련 위젯
- Stack: 위젯들의 공간적 위치를 관리할 때
- Positioned: Stack 내에서 절대 위치를 지정할 때
- Overlay: 앱 전체에 걸친 오버레이가 필요할 때
- Transform: 3D 변형과 함께 깊이감을 표현할 때