개요

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의 스택킹 컨텍스트와 유사한 시스템을 사용합니다:

기본 규칙

  1. 높은 z-index가 낮은 z-index보다 앞에 표시됩니다
  2. 같은 z-index일 때는 문서 순서(작성 순서)에 따라 결정됩니다
  3. 중첩된 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 변형과 함께 깊이감을 표현할 때