
이미지 비율이 자꾸 깨집니다 (BoxFit 정복)
사용자가 올린 사진이 찌그러져서 오이가 되었습니다. 디자이너는 화를 냅니다. BoxFit.cover, contain, fill의 차이를 명확히 이해하고, AspectRatio 위젯으로 완벽한 프레임을 짜봅시다.

사용자가 올린 사진이 찌그러져서 오이가 되었습니다. 디자이너는 화를 냅니다. BoxFit.cover, contain, fill의 차이를 명확히 이해하고, AspectRatio 위젯으로 완벽한 프레임을 짜봅시다.
로그인 화면을 만들었는데 키보드가 올라오니 노란 줄무늬 에러가 뜹니다. resizeToAvoidBottomInset부터 스크롤 뷰, 그리고 채팅 앱을 위한 reverse 팁까지, 키보드 대응의 모든 것을 정리해봤습니다.

안드로이드는 오는데 iOS는 조용합니다. 혹은 앱이 켜져 있을 때만 옵니다. Background/Terminated 상태 처리, APNs 인증서, 그리고 Notification Channel 설정까지 완벽하게 해결합니다.

안드로이드는 Xcode보다 낫다고요? Gradle 지옥에 빠져보면 그 말이 쏙 들어갈 겁니다. minSdkVersion 충돌, Multidex 에러, Namespace 변경(Gradle 8.0), JDK 버전 문제, 그리고 의존성 트리 분석까지 완벽하게 해결해 봅니다.

Debug에선 잘 되는데 Release에서만 죽나요? 범인은 '난독화'입니다. R8의 원리, Mapping 파일 분석, 그리고 Reflection을 사용하는 라이브러리를 지켜내는 방법(@Keep)을 정리해봤습니다.

소셜 미디어 앱을 만들 때였습니다. 사용자 프로필 사진을 보여주는 원형 아바타를 만들었는데, 테스트용으로 세로로 긴(Portrait) 사진을 넣었더니 얼굴이 홀쭉하게 찌그러졌습니다. 반대로 가로로 긴(Landscape) 풍경 사진을 넣었더니 납작하게 눌려버렸습니다.
디자이너가 달려와서 소리쳤습니다. "아니, 사진 비율을 유지해야지 이렇게 구겨버리면 어떡해요!"
저는 억울했습니다.
"아니, 위젯 크기를 width: 50, height: 50으로 정해줬으니까, 사진도 거기에 맞춰진 거잖아요?"
이것은 제가 BoxFit이라는 개념을 모르고 "이미지를 강제로 틀에 끼워 맞추려 했던" 무지함의 결과였습니다.
이미지 렌더링에는 두 가지 크기가 존재합니다.
이 서로 다른 두 크기를 어떻게 맞출 것인가? 이것을 결정하는 규칙이 BoxFit입니다.
기본값(BoxFit.fill)은 "무조건 액자에 맞춰라"입니다. 그래서 비율을 무시하고 찌그러뜨려서라도 꽉 채우는 것입니다.
"비율을 유지하면서, 액자를 꽉 채워라. 넘치는 건 잘라버려라(Crop)." 디자이너들이 가장 사랑하는 옵션입니다.
Container(
width: 200,
height: 200,
child: Image.network(
imageUrl,
fit: BoxFit.cover, // 👈 핵심!
),
)
이렇게 하면:
프로필 사진, 썸네일, 배경 이미지 등 90%의 상황에서 정답입니다.
"빈 공간이 생겨도 좋으니, 사진이 잘리면 안 된다. 원본 전체를 다 보여줘라." 상품 상세 페이지나, 미술 작품을 보여줄 때 씁니다.
Image.network(
imageUrl,
fit: BoxFit.contain,
)
이러면 200x200 액자 안에 긴 사진이 들어갈 때, 비율을 유지한 채 쏙 들어갑니다. 대신 남는 공간(Letterbox)이 투명하게 생깁니다.
만약 인스타그램 피드처럼 "가로 폭은 화면에 꽉 차고, 세로 높이는 사진 비율에 따라 알아서 변하게" 하고 싶다면?
이때는 고정된 height를 주면 안 됩니다. 대신 AspectRatio 위젯을 씁니다.
하지만 문제가 있습니다. 네트워크 이미지를 로딩하기 전까진 사진의 비율을 알 수 없다는 겁니다.
그래서 보통 서버에서 이미지 URL을 줄 때, width와 height 메타데이터를 같이 줘야 합니다.
// 서버에서 받은 데이터: { url: "...", w: 800, h: 600 }
final double aspectRatio = data.w / data.h;
AspectRatio(
aspectRatio: aspectRatio, // 800/600 = 1.33
child: Image.network(
data.url,
fit: BoxFit.cover, // 비율이 맞으므로 cover나 contain이나 똑같음
),
)
이렇게 하면, 이미지가 로딩되는 동안에도 스켈레톤(영역)이 정확한 크기로 잡혀 있어서, 로딩 후 레이아웃이 덜컹거리는(Layout Shift) 현상을 막을 수 있습니다. 이것이 훌륭한 사용자 경험(UX)의 비결입니다.
많은 분들이 CircleAvatar를 씁니다. 편하니까요.
하지만 CircleAvatar는 원래 프로필용으로 디자인된 거라, 커스텀하기가 까다롭습니다.
저는 Container + ClipRRect 조합을 추천합니다. 훨씬 유연합니다.
ClipRRect(
borderRadius: BorderRadius.circular(50), // 둥글게 깎기
child: Image.network(
imageUrl,
width: 100,
height: 100,
fit: BoxFit.cover, // 여기서도 cover 필수!
),
)
LayoutBuilder로 반응형 비율 만들기 깊이 들여다보기AspectRatio는 비율을 고정합니다(16:9 등).
하지만 "화면 너비가 400px 이상이면 4:3, 그 밑이면 1:1" 처럼 동적인 비율이 필요하다면요?
이때 LayoutBuilder가 등장합니다. 부모 위젯이 주는 크기 제약조건(Constraints)을 읽을 수 있습니다.
LayoutBuilder(
builder: (context, constraints) {
// 너비가 넓으면(태블릿) 와이드하게, 좁으면(폰) 정사각형으로
double ratio = constraints.maxWidth > 600 ? 16 / 9 : 1 / 1;
return AspectRatio(
aspectRatio: ratio,
child: Image.network(url, fit: BoxFit.cover),
);
},
)
이 패턴은 반응형 웹/앱 디자인에서 "이미지가 너무 커지는 것"을 방지할 때 필수적입니다.
패션 쇼핑몰 앱을 만들 때 겪은 일입니다.
옷 사진은 세로로 길고(Portrait), 신발 사진은 가로로 깁니다(Landscape).
이걸 일반적인 GridView에 넣으면 강제로 정사각형이 되어 이미지가 잘립니다.
디자이너는 "핀터레스트처럼 지그재그로, 원본 비율을 살려서 보여주세요"라고 합니다.
flutter_staggered_grid_view기본 GridView는 모든 아이템 크기가 같아야 합니다.
외부 패키지인 flutter_staggered_grid_view의 MasonryGridView를 써야 합니다.
이 위젯은 내부적으로 아이템 높이를 계산해서 벽돌 쌓듯이 차곡차곡 빈 공간을 채워줍니다.
이때 각 아이템은 위에서 배운 AspectRatio 위젯으로 감싸주면, 이미지 로딩 전에도 레이아웃이 잡혀서 스크롤이 매끄러워집니다.
앱 상단에 큰 이미지를 넣고, 스크롤할 때 이미지가 살짝 느리게 따라오는 Parallax(시차) 효과를 원하시나요?
CustomScrollView와 SliverAppBar를 쓰면 fit 속성이 더욱 중요해집니다.
SliverAppBar(
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
background: Image.network(
imageUrl,
fit: BoxFit.cover, // 스크롤 시 늘어났다 줄어들 때 빈틈없게!
),
),
)
여기서 BoxFit.cover를 안 쓰면, 스크롤을 당길 때(Bouncing Scroll) 이미지가 붕 떠버립니다.
패션 쇼핑몰 앱을 만들 때 겪은 일입니다.
옷 사진은 세로로 길고, 신발 사진은 가로로 깁니다.
이걸 그리드(GridView)에 예쁘게 넣어야 하는데, 디자이너는 "정사각형 타일"을 요구했습니다.
BoxFit.cover: 신발의 앞코가 잘림. 옷의 목 부분이 잘림.BoxFit.contain: 위아래 여백이 생겨서 그리드가 안 예쁨.Flutter에서는 어렵지만, Cloudinary 같은 이미지 CDN을 써서 해결했습니다.
이미지 URL에 g_auto(Gravity Auto) 파라미터를 붙여서, AI가 "중요한 피사체(얼굴, 상품)"를 중심으로 크롭하게 만들었습니다.
프론트엔드(Flutter)에서 할 수 있는 차선책은 Alignment 조정입니다.
Image.network(
userProfileUrl,
fit: BoxFit.cover,
alignment: Alignment.topCenter, // 얼굴은 보통 위에 있으니까!
)
이 한 줄 추가로 "목 잘린 사진"을 "얼굴이 나오는 사진"으로 살려냈습니다.
시나리오:
백엔드 API가 이미지 URL만 덜렁 주고, width, height 정보를 안 줍니다.
그래서 AspectRatio를 못 씁니다. 리스트 스크롤 할 때마다 이미지가 로딩되면서 레이아웃이 덜컥거립니다. (Layout Shift)
profile_card_w400_h300.jpgAspectRatio를 적용하세요.이게 바로 "이미지 최적화"의 끝판왕입니다.
BoxFit.cover 쓰면 머리가 잘리나요?"BoxFit.cover는 중앙(Center)을 기준으로 확대해서 자릅니다.
인물 사진은 얼굴이 상단에 있는 경우가 많아서, 가로로 긴 영역에 넣으면 머리가 잘려 보입니다.
해결책:
alignment 속성을 활용하세요.
Image.network(
url,
fit: BoxFit.cover,
alignment: Alignment.topCenter, // 기준점을 위로!
)
인물 사진은 topCenter, 풍경 사진은 center가 국룰입니다.
배경 이미지 위에 흰 글씨를 올리면, 밝은 사진일 때 글씨가 안 보입니다.
디자이너는 "그라데이션(Dim)"을 넣어달라고 합니다.
Stack과 Container로 검은색 투명도를 줘도 되지만, ShaderMask를 쓰면 더 섹시합니다.
ShaderMask(
shaderCallback: (rect) {
return LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black87],
).createShader(rect);
},
blendMode: BlendMode.darken,
child: Image.network(...),
)
이미지 아랫부분만 자연스럽게 어두워져서, 텍스트 가독성이 확 살아납니다.
BoxFit.cover (잘려도 됨, 꽉 차는 게 중요)BoxFit.contain (잘리면 안 됨, 여백 괜찮음)AspectRatio 사용 (서버에서 비율 정보 받아오기)ClipRRect + BoxFit.cover"사진이 찌그러졌어요"라는 말을 듣는다면, 당황하지 말고 fit 속성을 확인하세요.
당신의 UI는 이제 어떤 사진이 들어와도 우아하게 대처할 수 있습니다.