NotePad

[Ionic] Photo Gallery 앱을 모바일로 & Live Reload

졸려질려 2022. 9. 5. 20:05
반응형

출처 : https://ko.m.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC:Ionic-logo-landscape.svg

 

[Ionic] Ionic Angular 입문 및 Photo Gallery 앱 만들기

Cross-Platform Mobile App Development: Ionic Framework Ionic Framework's app development platform builds amazing cross-platform mobile, web, and desktop apps all with one shared code base and open-..

choboit.tistory.com

 이전 글에서 Ionic 에 대한 전반적인 지식과 첫 가이드 앱인 Photo Gallery 앱을 웹 버전으로 만들어보았다. 사진을 찍고, Grid 형태의 앨범으로 찍은 사진을 보여주며, 촬영한 사진을 Filesystem 에 저장하는 것까지 기능들을 구현했다. 이번 글에서는 Mobile 환경에서 Photo Gallery 앱을 실행시켜본다. Ionic 을 사용하는 가장 큰 이유는 Android, iOS 까지 동시에 개발 및 동작할 수 있다는 점일 것이다. 그렇기 때문에 본 글의 내용이 어쩌면 Ionic 을 배우는 것에서 가장 중요한 부분이라 생각된다.


1. Import Platform API

 모바일 환경에서도 Photo Gallery 앱이 동작하려면 조금 수정해줘야 할 부분이 있다. 우선, PhotoService 로 Ionic 의 Platform API 를 Import 시켜준다. 그리고 PhotoService 가 생성될 때 앱이 실행되는 환경이 어떤 Platform 인지 알 수 있는 platform 변수를 받도록 수정한다.

// src/app/services/photo.service.ts

import { platform } from '@ionic/angular';

export class PhotoService {
  ...
  private platform: Platform;
  
  constructor(platform: Platform) {
    this.platform = platform;
  }
  
  // other codes...
}

2. Platform-specific Logic

 먼저 수정해야할 부분은 촬영한 사진을 저장하는 기능 부분이다. 이전에 구현했던 "readAsBase64()" 메소드에서 어떤 플랫폼에서 앱이 실행되는지 체크한다. 만약 "hybrid" (Capacitor or Cordova) 에서 동작 중이라면, Filesystem API 의 "readFile()" 메소드를 사용하여 Base64 형식으로 사진 파일을 읽도록 한다. 다른 환경에서 동작 중일 때는 기존 코드대로 동작하도록 한다.

// src/app/services/photo.service.ts

export class PhotoService {
  ...
  
  private async readAsBase64(photo: Photo) {
    // "hybrid" will detect Cordova or Capacitor
    if (this.platform.is('hybrid')) {
      // Read the file into base64 format
      const file = await Filesystem.readFile({
        path: photo.path
      });
      
      return file.data;
    }
    else {
      // Fetch the photo, read as a blob, then convert to base64 format
      const response = await fetch(photo.webPath);
      const blob = await response.blob();
      
      return await this.convertBlobToBase64(blob) as string;
    }
  }
  
  ...
}

 다음으로, "savePicture()" 메소드를 수정한다. 모바일 환경에서 동작할 때, filepath 는 writeFile() 함수의 결과값을 사용하여 설정한다. 그리고, webviewPath 는 "Capacitor.convertFileSrc()" 메소드를 사용해서 설정한다.

// src/app/services/photo.service.ts

...
  private async savePicture(photo: Photo) {
    // Convert photo to base64 format, required by Filesystem API to save
    const base64Data = await this.readAsBase64(photo);

    // Write the file to the date directory
    const fileName = new Date().getTime() + ".jpeg";
    const savedFile = await Filesystem.writeFile({
      path: fileName,
      data: base64Data,
      directory: Directory.Data
    });

    if (this.platform.is('hybrid')) {
      // Display the new image by rewritting the 'file://' path to HTTP
      // Details: https://ionicframework.com/docs/building/webview#file-protocol
      return {
        filepath: savedFile.uri,
        webviewPath: Capacitor.convertFileSrc(savedFile.uri);
      }
    } else {
      // Use webPath to display the new image instead of base64
      // since it's already loaded into memory
      return {
        filepath: fileName,
        webviewPath: photo.webPath
    };
    }
  }
...

 마지막으로 loadSaved() 함수를 업데이트 해준다. 모바일에서는 <img src="x" /> 태그의 src 속성에 바로 사진 파일 경로를 올려주면 된다. 이전에 웹용으로 개발했던 코드는 웹에서만 가능하므로, loadSaved() 의 실 기능은 'hybrid' 가 아닐때 실행되도록 제한한다.

// src/app/services/photo.service.ts

...
  public async loadSaved() {
    // Retrieve cached photo array data
    const photoList = await Preferences.get({ key: this.PHOTO_STORAGE });
    this.photos = JSON.parse(photoList.value) || [];

    if (!this.platform.is('hybrid')) {
      // Display the photo by reading into Base64 format
      for (let photo of this.photos) {
        // Read each saved photo's data from the Filesystem
        const readFile = await Filesystem.readFile({
          path: photo.filepath,
          directory: Directory.Data
        });

        // Web platform only: Load the photo as base64 data
        photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`;
      }
    }
  }
...

 위와 같이 'hybrid' 환경이 아닐 때, 불러온 이미지 파일을 Base64 형식으로 변환하도록 제한해주었다. 이제 남은건 Android 와 iOS 에서 앱을 돌려보는 것이다.


3. Capacitor Setup

 Capacitor 는 웹 앱을 iOS, Android 와 같은 네이티브 플랫폼에서도 배포가 가능하도록 해주는 공식 런타임이다. 과거에는 Apache Cordova 를 사용했었으나, 지금은 Capacitor 를 통해 개발하는 것을 권장하고 있다. 두 런타임에 대한 차이점은 나중에 다루기로 한다.

 현재 "ionic serve" 를 실행하고 있다면, 중지를 시켜준다. 그리고 "ionic build" 를 실행하여 새로운 빌드를 시도하고, 에러가 나타난다면 잡아주도록한다.

 다행히 아무 에러 없이 빌드가 마쳐졌다. 다음 단계로, iOS 와 Android 프로젝트를 각각 생성한다.

// Create iOS Project
$ ionic cap add ios

// Create Android Project
$ ionic cap add android

ionic cap add ios
ionic cap add android

 폴더 안에 Android, iOS 프로젝트가 생성된 것을 확인할 수 있다. 위 폴더들은 Ionic 앱의 일부분임과 동시에 완전히 독립적인 기본 프로젝트들이다. 웹 디렉토리(Default: www) 를 업데이트하는 빌드 과정("ionic build") 을 수행할 때마다, 해당 업데이트 사항들을 네이티브 프로젝트에도 복사해줘야한다. 다음 명령어를 사용한다.

$ ionic cap copy

  새로운 플러그인을 추가하는 것과 같이 코드에서 네이티브와 관련된 부분을 업데이트 할 때는 "sync" 명령어를 실행한다.

$ ionic cap sync

4. iOS Deployment

Note : iOS 앱을 빌드하기 위해서 MacOS 의 컴퓨터가 필요하다.

 Capacitor iOS 앱은 Xcode 를 통해 설정 및 관리 되며, CocoaPods 로 의존성을 관리한다. iOS 기기로 앱을 실행시키기 전에, 설정해야할 부분들이 있다.

 먼저, Capacitor 를 "open" 하는 명령어를 실행하여, iOS 프로젝트를 Xcode 로 열어준다.

$ ionic cap open ios

 위 명령어를 실행하면, 터미널 실행과 함께 Xcode 가 열리게된다. (Xcode 가 열리기까지 시간이 조금 걸린다.)

Xcode 와 터미널

 일부 네이티브 플러그인이 정상적으로 작동하기 위해서는 사용자 권한을 얻는 것이 필수이다. Photo Gallery 앱은 Camera Plugin 을 포함하고 있다. iOS 는 Ionic 의 "Camera.getPhoto()" 함수를 최초에 호출하게 되면, model dialog 를 자동으로 보여준다. 그리고 사용자에게 Camera 권한이 필요함을 알려준다. 정확한 명칭은 "Privacy - Camera Usage" 이며, 해당 권한을 설정하기 위해서 "Info.plist" 파일을 수정해줘야한다.

 우선, App 의 [Info] 를 클릭하고, [Custom iOS Target Properties] 를 확장시켜준다.

High-level Parameters

 수정해야하는 "Info.plist" 목록을 나타내며, 현재는 High-level parameter 상태로 보여주고 있다. Xcode 가 기본적으로 High-level 로 보여주는 것인데, 지금은 Low-level parameter 로 보는 것이 좋다. Low-level parameter 상태로 보기 위해서는 [Custom iOS Target Properties] 에서 보여주는 리스트의 위에서 우클릭을 하여 [Raw Keys/Values] 를 클릭한다.

 [Raw Keys & Values] 메뉴를 클릭하면, 아래 사진과 같이 Low-level parameter 의 형태로 목록이 나타난다.

 High-level Parameter 와 Low-level Parameter 의 다른 점을 아래 사진으로 만들어보았다.

 Key 의 이름들이 실제 "Info.plist" 에 있는 이름으로 바뀐 것을 볼 수 있다. 이제 위 목록에서 "NSCameraUsageDescription" Key 를 추가하고, 카메라를 사용하는 목적을 Value 값으로 넣어준다.

Add Row 선택
NSCameraUsageDescription 선택

 우클릭 메뉴 중에 [Add Row] 를 클릭하면, 자동으로 새 Key 가 생성되며 그 안에 넣을 수 있는 Key 값들의 목록이 나타난다. 그 중에서 "NSCameraUsageDescription" 을 선택한다. 이제 해당 Key 의 Value 칸을 더블 클릭하여, 그 안에 카메라 사용 목적을 기입한다.

 위에 기입한 카메라 사용 목적은 권한 승인창이 열렸을 때 앱이 사용자에게 보여줄 내용이다. "NSCameraUsageDescription" 을 추가한 과정으로 "NSPhotoLibraryAddUsageDescription" 과 "NSPhotoLibraryUsageDescription" 을 추가한다.

 "NSCameraUsageDescription", "NSPhotoLibraryAddUsageDescription", "NSPhotoLibraryUsageDescription" 을 Value 값과 함께 모두 추가였다면, 다음 단계로 넘어간다.

 다음은 [Signing & Capabilities] 메뉴에서 [Team] 을 설정한다.

 필자는 본래 Android 개발자로 활동을 해서 Apple Developer 로 등록이 되어있지 않았다. 그래서 위 과정을 처음 했을 때, "Failed to register bundle identifier" 같은 에러가 떴다. Apple Developer Program 에 개인으로 등록하거나, 등록된 팀에 포함된 상태에서 위 설정을 해줘야 정상적으로 이루어질 수 있다.

 Team 까지 등록을 마쳤다면, Xcode 의 최상단에서 실제로 연결된 Apple 기기나 Simulator 로 키고 싶은 Apple 제품을 선택한 후에 "Build" 버튼을 눌러서 실행시켜준다.

기기 선택과 [Build&RUN] 버튼

 

 

 


5. Android Deployment

 Capacitor iOS 가 Xcode 와 CocoaPod 을 통해 관리된다면, Capacitor Android 는 Android Studio 를 통해서 관리된다. Android 환경에서 실행시키기 전에, iOS 실행 전에 했던 것처럼 사전 작업이 필요하다. 이 또한 "권한 설정"이다.

 우선, ionic 명령어를 사용하여 Android Project 를 열어준다.

$ ionic cap open android

Android Studio 와 터미널

 Android 의 권한 설정은 "AndroidManifest.xml" 파일에서 수행한다.

 왼쪽 프로젝트뷰에서 manifests 폴더를 더블 클릭하거나, manifests 폴더 안에 "AndroidManifest.xml" 파일을 더블 클릭하면 된다. 열린 AndroidManifest.xml 파일에서 하단 부에 "<!-- Permissions -->" 주석으로 표시된 곳이 있다. 그 곳에 필요한 권한을 명시해준다.

// android/app/src/main/AndroidManifest.xml

<manifest>
  ...
  <!-- Permissions -->
  ...
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

 기본으로 세팅되어있는 INTERNET 권한의 아래에 위 2개의 권한들을 명시해주면 된다. 권한을 추가했다면, 상단에 [RUN] 버튼을 클릭하여 실행시켜준다. 실제 기기가 연결되었다면, 기기 모델명이 [RUN] 버튼 옆에 나올 것이다. 그렇지 않다면 Android Emulator 를 생성 및 선택하여 실행시켜준다.

기기 선택과 [RUN] 버튼
Android Emulator 실행 화면

 UI 의 테마가 웹과 달라보이지만, 전반적인 레이아웃과 기능들은 Web에서 실행할 때와 똑같은 것을 확인할 수 있다. 이제 Photo Gallery 라는 이름에 맞는 기능들은 다 구현한 듯 보인다. 마지막으로, 사진을 삭제하는 기능과 Android, iOS 에서 "ionic serve" 처럼 Live Reload 가 가능한 Ionic 의 기능을 다룬다.


6. Live Reload

 현재 쓰고 있는 Live Reload 기능은 "ionic serve" 명령어를 통해 사용할 수 있었다. 그러나 해당 기능은 Web Browser 로 개발할 때는 유용했지만, 이제 모바일 환경까지 모두 사용하려면 좀 더 추가적인 조치가 필요하다.

 우선, Live Reload 를 사용하려는 모바일 플랫폼을 선택하고, 해당 플랫폼의 기기를 개발에 사용하고 있는 PC 와 연결한 후에 다음 명령어를 터미널에서 실행시킨다. 현재 필자는 Android Emulator 를 연결해보고자 한다.

// Run iOS
$ ionic cap run ios -l --external

// Run Android
$ ionic cap run android -l --external

 Android 를 실행시키는 명령어를 입력하면, "ionic serve" 를 한 것처럼 Android Emulator 창이 생기면서 실행된다. 터미널 창을 분할하여, "ionic serve" 와 "ionic cap run android -l --external" 을 각각 실행시켜서 Web 과 Android 동시 개발을 진행할 수 있다. 이제 남은 기능인 "사진 지우기" 기능을 구현한다.


7. Deleting Photos

 구현하고자 하는 기능은 모아둔 사진 중에서 지우고자 하는 사진을 하나 클릭하면 삭제가 되는 것이다. 그렇다면 각 사진을 보여주는 <ion-img> 태그 안에 클릭 이벤트 리스너 먼저 넣어준다.

// src/app/tab2/tab2.page.html

...
      <ion-col size="6" *ngFor="let photo of photoService.photos; index as position">
        <ion-img [src]="photo.webviewPath" (click)="showActionSheet(photo, position)"></ion-img>
      </ion-col>
...

 클릭 리스너를 추가했으니, 클릭 했을 때 실행될 "showActionSheet()" 메소드를 구현해준다. 로직 구현을 위해 "tab2.page.ts" 로 이동하고, Import 부분에서 "ActionSheetController" 를 추가한다. 그리고 constructor 에서 ActionSheetController 인자를 받도록 수정한다. 여기서 ActionSheet 란, 클릭했을 때 추가로 행할 수 있는 동작들을 보여주는 메뉴 Sheet 를 뜻한다. 예를 들어, 우클릭 했을 때 그에 맞는 메뉴가 나타나는 것과 같다.

...
import { ActionSheetController } from '@ionic/angular';
...
export class Tab2Page {

  constructor(public photoService: PhotoService,
              public actionSheetController: ActionSheetController) {}
    
  ...
}

 ActionSheetController 를 추가한 후에, showActionSheet() 메소드를 구현해준다. 우선, 사진을 지우는 기능은 실제로 PhotoService 에서 구현할 예정이다. Tab2 는 그저 선택된 사진이 어떤 사진인지 PhotoService 에 알려주는 역할이라 보면 된다. 이 때, 각 사진은 UserPhoto 형태로 정의되어 있기 때문에, PhotoService 에 전달하거나 HTML 에서 인자로 받을 때도 UserPhoto 가 어떤 형식인지 Tab2 는 알아야한다. 따라서, 전달하는 역할을 맡은 Tab2 에도 UserPhoto 가 무엇인지 인식할 수 있게 Import 시켜준다.

// src/app/tab2/tab2.page.ts

import { PhotoService, UserPhoto } from '../services/photo.service';

...

 UserPhoto 는 PhotoService 클래스에서 정의했으므로, PhotoService 와 함께 Import 할 수 있다.

// src/app/tab2/tab2.page.ts

...
  public async showActionSheet(photo: UserPhoto, position: number) {
    const actionSheet = await this.actionSheetController.create({
      header: 'Photos',
      buttons: [{
        text: 'Delete',
        role: 'destructive',
        icon: 'trash',
        handler: () => {
          this.photoService.deletePicture(photo, position);
        }
      }, {
        text: 'Cancel',
        icon: 'close',
        role: 'cancel',
        handler: () => {
          // Nothing to do, action sheet is automatically closed
        }
      }]
    });

    await actionSheet.present();
  }
...

 위 코드대로 업데이트 하게 되면, Tab2 에서 나열된 사진들 중에 하나를 클릭 했을 때, [Delete] 버튼과 [Cancel] 버튼이 나오게 된다. 이제 PhotoService 에서 행할 삭제 기능을 구현한다.

// src/app/services/photo.service.ts

...
  public async deletePicture(photo: UserPhoto, position: number) {
    // Remove this photo from the Photos reference data array
    this.photos.splice(position, 1);

    // Update photos array cache by overwriting the existing photo array
    Preferences.set({
      key: this.PHOTO_STORAGE,
      value: JSON.stringify(this.photos)
    });

    // Delete photo file from filesystem
    const filename = photo.filepath.substring(photo.filepath.lastIndexOf('/') + 1);

    await Filesystem.deleteFile({
      path: filename,
      directory: Directory.Data
    })
  }
...

 전달 받은 UserPhoto 를 PhotoService 가 내부에서 가지고 있는 photos 배열에서 찾아내서 삭제를 하는 방식이다. 이제 삭제 기능을 구현하면서 수정한 파일들을 저장해주면, Live Reload 기능 덕분에 Android Emulator 에 실행되어 있는 앱도 자동으로 업데이트 된다.

Delete 기능 구현

 ActionSheet 에 나온 기능들 중 [Delete] 버튼을 클릭하면, 사진이 사라지는 것을 확인할 수 있다. 이로써 Photo Gallery 앱의 모든 기능을 구현해보았다. 그와 동시에 Ionic 앱이 어떻게 만들어지고, 멀티 플랫폼에선 어떻게 실행시키는지 알 수 있었다. 이제는 목적에 맞게 개발하면서 부족한 부분을 구글링 해가면서 개발을 진행하는 것이 남았다.

반응형