NotePad

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

졸려질려 2022. 9. 2. 19:48
반응형

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

 

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-web standards.

ionicframework.com

 Android 와 iOS 뿐만 아니라 웹까지 다양한 멀티 플랫폼의 애플리케이션을 개발할 수 있는 프레임워크들이 있다. ReactNative, Ionic, Xamarin, Flutter 가 있다. 그 중에서 Ionic 에 대한 공부를 해볼 예정이다.

 IonicApache Cordova 를 사용하여 다양한 플랫폼에 맞게 앱 패키징이 가능하고, Angular 뿐만 아니라 React, Vue.js 로도 개발이 가능하다. ReactNative 는 React, Xamarin 은 C#, Flutter 는 Dart 로 개발하는 것에 반해, Ionic 은 Angular, React, Vue.js 중 하나로 개발이 가능하다. 그래서 아직까지는 ReactNative 가 크로스 플랫폼 프레임워크의 선두 주자이지만, Ionic 또한 만만치 않은 경쟁력을 가지고 있다고 볼 수 있다.

 방금 언급했듯이, Ionic 은 Angular, React, Vue.js 로 개발이 가능하다. 그 중에서도 Angular 를 통해 Ionic 을 개발하는 방법에 대해 본 글에 정리해보고자 한다. 조금 헷갈릴 수도 있지만, AngularAngularJS 는 다른 개념이다. AngularJS 는 JavaScript 를 기반으로 만들어진 웹 애플리케이션 프레임워크이고, Angular 는 TypeScript 를 기반으로 만들어진 프레임워크이다. 공식 사이트도 각각 다르기 때문에, 이름이 비슷하다고 하여 다른 사이트로 가서 정보를 혼동하는 일이 없기를 바란다.


1. Ionic Angular

@ionic/angular combines the core Ionic experience with the tooling and APIs that are tailored to Angular Developers.

 Ionic Angular 는 Angular 6.0.0 이상을 지원한다. Angular 의 업그레이드 전략 일부로써, Angular 는 API 의 변화가 발생될 때마다 개발자에게 피드백을 주고, 자동으로 업그레이드를 해주는 기능을 가지고 있다. 그래서 업데이트 충돌을 줄여주고, 항상 깔끔한 생태계를 유지할 수 있도록 해준다.

 Ionic 4 이상부터, 앱을 만들고 라우팅 하는데에 공식 Angular Stack 이 사용된다. Angular Stack 을 사용하면, 개발한 앱을 Angular 생태계에 한 조각으로 넣을 수 있다. 좀 더 다양한 기능을 원할 경우, "@ionic/angular-toolkit" 을 사용하여 Angular CLI 를 가져와 쓸 수 있고, 더 다양한 기능들을 사용할 수 있다.


2. Build First App

 Ionic Angular 앱 개발을 입문할 수 있도록 도와주는 가이드 과정이 공식 문서에 존재한다. 간단한 "Photo Gallery" 앱을 만들어서 Web, Android, iOS 에서 동일하게 동작하는 것을 확인한다.

출처 : https://ionicframework.com/docs/angular/your-first-app

 영상으로 알 수 있듯이, 지금부터 만들어볼 "Photo Gallery" 앱은 카메라로 사진을 찍어서, 그리드 형태의 앨범으로 보여주고, 기기에 로컬로 저장하는 것이다. 앱을 개발하면서 다음 사항들을 배워볼 수 있다.

  • Ionic UI Components 를 사용하여 Web, iOS, Andoid 에서 동일하게 실행되는 하나의 Angular 코드
  • Ionic 의 공식 네이티브 앱 런타임인, Capacitor 를 사용하여, Native iOS, Android 앱을 배포할 수 있는 점
  • Capacitor 의 Camera, Filesystem, Preferences API 들로 구성된 앱 기능들

 이제 차근차근 공식 문서의 가이드를 따라서 Photo Gallery 앱을 만들어보도록 하자.


3. 환경 구축

 Ionic 개발 환경을 구축하기 위해 다음 항목들을 설치해준다.

  • Node.js : Ionic 생태계와 상호작용을 위함. LTS 버전으로 설치
  • VSCode : 코드 에디터
  • CLI(Command-Line Interface) : Window - CMD / Mac,Linux - Terminal

위에 따라 현재 필자의 개발 환경은 다음과 같다.

- M1 Mac Pro 16, 2021
- macOS Monterey 12.5.1
- NodeJS v16.14.0
- VSCode
- iTerm

4. Install Ionic Tooling

$ npm install -g @ionic/cli native-run cordova-res

 NPM 을 통해 "Ionic CLI", "native-run", "cordova-res" 라는 패키지들을 글로벌로 설치한다. Ionic 을 사용할 것이니 "Ionic CLI" 를 설치하는 것이고, 각종 Device 와 Simulator/Emulator 에서 Native Binary 로 앱이 동작하도록 해주는 "native-run" 을 설치한다. 마지막으로, Splash 화면과 앱 아이콘을 생성해주는 "Cordova-res" 를 설치한다.

[Permission Error]
 "-g" 옵션을 통해 위 패키지들을 글로벌로 설치하게 되면, EACCES 권한 에러가 발생할 수 있다. 해당 에러에 대한 해결법과 그에 대한 자세한 정보는 링크를 참고한다.

 버전 확인를 통해서 Ionic CLI 가 정상적으로 설치되었는지 확인한다.


5. Create an App

 "Tabs" 라는 스타터 템플릿을 사용하는 Angular 앱을 생성하고, Capacitor 를 추가한다.

$ ionic start photo-gallery tabs --type=angular --capacitor

 이상없이 정상적으로 설치가 되고, 중간에 Account 를 생성하겠냐는 질문이 나온다.

 "y" 를 입력하여 넘겨준다. 필자는 이 과정을 거치기 전에 Ionic 계정을 Github 로 연동을 해두어서, "y" 를 입력했을 때 브라우저에 새 창이 나타나면서 로그인된 페이지가 떴다.

 위 문구들까지 나타났다면, 설치 과정이 끝난 것이다. 이미지에서 알 수 있듯이, 프로젝트를 만들고 다음에 해야할 과정들이 나열되어 있다. 위 순서대로 따라해도 되겠지만, 필자는 공식 문서의 과정을 따라간다.

 설치를 마치면, 설치 명령을 실행했던 현재 위치에 "photo-gallery" 라는 이름의 폴더와 하위에 필요한 파일들이 같이 설치된 것을 확인할 수 있다. 즉, "ionic start" 명령어 다음으로 넣었던 인자값의 이름으로 프로젝트 폴더가 생성되는 것이다. 이제 photo-gallery 폴더로 이동하여 Android 와 iOS 에서 앱을 동작할 수 있도록 해주는 Capacitor 를 설치한다.

$ npm install @capacitor/camera @capacitor/preferences @capacitor/filesystem

 Capacitor 도 정상적으로 설치되었다.

PWA Elements

 PWA 란, Progressive Web Application 의 줄임말로, 프로그레시브 웹 앱을 뜻한다. HTML, CSS, Javascript 로 만든 웹 애플리케이션을 Android, iOS 같은 네이티브 앱처럼 실행할 수 있는 것을 말한다. 우리가 스마트폰에서 웹 브라우저를 쓰면서 간혹 어떤 사이트를 들어갔을 때, 홈화면에 앱처럼 설치할 수 있다는 문구를 본 적이 있을 것이다. 그리고 웹 페이지에서 권유한 대로 홈화면에 추가하고, 추가된 앱 아이콘을 클릭하면 웹페이지가 아니라 네이티브 앱처럼 동작한다. 그러한 애플리케이션을 PWA, 프로그레시브 웹 앱이라 한다.

 Ionic 에서 사용하는 Capacitor 들 중에서 Camera API 를 포함한 일부 Capacitor 는 Ionic 의 PWA Elements Library 를 통해 웹 기반의 기능들과 UI 를 제공하기도 한다. 따라서 Photo-gallery 프로젝트에도 PWA Elements Library 를 추가 해주도록 한다.

$ npm install @ionic/pwa-elements

 라이브러리만 설치했을 뿐, 아직 실제 코드에 import 되지 않았다. 따라서, "src/main.ts" 파일에서 "@ionic/pwa-elements" 를 import 해준다.

 설치가 정상적으로 완료되었고, Dependency 를 관리하는 "package.json" 파일에도 "@ionic/pwa-elements" 가 추가되어있다. 만약, 설치가 완료되었음에도 "package.json" 에 추가되어있지 않다면, 위 이미지처럼 추가를 해주면 될 것이다.

// src/main.ts

...

import { defineCustomElements } from '@ionic/pwa-elements/loader'

...

// Call the element loader after the platform has been bootstrapped
defineCustomElements(window);

  위 코드를 "src/main.ts" 파일에 추가하여, pwa-elements 라이브러리를 import 해준다.


6. Run the App

 "Tabs" 라는 스타터 템플릿으로 사용하여 프로젝트를 생성했기 때문에, 간단한 UI가 구현되어 있다. 이제 실행을 시켜본다.

$ ionic serve

  위 명령어를 실행하면 새로운 브라우저 창이 생성되면서, 현재 실행시킨 Ionic 앱의 모습을 볼 수 있다.

 브라우저 화면이 아니라 모바일 웹 화면으로 보고 싶다면, 개발자 도구를 통해서 설정을 변경할 수 있다.

 브라우저의 개발자 도구(F12)를 열어서, 위 이미지에 표시한 버튼을 클릭하면 모바일 화면으로 웹페이지를 볼 수 있다. 현재 실행한 앱은 기본적인 Tab 기능과 그에 맞는 간단한 레이아웃을 제공해준다. 이제 해당 앱의 코드를 수정해서 Photo-Gallery 앱에 맞게 커스텀해보자.


7. Photo Gallery UI

 우선, Ionic CLI 는 "Live Reload" 기능을 가지고 있다. 현재 VSCode 는 소스 코드를 볼 수있는 코드 창과 터미널 창이 열려 있을 것이다. 터미널 창에는 "ionic serve" 명령어를 실행한 이후로 계속 유지가 되고 있을 것이다. 그 상태에서 코드를 수정하고, 저장을 하면 변화된 코드에 맞게 앱이 다시 리로드 되는 것이다.

 현재 빈 공간인 3개의 Tab 중에서, 2번째 Tab 창에 기능을 넣어보자. 2번째 Tab 창의 UI 를 수정하기 위해 "src/app/tab2/tab2.page.html" 파일을 에디터에 띄워준다.

 초기의 코드는 위 이미지처럼 되어있을 것이다. 크게 <ion-header> 부분과 <ion-content> 부분이 존재한다.

<ion-header>

 ion-header 는 상단 네비게이션과 툴바를 포함하는 요소이다. 그 중에서 <ion-title> 의 값이 현재 브라우저에서 최상단에 "Tab 2" 글자를 출력하고 있다. <ion-title> 의 값을 "Photo Gallery" 로 수정한다.

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Photo Gallery
    </ion-title>
  </ion-toolbar>
</ion-header>

Ionic App 브라우저 화면의 상단

<ion-content>

 ion-content 는 메인 기능들과 UI 를 넣을 공간이다. 이 곳에는 Photo Gallery 앱의 목적과 맞게, 기기의 카메라를 켜주는 버튼과 촬영한 사진을 Grid 형식으로 보여주도록 코드를 수정한다.

 먼저, 기기의 Camera 를 키는 버튼은 플로팅 버튼으로 구현한다.

<ion-content [fullscreen]="true">
  <ion-fab vertical="bottom" horizontal="center" slot="fixed">
    <ion-fab-button>
      <ion-icon name="camera"></ion-icon>
    </ion-fab-button>
  </ion-fab>
</ion-content>

 수직상으로 Bottom 쪽이면서, 수평상 가운데의 위치에 Camera 아이콘을 가진 플로팅 버튼을 추가하는 코드이다. 다음 사진처럼 UI 가 바뀌는 것을 확인할 수 있다.

Ionic App 브라우저 화면의 하단

 마지막으로, Bottom Navigation Bar 의 "Tab 2" 부분을 수정한다. Bottom Navigation Bar 의 UI 를 담당하는 파일은 "src/app/tabs/tabs.page.html" 이다. 해당 파일에서 "Tab 2" 부분을 아래와 같이 수정한다.

<ion-tabs>
...
    <ion-tab-button tab="tab2">
      <ion-icon name="images"></ion-icon>
      <ion-label>Photos</ion-label>
    </ion-tab-button>
...
</ion-tabs>

Ionic App 브라우저 화면의 하단

 위 사진과 같이 "Tab 2" 문자열과 메뉴 아이콘 이미지가 바뀐 것을 확인할 수 있다. UI 는 이 정도로 마무리하고, 사진을 찍고 나열하는 기능들을 구현한다.


8. Taking Photos with the Camera

 기기의 카메라로 촬영을 할 수 있는 Capacitor 의 Camera API 를 사용한다. Android 나 iOS 와 같은 모바일 기기에서는 조금 조정이 필요하지만, 지금은 웹 용으로 코드를 구현한다.

8-1) Photo Service

 모든 Capacitor 로직은 service class 로 캡슐화가 되어있다. 우선, "ionic generate" 명령어를 통해 "PhotoService" 를 생성한다.

$ ionic g service services/photo

  PhotoService 생성에 성공하면, "app" 폴더 안에 "services" 라는 폴더와 하위 파일들이 자동으로 생성된다. 하위 파일 중에서 "photo.service.ts" 파일을 열어준다. 그리고 카메라 기능을 키는 로직을 추가한다.

 먼저, Capacitor 에 대한 Dependency 를 Import 한다. 그리고 Camera, FileSystem, Storage 플러그인를 참조할 수 있도록 코드를 추가한다.

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

import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
import { Filesystem, Directory } from '@capacitor/filesystem';
import { Preferences } from '@capacitor/preferences';

 그리고 PhotoService 클래스 안에 "addNewToGallery" 라는 이름의 메소드를 추가한다. 해당 메소드는 기기로 사진을 찍고 저장하는 기능을 담당할 것이다. 먼저, 기기의 카메라를 키는 로직을 추가한다.

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

export class PhotoService {
...
  public async addNewToGallery() {
     // Take a photo
     const capturedPhoto = await Camera.getPhoto({
      resultType: CameraResultType.Uri,
      source: CameraSource.Camera,
      quality: 100
     });
  }
}

 위 코드에서 "Camera.getPhoto()" 함수가 기기의 카메라를 키고, 촬영 하도록 하는 함수이다. 이제 아까 만들었던 카메라 버튼을 클릭했을 때, 위의 "addNewToGallery()" 메소드가 호출되도록 연결 시켜주면 될 것이다.

 우선, Tab2 코드에 PhotoService 를 Import 시켜준다. 아까 UI 를 수정할 때는 HTML 코드에서 했지만, 이번에는 로직을 추가하기 때문에 TypeScript 파일에 코드를 추가한다. "src/app/tab2/tab2.page.ts" 파일을 열어준다. 그리고 다음과 같이 코드를 수정한다.

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

import { Component } from '@angular/core';

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

@Component({
  selector: 'app-tab2',
  templateUrl: 'tab2.page.html',
  styleUrls: ['tab2.page.scss']
})
export class Tab2Page {

  constructor(public photoService: PhotoService) {}

  addPhotoToGallery() {
    this.photoService.addNewToGallery();
  }
}

 이제 버튼을 클릭했을 때, Tab2Page 클래스에 있는 "addPhotoToGallery()" 메소드가 호출되도록 한다. "tab2.page.html" 파일로 이동해서 다음과 같이 코드를 수정한다.

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

<ion-content [fullscreen]="true">
  <ion-fab vertical="bottom" horizontal="center" slot="fixed">
    <ion-fab-button (click)="addPhotoToGallery()">
      <ion-icon name="camera"></ion-icon>
    </ion-fab-button>
  </ion-fab>
</ion-content>

 <ion-fab-button> 요소 안에 Click 이벤트에 대한 호출할 함수를 명시해주면, 카메라 버튼을 클릭했을 때 해당 함수가 호출될 것이다.

카메라 버튼을 클릭했을 때 화면

 위 사진은 USB 로 연결한 웹캠의 화면이며, 웹캠의 연결을 해제해도 노트북의 전면 카메라가 출력되는 것을 확인할 수 있다. 카메라를 키고 촬영까지 가능하니, 촬영한 이미지 파일을 앱으로 받아서 보여주는 것을 구현해본다.

8-2) Displaying Photos

 "photo.service.ts" 파일에서 PhotoService 클래스의 외부에 "UserPhoto" 라는 이름의 인터페이스를 추가한다. "UserPhoto" 는 사진의 메타데이터를 저장할 것이다.

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

export interface UserPhoto {
  filepath: string,
  webviewPath: string
}

 PhotoService 클래스 안에 UserPhoto 타입의 배열을 추가할 것이다. 배열에는 카메라로 찍은 사진들을 저장한다.

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

export class PhotoService {
  public photos: UserPhoto[] = [];

  ....
}

 "addNewToGallery()" 메소드에서 카메라로 찍은 사진을 받아 배열로 넣는 로직을 넣어준다. 새로 찍은 사진이 사진 배열의 맨 앞에 들어가도록 구현한다.

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

...
  public async addNewToGallery() {
    ....
    // Save into Array
    this.photos.unshift({
      filepath: "soon...",
      webviewPath: capturedPhoto.webPath
    });
  }
...
Array.prototype.unshift()
unshift() 메서드는 새로운 요소를 배열의 맨 앞쪽에 추가하고, 새로운 길이를 반환합니다.
출처 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift

 촬영한 사진을 배열로 저장했으니, 배열을 그리드 형식으로 출력해주면 좋을 것이다. "tab2.page.html" 에 Grdi Component 를 추가하여 각 Grid 에 사진을 추가하도록 구현한다. 추가하는 것은 PhotoService 의 photos 배열에서 하나씩 아이템을 꺼내서 <ion-img> 태그로 각 Component 를 만들어서 나열한다.

<ion-content [fullscreen]="true">
  <ion-grid>
    <ion-row>
      <ion-col size="6" *ngFor="let photo of photoService.photos; index as position">
        <ion-img [src]="photo.webviewPath"></ion-img>
      </ion-col>
    </ion-row>
  </ion-grid>

  <!-- ion-fab markup -->
</ion-content>

 위 코드를 "tab2.page.html" 파일에 추가하면 사진을 촬영한 후에 Tab2 의 메인화면에 Grid 형식으로 촬영한 사진들이 나열된다. 필자의 노트북인 M1 Mac Pro 의 내장 카메라로 촬영을 해보니, 촬영을 마치고 1초 정도 후에 앨범 형식으로 사진이 나열되었다. Photo Gallery 에 맞는 기능들이 다 갖춰진 듯 보이지만, 지금 상태로 브라우저를 새로고침하게 되면 촬영했던 사진들이 모두 초기화 된다. 이를 방지하기 위해서는 배열이 아니라 특정 저장소를 통해 사진을 저장 및 관리하는 기능이 필요하다.


9. Saving Photos to the Filesystem

 위에서 언급했듯이, 특정 데이터를 앱이 재시작되는 것과 별개로 독립적으로 유지시키기 위해서는 배열이나 변수가 아니라 저장소에 저장을 해줘야한다. 저장소라 하면 로컬, 원격 DB 등 다양히 존재하나, Photo Gallery 앱은 로컬 저장소에 FileSystem 을 통해 저장할 것이다. 그러기 위해서는 FileSystem 에 접근할 수 있는 방법을 알고, 목적에 맞는 메소드들을 알아야한다.

 Filesystem 에 데이터를 저장하는 방법은 간단하다. 먼저, PhotoService 클래스 안에 "savePicture()" 메소드를 생성하고, Photo 객체를 메소드로 전달한다.

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

export class PhotoService {
  ....
  
  private async savePicture(photo: Photo) { }

  ....
}

 사진을 촬영하는 동작을 담당하는 addNewToGallery() 메소드에서 촬영한 사진을 savePicture() 메소드로 전달하고 호출하도록 addNewToGallery() 메소드를 수정한다.

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

export class PhotoService {
  ...
  public async addNewToGallery() {
    // Take a photo
    const capturedPhoto = await Camera.getPhoto({
      resultType: CameraResultType.Uri,
      source: CameraSource.Camera,
      quality: 100
    });

    this.savePicture(capturedPhoto);
  }
  ...
}

 이제 Filesystem 에 Photo 를 저장하는 로직을 추가해줘야한다. 먼저, Filesystem 을 사용하기 위해서는 Capacitor 의 Filesystem API 를 사용해야 한다. 저장할 때는 Photo 객체가 아니라 base64 형식으로 변환시켜서 넣어줘야한다. 그리고 Filesystem API 의 "writeFile()" 함수를 호출하여 Filesystem 에 이미지를 저장한다. 마지막으로, 저장한 이미지를 앨범형식으로 보여줘야하는데, Tab 2 의 HTML 코드에서 PhotoService 의 photos 로부터 사진을 불러오기 때문에 photos 배열을 유지하되, 그 안에 데이터들은 Filesystem 으로부터 넣어주도록 한다. 코드는 다음과 같다.

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

export class PhotoService {
  ...
  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
    });

    // Use webPath to display the new image instead of base64
    // since it's already loaded into memory
    return {
      filepath: fileName,
      webviewPath: photo.webPath
    };
  }

  public async addNewToGallery() {
    ...

    const savedImageFile = await this.savePicture(capturedPhoto);

    // Save into Array
    this.photos.unshift(savedImageFile);
  }
}

 readAsBase64() 메소드는 구현되지 않았기 때문에 오류가 발생할 것이다. 위 코드의 로직을 통해 수정된 점을 살펴보면, addNewToGallery() 메소드 내에서 photos 배열에 넣는 데이터가 달라졌다. savePicture() 메소드는 Photo 객체를 base64 형식으로 파일을 변환하여 Filesystem API 의 writeFile() 메소드를 통해 로컬 저장소에 이미지를 저장한다. 그리고, 저장할 때 사용한 이미지의 URL 과 저장할 때 붙인 이름과 함께 UserPhoto 객체로 생성하여 반환한다. 반환된 UserPhoto 객체는 unshift() 메소드를 통해 배열의 맨 앞에 위치하게 되며, photos 배열의 일부분이 된다.

 이제 남은건 readAsBase64() 메소드를 구현하고 Photo Gallery 앱이 다시 잘 동작하는지 확인하는 것이다. readAsBase64() 메소드에서 구현할 기능은 파일의 형식을 변환하는 것이다. 이 기능은 Web, Mobile 에 따라 로직이 조금씩 다르다. 하지만 지금은 Web 에 맞게만 코드를 수정한다.

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

...
  private async readAsBase64(photo: Photo) {
    // 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;
  }

  private convertBlobToBase64 = (blob: Blob) => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onerror = reject;
    reader.onload = () => {
      resolve(reader.result);
    }

    reader.readAsDataURL(blob);
  })
...

 Web API 로 내장된 fetch() 함수를 통해 이미지 파일을 blob 형식으로 읽어낸다. 그리고 FileReader 의 "readAsDataURL()" 함수로 blob 파일을 base64 로 변환한다. 위 과정을 거치면, 촬영한 사진을 Filesystem 에 저장하고 앨범형식으로 볼 수 있다. 하지만 아직 부족한 점이 있다. Filesystem API 를 시작하면서 원했던 목표는 앱을 다시 실행시켜도 앨범에 사진들이 유지되어 있는 것이다. 그러나 현재 상황에서 앱을 재시작하게 되면 찍었던 사진들이 안나타나고 초기 화면으로 초기화 되어있다. Filesystem API 를 통해 저장하는 기능은 추가했으나, 불러오는 기능을 구현하지 않았기 때문이다.


10. Loading Photos from the Filesystem

 Filesystem 에 저장하는 것까지 구현했으나, Photo Gallery 에 저장된 사진을 불러오기 위해서는 각 사진 파일에 대한 참조값이 필요하고, 이미지를 Filesystem 을 저장하는 과정에서 이미지 파일과 각 참조값을 같이 저장해줘야한다. 이를 해결하기 위해 Preference API 를 사용한다. Preference API 는 Key-Value 형식으로 Photos 배열을 저장해줄 것이다.

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

export class PhotoService {
  public photos: UserPhoto[] = [];
  private PHOTO_STORAGE: string = 'photos';
  
  ...
}

 먼저, Preference 에 저장할 때 Key 값이 되어줄 변수를 선언 및 초기화 한다. Key-Value 형식으로 저장해야하므로, PHOTO_STORAGE 의 값이 Key 값이 되고, 바로 위에 있는 photos 배열이 Value 가 될 것이다.

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

export class PhotoService {
  ...
  public async addNewToGallery() {
    ...
    Preferences.set({
      key: this.PHOTO_STORAGE,
      value: JSON.stringify(this.photos)
    })
  }
}

 그 후, 위와 같이 addNewToGallery() 메소드 내에 마지막에 Preference API 로 Key-Value 를 저장하는 코드를 추가한다. 방금 언급했듯이, Key 값은 PHOTO_STORAGE 값이 되고, Value 값은 photos 배열을 JSON 형식으로 변환한 문자열이 된다. photos 배열을 하나의 문자열로 저장하기 위해 JSON 형식으로 변환 했으며, 이를 나중에 다시 사용하기 위해서는 JSON 에서 photos 배열로 바꾸는 파싱 작업이 필요하다. 이를 위해 "loadSaved()" 메소드를 추가한다.

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

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

    // more to come...
  }
}

 loadSaved() 메소드에서 JSON 문자열을 배열로 파싱해준다. 그런데 보통 Key 값을 통해 get() 을 하게 되면 Value 가 반환 될텐데, 위 코드를 보면 Value 에서 value 를 다시 불러야 배열로 받을 수 있다. 이를 알아보기 위해 photoList 를 Log 로 찍어보았다.

 photoList 를 로그로 찍어보았을 때 출력된 값이다. PHOTO_STORAGE 값으로 찾은 Value 값은 "value" 라는 Key 값과 저장한 배열이 JSON 문자열로 된 하나의 JSON 객체로 되어있었다. 그래서, photoList.value 를 통해 저장했던 JSON 문자열을 받을 수 있던 것이다.

 다시 본론으로 돌아와서, 파싱까지 마친 photos 배열에 접근하여 Gallery 에 표시해주면 된다. 모바일 환경에서는 <img src="x"> 태그를 사용해서 방금 받은 photos 배열의 파일 URL 을 통해 보여줄 수 있다. 하지만, Web 환경에서는 Filesystem 에서 받은 위 배열을 base64 형식으로 바꿔서 사용해야한다. 이 점은 savePicture() 메소드를 구현하면서 확인한 바 있다.  loadSaved() 메소드를 아래와 같이 수정한다.

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

export class PhotoService {
  ...
  
  public async loadSaved() {
    ...
    // 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}`;
    }
  }
}

 loadSaved() 메소드는 Filesystem 에 저장된 사진 파일들을 불러와서 photos 배열로 변환시켜주는 기능을 담당한다. 로직이 완성됐으니 호출을 할 부분을 정해야한다. 사진을 찍을 당시에는 photos 배열에 바로 저장이 되어서 상관이 없지만, 앱을 시작(재시작) 했을 때 저장된 이미지를 불러오는 기능이 필요하다. 즉, 앱이 시작됐을 때, loadSaved() 메소드를 호출하도록 한다.

 정확히는 앱이 시작되고, Tab 2 화면으로 들어갈 때 loadSaved() 메소드를 호출해줘야하므로, "tab2.page.ts" 에서 loadSaved() 메소드를 호출하는 코드를 추가한다. 그런데 Tab 2 화면으로 들어올 때 이벤트 함수가 기본적으로 제공되지 않는다. Android 로 비유를 하면, Activity 의 onCreate() 같은 함수가 기본적으로 제공되지 않는 것인데, 그 함수가 Ionic 에서는 "ngOnInit()" 함수이다.

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

export class Tab2Page {
  ...
  async ngOnInit() {
    await this.photoService.loadSaved();
  }
  ...
}

  Tab2Page 에 ngOnInit() 메소드를 추가하고, PhotoService 의 loadSaved() 메소드를 호출하도록 코드를 추가했다. 이제 브라우저를 새로고침하거나, Tab 간에 이동을 하더라도 FileSystem 에 저장했던 이미지들을 그대로 유지할 수 있다.


 Ionic 에서 제공해주는 입문자용 앱인 Photo Gallery 를 실습해보았다. 아무래도 Android 를 개발해왔다 보니, 실습을 하면서 Android 와 비교를 하며 개념들을 이해했다. Ionic 은 웹만 지원하는 것이 아니라 Android 와 iOS 에서도 앱을 실행시킬 수 있다. 다음 글에서는 위에 Photo Gallery 앱을 Android 와 iOS 에서 실행시켜보는 것을 실습할 예정이다. 이 또한 Ionic 의 공식 가이드 문서를 따라할 것이고, 그 과정을 정리해나갈 생각이다.

반응형