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

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

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


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

앵귤러 튜토리얼(Angular tutorial) - 24, with FireBase : alter 심화

야근없는 행복한 삶을 위해 ~
by 마샤와 곰 2020. 8. 31.

저번시간에는 pipe라는 함수를 통해서 take와 map에 대해서 알아보았습니다.
pipe는 "통로"의 의미로 구독하는 내용에 대해서 다양한 기능을 연결할 때 사용됩니다.

이번시간에 살펴볼 내용은 이제 pipe를 활용하여 데이터를 수정하는 방법입니다.
데이터 수정은 마찬가지로 collection함수를 통해 생성된 객체에 대해서 이루어지도록 되어 있습니다.
collectionArray라는 변수에 우리는 AngularFirestoreCollection객체를 key값은 데이터베이스 명칭으로 넣어주고 있습니다.

위 내용을 바탕으로 간단하게 함수를 서비스에 추가하여 줍니다.

* 대상 : ask.service.ts

  //생략..
  updateData(db_name: string){
    this.collectionArray[db_name];
  }
  //생략..

 

함수 이름을 updateData라고 만들었습니다.

그리고 단촐하게 collectionArray에서 우리가 사용할 대상을 가져오도록 하였습니다.
다음으로 할 작업은 수정을 위해서 데이터를 가공해야되는 점 입니다.
바로 위에 존재하는 testPipeTake함수에서 우리는, map이라는 함수를 통해서 데이터를 가공할 수 있었습니다.
일단 비슷한 모양으로 기능을 붙여보도록 합니다.

* 대상 : ask.service.ts

  //생략..
  updateData(db_name: string){
    this.collectionArray[db_name].stateChanges().pipe(
      map( (actions:any) => {
        return actions.map(a => {
          const data = a.payload.doc.data();  //데이터  
          const ID = a.payload.doc.id;  //고유 키 값
          return data;
        });
      })
    );
  }
  //생략..

 

여기서는 stateChanges함수를 사용하였습니다.
데이터를 단순하게 보는 것에는 valueChanges 함수가 좀 더 최적화 되어 있으며, 다양한 행동에 대해서 기능을 완성하려면 stateChanges함수를 사용합니다.
저번시간과 동일하게 map이 두번 호출되어있습니다.
첫번째 map은 stateChanges를 통해서 생성된 객체를 가공한다는 의미이며, 
두번째 map은 첫번째 map을 통해 생성된 객체에서 다양한 정보를 가져오는 의미 입니다.
자세히 보시면 id라는 고유 키 값이 나오는 것을 볼 수 있습니다.
여태껏 우리가 파이어스토어에 데이터를 등록 할 때 자동으로 만들어준 고유 키 값을 의미합니다.

이렇개 생성된 정보를 바탕으로 이제 수정하는 함수를 호출하여 보겠습니다.

* 대상 : ask.service.ts

  //생략..
  updateData(db_name: string){
    this.collectionArray[db_name].stateChanges().pipe(
      map( (actions:any) => {
        return actions.map(a => {
          const data = a.payload.doc.data();  //데이터  
          const ID = a.payload.doc.id;  //고유 키 값
          this.collectionArray[db_name].doc(ID).update({});  //수정합니다.
          return data;
        });
      })
    );
  }
  //생략..

 

내부 변수인 data에는 원본 데이터가 존재 합니다.
그리고 update함수에서는 key와 value형식으로 데이터를 받게 되어 있습니다.
우리는 ID값만 알면 이제 수정할 수 있게 되었습니다.
데이터를 받을 수 있도록 최종모습을 작성하여 봅니다.

* 대상 : ask.service.ts

  //생략..
  updateData(db_name: string, parameter : any, target_id : any){
    this.collectionArray[db_name].stateChanges().pipe(
      map( (actions:any) => {
        return actions.map(a => {
          const data = a.payload.doc.data();  //데이터  
          const ID = a.payload.doc.id;  //고유 키 값
          if(target_id  == ID){  //들어온 아이디 값이 일치한다면
            this.collectionArray[db_name].doc(ID).update(parameter);  //교체할 내용을 바꿉니다.
          }
          return data;
        });
      })
    ).subscribe();
  }
  //생략..

 

stateChanges를 통해서 생성된 기능은 구독(subscribe)을 하지 않으면 아무런 행동을 하지 않습니다.
일단 수정을 한번 해보도록 합니다.
app컴포넌트에는 우리가 데이터를 호출하는 기능을 만들어 놓았습니다.
그런데 그곳에서 우리는 고유 키 값을 가져오지는 못하였습니다.
값을 가져와서 구독하는 부분인 valueChanges에 기능을 조금 수정하여 봅니다.

* 대상 : app.component.ts

  //생략..
  constructor(private service : AskService) {   
    //service.addItem("board",{number:6,hello:'hello',today:new Date()});
    service.getItem("board").valueChanges({idField: 'idx'}).subscribe( arg => {  //idField값에 idx를 부여하였습니다.
      console.log(arg);
    });
    //service.testPipeTake();
  }  
  //생략..

 

idField라는 키 값에 idx라는 값을 대입하였습니다.
이렇게 되면 idx라는 키 값에 고유 아이디를 넣어주게 됩니다.

idx라는 값에 고유 아이디값이 들어갔습니다.

 

기존 데이터가 많아서 조금 지웠습니다.
이제 idx값 1개를 복사해서 수정하는 함수를 한번 호출하여 보도록 합니다.

* 대상 : app.component.ts

  //생략..
  constructor(private service : AskService) {   
    //service.addItem("board",{number:6,hello:'hello',today:new Date()});
    service.getItem("board").valueChanges({idField: 'idx'}).subscribe( arg => {
      console.log(arg);
    });
    //service.testPipeTake();
    service.updateData('board',{number:10, new_text:'hello updater'},'OHW97JhpZeCB6dEp7KXo');
  } 
  //생략..

데이터를 불러올때 콘솔이 1번 동작했으며, 수정하고나서 1번 더 동작했습니다.

 

기존에 존재하는 number 값을 10으로 바꾸고, 없던 항목인 new_text를 추가하여 보았습니다.
처음에 ask서비스의 getItem를 구독하면서 데이터가 console.log로 출력되었습니다.

그리고 updateData함수가 동작하여 데이터가 수정되었으므로 getItem함수가 구독중인 컬렉션이 변화하였기 때문에 다시 subscribe에 선언한 console.log가 동작 하였습니다.


그런데, updateData함수의 수정기능에서의 큰 문제점이 존재합니다.
먼저 사진부터 보겠습니다.

뭐죠? 이출력 문구는.....?

 

무엇인지는 모르겠지만 console.log로 엄청난 값이 나오는 것을 볼 수 있습니다.
이에 적용한 아래 코드를 살펴보도록 합니다.

* 대상 : app.component.ts

  //생략..
  constructor(private service : AskService) {   
    //service.addItem("board",{number:6,hello:'hello',today:new Date()});
    service.getItem("board").valueChanges({idField: 'idx'}).subscribe( arg => {
      console.log(arg);
    });
    //service.testPipeTake();
    service.updateData('board',{number:10, new_text:'hello updater1'},'OHW97JhpZeCB6dEp7KXo'); //1번호출
    service.updateData('board',{number:10, new_text:'hello updater2'},'OHW97JhpZeCB6dEp7KXo'); //2번호출
  }  
  //생략..

 

수정함수를 2번 호출하여 보았습니다. 

그랬더니 사진처럼 엄청난 출력이 발생하는 것을 볼 수 있습니다.

그 이유는,
ask서비스에서 만들어준 updateData함수는 stateChanges라는 함수를 호출하여 구독(subscribe)를 하는 함수 입니다.
stateChanges라는 말 처럼 상태가 변하는 것을 감지하는 함수이므로, 만약 우리가 수정을 2번이상 하는 경우에는 첫번째 수정행위가 상태변화를 불러 두번째 수정행위에 영향을주고,
두번째 수정행위가 상태변화를 불러 다시 첫번째 상태를 변화시키는 마치 무한 반복 현상과 같은 모습이 발생하게 되는 것 입니다.

 

해당 부분이 중요합니다!

위 코드처럼 수정 함수가 2번이상 동작하면 stateChanges함수가 2번 구독이 됩니다.
2번 구독되면 첫번째 수정이 상태변화를 불러 두번째 상태변화가 동작하고, 두번째 상태변화가 상태를 변화시켜 다시 첫번째를 동작시키게 됩니다.
결국 무한 반복 현상이 이루어지기 때문에 올바르지 않는 코드입니다.

이를 해결하기 위해서는 구독을 1번만 하도록 해 주어야 합니다.

* 대상 : ask.service.ts

import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { AngularFirestore, CollectionReference, AngularFirestoreCollection,DocumentChangeAction } from '@angular/fire/firestore';

import {take, map} from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class AskService {

  private DataBase: AngularFirestore;
  private itemsCollection: AngularFirestoreCollection<any>;
  private collectionArray = {};  //json형식으로 변경하여 줍니다. 

  //저장소(나중에 데이터베이스 서버)
  private readonly storage = {
    id: 'admin',
    passwd: '1234'
  }

  constructor(db: AngularFirestore) {   //모듈에서 만들어진 파이어 베이스 접속관련 객체
    this.DataBase = db;
  }
  //
  testPipeTake(){  //파이프와 테이크의 예제
    this.DataBase.collection<any>("test").stateChanges().pipe(
      take(1), 
      map(actions => {
        return actions.map(a => a.payload.doc.data());
      })
    ).subscribe(
      arg=>{
        console.log(arg);
      }
    );
  }

  //수정함수 입니다!!
  updateData(db_name: string, parameter : any, target_id : any){
    this.collectionArray[db_name].stateChanges().pipe(
      take(1), //구독행위는 1번만 하세요!
      map( (actions:any) => {
        return actions.map(a => {
          const data = a.payload.doc.data();  //데이터  
          const ID = a.payload.doc.id;  //고유 키 값
          if(target_id  == ID){  //들어온 아이디 값이 일치한다면
            this.collectionArray[db_name].doc(ID).update(parameter);  //교체할 내용을 바꿉니다.
          }
          return data;
        });
      })
    ).subscribe();
  }

  getItem(db_name: string) {
    if(this.collectionArray[db_name]){
      this.collectionArray[db_name] = null;
    }
    this.collectionArray[db_name] = this.DataBase.collection<any>(db_name, (ref: CollectionReference) => {
      //ref.where("hello","==",'hahaha') //일반 질의문
      //ref.where("world","array-contains", "aaa") // 배열인 경우
      //ref.orderBy('number', 'asc').startAt(0).limit(2);  //시작, 한계점 추가
      return ref;
    });  //리턴
    return this.collectionArray[db_name];
  }

  addItem(db_name : string, data : any){
    if(this.collectionArray[db_name] == null){
      this.collectionArray[db_name] = this.DataBase.collection<any>(db_name);
    }
    this.collectionArray[db_name].add(data);
  }


  //로그인을 시도하는 함수
  tryToLogin(param: any) {
    return new Observable(arg => {
      if (param.id == this.storage.id && param.passwd == this.storage.passwd) {
        arg.next({ status: true });
        localStorage.setItem('status', "true");
      } else {
        arg.next({ status: false, reason: 'wrong information' });
      }
      arg.complete();
    });
  }

  //가드가 구독하는 대상
  readonly isLogged: BehaviorSubject<boolean> = new BehaviorSubject(false);

  //로그인이 되었는지 확인하는 함수
  isLogIn(): void {
    if (localStorage.getItem('status') == 'true') {
      this.isLogged.next(true);
    } else {
      this.isLogged.next(false);
    }
  }

}

 

아주 간단하게 take라는 함수를 pipe에 추가하였습니다.
이제 app컴포넌트에서 다시 수정함수를 2번불러보도록 합니다.

3번 출력이 되었습니다.

 

처음 console.log는 getItem함수를 구독하였으므로 실행되었습니다.

두번째 console.log는 updateData함수가 동작하였기 때문에 실행되었고, 세번째도 updateData함수가 동작하였기 때문에 console.log가 실행되었습니다.

이제 수정하는 구독(subscribe)행위가 1번만 하도록 지정이 되어있으므로 더이상 무한루프의 수정행위는 발생하지 않습니다.
사실 구독자와 발행자로 이루어지는 개발 형태의 모습은 흔하지는 않습니다.
익숙해 질 때 까지 연습이 필요합니다. ^^
다음시간에는 수정기능의 최종 완성된 모습과, 삭제기능에 대해서 살펴보겠습니다.

 

thirdStudy.zip
0.01MB

 

* 여담 : stream에 대해서 
아마 rxjs를 검색하셔서 내용을 살펴보신 분이라면 스트림(stream)이라는 용어를 자주보게 됩니다.
구독자(subscriber), 발행자(publisher) 형식의 패턴은 데이터의 흐름(스트림)을 다양한 행동을 체이닝(chaining) 형식으로 만드는 형태로 이루어져 있습니다.
마치 딸기를 수확해서 딸기잼을 만드는 과정과 비슷한 개념이라 보시면 됩니다.
딸기잼을 사려는 사람은 구독자, 딸기잼을 만드는 사람은 발행자, 딸기잼이 수확되어 공장에서 만드는 행위가 스트림을 가공(pipe, map, take, filter 등)하는 행위라고 볼 수 있습니다.
여기 튜토리얼에서 따로 스트림이라는 용어를 언급하지 않아도 해당 내용은 포함되어 있습니다. ^^

 

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

댓글