Flutter에서 이미지를 표시할 때 원본 이미지의 비율(aspect ratio)을 유지하면서 자동으로 너비를 조정해야 하는 경우가 있다. 특히 네트워크 이미지를 로드할 때는 이미지의 원본 크기를 미리 알 수 없기 때문에 더 어려움이 있을 수 있다. 이 글에서는 고정된 높이를 유지하면서 이미지의 비율에 따라 너비를 자동으로 조정하는 방법을 살펴본다.
다음과 같은 요구사항이 있다고 가정해보자:
일반적으로 AspectRatio
위젯이나 FittedBox
를 사용할 수 있지만, 네트워크 이미지의 원본 비율을 알아내는 것이 관건이다.
Flutter에서는 ImageStreamListener
를 사용하여 이미지가 로드되기 전에 이미지의 메타데이터(너비와 높이)를 가져올 수 있다. 이 정보를 바탕으로 이미지의 비율을 계산하고, 그에 맞는 너비를 지정할 수 있다.
아래는 네트워크 이미지의 비율에 따라 자동으로 너비를 조정하는 위젯의 전체 구현 예제다:
import 'dart:async';
import 'package:flutter/material.dart';
class AspectRatioNetworkImage extends StatelessWidget {
final String imageUrl;
final double fixedHeight;
final BoxFit fit;
final Widget? placeholder;
final double defaultWidth;
const AspectRatioNetworkImage({
Key? key,
required this.imageUrl,
this.fixedHeight = 150.0,
this.fit = BoxFit.cover,
this.placeholder,
this.defaultWidth = 250.0,
}) : super(key: key);
Widget build(BuildContext context) {
return FutureBuilder<ImageInfo>(
future: _getImageInfo(imageUrl),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
// 이미지 정보를 바탕으로 비율 계산
final double aspectRatio = snapshot.data!.image.width / snapshot.data!.image.height;
// 고정된 높이를 기준으로 너비 계산
final calculatedWidth = fixedHeight * aspectRatio;
return Container(
height: fixedHeight,
width: calculatedWidth,
child: Image.network(
imageUrl,
height: fixedHeight,
width: calculatedWidth,
fit: fit,
),
);
} else {
// 로딩 중 상태
return LayoutBuilder(
builder: (context, constraints) {
// 부모 위젯의 제약조건 확인하여 로딩 중 너비 결정
final parentWidth = constraints.maxWidth;
final loadingWidth = parentWidth > 0 ? parentWidth * 0.8 : defaultWidth;
return Container(
height: fixedHeight,
width: loadingWidth,
child: placeholder ?? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 8),
Text("이미지 로딩 중..."),
],
),
),
);
}
);
}
},
);
}
// 이미지 정보를 가져오는 함수
Future<ImageInfo> _getImageInfo(String url) async {
final Completer<ImageInfo> completer = Completer();
final ImageStream stream = NetworkImage(url).resolve(const ImageConfiguration());
final ImageStreamListener listener = ImageStreamListener(
(ImageInfo info, bool _) => completer.complete(info),
onError: (dynamic exception, StackTrace? stackTrace) {
completer.completeError(exception);
},
);
stream.addListener(listener);
return completer.future;
}
}
이 위젯을 사용하는 방법은 다음과 같다:
// 기본 사용법
AspectRatioNetworkImage(
imageUrl: 'https://picsum.photos/1200/800',
fixedHeight: 150.0,
)
// 커스텀 로딩 플레이스홀더 사용
AspectRatioNetworkImage(
imageUrl: 'https://picsum.photos/1200/800',
fixedHeight: 200.0,
fit: BoxFit.contain,
placeholder: Center(child: Text('Loading...')),
defaultWidth: 300.0,
)
이 솔루션의 핵심 동작 원리는 다음과 같다:
_getImageInfo()
메서드는 NetworkImage
와 ImageStreamListener
를 사용하여 이미지가 실제로 로드되기 전에 이미지의 원본 너비와 높이 정보를 가져온다. 이 과정은 비동기적으로 이루어진다.
Future<ImageInfo> _getImageInfo(String url) async {
final Completer<ImageInfo> completer = Completer();
final ImageStream stream = NetworkImage(url).resolve(const ImageConfiguration());
final ImageStreamListener listener = ImageStreamListener(
(ImageInfo info, bool _) => completer.complete(info),
onError: (dynamic exception, StackTrace? stackTrace) {
completer.completeError(exception);
},
);
stream.addListener(listener);
return completer.future;
}
이미지 정보를 성공적으로 가져오면, 원본 이미지의 너비와 높이를 사용하여 종횡비(aspect ratio)를 계산한다:
final double aspectRatio = snapshot.data!.image.width / snapshot.data!.image.height;
이 비율을 사용하여 고정된 높이에 맞는 너비를 계산한다:
final calculatedWidth = fixedHeight * aspectRatio;
예를 들어, 원본 이미지가 1200×800 픽셀이라면 종횡비는 1.5가 된다. 고정 높이가 150픽셀이라면 계산된 너비는 225픽셀(150 * 1.5)이 된다.
이미지 메타데이터를 가져오는 동안에는 로딩 상태를 표시한다. LayoutBuilder
를 사용하여 부모 위젯의 제약조건을 확인하고, 적절한 너비를 결정한다:
LayoutBuilder(
builder: (context, constraints) {
final parentWidth = constraints.maxWidth;
final loadingWidth = parentWidth > 0 ? parentWidth * 0.8 : defaultWidth;
return Container(
height: fixedHeight,
width: loadingWidth,
child: placeholder ?? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 8),
Text("이미지 로딩 중..."),
],
),
),
);
}
)
이 패턴은 다양한 시나리오에서 유용하게 사용할 수 있다:
동일한 이미지에 대해 반복적으로 메타데이터를 가져오는 것을 방지하기 위해 이미지 정보를 캐싱할 수 있다:
// 정적 캐시 맵 추가
static final Map<String, ImageInfo> _imageInfoCache = {};
// _getImageInfo 메서드 수정
Future<ImageInfo> _getImageInfo(String url) async {
// 캐시된 정보가 있으면 반환
if (_imageInfoCache.containsKey(url)) {
return _imageInfoCache[url]!;
}
final Completer<ImageInfo> completer = Completer();
final ImageStream stream = NetworkImage(url).resolve(const ImageConfiguration());
final ImageStreamListener listener = ImageStreamListener(
(ImageInfo info, bool _) {
_imageInfoCache[url] = info; // 캐시에 저장
completer.complete(info);
},
onError: (dynamic exception, StackTrace? stackTrace) {
completer.completeError(exception);
},
);
stream.addListener(listener);
return completer.future;
}
네트워크 이미지 로드 실패에 대한 적절한 에러 처리를 추가할 수 있다:
FutureBuilder<ImageInfo>(
// ...
builder: (context, snapshot) {
if (snapshot.hasError) {
return Container(
height: fixedHeight,
width: defaultWidth,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error, color: Colors.red),
SizedBox(height: 8),
Text("이미지를 불러올 수 없습니다"),
],
),
),
);
}
// ...
}
)
Flutter에서 네트워크 이미지의 원본 비율을 유지하면서 높이를 고정하고 너비를 자동으로 조정하는 방법을 알아보았다. 이 접근 방식은 ImageStreamListener
를 사용하여 이미지의 메타데이터를 먼저 가져온 후, 계산된 비율에 따라 컨테이너의 크기를 조정하는 방식으로 작동한다.
이 기술을 사용하면 UI 디자인에서 이미지의 원본 비율을 존중하면서도 레이아웃의 일관성을 유지할 수 있다. 특히 사용자가 업로드한 이미지나 다양한 비율의 이미지를 표시해야 하는 애플리케이션에서 유용하다.