Image class
이미지를 표시하는 위젯입니다.
공식 문서 코드
앱을 제작할 때에 필수불가결하게 있어야 하는 요소가 있는데 뭐니 뭐니 해도 바로 Image, 즉 사진이다. 앱에 이 사진이 어떤 종류 든 간에 일단 들어가야 보는 맛이 난다. 없는 앱이 있다면 진짜 간소한 기능만 가지고 있는 앱이거나 사진이 필요 없을 정도로 기본에 충실하게 디자인을 완벽하게 해 놓은 앱일 것이다. Flutter 를 배우게 되면 가장 먼저 배우는 위젯 중에 하나가 Image 위젯인데 한번 거기서 조금만 더 심도 있게 Image 위젯에 대하여 알아보겠다.
하위 속성
| 속성명 | 타입 | 기본값 | 설명 |
| image | ImageProvider | – | 표시할 이미지의 소스를 정의함 |
| width | double? | null | 이미지의 너비를 지정함 |
| height | double? | null | 이미지의 높이를 지정함 |
| fit | BoxFit? | null | 이미지를 주어진 크기에 어떻게 맞출지 정의함 |
| alignment | AlignmentGeometry | Alignment.center | 이미지를 자식 영역 내에서 정렬하는 방법 |
| repeat | ImageRepeat | ImageRepeat.noRepeat | 이미지를 반복할지 여부를 지정함 |
| color | Color? | null | 이미지에 색상을 입혀 혼합할 수 있음 |
| colorBlendMode | BlendMode? | BlendMode.srcIn | color 속성과의 혼합 방식을 정의함 |
| opacity | Animation<double>? | null | 이미지의 불투명도(애니메이션 가능)를 지정함 |
| filterQuality | FilterQuality | FilterQuality.medium | 렌더링 품질을 설정함 |
| frameBuilder | ImageFrameBuilder? | null | 프레임 로딩 시 사용자 정의 위젯을 렌더링하는 함수 |
| loadingBuilder | ImageLoadingBuilder? | null | 이미지 로딩 중 표시할 위젯을 정의함 |
| errorBuilder | ImageErrorWidgetBuilder? | null | 이미지 로딩 실패 시 대체할 위젯을 정의함 |
| semanticLabel | String? | null | 접근성을 위한 이미지 설명 텍스트 |
| excludeFromSemantics | bool | false | 접근성 설명에서 제외할지 여부 |
| matchTextDirection | bool | false | RTL 텍스트 방향일 때 이미지 좌우 반전 여부 |
| gaplessPlayback | bool | false | 이미지 소스 변경 시 이전 이미지 유지 여부 |
| centerSlice | Rect? | null | 9-패치 렌더링을 위한 중심 사각형 영역 지정 |
| isAntiAlias | bool | false | 회전 시 이미지의 톱니현상 방지를 위한 안티앨리어싱 여부 |
Image 위젯을 사용하는 방법은 크게 두 가지로 나눌 수 있다.
첫 째는 Image 위젯 그대로 사용하는 것이다.
Image(image: AssetImage('assets/faker.jpeg'))
Image 위젯은 필수 파라미터로 image: 를 주는데 여기서 본인이 사용할 이미지 형식에 맞는 위젯(ex:AssetImage)을 사용해 주면 된다.
둘 째는 Image의 생성자를 사용하는 방법이다.
Image.asset('assets/faker.jpeg')
일반적으로는 첫 번째 코드처럼 사용하기보단 두 번째 코드처럼 생성자를 사용하여 빠르게 사용하는 편이다.
그렇다면 어떠한 이미지 형식이 있고 어떻게 사용해야 할까?
network
url 로 이미지를 불러와서 사용하는 형식이다.
api 같은 것으로 받아 올 때 주로 사용하게 되며 Flutter 공식 문서에서는 이미지와 관련되어 있는 예제를 보여줄 때 미리 깃허브에 만들어둔 이미지를 url 로 불러와 사용해주고 있다.
먼저 Image 위젯 그대로 사용하기 위해선 NetWorkImage 위젯을 사용해줘야 한다.
Image(image: NetworkImage('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg'),)
나는 잘 안 쓰게 되는데 예제에서 가끔씩 이 형식으로 쓰는 경우가 있긴 하다.
밑은 Image.network 생성자로 사용하는 방식이다.
Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg')
network 형식의 이미지는 결국에는 '불러오는 것' 이기 때문에 필연적으로 불러오는 시간이 생길 수밖에 없다.
그렇다면 그 불러오는 시간 동안 그 이미지 위젯이 위치하고 있는 곳은 빈 공간, 즉 아무것도 보여주지 않게 되는데 이는 사용자 입장에서 좀 대충 만든 느낌을 줄 수 있다. (실제로 그런 거긴 하다.)

이럴 때에는 이미지에 로더를 만들어줘야 하는데 즉 흔히 생각하는 로딩바, 이미지를 보여주기 전에 보여줄 간단한 컬러 박스를 만들어줘야 한다.
그렇다면 로더 동작은 어떻게 만들까? 간단하다. Image 위젯이나 Image.network 생성자의 하위 파라미터로 로더를 제~발 만들어 달라고 loadingBuilder 를 그냥 준다. 그거 사용하면 되는데 아래와 같이 하면 된다.
Image.network(
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
loadingBuilder: (context, child, loadingProgress) {
return loadingProgress == null ? child : CircularProgressIndicator();
},
)
위 코드는 현재 이러한 동작을 하고 있다.
이미지가 아직 다 불러와지지 않았다면 '그렇다면 CircularProgressIndicator 위젯을 화면에 보여줄 거임' 이러는 거고 이미지가 다 불러와졌다면 '다 불러와졌으면 child 보여줄거임' 이러는 동작의 코드이다.

loadingBuilder 는 이름처럼 빌더함수인데 인자값으로 context, child, loadingProgress 를 준다.
설명을 하자면 context 는 일반적인 BuildContext 로 위젯트리에 접근할 때 사용하게 된다.
지금 현재 코드에서는 그냥 CircularProgressIndicator 위젯을 사용하고 있지만 안에서 위의 유튜브 스크린샷처럼 컨테이너를 직접 제작해도 된다.
child 는 이미지 로딩이 완료되었을 때 최종적으로 표시될 Image 위젯으로 네트워크 이미지가 완전히 로드되면 이 child 가 화면에 표시 되게 된다. 즉 우리가 불러오고 싶은 네트워크 이미지가 child 라는 소리이다.
loadingProgress 는 이미지가 로드 중일 때 로딩 상태를 나타내는데 이미지가 로드 중이라면 로딩 중이면 non-null, 로딩이 완료되면 null 상태를 가지게 된다. 따라서 일반적으로 loadingProgress == null 여부로 로딩 완료를 판단하게 된다.
이 말은 즉슨 밑의 코드도 위의 코드와 100% 일치하는 동작을 한다는 것이다.
Image(
image: NetworkImage('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg'),
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return CircularProgressIndicator();
},
)

asset
일반적으로 가장 많이 사용하는 이미지 형식이다.
프로젝트 내에 저장되어 있는 이미지를 직접적으로 불러와 빠르게 이미지를 보여줄 수 있다.
단, dartpad 같은 환경에서는 이미지를 저장해 놓은 레포지토리 같은 것이 존재하지 않기 때문에 사용할 수 없다.
사용하기 위해선 프로젝트 내의 pupspec.yaml 파일 안에서 이미지를 사용해야 한다고 선언을 해줘야 한다.

완전 생초보자 기준으로 설명하겠다.
프로젝트를 만들면 기본적으로 위 사진과 같은 구조를 가지고 있을 것인데 여기서 프로젝트 디렉토리를 우클릭해서 New 를 클릭해줘야 한다. (가장 위에 위치해 있는 게 프로젝트 디렉토리이다.)
New 에서 Directory 를 선택하면 이름을 정할 수 있게 되는데 여기서 이름을 assets 으로 지정해 준다.

이렇게 새로운 디렉토리를 만들었다면 assets 디렉토리가 생성이 되었을 것인데 이제 이것을 pupspec.ymal 에서 사용해 주겠다가 선언을 해놔야 한다.

이제 이 안에서 assets 라고 적힌 부분을 찾아주자.
![]() |
![]() |
처음엔 왼쪽 사진처럼 되어 있을 텐데 주석(#)을 풀고 오른쪽 사진처럼 적어주면 "사진은 내 프로젝트 폴더에 안에 바로 위치해 있는 assets 폴더를 사용해 주겠다~"라고 작성한 것이다.
assets:
- assets/
pupspec.yaml 파일은 띄어쓰기가 굉장히 중요하기 때문에 꼭 지키고 사용하자. (두 칸씩)
이제 이러면 준비는 끝났다. 사용하고 싶은 이미지를 assets 안에다가 넣어주면 된다.

이제 드디어 사용을 해줄 건데 Image 위젯 자체를 사용하려면 AssetImage 라는 위젯을 사용해줘야 한다.
Image(image: AssetImage('assets/faker.jpeg'),)
근데 이러면 조금 귀찮으니 보통은 Image.asset 을 바로 사용해 준다.
Image.asset('assets/faker.jpeg')
이제 끝이다. 솔직히 assets 는 뭐 내가 초보자 기준으로 설명해서 그렇지 젤 별 거 없다.

file
로컬 파일 시스템에서 이미지를 불러올 때 사용하는 형식으로 즉, 사용자의 파일에 접근하여 불러오는 형식이다.
이는 SNS 에 이미지를 업로드할 때 가장 많이 사용하는 방식의 이미지 형식이다.
이 형식은 먼저 File 위젯, 객체를 이해하고 있어야 한다. 간단하게 설명하자면 File 은 dart:io 의 File 클래스를 사용하며, 로컬에 존재하는 파일 경로를 통해 이미지를 불러오게 된다. File 생성자에 전달된 경로는 절대경로나 앱 내의 경로여야 한다.
당연하겠지만 dartpad 같은 web 환경에서는 사용하지 못한다. (로컬 파일에 접근해야 하기 때문에)
사용하는 방법은 조금은 복잡하다. 대표적으로 사용하는 방법 두 가지가 있다.
먼저 첫 번째 방법으로는 image_picker 패키지를 사용하여 선택한 이미지를 파일로 불러오는 방법이다.

먼저 pup.dev 에서 image_picker 를 복사하여 pupspec.yaml 에다가 넣어줘야 한다.
굳이 이렇게 하기 싫다면 그냥 터미널에서 바로 pup add 시켜도 된다.
flutter pub add image_picker

암튼 이렇게 가져와서 pubspec.yaml 에 정상적으로 들어갔다면 이제 사용해주면 된다.
(전체코드)
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
void main() => runApp(MaterialApp(home: Test()));
class Test extends StatefulWidget {
const Test({super.key});
@override
State<Test> createState() => _TestState();
}
class _TestState extends State<Test> {
File? imageFile;
Future<void> pickImage() async {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() {
imageFile = File(image.path);
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: imageFile != null ?
Image(image: FileImage(imageFile!)) : Text("이미지를 선택해 주세요"),
),
floatingActionButton: FloatingActionButton(
onPressed: pickImage,
child: Icon(Icons.image),
),
);
}
}
File? imageFile;
먼저 가져올 이미지를 넣을 File 타입의 변수를 선언해 준다.
Future<void> pickImage() async {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() {
imageFile = File(image.path);
});
}
}
그리고 pickImage() 라는 메서드를 만들어주면 되는데 이 메서드는 비동기적으로 실행되고 갤러리에 접근하여 파일을 선택할 수 있게 된다. 그리고 선택된 그 파일을 방금 위에서 만들었던 imageFile 에 넣게 만들어주면 된다.
imageFile = File(image.path); // File 형식: 가져온 이미지의 path(경로)
이제 사용할 때에는 Image.file 생성자를 활용하여 바로 만들어둔 파일타입의 변수를 넣어주면 된다.
Image.file(imageFile!)
// 당연하게도 그냥 이것도 완벽하게 일치하는 코드이니 취향대로 사용
Image(image: FileImage(imageFile!))
암튼 이렇게 이미지를 넣어줄 건데 pickImage 를 실행하기 전에는 imageFile 은 계속 null 인 상태일 것이다. 이는 즉 화면에 표시될 이미지가 없다는 것인데 그것을 방지하기 위해서 삼항연사자를 활용하여 텍스트를 띄워주면 된다.
imageFile != null ?Image(image: FileImage(imageFile!)) : Text("이미지를 선택해 주세요")
이러면 아주 간단하게 사용자가 선택한 이미지를 가져오는 동작이 완성된다.

두 번째 방법으로는 직접적으로 로컬에 접근해서 가져오는 방법이다. (굉~장히 비추천하기 때문에 눈으로만 보는 걸 추천한다.)
근데 flutter 에서는 이미지를 가져올 권한이 없기 때문에 패키지 없이 Native 와 연동해서 사용해 줄 것이다.
이렇게 직접적으로 사용할 경우 당연하겠지만 경로는 직접 적어야 한다.
안드로이드 에뮬레이터의 다운로드 폴더 안에 있는 이미지에 접근해 보겠다.

근데 일단 안드로이드 환경에서 접근하려면 또 해야 되는 것이 있는데 바로 AndroidManifest.xml 에서 퍼미션을 추가해줘야 한다.

<manifest> 안에 다가 넣어주면 된다.
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
근데 Android 13(API 33) 이상이라면 READ_MEDIA_IMAGES 또는 READ_MEDIA_VIDEO 로 대체해야 한다.
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
내 AVD 환경은 API 33 이상이기 때문에 밑의 코드를 써주겠다.

그리고 이제 앱을 딱 열면 액세스 권한 요청을 해줘야 한다. 그 요청을 위해 코틀린 코드를 작성을 해주겠다.
(전체코드)
package com.example.untitled3
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example.permission/channel"
private val REQUEST_CODE = 1001
private var resultCallback: MethodChannel.Result? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "requestStoragePermission") {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
Manifest.permission.READ_MEDIA_IMAGES
else
Manifest.permission.READ_EXTERNAL_STORAGE
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
resultCallback = result
ActivityCompat.requestPermissions(this, arrayOf(permission), REQUEST_CODE)
} else {
result.success(true)
}
} else {
result.notImplemented()
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE) {
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
resultCallback?.success(granted)
resultCallback = null
}
}
}
그리고 이제 화면에 실질적으로 표시해 줄 dart 코드이다,
(전체코드)
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MaterialApp(home: Test()));
class Test extends StatefulWidget {
const Test({super.key});
@override
State<Test> createState() => _TestState();
}
class _TestState extends State<Test> {
File? imageFile;
@override
void initState() {
super.initState();
requestAndLoadImage();
}
Future<void> requestAndLoadImage() async {
const channel = MethodChannel('com.example.permission/channel');
final bool granted = await channel.invokeMethod('requestStoragePermission');
if (!granted) return;
final file = File('/storage/emulated/0/Download/pepe-the-frog.jpg');
final exists = await file.exists();
if (!exists) return;
setState(() {
imageFile = file;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: imageFile != null ?
Image.file(imageFile!) : const Text('이미지를 불러오는 중이거나 없습니다'),
),
);
}
}
안드로이드 애뮬레이터의 기본적인 다운로드 폴더 경로는 이렇게 된다.
File('/storage/emulated/0/Download/pepe-the-frog.jpg');
이 경로에다가 접근할 파일명을 적어주면 되는데 나 같은 경우엔 다운로드하여놓은 "pepe-the-frog.jpg" 파일을 넣어주었다.
이렇게 하고 실행을 하면 우리가 흔히 보는 권한 요청 다이얼로그가 뜨게 된다.

코틀린에서 작성한 코드가 이거 띄워주는 코드이다.
암튼 Allow, 수락해 주고 들어가 보면

어우 잘 나오는 모습이다. 사실 이건 그냥 겁나 비효율적으로 사진을 가져오는 방법이었지만 그냥 보여주고 싶었다. 음음
이렇게 Image 위젯에 대해서와 그 파일 형식에 대해서 알아보았다. 사실 memory 형식은 다루지 않았는데 솔직히 거의 사용할 일이 없다 보니 굳이 다루지 않았다. 암튼 도움이 되었길 바라며 마치겠다.

