본문 바로가기
블로그 이미지

방문해 주셔서 감사합니다! 항상 행복하세요!

  
   - 문의사항은 메일 또는 댓글로 언제든 연락주세요.
   - "해줘","답 내놔" 같은 질문은 답변드리지 않습니다.
   - 메일주소 : lts06069@naver.com


앵귤러, 리엑트, 뷰/Angular Tutorial(old)

앵귤러 튜토리얼 (Angular tutorial) -26 with 파이어 베이스

야근없는 행복한 삶을 위해 ~
by 마샤와 곰 2019. 9. 6.

* 제가 다시 작성한 최신 튜토리얼 수정본 입니다. 아래 주소를 통해서 진행하시는 것을 권장 드립니다. ^^

lts0606.tistory.com/328

 

앵귤러 튜토리얼(Angular tutorial) - 1

안녕하세요. 앵귤러에 대해서 알아보기위해 이곳을 찾아주신 분 들께 감사의 말씀 드립니다.^^ 천천히, 초심자도 조금 더 쉽게 접근할 수 있도록 내용을 구성하여 보겠습니다. 어��

lts0606.tistory.com

 

이번시간에는 미루어 두었던 파이어베이스에 파일 업로드 하는 방법에 대해 알아 보겠다.

먼저 파일 업로드를 위해서는 파일 업로드 기능부터 만들어야하는데, input type file을 활용하여 진행하여 보도록 하자.

app.component.html 파일에 아래처럼 파일 테그와 파일을 전송하는 버튼을 추가하자.

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">

<div class="container">
    <input type="button" value='로그아웃' class='btn btn-info' (click)='logOut();'/>
  <br><br>
    <input type='file' (change)='eventListen($event)'/> <!-- 파일 선택 -->
    <input type="button" value='업로드' class='btn btn-success' (click)='upload();'/>  <!-- 전송 -->
  <br><br>
  <div>
    <input type="text" [(ngModel)]="text" class='form-control' style='width:30%; display: inline-block;' />
    <input type="text" [(ngModel)]="desc" class='form-control' style='width:30%; display: inline-block;' />
    <input type="button" value="등록" (click)='insert()' class='btn btn-primary' style='float: right;' />
  </div>
  <br>
  <table class='table table-striped'>
    <tr *ngFor='let items of list;let i = index'>
      <td *ngIf="items">
        <input type="text" value="{{items.text}}" (change)='items.text = $event.target.value' class='form-control'/>
      </td>
      <td *ngIf="items">
        <input type="text" value="{{items.desc}}" (change)='items.desc = $event.target.value' class='form-control' />
      </td>
      <td *ngIf="items" (click)='update(items)'>
        <input type="button" value='수정' class='btn btn-info' />
      </td>
      <td *ngIf="items" (click)='delete(items.idx)'>
        <input type="button" value='삭제' class='btn btn-warning'/>
      </td>
    </tr>
  </table>
</div>

 

Input type file 테그에 change라는 이벤트 속성을 부여하여 파일이 변경되는 이벤트를 감지하도록 하였다.

이를 받는 app.component.ts에 html에서 선언한 함수를 완성시켜 보자.

import { Component } from '@angular/core';
import { DbConnectorService } from './db-connector.service';
import {SessionService} from './session.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  
  private list : any = new Array(); //리스트
  private name = 'newBoard';

  private text : any;
  private desc : any;

  constructor(private service : DbConnectorService, private session : SessionService) {   
    service.getItem(this.name).subscribe((res : any)=>{ 
      console.log(res);
      this.list = res;
    });
  }  

  makeID(length) {  
    var result = '';
    var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    var charactersLength = characters.length;
    for ( var i = 0; i < length; i++ ) {
       result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }
    return result;
 }

 insert(){
   var param = {
    text : this.text,
    desc : this.desc,
    idx : this.makeID(30)
   }
   this.service.setItem(this.name,param);
 }


 update(item){  
  console.log(item);
  if(confirm('수정하시겠습니까?')){
    this.service.updatItem(this.name, {
      text : item.text,
      desc : item.desc
    }, item.idx);
  }
 }


 delete(idx){
  if(confirm('삭제하시겠습니까?')){
    this.service.deleteItem(this.name, idx);
  }
 }

 logOut(){
   this.session.logOut();
 }


 private path : string = "/web_folder/";  //뭐지..?
 private files : FileList; 

 eventListen(event) : void{  //이벤트 받는 부분
   this.files = event.target.files;  //이벤트를 받아서 files에 담아둔다.
 }

 upload() : void{  //버튼을 누르면 실행!
   console.log(this.files);
   for(var i = 0; i < this.files.length; i++){
    console.log(this.files[i].name, this.files[i]);  //첫번째는 이름, 두번째는 해당 객체이다.
   }
 }

}

eventListen이라는 함수는 파일테그의 이벤트를 감지하는 녀석이다. 해당 이벤트에서 파일객체를 가져오는 방법은 어렵지 않다. upload버튼은 파일을 업로드하는 기능이다.

여기까지 완료하였다면 일단 기본 틀은 완성 한 것 이다.

 

그러면, 이전시간에 만들어 두었던 DbConnectorService를 살펴보도록하자. DbConnectorService 파일 이름은 db-connector.service.ts로 하였었다.

  //생략..
  private storage : AngularFireStorage;
  constructor(@Host() @Optional() db : AngularFirestore,@Host() @Optional()strg : AngularFireStorage) { //Host와 Optional 데코레이터
    this.DataBase = db;
    this.storage = strg;  //요기!!
  }
  //생략..  

 

이미 우리는 storage라는 객체가 생성자를 통해 이미 전달받고 있었다. 물론 사용하지는 않았었지만 말이다.

해당 객체를 활용해서 파일 데이터를 파이어베이스에 저장하는 것이 가능 하다.

그렇다면 해당 객체를 활용한 파일 업로드 기능, 삭제기능, 정보 가져오기 기능을 만들어보도록 하자.

db-connector.service.ts에 추가될 아래 소스코드를 살펴 보자.  

import { Injectable, Host, Optional } from '@angular/core';

import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { BehaviorSubject } from 'rxjs';
import { AngularFireStorage} from '@angular/fire/storage';
import { map } from "rxjs/operators"; // map이다.

@Injectable({
  providedIn: 'root'
})
export class DbConnectorService {
  //데이터 베이스 관련 객체
  private DataBase : AngularFirestore;
  //컬렉션을 담아두는 배열
  private collections = new Array();
  //나중에 쓸 저장소 관련 객체
  private storage : AngularFireStorage;

  constructor(@Host() @Optional() db : AngularFirestore,@Host() @Optional()strg : AngularFireStorage) { //Host와 Optional 데코레이터
    this.DataBase = db;
    this.storage = strg;
  }

  //컬렉션이 있는지 조사해서 해당 내용을 리턴하는 함수
  private getCollections (db_name : string) : AngularFirestoreCollection<any>{
    var result = this.collections[db_name];  //배열에 값을 확인
    if(result == undefined || result == null){  //배열에 값이 비어있다면
      this.collections[db_name] = this.DataBase.collection<any>(db_name, (ref) =>ref);   //새로 만들어준다.
      result = this.collections[db_name];
    }
    return result;
  }

  getItem(db_name : string){
    var items : BehaviorSubject<any[]> = new BehaviorSubject([]);
    this.getCollections(db_name).valueChanges().forEach((val)=>{  //데이터를 가져오기
      items.next(val); 
    });    
    return items; 
  }

  setItem(db_name : string, param : any){  //데이터 등록
    this.getCollections(db_name).add(param);  
  }

  updatItem(db_name : string, param: any, key:any){   //key 값 추가
    var subscription = this.getCollections(db_name).stateChanges().pipe(map( changes => { //pip 함수는 map 함수를 붙여주는 역할을 한다.
      return changes.map(a=>{  //map 함수는 데이터의 내용을 바꾸는 역할을 한다.
        const data = a.payload.doc.data() as any;
        const ID = a.payload.doc.id; //고유 아이디 값
        if(data.idx == key){
          this.getCollections(db_name).doc(ID).update(param);  //id 값을 통해 수정한다.
        }
        return data;
      });
    })).subscribe((oo)=>{  //해당 수정행위를 구독한다.
      subscription.unsubscribe();  //getItem 함수를 통해 구독을 하고 있기 때문에 여기서 수정한 구독행위는 바로 종료시킨다.
                                   //만약 해당 구독행위를 종료하지 않으면 구독행위가 다중 발생하여 무한loop 수정 행위가 발생한다.
    });
  }

  deleteItem(db_name : string, key: any){
    console.log(db_name, key);
    var subscription = this.getCollections(db_name).stateChanges().pipe(map( changes => { //pip 함수는 map 함수를 붙여주는 역할을 한다.
      return changes.map(a=>{  //map 함수는 데이터의 내용을 바꾸는 역할을 한다.
        const data = a.payload.doc.data() as any;
        const ID = a.payload.doc.id; //고유 아이디 값
        if(data.idx == key){
          this.getCollections(db_name).doc(ID).delete();  //key를 통해 삭제한다.
          return data;
        }
        return data;
      });
    })).subscribe((oo)=>{  //해당 수정행위를 구독한다.
      subscription.unsubscribe();  //getItem 함수를 통해 구독을 하고 있기 때문에 여기서 수정한 구독행위는 바로 종료시킨다.
    });
  }

  //여기 아래부터가 파일관련된 내용이다 --------------------------
  fileUpload(filepath, file) : void{
    this.storage.upload(filepath, file);
  }

  deleteFile(path : string){
    const refer = this.storage.ref(path);
    refer.delete();
  }

  getFileAddress(){
    return this.storage;
  }

}

 

대충 보기만 해서는 매우 쉬워보인다. 사실 파일업로드는 앞선 페이지에서 한 일반 텍스트 CRUD 보다 훨씬 쉽다.

해당 내용을 토대로 일단 업로드 기능부터 완성시켜 보자.

app.components.ts 파일의 기능을 완성하여 보자.

import { Component } from '@angular/core';
import { DbConnectorService } from './db-connector.service';
import {SessionService} from './session.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  
  private list : any = new Array(); //리스트
  private name = 'newBoard';

  private text : any;
  private desc : any;

  constructor(private service : DbConnectorService, private session : SessionService) {   
    service.getItem(this.name).subscribe((res : any)=>{ 
      console.log(res);
      this.list = res;
    });
  }  

  makeID(length) {  
    var result = '';
    var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    var charactersLength = characters.length;
    for ( var i = 0; i < length; i++ ) {
       result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }
    return result;
 }

 insert(){
   var param = {
    text : this.text,
    desc : this.desc,
    idx : this.makeID(30)
   }
   this.service.setItem(this.name,param);
 }


 update(item){  
  console.log(item);
  if(confirm('수정하시겠습니까?')){
    this.service.updatItem(this.name, {
      text : item.text,
      desc : item.desc
    }, item.idx);
  }
 }


 delete(idx){
  if(confirm('삭제하시겠습니까?')){
    this.service.deleteItem(this.name, idx);
  }
 }

 logOut(){
   this.session.logOut();
 }


 private path : string = "/web_folder/";
 private files : FileList;

 eventListen(event) : void{
   this.files = event.target.files;
 }

 upload() : void{ 
   console.log(this.files);
   for(var i = 0; i < this.files.length; i++){
    console.log(this.files[i].name, this.files[i]);
    this.service.fileUpload( this.path + this.files[i].name, this.files[i]);
   }
 }


}

파일 업로드가 저거 한방에 끝나다니...매우 간단해 보인다.

그럼 이제 업로드가 가능한 것 입니까?

대답은 아직이다. 우리는 파이어베이스에 데이터베이스만 사용하도록 설정하였을 뿐 저장소 세팅은 하지를 않았다. 저장소 세팅을 해 보도록 하자.

 

먼저, 파이어 베이스에 접속한다.

이후에 Storage를 눌러서 위와 같은 화면으로 이동하자.

'시작하기' 버튼을 눌러서 다음단계로 진행하자.

 

네..허용하겠습니다. 다음버튼을 누르자~
뭔가 작업이 진행된다.

 

조금 기다리면 아래사진과 같이 완성이 된다.

 

여기서 흥분(?)하지 말고 규칙 탭을 눌러 이동하자. 그리고 아래처럼 조건(?)을 추가하여 주자.

권한에 따른 읽기 쓰기 여부이다. 일단 전부허용 개념으로 가자.

 

자, 이제 다시 돌아와서 파일 1개를 업로드 하여 보자.

뭔가 오류없이 잘 들어간 것 같다.

 

다시 파이어 베이스 콘솔창으로가자. 업로드가 되었는지 새로고침을 눌러보자.

오..업로드 되었다!!

 

여기까지가 업로드 방법이다..

이제부터 다운로드를 해 볼 차레인데...다운로드는 긴장하는게 좋다.

크로스 도메인이라고 들어보았는가? 파이어베이스에서는 크로스 도메인에 대한 정책으로 인하여 사실 파일 다운로드를 앵귤러에서 처리하기가 조금 어렵다.

즉, 비동기 방식에서 파일을 다운로드 받으려면 뭔가(?)를 좀 해주어야 한다.

자, 좀 복잡하지만 해보도록 하자.

 

아래 사이트의 내용을 살펴보자.

구글 클라우드..sdk??

 

저건 구글에서 클라우드 형태로 무언가 서비스를 운용하는 것에대한 각종 세팅을 할 수 있는 프로그램을 의미한다.

뭔소리냐..구글에서 제공하는 클라우드서버, 파이어베이스 데이터베이스, 파이어베이스 저장소 등등 이러한 모든 것에 대한 개발과 관련된, 또는 환경 설정과 관련된 세팅을 할 수 있도록 해주는 것을 의미한다.

아무튼..아래 사이트를 타고 구글 sdk를 설치하여 보자.

https://cloud.google.com/sdk/docs/quickstart-windows

 

Windows 빠른 시작  |  Cloud SDK 문서  |  Google Cloud

Windows 빠른 시작 이 페이지에서는 Google Cloud SDK를 설치하고, 초기화한 후 명령줄에서 코어 gcloud 명령어를 실행하는 방법을 보여줍니다. 참고: 프록시/방화벽을 사용하는 경우 설치에 대한 자세한 내용은 프록시 설정 페이지를 참조하세요. 시작하기 전에 프로젝트가 없으면 Google Cloud Platform 프로젝트를 만듭니다. Google Cloud SDK 설치 프로그램을 다운로드합니다. 설치 프로그램을 실행하고 안내 메시지를 따

cloud.google.com

 

sdk와 관련된 파일을 다운로드 받아서 설치하여보자.

설치가 진행된다.
설치 다 했으면 콘솔을 실행한다.

 

실행된 콘솔에서는 gcloud init을 입력하여 아래처럼 하여보자...

아..파일다운로드..

실행된 콘솔에서 gcloud init을 입력하고, 로그인할꺼냐는 질문에  Y를 입력한다.

 

Y로 입력하니 갑자기 웹 브라우저가 실행되면서 로그인 하랜다...파이어베이스에서 사용중인 구글 계정으로 로그인하자.

로그인하면 나나타는 창

 

로그인하면 아래처럼 인증완료 창이 뜬다.

왓..인증되었단다..

 

우리가 사용중인 파이어베이스에 크로스 도메인을 위해 설정파일을 업로드 해주도록 하자. * 콘솔창 끄지 말자!!

아래처럼 cors.json 파일을 만들어주자.

[
  {
    "origin": ["*"],
    "method": ["GET"],
    "maxAgeSeconds": 3600
  }
]

 

아래 명령어를 입력해서 해당 파일을 전송하면 크로스 도메인 허용이 설정이 된다.

아래 저장소 주소는 우리가 app.module.ts에서 사용했던 storageBucket 주소를 의미한다.

gsutil cors set cors.json gs://저장소 주소

거의 다 왔다..

 

이제 다운로드 기능을 만들어서 확인하여 보자.

app.component.html에서 다운로드 버튼을 만들어 주자.

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">

<div class="container">
    <input type="button" value='로그아웃' class='btn btn-info' (click)='logOut();'/>
  <br><br>
    <input type='file' (change)='eventListen($event)'/> <!-- 파일 선택 -->
    <input type="button" value='업로드' class='btn btn-success' (click)='upload();'/>  <!-- 전송 -->
    <input type="button" value='다운로드' class='btn btn-success' (click)='download();'/> <!-- 받기 -->
  <br><br>
  <div>
    <input type="text" [(ngModel)]="text" class='form-control' style='width:30%; display: inline-block;' />
    <input type="text" [(ngModel)]="desc" class='form-control' style='width:30%; display: inline-block;' />
    <input type="button" value="등록" (click)='insert()' class='btn btn-primary' style='float: right;' />
  </div>
  <br>
  <table class='table table-striped'>
    <tr *ngFor='let items of list;let i = index'>
      <td *ngIf="items">
        <input type="text" value="{{items.text}}" (change)='items.text = $event.target.value' class='form-control'/>
      </td>
      <td *ngIf="items">
        <input type="text" value="{{items.desc}}" (change)='items.desc = $event.target.value' class='form-control' />
      </td>
      <td *ngIf="items" (click)='update(items)'>
        <input type="button" value='수정' class='btn btn-info' />
      </td>
      <td *ngIf="items" (click)='delete(items.idx)'>
        <input type="button" value='삭제' class='btn btn-warning'/>
      </td>
    </tr>
  </table>
</div>

 

이어서 다운로드 버튼기능을 추가하여주자.

import { Component } from '@angular/core';
import { DbConnectorService } from './db-connector.service';
import {SessionService} from './session.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  
  private list : any = new Array(); //리스트
  private name = 'newBoard';

  private text : any;
  private desc : any;

  constructor(private service : DbConnectorService, private session : SessionService) {   
    service.getItem(this.name).subscribe((res : any)=>{ 
      console.log(res);
      this.list = res;
    });
  }  

  makeID(length) {  
    var result = '';
    var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    var charactersLength = characters.length;
    for ( var i = 0; i < length; i++ ) {
       result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }
    return result;
 }

 insert(){
   var param = {
    text : this.text,
    desc : this.desc,
    idx : this.makeID(30)
   }
   this.service.setItem(this.name,param);
 }


 update(item){  
  console.log(item);
  if(confirm('수정하시겠습니까?')){
    this.service.updatItem(this.name, {
      text : item.text,
      desc : item.desc
    }, item.idx);
  }
 }


 delete(idx){
  if(confirm('삭제하시겠습니까?')){
    this.service.deleteItem(this.name, idx);
  }
 }

 logOut(){
   this.session.logOut();
 }


 private path : string = "/web_folder/";
 private files : FileList;

 eventListen(event) : void{
   this.files = event.target.files;
 }

 upload() : void{ 
   console.log(this.files);
   for(var i = 0; i < this.files.length; i++){
    console.log(this.files[i].name, this.files[i]);
    this.service.fileUpload( this.path + this.files[i].name, this.files[i]);
   }
 }

 download() : void{
  var obj = this.service.getFileAddress();
  const refer = obj.ref(this.path + 'aaa.txt');  //참고사항을 가져온다.
  refer.getDownloadURL().subscribe(url=>{  //해당 주소에 대해서 구독실시
    var xhr = new XMLHttpRequest();
    xhr.responseType = 'blob';  //파일을 블랍형태로 받아서
    xhr.onload = function(event) {
      var blob = xhr.response;
      console.log(blob);
      var a = document.createElement('a');  //임의의 테그를 만든 후
      a.href = window.URL.createObjectURL(blob);
      a.download = name;
      a.dispatchEvent(new MouseEvent('click'));  //이벤트를 발생시킨다.
    };
    xhr.open('GET', url, true);
    xhr.send();
  });
 }

}

 

이제 다운로드 버튼을 클릭하여보자.

 

파일이 받아진다..!

 

와..파일 업로드는 할만한데 다운로드는 뭔가 환경구성이 절반인 것 같다.

 

여기서 그러면 의문점이 드는 것이..

파일 저장소에 저장된 파일 이름은 어떻게 관리하느냐 이다.

이에 대한 답은!

파일 이름 및 주소 값은 파이어베이스 데이터베이스에 저장해야 되는 것 이며, 파일 저장소는 단순히 저장소의 역할만 해야되는 것 이다.

즉, 파일이름 및 정보를 파이어베이스 데이터베이스에 저장하고 파일은 저장소에 보관하는 개념으로 만들어야 하는 것 이다.

해당 기능의 완성과 삭제기능은 어렵지 않으므로 한번 직접 해 보기 바란다.

 

다음시간에는 여태껏 진행해 본 내용에 대해 전반적인 내용에 대한 정리를 해 보도록 하겠다.

cors.json
0.00MB
src.zip
0.02MB

반응형
* 위 에니메이션은 Html의 캔버스(canvas)기반으로 동작하는 기능 입니다. Html 캔버스 튜토리얼 도 한번 살펴보세요~ :)
* 직접 만든 Html 캔버스 애니메이션 도 한번 살펴보세요~ :)

댓글