본문 바로가기
코딩과 알고리즘

플러터 체험기 5. 갤러리 사진 뷰어

스마트폰으로 사람들이 가장 많이 하는 것은 무엇일까요?

전화통화가 원래 주목적이겠지만 어떤 사람들은 사진을 찰영하기도 합니다.

휴대용 카메라이기도 한 스마트폰의 또 다른 용도인 카메라로 찍은 사진은 갤러리에 보관되지요.
걸러리에 보관한 사진을 꺼내 보면서 흐뭇~한 과거와 추억을 회상하기도 합니다.

오늘은 갤러리에 보관한 사진을 열어 보는 간단한 뷰어를 연습해 보았습니다.

실용앱은 아니고 그냥 단순한 기능 테스트용 예제이며
생각보다는 코드가 짧다고 느껴지는데, 여러분 생각은 어떨지 모르겠네요.

※ 2024. 2. 25일 테스트하였습니다.

그럼 오늘도 '심심하면 지는거다'라는건 아니고~
지루하지 않게 CCM 뮤직 하나 들어어보시면서 렛츠 고우하시면 어떨까요~ ^O^


이미지 피커를 이용한 대략적인 구상

갤러리에서 이미지나 여러 파일들을 선택하는 걸 플러터에서는 image_picker 라고 하는데요.
아래와 같이 사용할 수 있습니다.

1. pubspec.yaml 파일에 image_picker 외부 위젯를 추가합니다.

2. 사진을 담을 변수를 정의하고 사진 선택전,후 상태에 따라 각각 다른 화면을 구성합니다.
   사진 선택 후에는 사진이 보이도록 하면 금상 첨화!

사진 선택 전
사진 선택 후

3. 사용자의 액션에 의해 이미지 피커로 사진을 선택, 사진을 담을 변수에 사진을 쏙 넣어줍니다.

특히 2번의 경우 상태 기반 방식이라 하여,
사용자의 액션에 따라서 사진을 갱신하는 어떤 기능을 개발하는 게 아니라
그냥 사진 위치의 상태값이 바뀌었기에 사진이 자동으로 바뀐다라는 개념입니다.
화면을 갱신하라는 지시를 내리긴 하지만요.


1. 이미지 피커 종속성 추가

이미지 피커를 사용하려면 현재 열어놓은 플러터 프로젝트에 종속성을 추가해주어야 하는데요.
프로젝트 최상위 폴더의 pubspec.yaml 파일을 열어 아래와 같이 추가, 저장만 해주면 됩니다. 
비주얼 스튜디오 코드로 작업하는데 지가 알아서 다 설치해 주더라구요. ^O^ ~♬

pubspec.yaml ( 빨강색 참조 )


    :
dependencies:

  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.6

  image_picker: ^1.0.7
    :


기본 코드 구성

그리고 플러터 기본 코드를 구성합니다.
아래는 흰 화면에 아무런 내용이 없는 안드로이드 스타일의 기본 플러터 코드인데요.

이 소스를 기반으로 시작하도록 하겠습니다. 참고로 HomeScreen 부분만 수정할 겁니다.

lib/main.dart

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: SafeArea(
          child: HomeScreen(),
        ),
      ),
    );
  }
}

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});
  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return const Center();
  }
}

2-1. 사진을 담을 변수 정의

엑스 파일이라고 들어보셨나요?
처음에 플러터 예제 코드를 보고 눈을 의심했습니다.
'미해결 난제? 초자연 현상?'인가 라는 생각이 들더라구요 =_=..
재미나게도 플러터는 파일을 담는 변수를 엑스파일(XFile) 이라는 이름으로 정의합니다.
아래처럼 말이지요.

XFile phoneFile;

하지만 플러터는 다트(Dart) 문법을 사용하기에 아래와 같이 중간에 ?를 끼워 넣어 정의해야 오류가 나지 않습니다.
null을 허용한다는 규칙인데요. 뭐 사실 몰라도 큰 지장은 없습니다. XFile 변수는 항상 이렇게 사용하면 되니까요.

XFile? phoneFile;

이렇게 정의하면 파일의 위치라든가 여러 정보를 담을 수 있는 변수 phoneFile 이 생성되는 것이지요.
( 약간의 지식 팁 : 변수 정의시 기본값을 할당하지 않으면 null 값이 할당됩니다. )

HomeScreen 과 _HomeScreenState 는 실제 화면을 처리하는 위젯인데요.
위 변수는 HomeScreen 에서 정의하든, _HomeScreenState 에서 정의하든 모두 사용할 수 있습니다.
이 경우 어디에 정의하면 될까요?
보통 상위 위젯과 연결해야 하는 변수는 HomeScreen 에 정의하고,
그렇지 않은 경우 _HomeScreenState 에 정의합니다.

이 경우 상위 위젯 MyApp 과 연결할 필요가 없기 때문에,
_HomeScreenState 에 정의하면 됩니다. 아래처럼 말이지요.


     :
class _HomeScreenState extends State<HomeScreen> {
  // 선택한 파일
  XFile? phoneFile;
  @override
  Widget build(BuildContext context) {
      :


2-2. 사진 선택에 따라 화면 표시를 다르게

이어서 사진 선택 기능을 구현하기 전에 앞서, 선택한 사진에 따라 다른 화면을 보여주는 화면을 구성해 보았습니다.

화면 구성 요소야 일일히 설명 드릴 필요 없이 바로 소스를 공유하자면,
아래 소스의 내용이


import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io'; // File() 위젯 사용을 위한 모듈

   :
   
class _HomeScreenState extends State<HomeScreen> {
  // 선택한 파일
  XFile? phoneFile;
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: [
          const Text(
            '갤러리 선택',
            style: TextStyle(color: Colors.black, fontSize: 50),
          ),
          (phoneFile == null)
              // 안내 문구 또는
              ? const Text('선택 파일이 없습니다. 갤러리 선택을 누르세요')
              // 이미지 표시
              : Expanded(
                  child: Image.file(File(phoneFile!.path)),
                )
        ],
      ),
    );
  }
}


아래 화면을 구성하는 것이지요.

특이한 점은 아래와 같이 조건에 따라 각기 다른 위젯을 아주 간단하게 전환할 수 있다는 점인데요.

(phoneFile == null) ? 위젯1() : 위젯2()

photoFile 이 null 인 경우 사진을 선택하기 전이기 때문에 위젯1()을 보여주고
사진을 선택한 후에는 null 이 아니어 위젯2() 를 보여준다는 점입니다.
매우 간단히 처리하도록 되어 있더라구요~

참고로 사진을 보여주는 핵심 부분은  아래 코드인데요.

Image.file(File(phoneFile!.path))

실제로 사진을 선택하는 기능을 구현하면 phoneFile!.path 에는 아래와 같이 앱의 캐시 폴더 경로가 들어 있습니다. ( 실제 기기에서 조사해 본 값입니다. )
"/data/user/0/com.example.mp_ex/cache/b9b462d6-fe21-4d8e-984f-268d52cec1b0/neom-gYmExXn83_8-unsplash.jpg"

위 값을 그대로 찍어봐야 글자만 표시될 뿐입니다.
그래서 이미지로 변환해 보여주야 하는데요. 그 코드가 바로 Image.file(File(사진경로)) 부분 입니다.
그래서 사진을 선택한 후에는 아래와 같이 보이는 것이지요.

사진 선택 후


3-1. 터치 액션! 제스쳐!

앞에서는 사진을 선택하면 이렇게 보여준다라는 것만 정의했을 뿐인데요.
아직 터치하면 사진을 선택하는 부분은 작업하지 않았습니다.
터치영역을 인식하는 동작을 먼저 살펴보겠습니다.

플러터에서는 사용자가 수행하는 모든 액션을 제스쳐(Gesture)로 해석하는데요.
보통은 터치 동작에 GestureDetector() 위젯을 사용합니다.
사용법은 아래와 같은데요.

1. 터치할 위젯 영역을 GestureDetector() 위젯으로 감쌉니다.

GestureDetector(
  child: 원래위젯(),
),

2. 제스쳐 동작을 정의하여 구현합니다.
보통 터치동작을 정의하여 구현할 때는 onTap 파라미터를 사용하는데요.

아래와 같이 직접 코드를 안에 넣기도 하지만,

GestureDetector(
  onTap: () {
     : // 여기에 직접 기능을 짜 넣음      
  },
  child: 원래위젯(),
),

별도의 메소드로 기능을 정의하고 연결하는 방법도 있습니다.
코드 내용이 많다면 이 방법이 훨씬 나아 보이더라구요.

GestureDetector(
  onTap: 메소드,
  child: 원래위젯(),
),

  :
  
void 메소드()
{
   : // 실행할 기능
}

그래서 후자를 선택했습니다. 적용한 코드는 아래와 같습니다.


   :
class _HomeScreenState extends State<HomeScreen> {
  // 선택한 파일
  XFile? phoneFile;
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: [
          // 제스쳐 검사기
          GestureDetector(
            // 탭(터치) 제스쳐 콜백 정의, 그 밖에 길게 누르기, 드레그 등 여러가지
            onTap: fileSelect,
            // 제스쳐 영역의 UI
            child: const Text(
              '갤러리 선택',
              style: TextStyle(color: Colors.black, fontSize: 50),
            ),
          ),
      :


그래서 아래 영역을 터치하면 fileSelect() 메소드가 실행되는 것이지요. 


3-2. 사진 선택

이제 앞에서 사진을 선택하기 위해 터치했으니 실제 갤러리 창에서 사진을 선택할 차례입니다.
사진을 선택하는 코드는 아래와 같은데요.

final file = await ImagePicker().pickImage(
  source: ImageSource.gallery,
);

await 라는 부분은 사진을 선택을 마칠때까지 '기다려!'라는 의미와 같습니다.
그래야 사진 선택 후 선택한 사진을 가지고 이어서 작업을 진행할 수 있습니다.


   :
class _HomeScreenState extends State<HomeScreen> {

   :

  void fileSelect() async {
    final file = await ImagePicker().pickImage(
      source: ImageSource.gallery,
    );    
  }
}


3-3. 파이널! 사진 변수값 교체

3-2 과정에서 사진을 선택한 경우 file 변수에는 사진의 정보가 입력됩니다.
반면 사진을 선택하지 않은 경우 null 값이 입력되는데요.

사진을 선택한 경우만 phoneFile 변수를 변경해줄 필요가 있습니다.
해당 코드는 아래와 같습니다. ( 빨간색 )


     :
  void fileSelect() async {
    final file = await ImagePicker().pickImage(
      source: ImageSource.gallery,
    );
    if (file != null) {
      phoneFile = XFile(file.path);
      setState(() {});
    }
  }
   :


그래서 최종적으로 사진을 정상 선택한 경우에만, 앞의 2-2의 조건에 따라 화면이 아래와 같이 바뀌는 것이지요.

사진 선택 후


마무~리

간결한 표현력이 부족한 관계로 생각보다 설명이 길었네요.

최종 전체코드 공유 드리며 글을 마치려고 합니다.
오늘도 방문, 구독, 댓글 남겨주시는 모든 분들께 감사드리며 20000-!

main.dart 코드

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io'; // File() 위젯

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: SafeArea(
          child: HomeScreen(),
        ),
      ),
    );
  }
}

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});
  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  // 선택한 파일
  XFile? phoneFile;
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: [
          // 제스쳐 검사기
          GestureDetector(
            // 탭(터치) 제스쳐 콜백 정의, 그 밖에 길게 누르기, 드레그 등 여러가지
            onTap: fileSelect,
            // 제스쳐 영역의 UI
            child: const Text(
              '갤러리 선택',
              style: TextStyle(color: Colors.black, fontSize: 50),
            ),
          ),
          //     // 파일 선택 여부에 따라
          (phoneFile == null)
              // 안내 문구 또는
              ? const Text('선택 파일이 없습니다. 갤러리 선택을 누르세요')
              // 이미지 표시
              : Expanded(
                  child: Image.file(File(phoneFile!.path)),
                )
        ],
      ),
    );
  }

  // 갤러리 선택 터치시 콜백 함수
  // 내부에서 await 사용 위해 async 함수로 정의
  void fileSelect() async {
    // 이미지 선택, 선택완료까지 대기
    final file = await ImagePicker().pickImage(
      // 폰의 갤러리 폴더를 기본 선택
      // ImageSource.gallery(갤러리), ImageSource.camera(카메라)
      source: ImageSource.gallery,
    );
    // 파일을 선텍한 경우만
    if (file != null) {
      // 선택 파일 변경
      phoneFile = XFile(file.path);
      // UI 갱신을 위해 빈 setState 호출
      setState(() {});
    }
  }
}