개요

IntrinsicWidth는 자식 위젯의 고유(intrinsic) 너비로 크기를 조정하는 위젯입니다.

IntrinsicWidth는 자식 위젯이 원하는 고유 너비를 가질 수 있도록 강제합니다. 이는 Column 내에서 여러 자식들의 너비를 일치시키거나, 자식의 내용에 따라 동적으로 너비를 조정해야 할 때 유용합니다. IntrinsicHeight와 마찬가지로 성능 비용이 높으므로 신중하게 사용해야 합니다.

언제 사용하나요?

  • Column의 자식들이 모두 같은 너비를 가져야 할 때
  • 자식 위젯의 내용에 따라 너비가 결정되어야 할 때
  • 다양한 너비의 위젯들을 일치시켜야 할 때
  • 버튼이나 칩의 너비를 내용에 맞춰야 할 때
  • CrossAxisAlignment.stretch가 작동하지 않을 때

기본 사용법

IntrinsicWidth({
  child: Column({
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: [
      Container({
        color: 'blue',
        height: 50,
        child: Text('짧은')
      }),
      Container({
        color: 'red',
        height: 50,
        child: Text('이것은 훨씬 더 긴 텍스트입니다')
      })
    ]
  })
})

Props

child

값: Widget | undefined

고유 너비로 크기가 조정될 자식 위젯입니다.

IntrinsicWidth({
  child: Container({
    color: 'blue',
    child: Text('내용에 맞춰 너비가 조정됩니다')
  })
})

작동 원리

IntrinsicWidth는 다음과 같이 작동합니다:

  1. 자식 위젯에게 “주어진 높이에서 당신의 최소 너비는 얼마입니까?”라고 묻습니다.
  2. 자식이 응답한 너비를 사용하여 제약 조건을 만듭니다.
  3. 이 제약 조건으로 자식을 레이아웃합니다.

이 과정은 추가적인 레이아웃 패스가 필요하므로 성능 비용이 발생합니다.

실제 사용 예제

예제 1: 동일한 너비의 버튼들

const UniformWidthButtons = () => {
  return IntrinsicWidth({
    child: Column({
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        ElevatedButton({
          onPressed: () => console.log('저장'),
          child: Text('저장')
        }),
        SizedBox({ height: 8 }),
        ElevatedButton({
          onPressed: () => console.log('저장하고 계속하기'),
          child: Text('저장하고 계속하기')
        }),
        SizedBox({ height: 8 }),
        ElevatedButton({
          onPressed: () => console.log('취소'),
          child: Text('취소')
        })
      ]
    })
  });
};

예제 2: 동적 너비의 배지

const DynamicBadge = ({ text, color }) => {
  return IntrinsicWidth({
    child: Container({
      padding: EdgeInsets.symmetric({ horizontal: 12, vertical: 6 }),
      decoration: BoxDecoration({
        color: color || 'blue',
        borderRadius: BorderRadius.circular(16)
      }),
      constraints: Constraints({ minWidth: 32 }),
      child: Center({
        child: Text(text, {
          style: TextStyle({
            color: 'white',
            fontSize: 14,
            fontWeight: 'bold'
          })
        })
      })
    })
  });
};

// 사용 예시
Row({
  children: [
    DynamicBadge({ text: '3', color: 'red' }),
    SizedBox({ width: 8 }),
    DynamicBadge({ text: '99+', color: 'orange' }),
    SizedBox({ width: 8 }),
    DynamicBadge({ text: 'NEW', color: 'green' })
  ]
})

예제 3: 드롭다운 메뉴

const DropdownMenu = ({ items, selectedIndex }) => {
  return IntrinsicWidth({
    child: Container({
      decoration: BoxDecoration({
        border: Border.all({ color: '#E0E0E0' }),
        borderRadius: BorderRadius.circular(8)
      }),
      child: Column({
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: items.map((item, index) => 
          GestureDetector({
            onTap: () => console.log(`선택: ${item}`),
            child: Container({
              padding: EdgeInsets.symmetric({ horizontal: 16, vertical: 12 }),
              decoration: BoxDecoration({
                color: index === selectedIndex ? '#E3F2FD' : 'white',
                border: index > 0 ? Border({
                  top: BorderSide({ color: '#E0E0E0' })
                }) : null
              }),
              child: Row({
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(item, {
                    style: TextStyle({
                      color: index === selectedIndex ? '#2196F3' : '#333',
                      fontWeight: index === selectedIndex ? 'bold' : 'normal'
                    })
                  }),
                  if (index === selectedIndex) Icon({
                    icon: Icons.check,
                    size: 18,
                    color: '#2196F3'
                  })
                ]
              })
            })
          })
        )
      })
    })
  });
};

예제 4: 툴팁 컨테이너

const TooltipContainer = ({ title, description }) => {
  return IntrinsicWidth({
    child: Container({
      padding: EdgeInsets.all(12),
      decoration: BoxDecoration({
        color: '#333333',
        borderRadius: BorderRadius.circular(8),
        boxShadow: [
          BoxShadow({
            color: 'rgba(0, 0, 0, 0.2)',
            blurRadius: 8,
            offset: { x: 0, y: 4 }
          })
        ]
      }),
      constraints: Constraints({ maxWidth: 250 }),
      child: Column({
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(title, {
            style: TextStyle({
              color: 'white',
              fontSize: 14,
              fontWeight: 'bold'
            })
          }),
          if (description) ...[
            SizedBox({ height: 4 }),
            Text(description, {
              style: TextStyle({
                color: 'rgba(255, 255, 255, 0.8)',
                fontSize: 12
              })
            })
          ]
        ]
      })
    })
  });
};

예제 5: 가격 테이블 헤더

const PriceTableHeader = ({ plans }) => {
  return Row({
    children: plans.map((plan, index) => 
      Expanded({
        child: IntrinsicWidth({
          child: Container({
            margin: EdgeInsets.symmetric({ horizontal: 4 }),
            padding: EdgeInsets.all(16),
            decoration: BoxDecoration({
              color: plan.featured ? '#2196F3' : '#F5F5F5',
              borderRadius: BorderRadius.circular(8),
              border: plan.featured ? null : Border.all({ color: '#E0E0E0' })
            }),
            child: Column({
              children: [
                if (plan.featured) Container({
                  padding: EdgeInsets.symmetric({ horizontal: 8, vertical: 4 }),
                  margin: EdgeInsets.only({ bottom: 8 }),
                  decoration: BoxDecoration({
                    color: '#FF9800',
                    borderRadius: BorderRadius.circular(4)
                  }),
                  child: Text('인기', {
                    style: TextStyle({
                      color: 'white',
                      fontSize: 12,
                      fontWeight: 'bold'
                    })
                  })
                }),
                Text(plan.name, {
                  style: TextStyle({
                    fontSize: 20,
                    fontWeight: 'bold',
                    color: plan.featured ? 'white' : '#333'
                  })
                }),
                SizedBox({ height: 8 }),
                Text(`₩${plan.price.toLocaleString()}`, {
                  style: TextStyle({
                    fontSize: 28,
                    fontWeight: 'bold',
                    color: plan.featured ? 'white' : '#2196F3'
                  })
                }),
                Text('/월', {
                  style: TextStyle({
                    fontSize: 14,
                    color: plan.featured ? 'rgba(255, 255, 255, 0.8)' : '#666'
                  })
                })
              ]
            })
          })
        })
      })
    )
  });
};

성능 고려사항

IntrinsicWidth는 IntrinsicHeight와 동일한 성능 특성을 가집니다:

  1. O(N²) 복잡도: 깊이 중첩된 IntrinsicWidth는 지수적으로 느려집니다.
  2. 레이아웃 패스: 추가적인 측정 단계가 필요합니다.
  3. 스크롤 뷰에서 주의: 많은 아이템이 있는 리스트에서는 피하세요.

대안

성능이 중요한 경우 다음 대안을 고려하세요:

  1. 고정 너비 사용: 가능하면 명시적인 너비를 지정
  2. ConstrainedBox: 최소/최대 너비 제약만 필요한 경우
  3. Wrap 위젯: 동적 레이아웃이 필요한 경우
  4. LayoutBuilder: 부모 크기에 따라 조정이 필요한 경우

주의사항

  • IntrinsicWidth는 성능 비용이 높으므로 꼭 필요한 경우에만 사용하세요.
  • 깊이 중첩된 IntrinsicWidth는 피하세요.
  • 무한 너비 제약 조건에서는 작동하지 않습니다.
  • Row 내에서 Expanded와 함께 사용할 때는 주의가 필요합니다.

관련 위젯

  • IntrinsicHeight: 자식의 고유 높이로 크기 조정
  • Wrap: 자동 줄바꿈이 가능한 유연한 레이아웃
  • FittedBox: 자식을 사용 가능한 공간에 맞춤
  • ConstrainedBox: 크기 제약을 적용