사이드프로젝트에 파이어베이스를 도입하며, 공식문서를 읽고 정리한 내용입니다.
yarn add firebase //firebase SDK 다운로드
FirseBase.tsx
import firebase from 'firebase';
import 'firebase/auth';
import 'firebase/firestore';
import 'firebase/storage';
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_API_KEY,
authDomain: process.env.NEXT_PUBLIC_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_PROJECTID,
storageBucket: process.env.NEXT_PUBLIC_STORAGEBUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_MESSAGINGSENDERID,
appId: process.env.NEXT_PUBLIC_APPID,
measurementId: process.env.NEXT_PUBLIC_MEASUREMENTID
};
// Initialize Firebase
if(!firebase.apps.length){
firebase.initializeApp(firebaseConfig);
}else{
firebase.app();
}
export const firebaseInstance = firebase;
// 계정관련
export const authService = firebase.auth();
// 실시간 데이터베이스
export const dbService = firebase.database();
// 스토리지 서비스
export const storageService = firebase.storage();
let provider = new firebaseInstance.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider)
.then((result:any)=>{
// 1. 소셜 로그인 후에 있는 계정 인지 확인
dbService
.ref('users/'+result.user?.uid)
.on('value', (snapshot:any)=>{
const data = snapshot.val();
// 2-A. 있는 계정이면 해당 계정 다이어리 페이지로 이동
if(data){
router.push('/diary/');
}
// 2-B. 없는 계정이면 회원 등록 페이지로 이동
else{
router.push('/register/');
}
})
})
.catch((error:any)=>{
console.log(error);
});
파이어베이스에서는 이들을 고유하게 식별하는 해당 ID 토큰을 만들고 FIrebase 실시간 데이터베이스 및 Cloud Storage 와 같은 여러 리소스에 대한 액세스를 허용한다고 한다.
firebase.auth()
.onAuthStateChanged((user:any)=>{
if(user){
const {email, refreshToken, uid}= user;
}
커스텀 백엔드 서버에서는 이 ID 토큰 (JWT)를 재사용하여 사용자를 식별한다. 위의 코드를 통해 현재 접속한 유저의 토큰을 가져오고, 현재 로그인 접속 상태를 실시간으로 확인할 수 있다.
파이어베이스 실시간 데이터베이스로 사용자 정보를 담고 있는 JSON 객체 생성
데이터베이스 데이터 구조 : JSON 트리롤 구성되어 있다. SQL 데이터베이스와 달리 테이블이나, 레코드가 없으며, JSON 트리에 추가된 데이터는 연결된 키를 갖는 JSON 구조의 노드가 된다. 사용자 ID 또는 의미 있는 이름과 같은 고유 키로 직접 지정할 수 도 있고 push()
를 사용하여 자동으로 지정할 수도 있다.
사용자 프로필은 일반적으로 /users/$uid 와 같은 경로에 위치 시킨다.
{
"users": {
"alovelace": {
"name": "Ada Lovelace",
"contacts": { "ghopper": true },
},
"ghopper": { ... },
"eclarke": { ... }
}
}
Firebase 실시간 데이터베이스는 최대 32단계의 데이터 중첩을 허용하므로 중첩을 기본 구조로 도입해도된다 생각할 수 있지만, 데이터베이스의 특정 위치에서 데이터를 가져오면 모든 하위 노드가 함께 검색되기 때문에 보안에 문자게 발생한다. 또한 사용자에게 데이터베이스의 특정 노드에 대한 읽기 또는 쓰기 권한을 부요하면 해당 노드 하위에 속한 모든 데이터에 대한 권한이 부여되는 것과 마찬가지 이다.
비정규화를 통해, 데이터를 다른 경로로 분할하며, 별도의 호출없이 효율적으로 다운로드 할 수 있다. 아래와 같이 채팅의 경우 unique ID 를 가지고 채팅을 구성하는 데이터를 쪼개 관리하면, 중첩 문제를 해결할 수 있다.
{
// Chats contains only meta info about each conversation
// stored under the chats's unique ID
"chats": {
"one": {
"title": "Historical Tech Pioneers",
"lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
"timestamp": 1459361875666
},
"two": { ... },
"three": { ... }
},
// Conversation members are easily accessible
// and stored by chat conversation ID
"members": {
// we'll talk about indices like this below
"one": {
"ghopper": true,
"alovelace": true,
"eclarke": true
},
"two": { ... },
"three": { ... }
},
// Messages are separate from data we may want to iterate quickly
// but still easily paginated and queried, and organized by chat
// conversation ID
"messages": {
"one": {
"m1": {
"name": "eclarke",
"message": "The relay seems to be malfunctioning.",
"timestamp": 1459361875337
},
"m2": { ... },
"m3": { ... }
},
"two": { ... },
"three": { ... }
}
}
사용자와 그룹 간의 양방향 관계를 예로 들자면, 사용자는 그룹에 속할 수 있고 그룹은 사용자의 목록으로 구성된다. 이 때 사용자가 어떠한 그룹에 속할지를 결정하려면 문제가 다소 복잡해진다.
이처럼 사용자는 group의 키값만 가지고 있고 /users/group_id 를 읽어, 해당 그룹에 속한 사용자를 가져오는 방법으로 데이터를 검색하면 효율적으로 데이터를 가져올 수 있게 된다.
// An index to track Ada's memberships
{
"users": {
"alovelace": {
"name": "Ada Lovelace",
// Index Ada's groups in her profile
"groups": {
// the value here doesn't matter, just that the key exists
"techpioneers": true,
"womentechmakers": true
}
},
...
},
"groups": {
"techpioneers": {
"name": "Historical Tech Pioneers",
"members": {
"alovelace": true,
"ghopper": true,
"eclarke": true
}
},
...
}
}
var database = firebase.database();
set()
사용한다.
function writeUserData(userId, name, email, imageUrl) {
firebase.database().ref('users/' + userId).set({
username: name,
email: email,
profile_picture : imageUrl
});
}
onValue()
를 사용해서 이벤트를 관찰한다. 이벤트 발생 시점에 특정 경로에 있던 콘텐츠의 정적 스내샷을 읽는다. 이 메소드는 리스너가 연결될 때 한 번 트리거된 이후 하위 요소를 포함하여 데이터가 변경될 때마다 다시 트리거 된다.(실시간 구현 가능)
var starCountRef = firebase.database().ref('posts/' + postId + '/starCount');
starCountRef.on('value', (snapshot) => {
const data = snapshot.val(); // val() 메소드를 사용해 snapshot 의 데이터를 확인
updateStarCount(postElement, data);
});
데이터를 한번만 읽고 싶다면 → get()
const dbRef = firebase.database().ref();
dbRef.child("users").child(userId).get().then((snapshot) => {
if (snapshot.exists()) {
console.log(snapshot.val());
} else {
console.log("No data available");
}
}).catch((error) => {
console.error(error);
});
다른 하위 노드를 덮어쓰지 않고, 특정 하위 노드에 업데이트를 수행하기 위해서는 update()
메소드를 사용한다. 이때, 키 경로를 지정하여 더 낮은 하위 항목을 업데이트 할 수 있다.
function writeNewPost(uid, username, picture, title, body) {
// A post entry.
var postData = {
author: username,
uid: uid,
body: body,
title: title,
starCount: 0,
authorPic: picture
};
// Get a key for a new Post.
var newPostKey = firebase.database().ref().child('posts').push().key;
// Write the new post's data simultaneously in the posts list and the user's post list.
var updates = {};
// 동시에 업데이트 가능하다.
updates['/posts/' + newPostKey] = postData;
updates['/user-posts/' + uid + '/' + newPostKey] = postData;
return firebase.database().ref().update(updates);
}
위의 예시에서는 첫번째로
/posts/$postid
, /user-posts/$userid/$postid
에 동시에 사용자 게시물을 작성한다.이 방식을 통해 두 위치에 새 게시물을 생성한 것처럼 update() 를 한 번만 호출해 JSON 트리의 여러 위치에서 동시에 업데이트를 수행할 수 있다.
이는, 트랜잭션 처리가 가능하다. (즉, 모든 업데이트가 한꺼번에 성공하거나 실패할 수 있다.)
해당 데이터 위치의 참조에 remove()
를 호출한다.
set()
또는 update()
등의 다른 쓰기 작업 값으로 null
을 지정하여 삭제할 수도 있다.
update()
를 사용하면 API 호출 한 번으로 여러 하위 항목을 삭제할 수 있다.