Flutter에서 이미지를 표시할 때 원본 이미지의 비율(aspect ratio)을 유지하면서 자동으로 너비를 조정해야 하는 경우가 있다. 특히 네트워크 이미지를 로드할 때는 이미지의 원본 크기를 미리 알 수 없기 때문에 더 어려움이 있을 수 있다. 이 글에서는 고정된 높이를 유지하면서 이미지의 비율에 따라 너비를 자동으로 조정하는 방법을 살펴본다.

문제 상황

다음과 같은 요구사항이 있다고 가정해보자:

  1. 이미지의 높이는 150픽셀로 고정되어 있다.
  2. 이미지의 너비는 원본 이미지의 비율에 맞게 자동으로 조정되어야 한다.
  3. 이미지 로딩 중에도 적절한 크기의 컨테이너를 표시해야 한다.

일반적으로 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,
)

동작 원리 설명

이 솔루션의 핵심 동작 원리는 다음과 같다:

1. 이미지 메타데이터 가져오기

_getImageInfo() 메서드는 NetworkImageImageStreamListener를 사용하여 이미지가 실제로 로드되기 전에 이미지의 원본 너비와 높이 정보를 가져온다. 이 과정은 비동기적으로 이루어진다.

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;
}

2. 비율 계산 및 적용

이미지 정보를 성공적으로 가져오면, 원본 이미지의 너비와 높이를 사용하여 종횡비(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)이 된다.

3. 로딩 중 상태 처리

이미지 메타데이터를 가져오는 동안에는 로딩 상태를 표시한다. 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("이미지 로딩 중..."),
          ],
        ),
      ),
    );
  }
)

실제 적용 사례

이 패턴은 다양한 시나리오에서 유용하게 사용할 수 있다:

  1. 이미지 갤러리 - 고정된 높이에서 이미지들이 자연스러운 비율로 표시된다.
  2. 이미지 업로드 미리보기 - 사용자가 업로드한 이미지의 비율을 유지하면서 표시한다.
  3. 배너 이미지 - 이미지의 중요한 부분이 잘리지 않도록 비율을 유지한다.

고려사항 및 최적화

1. 메모리 캐싱

동일한 이미지에 대해 반복적으로 메타데이터를 가져오는 것을 방지하기 위해 이미지 정보를 캐싱할 수 있다:

// 정적 캐시 맵 추가
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;
}

2. 에러 처리

네트워크 이미지 로드 실패에 대한 적절한 에러 처리를 추가할 수 있다:

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 디자인에서 이미지의 원본 비율을 존중하면서도 레이아웃의 일관성을 유지할 수 있다. 특히 사용자가 업로드한 이미지나 다양한 비율의 이미지를 표시해야 하는 애플리케이션에서 유용하다.

profile
프론트 요정임

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN