개요

Flex는 자식 위젯들을 1차원 배열로 표시하는 위젯입니다.

Flex 위젯을 사용하면 자식들이 배치되는 축(가로 또는 세로)을 제어할 수 있습니다. 이를 주축(main axis)이라고 합니다. 주축을 미리 알고 있다면 Row(가로인 경우) 또는 Column(세로인 경우)을 대신 사용하는 것을 고려해보세요.

이 위젯의 주축 방향으로 사용 가능한 공간을 채우도록 자식을 확장하려면 자식을 Expanded 위젯으로 감싸세요.

Flex 위젯은 스크롤하지 않으며, 일반적으로 사용 가능한 공간보다 많은 자식을 Flex에 배치하는 것은 오류로 간주됩니다. 위젯들이 공간이 부족할 때 스크롤할 수 있게 하려면 ListView 사용을 고려하세요.

참조: https://api.flutter.dev/flutter/widgets/Flex-class.html

언제 사용하나요?

  • Row나 Column보다 동적으로 방향을 결정해야 할 때
  • 런타임에 가로/세로 레이아웃을 변경해야 할 때
  • 반응형 디자인에서 화면 크기에 따라 방향이 바뀔 때
  • 공통 레이아웃 컴포넌트를 만들 때
  • 복잡한 레이아웃 시스템의 기반으로 사용할 때

기본 사용법

// 가로 방향 (Row와 동일)
Flex({
  direction: Axis.horizontal,
  children: [
    Container({ width: 50, height: 50, color: 'red' }),
    Container({ width: 50, height: 50, color: 'green' }),
    Container({ width: 50, height: 50, color: 'blue' })
  ]
})

// 세로 방향 (Column과 동일)
Flex({
  direction: Axis.vertical,
  children: [
    Container({ width: 100, height: 50, color: 'red' }),
    Container({ width: 100, height: 50, color: 'green' }),
    Container({ width: 100, height: 50, color: 'blue' })
  ]
})

// 동적 방향
const [isHorizontal, setIsHorizontal] = useState(true);

Flex({
  direction: isHorizontal ? Axis.horizontal : Axis.vertical,
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: widgets
})

Props

direction (필수)

값: Axis

자식들이 배치될 주축의 방향입니다.

  • Axis.horizontal: 가로 방향 (Row와 동일)
  • Axis.vertical: 세로 방향 (Column과 동일)
Flex({
  direction: Axis.horizontal,  // 가로 배치
  children: children
})

Flex({
  direction: Axis.vertical,   // 세로 배치
  children: children
})

mainAxisAlignment

값: MainAxisAlignment (기본값: MainAxisAlignment.start)

주축을 따라 자식들을 정렬하는 방법입니다.

  • MainAxisAlignment.start: 시작점에 정렬
  • MainAxisAlignment.end: 끝점에 정렬
  • MainAxisAlignment.center: 중앙 정렬
  • MainAxisAlignment.spaceBetween: 양 끝에 붙이고 사이 공간 균등 분배
  • MainAxisAlignment.spaceAround: 각 자식 주위에 균등한 공간
  • MainAxisAlignment.spaceEvenly: 모든 공간을 균등 분배
Flex({
  direction: Axis.horizontal,
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: children
})

crossAxisAlignment

값: CrossAxisAlignment (기본값: CrossAxisAlignment.center)

교차축을 따라 자식들을 정렬하는 방법입니다.

  • CrossAxisAlignment.start: 교차축 시작점
  • CrossAxisAlignment.end: 교차축 끝점
  • CrossAxisAlignment.center: 교차축 중앙
  • CrossAxisAlignment.stretch: 교차축으로 늘리기
  • CrossAxisAlignment.baseline: 텍스트 기준선 정렬
Flex({
  direction: Axis.horizontal,
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: children
})

verticalDirection

값: VerticalDirection (기본값: VerticalDirection.down)

세로 방향의 순서를 지정합니다.

  • VerticalDirection.down: 위에서 아래로
  • VerticalDirection.up: 아래에서 위로
Flex({
  direction: Axis.vertical,
  verticalDirection: VerticalDirection.up,  // 아래에서 위로
  children: children
})

mainAxisSize

값: MainAxisSize (기본값: MainAxisSize.max)

주축 방향으로 차지할 공간의 크기입니다.

  • MainAxisSize.max: 가능한 최대 공간 차지
  • MainAxisSize.min: 자식들에 필요한 최소 공간만 차지

clipped

값: boolean (기본값: false)

자식들이 Flex 영역을 벗어날 때 클리핑할지 여부입니다.

children (필수)

값: Widget[]

배치할 자식 위젯들의 배열입니다.

실제 사용 예제

예제 1: 반응형 내비게이션 바

const ResponsiveNavBar = ({ isDesktop }) => {
  return Container({
    padding: EdgeInsets.all(16),
    decoration: BoxDecoration({
      color: 'white',
      boxShadow: [
        BoxShadow({
          color: 'rgba(0,0,0,0.1)',
          blurRadius: 4,
          offset: { x: 0, y: 2 }
        })
      ]
    }),
    child: Flex({
      direction: isDesktop ? Axis.horizontal : Axis.vertical,
      mainAxisAlignment: isDesktop 
        ? MainAxisAlignment.spaceBetween 
        : MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        Text('Logo', {
          style: TextStyle({
            fontSize: 24,
            fontWeight: 'bold'
          })
        }),
        Flex({
          direction: isDesktop ? Axis.horizontal : Axis.vertical,
          mainAxisSize: MainAxisSize.min,
          children: [
            NavItem({ title: 'Home', active: true }),
            NavItem({ title: 'About' }),
            NavItem({ title: 'Services' }),
            NavItem({ title: 'Contact' })
          ]
        })
      ]
    })
  });
};

예제 2: 동적 폼 레이아웃

const DynamicFormLayout = ({ layout, fields }) => {
  return Container({
    padding: EdgeInsets.all(16),
    child: Flex({
      direction: layout === 'horizontal' ? Axis.horizontal : Axis.vertical,
      crossAxisAlignment: layout === 'horizontal' 
        ? CrossAxisAlignment.start 
        : CrossAxisAlignment.stretch,
      children: fields.map((field, index) => {
        if (layout === 'horizontal') {
          return Expanded({
            child: Container({
              margin: EdgeInsets.only({ 
                right: index < fields.length - 1 ? 16 : 0 
              }),
              child: FormField({ field })
            })
          });
        } else {
          return Container({
            margin: EdgeInsets.only({ 
              bottom: index < fields.length - 1 ? 16 : 0 
            }),
            child: FormField({ field })
          });
        }
      })
    })
  });
};

예제 3: 카드 액션 버튼

const CardActions = ({ actions, vertical = false }) => {
  return Container({
    padding: EdgeInsets.all(16),
    child: Flex({
      direction: vertical ? Axis.vertical : Axis.horizontal,
      mainAxisAlignment: vertical 
        ? MainAxisAlignment.start 
        : MainAxisAlignment.end,
      mainAxisSize: MainAxisSize.min,
      children: actions.map((action, index) => 
        Container({
          margin: vertical 
            ? EdgeInsets.only({ bottom: index < actions.length - 1 ? 8 : 0 })
            : EdgeInsets.only({ left: index > 0 ? 8 : 0 }),
          child: ElevatedButton({
            onPressed: action.onPressed,
            child: Text(action.label)
          })
        })
      )
    })
  });
};

예제 4: 통계 대시보드

const StatsDashboard = ({ stats, orientation }) => {
  return Container({
    padding: EdgeInsets.all(24),
    child: Flex({
      direction: orientation === 'horizontal' ? Axis.horizontal : Axis.vertical,
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: stats.map(stat => 
        Expanded({
          child: Container({
            margin: EdgeInsets.all(8),
            padding: EdgeInsets.all(20),
            decoration: BoxDecoration({
              color: 'white',
              borderRadius: BorderRadius.circular(12),
              boxShadow: [
                BoxShadow({
                  color: 'rgba(0,0,0,0.05)',
                  blurRadius: 6,
                  offset: { x: 0, y: 2 }
                })
              ]
            }),
            child: Column({
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(stat.value, {
                  style: TextStyle({
                    fontSize: 32,
                    fontWeight: 'bold',
                    color: stat.color
                  })
                }),
                SizedBox({ height: 8 }),
                Text(stat.label, {
                  style: TextStyle({
                    fontSize: 16,
                    color: '#666'
                  })
                })
              ]
            })
          })
        })
      )
    })
  });
};

예제 5: 적응형 이미지 갤러리

const AdaptiveGallery = ({ images, screenWidth }) => {
  const direction = screenWidth > 768 ? Axis.horizontal : Axis.vertical;
  const itemsPerRow = screenWidth > 768 ? 3 : 1;
  
  const chunks = [];
  for (let i = 0; i < images.length; i += itemsPerRow) {
    chunks.push(images.slice(i, i + itemsPerRow));
  }
  
  return SingleChildScrollView({
    child: Flex({
      direction: Axis.vertical,
      children: chunks.map(chunk => 
        Container({
          margin: EdgeInsets.only({ bottom: 16 }),
          child: Flex({
            direction: direction,
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: chunk.map(image => 
              Expanded({
                child: Container({
                  margin: EdgeInsets.symmetric({ horizontal: 8 }),
                  child: AspectRatio({
                    aspectRatio: 1.0,
                    child: Image({
                      src: image.url,
                      objectFit: 'cover'
                    })
                  })
                })
              })
            )
          })
        })
      )
    })
  });
};

Flex vs Row/Column

언제 Flex를 사용할까?

// ✅ 동적 방향이 필요할 때
Flex({
  direction: isPortrait ? Axis.vertical : Axis.horizontal,
  children: children
})

// ✅ 공통 컴포넌트를 만들 때
const FlexibleLayout = ({ direction, children }) => {
  return Flex({ direction, children });
};

// ❌ 고정된 방향일 때는 Row/Column이 더 간단
Row({ children })  // Flex({ direction: Axis.horizontal, children }) 대신
Column({ children })  // Flex({ direction: Axis.vertical, children }) 대신

주의사항

  • Flex는 스크롤하지 않으므로 오버플로우 시 ListView를 고려해야 합니다
  • 자식들이 여러 줄로 줄바꿈되지 않으므로 Wrap 위젯을 고려해야 합니다
  • 자식이 하나뿐이면 Align이나 Center를 사용하는 것이 더 적합합니다
  • direction 변경 시 레이아웃이 완전히 재계산되므로 성능을 고려해야 합니다
  • Expanded나 Flexible은 Flex의 direction과 일치하는 축에서만 작동합니다

관련 위젯

  • Row: 가로 방향 고정 Flex (direction: Axis.horizontal)
  • Column: 세로 방향 고정 Flex (direction: Axis.vertical)
  • Wrap: 자식들이 줄바꿈 가능한 레이아웃
  • ListView: 스크롤 가능한 선형 레이아웃
  • Expanded: Flex 내에서 공간을 확장하는 위젯
  • Flexible: Flex 내에서 유연한 크기를 가지는 위젯