[ ionic 2: Storage ] 디바이스 데이타베이스 이용한 Todo App 만들기

ionic 2에서 기기 내에 있는 database를 활용하는 방법을 알아보자. 코딩을 배울 때 자주 등장하는 Todo App 을  기기 내 저장소(database)를 사용하여 만들어보고자 한다.

기기 외부에 있는 데이타베이스 서버에 연결하려면, 인터넷 액세스가 가능해야한다. 따라서, 모바일 앱의 사용환경이 제한될 수 밖에 없다. 하지만, 내부 저장소를 이용하면 개인적인 데이타를 쉽게 저장하고 언제든지 뽑아볼 수 있다.

node js , ionic, cordova 가 설치되어있다는 전제아래 시작한다. 기본적으로 angular 2를 어느 정도 활용할 수 있어야, 본 글의 내용을 이해할 수 있을 것이다.  ionic 을 사용해보지 않은 경우, 링크를 방문하여 설치와 관련된 글을 읽어보자.

ionic2 todoStorage 프로젝트 생성

ionic 2 cli 를 이용하여 ionic 2 프로젝트를 생성하고 해당 폴더로 이동한다.  명령프롬프트에서 아래와 같이 입력한다.

$ ionic start todoStorage blank --v2
$ cd todoStorage

 

VS code 로 프로젝트를 열어 본 이미지다. 여러 폴더 중 우리는 주로 src 폴더를 만지게 된다. 명령프롬프트에서 아래와 같이 실행하면, 인터넷 브라우저가 열리면서 빈 ionic 페이지가 표시된다.

$ ionic serve

 

 

■ 기본 틀 구성

코딩은 데생과 닮은 면이 있다. 데생에서 먼저 큰 그림을 그리고 세부묘사를 하듯이, 코딩을 할 때에도 복잡한 기능을 생각하기 전에, 문제를 최대한 단순화시켜서 윤곽을 잡는 것으로 시작한다.

>>todoStorage/src/pages/home/home.ts

메인 페이지를 관장하는 typescript 파일이다.  todo item 을 담은 array를 만들어 주었다.

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';

@Component({
   selector: 'page-home',
   templateUrl: 'home.html'
})
export class HomePage {
   items: any[];
   constructor(
      public navCtrl: NavController
   ) {
      this.items = ["Eat lunch","Walk the dog","Watch movie"];
   }
}

 

※ items 변수의 type 설정에서 any[ ] 대신에 인터페이스를 이용하여 item 모델을 만들면 좋겠지만, 글이 장황해지므로 여기서는 생략한다.

>>todoStorage/src/pages/home/home.html

위의 home.ts 와 연결된 html 파일이다.

<ion-header>
   <ion-navbar>
       <ion-title>
            Todo Storage
       </ion-title>
   </ion-navbar>
</ion-header>

<ion-content padding>
   <ion-list>
        <ion-item *ngFor="let item of items">{{item}}</ion-item>
   </ion-list>
   <button ion-button full>Add Item</button>
</ion-content>

인터넷 네이게이션 창이 업데이트 되었다.

 

항목 추가를 위한 페이지(View)를 만든다. 명령프롬프트에서 아래와 같이 입력한다.

$ ionic generate page add-item
또는 
$ ionic g page add-item

>>todoStorage/src/pages/add-item 폴더와 그 안에 add-item.html, add-item.scss, add-item.ts 파일이 생성된다.

이 페이지 컴포넌트가 다른 컴포넌트에서 참조되려면 먼저 src/app/app.module.ts 파일에서 설정해주어야 한다.

>>todoStorage/src/app/app.module.ts

import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
//AddItemPage 를 import 한다.
import {AddItemPage} from '../pages/add-item/add-item';

@NgModule({
  declarations: [
    MyApp,
    HomePage,
    AddItemPage //여기 추가
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage,
    AddItemPage //여기도 추가
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
export class AppModule {}

 

첫 페이지에서 Add Item 버턴을 클릭하면, 이 페이지로 넘어가게 설정한다. home.ts 에서 add-item 페이지 컴포넌트를 import 하고,  goToAddItemPage() 이라는 기능을 설정한다. navCtrl 의 push 메서드를 사용하여 이동할 페이지(View)를 설정하고 함께 넘겨줄 변수(items)를 포함시킨다.  변수를 포함하여 넘겨줄 때는 ionic-angular로 부터 NavParams 컴포넌트를 import 해주어야한다.

>>todoStorage/src/pages/home/home.ts

import { Component } from '@angular/core';
//NavParams 추가
import { NavController, NavParams } from 'ionic-angular';
// AddItemPage 추가
import {AddItemPage} from '../add-item/add-item';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
  items:any[];
  constructor(public navCtrl: NavController) {
    this.items = ["Eat lunch","Walk the dog","Watch movie"];
  }
  //네비게이션 컨트롤러 기능 설정
  goToAddItemPage(){
    this.navCtrl.push(AddItemPage,{items:this.items});
  }
}

home.html 파일의 버턴에 click 이벤트를 추가하고 goToAddItemPage() 기능을 붙여준다. 아래 13번째 줄 참조.

>>todoStorage/src/pages/home/home.html

<ion-header>
  <ion-navbar>
    <ion-title>
      Todo Storage
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-list>
    <ion-item *ngFor="let item of items">{{item}}</ion-item>
  </ion-list>
  <button ion-button full (click)="goToAddItemPage()">Add Item</button>
</ion-content>

 

Add Item 버턴을 클릭하면 add-item 페이지로 이동한다.

add-item.html 은 비어있다. 여기에 item 을 입력할 수 있는 input 란 만들고, 저장기능을 담당하는 click 이벤트 버턴을 추가하고 save() 기능을 부여하자.

>>todoStorage/src/pages/add-item/add-item.html

<ion-header>
  <ion-navbar>
    <ion-title>Add Item</ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-item>
    <ion-input placeholder="Add A Task" [(ngModel)]='newItem'></ion-input>
  </ion-item>
  <button ion-button full (click)="save()">Save</button>
</ion-content>

add-item.ts 파일에서 save() 기능을 정의해주어야한다. 이 때, home.ts로부터 넘어온 items 값을 NavParams 를 통해 넘겨받는다.

>>todoStorage/src/pages/add-item/add-item.ts

import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';

@Component({
  selector: 'page-add-item',
  templateUrl: 'add-item.html'
})
export class AddItemPage {
  newItem:string;

  constructor(public navCtrl: NavController, public navParams: NavParams) {

  }

  ionViewDidLoad() {
    //확인차원에서 넘겨받은 items 값을 콘솔에 표시해본다.
    console.log(this.navParams.data.items);
  }
  save(){
    //새로 입력한 newItem 값은 items array에 추가한다.
    this.navParams.data.items.push(this.newItem);
    //현재 창을 닫고 home 뷰로 돌아간다.
    this.navCtrl.pop();
  }

}

 

Add Item 뷰(View)에서 test 를 입력하면 Todo Storage에 items 값이 더해져있다. 여기까지 리스트를 표시하고 추가하는 기능은 구현되었다. 이제 삭제기능을 만들어보자.

home.html 의 ion-list 안에서  ion-item-sliding 태그를 활용하여 list를 다시 작성해본다. ion-item-sliding 에는 옆으로 밀어서 추가적인 내용이 나타나도록 하는 기능이 들어있다. 추가적인 내용이 들어가는 ion-item-options태그 안에 button을 생성하고 삭제 기능을 담당하는 click이벤트, deleteItem(item) 을 설정한다.

>>todoStorage/src/pages/home/home.html

<ion-header>
  <ion-navbar>
    <ion-title>
      Todo Storage
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-list>
    //ion-item-sliding 기능을 추가한다.
    <ion-item-sliding *ngFor="let item of items">
      <ion-item>{{item}}</ion-item>
      <ion-item-options>
        //삭제버턴
        <button ion-button expandable (click)="deleteItem(item)"><ion-icon name="trash"></ion-icon>Delete</button>
      </ion-item-options>
    </ion-item-sliding>
  </ion-list>
  <button ion-button full (click)="goToAddItemPage()">Add Item</button>
</ion-content>

home.ts 로 이동하여 deleteItem(item) 함수 기능을 추가한다. for loop 을 이용해 삭제해도 되겠지만, 여기서는 array.filter 메서드를 사용했다.

>>todoStorage/src/pages/home/home.ts

import { Component } from '@angular/core';
import { NavController,NavParams } from 'ionic-angular';

import {AddItemPage} from '../add-item/add-item';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
  items:any[];
  constructor(public navCtrl: NavController) {
    this.items = ["Eat lunch","Walk the dog","Watch movie"];
  }

  goToAddItemPage(){
    this.navCtrl.push(AddItemPage,{items:this.items});
  }
  //삭제 기능을 담당하는 함수 설정
  deleteItem(item){
    this.items = this.items.filter((res)=>res !== item);
  }
}

 

이제 리스트 표시, 추가, 삭제 기능이 구현되었다. 하지만, 데이타베이스에 저장되는 기능을 아직 적용하지 않았기 때문에, 새로고침을 했을 때 데이타가 초기화되어버린다. 기기 내 데이타베이스를 활용하면 데이타를 잃지 않고 저장할 수 있다.

■ 디바이스 데이타베이스 활용

sqllite, webslq,indexeddb 등 기기마다 사용되는 디바이스 데이타베이스의 종류는 다르다. ionic 2 는 크로스플랫폼이다. 기기마다 다른 내부 데이타베이스를 가지고 있을 수 있기 때문에 ionic2 Storage 컴포넌트가 자동으로 판단하여 사용할 데이타베이스에 맞는 기능을 구현해준다. 물론, 추가적인 코딩을 통해 원하는 데이타베이스를 설정할 수 있다. ionic2 Storage를 아예 사용하지 않고, SQLlite 등 ionic-native 에서 지원하는 개별 데이타베이스 컴포넌트를 활용해도 된다. 여기서는 ionic2 Storage 컴포넌트를 활용하도록 한다.

Angular2 나 ionic2에서는 보통 데이타베이스와 관련된 기능을 하나의 Service 컴포넌트로 분리해서 다른 컴포넌트에 Injection 하는 방법을 사용한다. Service 컴포넌트는 반복적으로 여러 컴포넌트에 걸쳐서 공유되는 기능을 모아놓고 필요할 때마다 꺼내 쓰는 용도로 사용된다. Service 컴포넌트를 만들지 않고 해당 뷰(View) 내에서 직접적으로 따로따로 기능을 구현해도 되지만, 이렇게 되면 일이 많아진다.

우리가 만드는 Todo 앱의 경우, 너무 간단한 앱이다. 그래서, Service 컴포넌트를 따로 구성할 필요가 없다. home.ts, add-item.ts 내에서 구현해도 그만이다. 다만, 연습용이므로 굳이 Service 컴포넌트를 구성해본다.

todo.service.ts 파일을 다음 경로에 만든다.

>>todoStorage/src/pages/sevices/todo.service.ts

import { Storage } from '@ionic/storage';
import { Injectable } from '@angular/core';

@Injectable()
export class TodoService {
    constructor(public storage: Storage) {
    }
    getTodos(){
        return this.storage.get('todos');
    }
    setTodos(todos){
        return this.storage.set('todos',JSON.stringify(todos));
    }
}

import 된 Injectable 컴포넌트는 Service 컴포넌트를 Injection 가능하게 해준다. Storage 컴포넌트는 말그대로 ionic2의 Storage 기능을 담당한다. Storage 컴포넌트의 대표적인 메서드로 get(key:string), set(key:string, value:string) 이 있다. api 문서를 보면 기타 메서드들이 나열되어있으니 읽어보도록하자.

get과 set메서드는 Promise를 반환한다. 위에 사용된 set 메서드에서 보듯이 todos 오브젝트를 스트링으로 변환하여 저장하고 있다. mongodb 등 다른 데이타베이스를 사용할 때와 마찬가지다.

서비스 컴포넌트를 만들었으니, app.module.ts 에 등록해주어야 사용할 수 있다.

>>todoStorage/src/app/app.module.ts

import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';

import {AddItemPage} from '../pages/add-item/add-item';
//Storage 를 불러온다.
import { Storage } from '@ionic/storage';
//TodoService 를 불러온다.
import {TodoService} from '../pages/services/todo.service';

@NgModule({
  declarations: [
    MyApp,
    HomePage,
    AddItemPage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage,
    AddItemPage
  ],
  //providers array에 Storage와 TodoService를 추가해준다.
  providers: [Storage,TodoService,{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
export class AppModule {}

 

이렇게 TodoService 를 등록했으면, home.ts와 add-item.ts에 Injection해서 활용할 수 있다. 먼저 home.ts 파일을 수정하여, items를 데이타베이스에서 가져오고 삭제 할 수 있도록 해보자.

>> todoStorage/src/pages/home/home.ts

import { Component } from '@angular/core';
import { NavController,NavParams } from 'ionic-angular';

import {AddItemPage} from '../add-item/add-item';
//TodoService 를 불러온다.
import {TodoService} from '../services/todo.service';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
  items:any[];
  // constructor 에 TodoService를 todoService라는 이름으로 injection한다.
  constructor(
    public navCtrl: NavController,
    private todoService:TodoService
  ) {
    // todoService에서 items를 불러온다.
    this.todoService.getTodos().then(res=>{
      this.items = JSON.parse(res) || [];
    });
  }

  goToAddItemPage(){
    this.navCtrl.push(AddItemPage,{items:this.items});
  }
  deleteItem(item){
    this.items = this.items.filter((res)=>res !== item);
    // todoService에서 items를 업데이트한다.
    this.todoService.setTodos(this.items).then(res=>{
      this.items = JSON.parse(res) || [];
    });
  }
}

다음은 add-item.ts 파일에서 TodoService를 이용하여 기기 내 데이타베이스에  item을 추가할 수 있도록 하자.

>>todoStorage/src/pages/add-item/add-item.ts

import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';

//TodoService 를 불러온다.
import {TodoService} from '../services/todo.service';

@Component({
  selector: 'page-add-item',
  templateUrl: 'add-item.html'
})
export class AddItemPage {
  newItem:string;
  //items 변수 추가
  items:any[];
  // constructor 에 TodoService를 todoService라는 이름으로 injection한다.
  constructor(
    public navCtrl: NavController, 
    public navParams: NavParams,
    private todoService:TodoService
  ) {

  }

  ionViewDidLoad() {
    //확인차원에서 넘겨받은 items 값을 콘솔에 표시해본다.
    console.log(this.navParams.data.items);
  }
  save(){
    //this.navParams.data.items 이 null 값으로 넘어올 때 array를 반환하도록 처리.
    this.items = this.navParams.data.items || [];
    //새로운 item을 items에 추가.
    this.items.push(this.newItem);
    //todoService를 사용하여 데이타베이스 업데이트.
    this.todoService.setTodos(this.items).then(res=>{
      return console.log('New item added.');
    });
    //현재창을 닫고 homePage View로 전환
    this.navCtrl.pop();
  }
}

 

 

이제 refresh (새로고침)을 해도 데이타가 날아가지 않는다. 로컬 데이타베이스를 이용한 Todo App 이 완성되었다. Chrome 브라우저에서는 websql 이 사용되었다.

■ 추가적인 데이타베이스 설정

원한다면 Storage 에서 데이타베이스 종류와 이름을 정의할 수 있다. app.module.ts 에서 설정해준다.

>>todoStorage/src/app/app.module.ts

import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';

import {AddItemPage} from '../pages/add-item/add-item';
import { Storage } from '@ionic/storage';
import {TodoService} from '../pages/services/todo.service';

//Storage Configuration 데이타베이스 종류와 이름을 설정
export function provideStorage() {
  return new Storage(['sqlite', 'websql', 'indexeddb'], { name: '__mydb' });
}

@NgModule({
  declarations: [
    MyApp,
    HomePage,
    AddItemPage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage,
    AddItemPage
  ],
  //Storage 대신에 { provide: Storage, useFactory: provideStorage }를 추가해준다.
  providers: [
    { provide: Storage, useFactory: provideStorage },
    TodoService,
    {provide: ErrorHandler, useClass: IonicErrorHandler}
  ]
})
export class AppModule {}

 

■ 에뮬레이터에서 실행

ionic serve 를 사용해 web browser에서 실행되는 모습을 살펴보았다. 이번에는 에뮬레이터에서 실행해보자. 안드로이드와 iphone에서 사용되는 SQLlight를 사용하려면, 먼저 cordova 플로그인을 하나 더해주어야 한다. 프로젝트 루트 명령창에서 다음 명령을 실행한다.

$ cordova plugin add cordova-sqlite-storage --save

안드로이드이든 아이폰이든 원하는 platform을 추가해주어야한다. 여기서는 안드로이드 플랫폼을 추가해본다. 안드로이드 플랫폼에서 에뮬레이션을 하려면, 안드로이드 스튜디오가 설치되어있어야 한다. 프로젝트 루트 명령창에서 다음 명령을 실행한다.

$ cordova platform add android

에뮬레이션을 위한 준비는 끝났다. 다음 명령을 실행하면, 에뮬레이터가 실행된다.

$ ionic run android
//debug 모드로 실행하려면
$ ionic run android --debug
//production 모드로 실행하려면
$ ionic run android --prod

그냥 실행시키면 아이오닉2가 너무 느리게 반응하는 경우가 있다. 이럴때는 개발모드가 아니라 production 모드로 실행하면, native app 과 비교해도 손색이 없을 만큼 빨리 반응하는 경우가 많다.

자신의 안드로이드 폰에서 실행해보려면, 안드로이드 폰의 개발자 옵션을 활성화하고, 폰 개발자 옵션에서 USB 디버깅을 허용으로 변경한다. 그런 후에 USB에 연결하고 다음 명령을 실행한다.

$ ionic run android --device --prod

 

 

댓글 남기기