본문 바로가기
CodeLab/Firebase

Firebase Realtime Database 조금 더 자세히

by 블리드카가 2018. 1. 9.
728x90

Firebase Realtime Database의 경우 개발자가 흔하게 접할수 있는 SQL 데이터베이스와는 확연히 다릅니다. JSON형태의 NoSQL 데이터베이스이며 실시간 동기화라는 특별한 기능 덕분에 강력한 기능을 앱상에서 구현할 수 있지만, 그로 인해 형태는 같은 NoSQL 이지만 일반적인 NoSQL 데이터베이스와도  다르게 신경써야하는 부분들이 있습니다.

 앞서 Realtime Database챕터와 예제 챕터들을 거치면서 많이 다루어보았지만, 조금 더 익숙해지기 위해 간단한 예시들로 다양한 상황들을 연습해보겠습니다.

1) SQL to Firebase

흔하게 사용하는 SQL쿼리를 Firebase 메소드로 변형해보겠습니다.

예제를 시작하기전에 아래의 링크의 파일을 받습니다. 앞서 예제를 하면서 git에서 파일을 다운로드 받는 방법은 나와있으니 생략하도록 하겠습니다.


파일을 다운로드 받으면 user.json파일이 있습니다. Firebase 콘솔화면에서 연습용 프로젝트를 만들고 Database 항목 Realtime Database 로 들어갑니다.

화면 우측에 3개 점으로된 버튼을 클릭하시면 JSON 파일을 입력할거나 반대로 추출 백업할 수 있는 메뉴가 있습니다. 이곳에서 'JSON 가져오기' 로 user.json 데이터를 입력해줍니다.
 




이렇게 데이터가 입력되었음을 확인 할수 있습니다.

그리고 쿼리 연습을 위해 규칙탭에서 권한을 아래와 같이 변경합니다. 읽기 권한을 누구든지 읽을 수 있게 변경하였습니다.




연습을 위해 간단하게 프로젝트를 만들고 firebase-app.js와 firebase-database.js 만 html에 include 하고 본인이 생성한 프로젝트코드가 입력된 초기화 코드를 입력합니다.

그리고 아래와 같이 script에 가장 root를 참조할 부분의 변수를 선언하고 할당합니다.

var rootRef = firebase.database().ref();


users데이터 바로 아래에는 user별로 키 값이 입력이 되어 있습니다. 키값에 따라 user 데이터를 검색하는 쿼리를 SQL형태로 나타내면 아래와 같습니다.

SELECT * FROM Users WHERE UID = 1; 

위 SQL 쿼리를 firebase로 바꿔보겠습니다. 

rootRef.child('users').child('1').once('value', function(data){
    console.log('1번 :' , data.val());
});


이번에는 키값이 아닌 child 에 있는 값을 검색하겠습니다. 그 중 'Backho Kang'을 검색하겠습니다. SQL로 나타내면 아래와 같습니다.

SELECT * FROM Users WHERE name= 'Backho Kang'; 

다시 firebase 로 바꿔보겠습니다.

rootRef.child('users').orderByChild('name').equalTo('Backho Kang').once('value', function(data){
    console.log('2번 :' , data.val());
});

이번에는 데이터를 3개만 가져오도록하겠습니다. SQL로 표현하면 아래와 같습니다.

SELECT * FROM Users LIMIT 3 ; 

다시 firebase 로 바꿔보겠습니다.

rootRef.child('users').limitToFirst(3).once('value', function(data){
    console.log('3번 :' , data.val());
});

이번에는 데이터를 이메일 S로 시작하는 user를 가져오겠습니다.

SELECT * FROM Users WHERE email LIKE 'S%' ; 

다시 firebase 로 바꿔보겠습니다. 여기서 특이한 부분은 문자열 비교시에 유니코드가 때때로 문제를 일으키기 때문에 endAt 메소드에 유니코드 중 가장 큰값에 해당하는 문자를 붙여주어야합니다. 

rootRef.child('users').orderByChild('email').startAt('S').endAt('S\uf8ff').once('value', function(data){
    console.log('4번 :' , data.val());
});

이번에는 나이가 30미만인 인원을 찾겠습니다. SQL 쿼리는 다음과 같습니다.

SELECT * FROM Users WHERE age < 30 ; 

다시 firebase 로 바꿔보겠습니다.

rootRef.child('users').orderByChild('age').endAt(29).once('value', function(data){
    console.log('5번 :' , data.val());
});


이번에는 나이가 30이상인 인원을 찾겠습니다. SQL 쿼리는 다음과 같습니다.

SELECT * FROM Users WHERE age >= 30 ; 

다시 firebase 로 바꿔보겠습니다.

rootRef.child('users').orderByChild('age').startAt(30).once('value', function(data){
    console.log('6번 :' , data.val());
});

이번에는 나이가 19이상 30이하 의 나이를 찾아보겠습니다.

SELECT * FROM Users WHERE age >= 19 AND age <= 30 ; 

다시 firebase 로 바꿔보겠습니다.

rootRef.child('users').orderByChild('age').startAt(19).endAt(30).once('value', function(data){
    console.log('7번 :' , data.val());
});

이번에는 대전에 살면서 나이가 19세인 인원을 찾겠습니다.

SELECT * FROM Users WHERE age = 19 AND city='Daejun'

다시 firebase 로 바꿔보겠습니다. 안타깝게도 firebase 에서는 orderBy 함수를 한번에 하나 밖에 사용하지 못합니다. 그렇기 때문에 두가지 상이한 필드에서 조건이 필요한 경우에는 하나의 필드로 데이터를 구한 후 다시 확인 하는 방법을 거칠 수도 있지만, 미리 age_city라는 열을 만들어서 사용하면 부가적인 작업없이 데이터를 구할 수 있게 됩니다.

rootRef.child('users').orderByChild('age_city').equalTo('19_Daejun').once('value', function(data){
    console.log('8번 :' , data.val());
});


2) Join 

정형화된 SQL 데이터베이스에서는 정규화된 테이블들을 하나의 데이터로 보여주기 위해 특정 칼럼의 데이터를 Join하여 데이터를 보여주게 됩니다. Firebase Database에서는 여러 항목들의 데이터를 한번에 Join하여 보여줄 수 있는 방법은 없지만, 대신에 Join하여 보여주고자하는 데이터를 다시 검색하여 가져오는 방식으로 보여주게 됩니다.

이번 예제는 따로 예제 프로젝트를 생성하지 않고, 채팅앱(Firebase 채팅앱만들기-목차)의 예제를 그대로 사용하겠습니다. 

데이터 구조는 앞서 진행하셨다면 아시겠지만 아래와 같은 형태로 이루어져 있습니다.





Join을 연습하기 위해 Database 권한 규칙을 조금 변경해주어야만 합니다. 아래와 같이 Firebase console > Database > 규칙 에서 추가해주세요.

"RoomUsers" :{
      "$roomId":{
        ".read": "auth != null"
        ,"$uid":{
          ".read": "auth != null",
          ".write": "auth != null"
        }
      }
    }

7장의 예제 프로젝트에서 setLogin 메소드 안에서 아래의 코드를 추가해 줍니다.

firebase.database().ref('RoomUsers/방ID').on('child_added', function(snap){
    firebase.database().ref('Users/'+ snap.key).on('value', function(usersnap){
        console.log('방에 속한 유저 : ', usersnap.val());
    });
});

이렇게 Firebase 의 Join은 Join하고자하는 항목 2개중 하나의 데이터를 구하고 구한 데이터로 나머지 데이터를 구하는 방식으로 이루어 집니다.

3) 보안 규칙

Firebase Realtime Database 에서는 보안 규칙이 무척 중요합니다. 보안 규칙을 통해 데이터의 접근 권한을 설정할 수 있고, 데이터의 형식 또는 유효성 검증을 할 수 있습니다.

{
    "rules" : {
        "users" :{
            "$uid" : {
                ".read" : true,
                ".write" : true,
                ".validate" : true
            }
        }
    }
}


  • ".read"  : 이 지점의 데이터를 어떤 이가 읽을 수 있을지 결정합니다.
  • ".write" : 이 지점의 데이터를 어떤 이가 쓸 수 있을지 결정합니다.
  • ".validate" : 이 지점의 데이터가 어떤 데이터가 유효한지 결정합니다.

3가지 모두 true 값인 경우에 데이터가 허용이 됩니다.

$uid는 변수로서 사용이 됩니다. 'users/1', 'users/suwon' 에서 '$uid' 값은 '1' , 'suwon'이 됩니다. uid 로 표기할 필요는 없습니다. '$userkey' 등 원하는 이름으로 바꿀 수 있습니다. 이 변수 값으로 특별한 권한과 유효성 검증을 설정할 수 있습니다.

간단하게 아래의 규칙은 users uid 값이 1인 사람 많이 읽고, 쓸 수 있습니다.

{
    "rules" : {
        "users" :{
            "$uid" : {
                ".read" : "$uid == 1",
                ".write" : "$uid == 1",
                ".validate" : true
            }
        }
    }
}


보통 흔히 많이 쓰는 인증된 사람많이 읽고 쓰기 권한을 부여해보겠습니다.

{
    "rules" : {
        "users" :{
            "$uid" : {
                ".read" : "auth.uid == $uid",
                ".write" : "auth.uid == $uid",
                ".validate" : true
            }
        }
    }
}

'auth' 는 서버에서 미리 정의 된 변수입니다. 하위 항목으로 uid, provider, token  이 있습니다. 여기서 provider 는 google, github, facebook 이 해당합니다.

아래는 인증되고 google로 로그인 한 사람만 읽을 수 있습니다.

{
    "rules" : {
        "users" :{
            "$uid" : {
                ".read" : "auth.uid == $uid && auth.provider == 'google'",
                ".write" : "auth.uid == $uid",
                ".validate" : true
            }
        }
    }
}

auth 하위 token은 또 여러가지 정보를 포함합니다. email, email_verified, name, phone_number 등이 있습니다.

아래는 인증이 되고 인증된 이메일이  gmail.com계정일 경우만 읽을 수 있습니다. 보시는바와 같이 matches함수로 정규식 표현이 가능합니다.

{
    "rules" : {
        "users" :{
            "$uid" : {
                ".read" : "auth.uid == $uid && auth.token.email.matches(/.*@gmail.com$/",
                ".write" : "auth.uid == $uid",
                ".validate" : true
            }
        }
    }
}


이번에는 입력되어 있는 데이터 중 age가 20 이상인 데이터만 읽겠습니다.

{
    "rules" : {
        "users" :{
            "$uid" : {
                ".read" : "data.child('age').val() > 20",
                ".write" : "auth.uid == $uid",
                ".validate" : true
            }
        }
    }
}

여기서 data는 입력되어 있는 데이터를 나타내는 변수이며, child는 하위 child를 탐색하는 함수입니다. 반대로 parent함수는 상위 노드를 탐색합니다.

아래는 adult라는 항목을 users 정보가 age 20 보다 큰 인원만 읽을 수 있도록 허용합니다. root는 최상단 위치를 의미하는 변수 입니다.
{
    "rules" : {
        "adult" :{
            ".read" : "root.child('users').child(auth.uid).child('age').val() > 20",
        }
    }
}


이제 데이터의 유효성을 검증해보겠습니다. 입력되는 데이터 중에 'name' 이란 항목을 String 형태로 강제하겠습니다.

{
    "rules" : {
        "users" :{
            "$uid" : {
                ".read" : "auth.uid == $uid",
                ".write" : "auth.uid == $uid",
                ".validate" : "newData.child('name').isString()"
            }
        }
    }
}

 newData는 새롭게 입력되는 데이터를 표현해주는 변수입니다. 새롭게 입력되는 데이터의 하위 필드 중 'name'이랑 항목이 isString이라는 함수로 String여부를 판별하여 검증합니다. 

  • isString() - 문자열 검증
  • isNumber() - 숫자여부 검증
  • isBoolean() - Boolean타입 검증

아래는 새로운 데이터의 하위 필드가 'name'이라는 항목과 'age'라는 항목이 있는지 여부 hasChildren이라는 항목으로 체크합니다.

{
    "rules" : {
        "users" :{
            "$uid" : {
                ".read" : "auth.uid == $uid",
                ".write" : "auth.uid == $uid",
                ".validate" : "newData.hasChildren(['name', 'age'])"
            }
        }
    }
}




728x90