Firebase Web 채팅앱 만들기 - 13. Storage를 이용한 파일 전송기능

  • 채팅창에서 아래와 같은 파일 전송기능을 구현 합니다.

13-1

13-2

1. Firebase Storage 설정 작업

  • Firebase console 로 들어가서 Storage 메뉴로 들어가 시작하기 버튼을 누릅니다.
  • 시작하기 버튼을 눌러야 기본적인 권한이 생성이 되고 Bucket이 생성이 되며 Storage를 사용할 수 있는 상태가 됩니다.

13-3

2. 코드 작성

          /**
           * 초기 필드 변수 할당
           */
          FirebaseChat.prototype.init = function(){
              //...생략
              this.iBtnAttach = document.getElementById('iBtnAttach');
              this.attachFile = document.getElementById('attachFile');
          }



          /**
           * 초기 이벤트 바인딩
           */
          FirebaseChat.prototype.initEvent = function(){
              //...생략
              this.iBtnAttach.addEventListener('click', this.onBtnAttachClick.bind(this));
              this.attachFile.addEventListener('change', this.onAttachFile.bind(this));
          }

          /**
           *  첨부파일 버튼 클릭
           */
          FirebaseChat.prototype.onBtnAttachClick = function(){
              this.attachFile.click();
          }

          /**
           * 첨부파일 전송
           */
          FirebaseChat.prototype.onAttachFile = function(event){

              $('#dnModal').modal('open');
              var files = event.target.files;
              var filesLength = files.length;
              var progressBar = document.getElementById('dvProgressBar');
              var fileName = files[0].name;
              var path = FirebaseChat.yyyyMMddHHmmsss().substr(0, 8) +'/'+this.roomId + '/' + this.auth.currentUser.uid + '/' + fileName;
              var uploadTask = firebase.storage().ref().child(path).put(files[0]);

              var cbProgress = function(snapshot){ // 진행 과정
                  var progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
                  progressBar.style.width = progress+'%';
              }

              var cbError = function(error) { // 에러발생
                  console.log(error);
                  $('#dnModal').modal('close');
                  alert('업로드 중 에러가 발생하였습니다.');
              }
              var cbComplete = function() { // 완료
                  //프로그레스바 닫기
                  $('#dnModal').modal('close');

                  //완료 다운로드 링크 메세지 보내기
                  this.saveMessages(null, uploadTask.snapshot.downloadURL, fileName);

                  //files 리셋
                  event.target.value='';
              }
              //프로그레스바
              uploadTask.on('state_changed', cbProgress.bind(this) , cbError.bind(this), cbComplete.bind(this));
          }

          /**
           * 메세지 전송
           */
          FirebaseChat.prototype.saveMessages = function(inviteMessage, downloadURL, fileName){
              var user = this.auth.currentUser;
              var msg = this.dvInputChat.innerHTML.trim();
              //초대메세지
              if(inviteMessage && inviteMessage.length > 0){
                  msg = inviteMessage;
              }
              //파일전송 메세지
              if(downloadURL && fileName){
                  msg = "<a class='waves-effect waves-light btn blue' download='"+fileName+"' href='"+ downloadURL +"'>다운로드</a></br><span class=''>파일명 : "+ fileName +"</span>";
              }


              if(msg.length > 0){
                  this.dvInputChat.focus();
                  this.dvInputChat.innerHTML = '';
                  var multiUpdates = {};
                  var messageRefKey = this.messageRef.push().key; // 메세지 키값 구하기
                  var convertMsg = downloadURL ? msg : FirebaseChat.convertMsg(msg); //다운로드 URL일 경우 convert하지 않음


                  //...생략
                          

                  //유저별 룸리스트 저장
                  var roomUserListLength = this.roomUserlist.length;
                  if(this.roomUserlist && roomUserListLength > 0){
                      for(var i = 0; i < roomUserListLength ; i++){
                          multiUpdates['UserRooms/'+ this.roomUserlist[i] +'/'+ this.roomId] = {
                              roomId : this.roomId,
                              roomUserName : this.roomUserName.join(this.SPLIT_CHAR),
                              roomUserlist : this.roomUserlist.join(this.SPLIT_CHAR),
                              roomType : roomUserListLength > 2 ? this.MULTI : this.ONE_VS_ONE,
                              roomOneVSOneTarget : roomUserListLength == 2 && i == 0 ? this.roomUserlist[1] :  // 1대 1 대화이고 i 값이 0 이면
                                  roomUserListLength == 2 && i == 1 ? this.roomUserlist[0]   // 1대 1 대화 이고 i값이 1이면
                                      : '', // 나머지
                              lastMessage : downloadURL ? '다운로드' : convertMsg,
                              profileImg : user.photoURL ? user.photoURL : '',
                              timestamp: firebase.database.ServerValue.TIMESTAMP

                          };
                      }
                  }
                  this.database.ref().update(multiUpdates);
              }
          }
  • 코드 설명 :
    • 채팅방 화면에서 클립모양의 아이콘을 클릭 하면 file 타입의 input 태그를 클릭하게 되고, 파일이 선택되어지면 onAttachFile 메소드가 실행.
    • onAttachFile 메소드 :
      • 메소드의 시작코드는 파일 다운로드 팝업을 띄움
      • 선택한 파일은 event.target.files 에 데이터가 들어옴
      • 파일을 하나 선택할 뿐이지만 형태는 fileList 객체로 데이터가 전달됨.
      • 배열처럼 첫번째 인덱스의 파일을 선택하여 파일명 변수에 할당
        var files = event.target.files;
        var fileName = files[0].name;
      
      • 파일을 저장하는 위치는 ‘/날짜(yyyyMMdd)/방ID/유저UID/파일명’
      • 파일 저장 경로 중에 날짜를 둔것은 Firebase Functions 기능을 통해서 주기적으로 삭제를 실시하기 위함
        var path = FirebaseChat.yyyyMMddHHmmsss().substr(0, 8) + '/'+this.roomId + '/' + this.auth.currentUser.uid + '/' + fileName;
      
      • 실질적으로 파일을 저장하는 코드 부분
      • ref메소드는 Realtime Database와 비슷하게 저장되는 경로를 의미 합니다.
      • 실질적인 저장은 Google Cloud Storage에 저장
      • put 메소드는 UploadTask 객체를 반환 합니다.
      • UploadTask 객체로 업로드 관리를 수행
        var uploadTask = firebase.storage().ref().child(path).put(files[0]);
      
      • UploadTask객체는 ‘state_changed’ 이벤트를 발생.
      • 아래의 코드는 ‘state_changed’ 이벤트를 받았을 때 3가지의 콜백 함수를 파라미터로 지정한 코드.
      • 첫번째 콜백함수는 진행 중 수행하는 함수이고, 두번째는 에러가 발생했을때, 세번째는 업로드가 완료됬을 때 수행됩니다
        uploadTask.on('state_changed', cbProgress.bind(this) , cbError.bind(this), cbComplete.bind(this));
      
      • 진행 중 수행하는 콜백 함수인 cbProgresss를 살펴보면, 프로그래스바의 게이지의 넓이를 조정
        var cbProgress = function(snapshot){ // 진행 과정
            var progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
            progressBar.style.width = progress+'%';
        }
      
      • 에러 콜백함수인 cbError을 살펴보면, 사용자에게 에러가 발생하였음을 alert창으로 알리고, 콘솔에 에러내용을 출력
        var cbError = function(error) { // 에러발생
            console.log(error);
            $('#dnModal').modal('close');
            alert('업로드 중 에러가 발생하였습니다.');
        }
      
      • 마지막으로 완료 콜백 함수인 cbComplete 함수를 보면, 팝업 모달창을 닫고, 다운로드 url정보가 담긴 메세지를 채팅방에 메세지를 전송
        var cbComplete = function() { // 완료
            //프로그레스바 닫기
            $('#dnModal').modal('close');
              
            //완료 다운로드 링크 메세지 보내기
            this.saveMessages(null, uploadTask.snapshot.downloadURL, fileName);
              
            //files 리셋
            event.target.value='';
        }
      

3. Storage 권한 부여

  • Firebase Storage는 무료의 경우 저장용량 5GB,다운로드 크기 하루 1GB, 하루 업로드 20000회, 다운로드 50000회의 제한이 있습니다.
  • 유료 종량제 요금의 경우 이러한 제한은 없지만, 사용한 만큼 비용이 청구
  • 이러한 이유로 Storage는 보안 설정이 중요합니다.
  • 보안 설정을 통해서 접근 권한이나 업로드 용량, 업로드 가능한 파일 타입을 지정할 수 있습니다.

  • 아래 코드는 초기 기본 권한
  • Authentication을 통해서 인증 받은 사람들에게 읽기와 쓰기 권한이 부여
  • 프로젝트에 storage.rules 파일에서 확인 가능
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if request.auth!=null;
    }
  }
}
  • 수정할 권한 내용은 아래와 같습니다.
    • 읽기권한은 인증받은 유저 모두에게 권한을 부여하나 쓰기 권한은 본인의 유저UID 경로에만 접근해서 저장
    • 이미지 파일과 zip확장자를 가진 파일만 업로드가 되도록 하고, 용량은 5메가바이트 미만
service firebase.storage {
  match /b/{bucket}/o {
    match /{yyyyMMdd}/{roomId}/{userUid}/{file} {
      allow read: if request.auth!=null;
      allow write: if request.auth!=null
                    && request.auth.uid == userUid
                    && (request.resource.contentType.matches('image/.*') || file.matches(".*.zip"))
                    && request.resource.size < 5 * 1024 * 1024;

    }
  }
}