본문 바로가기
flutter/Widget of the Week

Flutter[플러터] / Image 를 사용하여 이미지 넣기 (사진, 네트워크, 인터넷 이미지, 파일, 로딩, 로더, 코틀린, 네이티브, image_picker, 이미지 가져오기) Image (Flutter Widget of the Week)

by ch5c 2025. 6. 22.
반응형

Image class

이미지를 표시하는 위젯입니다.

https://youtu.be/7oIAs-0G4mw

공식 문서 코드

 


앱을 제작할 때에 필수불가결하게 있어야 하는 요소가 있는데 뭐니 뭐니 해도 바로 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();
  },
)

위 코드에서 계속 사용하고 있는 url 이미지

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 형식은 다루지 않았는데 솔직히 거의 사용할 일이 없다 보니 굳이 다루지 않았다. 암튼 도움이 되었길 바라며 마치겠다.

 

반응형