개요

LimitedBox는 자식이 무한 제약(unbounded constraints)을 받을 때만 크기 제한을 적용하는 위젯입니다.

이 위젯은 주로 ListView, Row, Column 등의 스크롤 가능한 위젯이나 무한 공간을 제공하는 위젯 내에서 자식이 무제한으로 커지는 것을 방지할 때 사용됩니다. 부모가 이미 유한한 제약을 제공하는 경우에는 LimitedBox가 아무 작업도 하지 않습니다.

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

언제 사용하나요?

  • ListView, Column, Row 등에서 무한 확장을 방지하고 싶을 때
  • 스크롤 가능한 컨테이너 내에서 자식의 최대 크기를 제한하고 싶을 때
  • 무한 제약 환경에서만 선택적으로 크기 제한을 적용하고 싶을 때
  • 컨테이너가 너무 커지는 것을 방지하면서 유연성을 유지하고 싶을 때
  • 반응형 레이아웃에서 안전한 최대 크기를 설정하고 싶을 때

기본 사용법

// 세로 스크롤 리스트 내에서 가로 크기 제한
ListView({
  children: [
    LimitedBox({
      maxWidth: 300,
      child: Container({
        color: 'blue',
        child: Text('너비가 300으로 제한됨')
      })
    })
  ]
})

// 가로 스크롤 리스트 내에서 세로 크기 제한
SingleChildScrollView({
  scrollDirection: Axis.horizontal,
  child: Row({
    children: [
      LimitedBox({
        maxHeight: 200,
        child: Container({
          color: 'red',
          child: Text('높이가 200으로 제한됨')
        })
      })
    ]
  })
})

// 둘 다 제한
Column({
  children: [
    LimitedBox({
      maxWidth: 400,
      maxHeight: 300,
      child: Container({
        color: 'green',
        child: Text('너비 400, 높이 300으로 제한')
      })
    })
  ]
})

Props

maxWidth

값: number (기본값: Infinity)

무한 너비 제약에서 적용할 최대 너비입니다.

부모가 이미 유한한 너비 제약을 제공하는 경우, 이 값은 무시됩니다.

LimitedBox({
  maxWidth: 300,    // 무한 너비 제약에서 최대 300
  child: child
})

LimitedBox({
  maxWidth: Infinity,  // 너비 제한 없음 (기본값)
  child: child
})

maxHeight

값: number (기본값: Infinity)

무한 높이 제약에서 적용할 최대 높이입니다.

부모가 이미 유한한 높이 제약을 제공하는 경우, 이 값은 무시됩니다.

LimitedBox({
  maxHeight: 200,   // 무한 높이 제약에서 최대 200
  child: child
})

LimitedBox({
  maxHeight: Infinity,  // 높이 제한 없음 (기본값)
  child: child
})

child

값: Widget | undefined

크기 제한이 적용될 자식 위젯입니다.

실제 사용 예제

예제 1: 리스트 뷰 내 카드 크기 제한

const LimitedCardList = ({ cards }) => {
  return ListView({
    padding: EdgeInsets.all(16),
    children: cards.map(card => 
      Container({
        margin: EdgeInsets.only({ bottom: 16 }),
        child: LimitedBox({
          maxWidth: 600,  // 카드 최대 너비 제한
          child: Container({
            padding: EdgeInsets.all(20),
            decoration: BoxDecoration({
              color: 'white',
              borderRadius: BorderRadius.circular(12),
              boxShadow: [
                BoxShadow({
                  color: 'rgba(0,0,0,0.1)',
                  blurRadius: 8,
                  offset: { x: 0, y: 2 }
                })
              ]
            }),
            child: Column({
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(card.title, {
                  style: TextStyle({
                    fontSize: 18,
                    fontWeight: 'bold',
                    marginBottom: 8
                  })
                }),
                Text(card.description, {
                  style: TextStyle({
                    fontSize: 14,
                    color: '#666',
                    lineHeight: 1.4
                  })
                }),
                SizedBox({ height: 16 }),
                Row({
                  mainAxisAlignment: MainAxisAlignment.end,
                  children: [
                    TextButton({
                      onPressed: card.onAction,
                      child: Text('더보기')
                    })
                  ]
                })
              ]
            })
          })
        })
      })
    )
  });
};

예제 2: 가로 스크롤 이미지 갤러리

const HorizontalImageGallery = ({ images }) => {
  return SingleChildScrollView({
    scrollDirection: Axis.horizontal,
    padding: EdgeInsets.all(16),
    child: Row({
      children: images.map((image, index) => 
        Container({
          margin: EdgeInsets.only({ 
            right: index < images.length - 1 ? 16 : 0 
          }),
          child: LimitedBox({
            maxHeight: 300,  // 이미지 최대 높이 제한
            child: AspectRatio({
              aspectRatio: image.aspectRatio || 1.0,
              child: Container({
                decoration: BoxDecoration({
                  borderRadius: BorderRadius.circular(8),
                  boxShadow: [
                    BoxShadow({
                      color: 'rgba(0,0,0,0.2)',
                      blurRadius: 6,
                      offset: { x: 0, y: 2 }
                    })
                  ]
                }),
                child: ClipRRect({
                  borderRadius: BorderRadius.circular(8),
                  child: Image({
                    src: image.url,
                    objectFit: 'cover'
                  })
                })
              })
            })
          })
        })
      )
    })
  });
};

예제 3: 반응형 텍스트 에디터

const ResponsiveTextEditor = ({ content, onChange }) => {
  return Container({
    padding: EdgeInsets.all(16),
    child: Column({
      children: [
        // 도구모음
        Container({
          padding: EdgeInsets.all(12),
          decoration: BoxDecoration({
            color: '#F5F5F5',
            borderRadius: BorderRadius.only({
              topLeft: Radius.circular(8),
              topRight: Radius.circular(8)
            })
          }),
          child: Row({
            children: [
              IconButton({ icon: Icons.bold }),
              IconButton({ icon: Icons.italic }),
              IconButton({ icon: Icons.underline }),
              Spacer(),
              Text('${content.length}/1000', {
                style: TextStyle({ 
                  fontSize: 12, 
                  color: '#666' 
                })
              })
            ]
          })
        }),
        // 에디터 영역
        Expanded({
          child: LimitedBox({
            maxWidth: 800,   // 데스크톱에서 너무 넓어지지 않도록
            maxHeight: 600,  // 무한 높이에서 최대 높이 제한
            child: Container({
              width: double.infinity,
              padding: EdgeInsets.all(16),
              decoration: BoxDecoration({
                color: 'white',
                border: Border.all({ color: '#E0E0E0' }),
                borderRadius: BorderRadius.only({
                  bottomLeft: Radius.circular(8),
                  bottomRight: Radius.circular(8)
                })
              }),
              child: TextField({
                value: content,
                onChanged: onChange,
                multiline: true,
                maxLines: null,
                decoration: InputDecoration({
                  border: InputBorder.none,
                  hintText: '여기에 내용을 입력하세요...'
                })
              })
            })
          })
        })
      ]
    })
  });
};

예제 4: 무한 스크롤 피드

const InfiniteScrollFeed = ({ posts, onLoadMore }) => {
  return NotificationListener({
    onNotification: (notification) => {
      if (notification.metrics.pixels >= 
          notification.metrics.maxScrollExtent - 200) {
        onLoadMore();
      }
      return false;
    },
    child: ListView.builder({
      itemCount: posts.length + 1,
      itemBuilder: (context, index) => {
        if (index >= posts.length) {
          return Container({
            padding: EdgeInsets.all(16),
            child: Center({
              child: CircularProgressIndicator()
            })
          });
        }
        
        const post = posts[index];
        return Container({
          margin: EdgeInsets.symmetric({ horizontal: 16, vertical: 8 }),
          child: LimitedBox({
            maxWidth: 700,  // 포스트 최대 너비 제한
            child: Container({
              padding: EdgeInsets.all(16),
              decoration: BoxDecoration({
                color: 'white',
                borderRadius: BorderRadius.circular(12),
                border: Border.all({ color: '#E0E0E0' })
              }),
              child: Column({
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // 헤더
                  Row({
                    children: [
                      CircleAvatar({
                        radius: 20,
                        backgroundImage: NetworkImage(post.author.avatar)
                      }),
                      SizedBox({ width: 12 }),
                      Expanded({
                        child: Column({
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(post.author.name, {
                              style: TextStyle({ fontWeight: 'bold' })
                            }),
                            Text(post.timestamp, {
                              style: TextStyle({ 
                                fontSize: 12, 
                                color: '#666' 
                              })
                            })
                          ]
                        })
                      })
                    ]
                  }),
                  SizedBox({ height: 12 }),
                  // 내용
                  Text(post.content),
                  if (post.image) ...[
                    SizedBox({ height: 12 }),
                    LimitedBox({
                      maxHeight: 400,  // 이미지 최대 높이 제한
                      child: ClipRRect({
                        borderRadius: BorderRadius.circular(8),
                        child: Image({
                          src: post.image,
                          width: double.infinity,
                          objectFit: 'cover'
                        })
                      })
                    })
                  ]
                ]
              })
            })
          })
        });
      }
    })
  });
};

예제 5: 가변 그리드 레이아웃

const VariableGridLayout = ({ items }) => {
  return SingleChildScrollView({
    child: Wrap({
      spacing: 16,
      runSpacing: 16,
      children: items.map((item, index) => 
        LimitedBox({
          maxWidth: 300,   // 각 그리드 아이템 최대 너비
          maxHeight: 400,  // 각 그리드 아이템 최대 높이
          child: Container({
            width: double.infinity,
            padding: EdgeInsets.all(16),
            decoration: BoxDecoration({
              gradient: LinearGradient({
                colors: item.colors,
                begin: Alignment.topLeft,
                end: Alignment.bottomRight
              }),
              borderRadius: BorderRadius.circular(16),
              boxShadow: [
                BoxShadow({
                  color: 'rgba(0,0,0,0.1)',
                  blurRadius: 10,
                  offset: { x: 0, y: 4 }
                })
              ]
            }),
            child: Column({
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Icon({
                  icon: item.icon,
                  size: 48,
                  color: 'white'
                }),
                SizedBox({ height: 16 }),
                Text(item.title, {
                  style: TextStyle({
                    fontSize: 18,
                    fontWeight: 'bold',
                    color: 'white'
                  })
                }),
                SizedBox({ height: 8 }),
                Expanded({
                  child: Text(item.description, {
                    style: TextStyle({
                      fontSize: 14,
                      color: 'rgba(255,255,255,0.8)',
                      lineHeight: 1.4
                    })
                  })
                }),
                SizedBox({ height: 16 }),
                Align({
                  alignment: Alignment.centerRight,
                  child: ElevatedButton({
                    onPressed: item.onTap,
                    style: ElevatedButton.styleFrom({
                      backgroundColor: 'rgba(255,255,255,0.2)',
                      foregroundColor: 'white'
                    }),
                    child: Text('보기')
                  })
                })
              ]
            })
          })
        })
      )
    })
  });
};

제약 조건 동작 이해

유한 제약 vs 무한 제약

// 유한 제약 환경 (LimitedBox 효과 없음)
Container({
  width: 400,    // 명확한 너비 제약
  height: 300,   // 명확한 높이 제약
  child: LimitedBox({
    maxWidth: 200,   // 이 값은 무시됨 (부모가 이미 유한 제약)
    maxHeight: 150,  // 이 값은 무시됨 (부모가 이미 유한 제약)
    child: Container({ color: 'blue' })
  })
})

// 무한 제약 환경 (LimitedBox 효과 있음)
Column({  // Column은 세로로 무한 공간 제공
  children: [
    LimitedBox({
      maxHeight: 200,  // 이 값이 적용됨
      child: Container({ color: 'red' })
    })
  ]
})

언제 효과가 있는가?

// ✅ 효과 있는 경우들
ListView({  // 세로 무한 공간
  children: [
    LimitedBox({ maxHeight: 300, child: child })
  ]
})

Row({  // 가로 무한 공간
  children: [
    LimitedBox({ maxWidth: 200, child: child })
  ]
})

SingleChildScrollView({  // 스크롤 방향으로 무한 공간
  child: LimitedBox({ maxHeight: 400, child: child })
})

// ❌ 효과 없는 경우들
Container({
  width: 300,  // 이미 유한 제약
  child: LimitedBox({ maxWidth: 200, child: child })  // 무시됨
})

SizedBox({
  height: 200,  // 이미 유한 제약
  child: LimitedBox({ maxHeight: 100, child: child })  // 무시됨
})

주의사항

  • LimitedBox는 무한 제약에서만 작동합니다
  • 부모가 이미 유한한 제약을 제공하면 아무 효과가 없습니다
  • ConstrainedBox와는 다르게 조건부로만 작동합니다
  • 음수 값은 허용되지 않습니다
  • Infinity 값을 사용하면 제한이 없어집니다

관련 위젯

  • ConstrainedBox: 항상 제약 조건을 적용하는 위젯
  • SizedBox: 고정 크기를 지정하는 위젯
  • UnconstrainedBox: 제약을 완전히 제거하는 위젯
  • OverflowBox: 부모 크기를 무시하고 자식 크기를 허용하는 위젯
  • FractionallySizedBox: 부모 크기의 비율로 자식 크기를 설정하는 위젯