Firebase 찍먹하기

Yeshin Lee
11 min readNov 10, 2024

--

구글의 모바일과 웹 앱 개발 플랫폼으로 소개되는 Firebase는 데이터를 저장하거나 배포 기능 등 서비스를 만드는데 다양한 지원을 해주므로 개발 시간을 단축시킬 수 있다. 개발자가 설정하는 많은 부분을 Firebase가 대신 하다보니 앱을 커스텀하는 경우에는 추천하지 않는다.

시작 그리고 사용자 인증하기

Firebase를 사용하기 위해서 프로젝트에 Firebase app을 연동시켜야한다.

Firebase 콘솔에서 프로젝트를 생성시 주어지는 여러 환경 변수로 초기화한다. getAuth 함수를 이용해 반환되는 인증 인스턴스(auth)로 인증 기능을 구현할 것이다.

const firebaseConfig = {
apiKey:
authDomain:
projectId:
storageBucket:
messagingSenderId:
appId:
}

const app = initializeApp(firebaseConfig);

export const auth = getAuth(app);

authStateReady로 초기 사용자 인증이 준비되었다는 것을 알린다.

일반적으로 계정을 만들려면 직접 이메일(또는 아이디), 비밀번호를 생성하거나 Third-party를 사용할 수 있다. 먼저 이메일, 비밀번호로 계정을 생성하려면 createUserWithEmailAndPassword(auth, email, password) 함수를 사용할 수 있다. 참고로 auth 인스턴스에서는 지원하지 않는다.

계정이 성공적으로 만들어지면, 새 User 객체를 반환하며 자동으로 로그인 된다. 생성된 계정은 Firebase User 콘솔에서도 확인할 수 있다.

반환값으로 생성된 계정 정보를 알 수 있다.

만약, 해당 이메일이 이미 존재하거나 비밀번호가 요구 조건을 충족하지 않는다면 계정 생성에 실패한다. 여기서 이메일로 유저를 식별하는 기준이므로 유일성(unique)을 가져야 한다.

Firebase 인증 SDK는 Google, Facebook, Twitter 및 GitHub 계정으로 로그인할 수 있도록 지원한다. 먼저 Firebase 콘솔에서 해당 Third party에 대한 provider를 추가하는 작업이 필요하다.

// login by Github
const provider = new GithubAuthProvider();

// login by Google
const provider = new GoogleAuthProvider();
GithubAuthprovider는 credentials를 보여준다.

더 나은 보안을 위해 이메일 링크로 2차 인증을 하는 방법도 있다.

Firestore과 Storage

Firestore(Firebase 데이터베이스)는 실시간 업데이트와 오토 스케일링 등을 지원한다. 또한 Firebase는 NoSQL 기반이므로 collection과 document로 구성된, flexible한 특징을 갖는다. Firestore를 생성할 때 2가지 모드가 주어지는데, Test mode로 생성하면 기본 보안규칙이 모두에게 허용되면서 30일의 유효기간이 주어진다.

Firestore는 텍스트 기반 데이터(텍스트 문자열, 배열, 맵 등)을 저장하는 반면, Storage는 파일 기반 데이터(이미지, 비디오, 문서 등)를 담는다. 여기서 중요한 것은 Storage는 데이터베이스가 아닌 백엔드 부분(유저 인증, 알람 전송, 서버리스 기능 등)을 지원하는 서비스(BaaS; Backend-as-a-Service)라는 것이다.

인증 모듈을 불러온 과정과 동일하게 Firestore과 Storage 각각 프로젝트에 연결한다.

const app = initializeApp(firebaseConfig);

// ...
export const db = getFirestore(app);
export const storage = getStorage(app);

다음은 사용자 프로필 사진을 업데이트하는 로직이다.

// set path for store file in Storage
const locationRef = ref(storage, `avatars/${user.uid}`);

// upload file
const result = await uploadBytes(locationRef, file);
// get download url about uploaded file
const avatarUrl = await getDownloadURL(result.ref);

setAvatar(avatarUrl);
// save file rmfjreference at Firestore document
await updateProfile(user, { photoURL: avatarUrl });

Storage에 담으려는 데이터 종류에 따라 사용하는 함수가 다르다.

  • uploadBytes: 바이너리 데이터(Blob, Uint8Array, ArrayBuffer) 업로드. 파일(이미지, 동영상 등)은 바이너리 데이터로 처리된다.
  • uploadString: 문자열 데이터(HTML 파일이나 단순 문자열 등) 업로드

uploadString 함수로 보아 Storage에서도 텍스트 데이터를 저장할 수 있다. 다만 어떤 종류의 텍스트인지에 따라 저장 위치가 달라진다.

  • Firestore: 구조화된 데이터의 일부로 쿼리 및 인덱싱이 필요한 텍스트(사용지 프로필 정보, 제목, 짧은 메시지 등)
  • Storage: 대용량 또는 포맷팅이 필요한, 파일 형태의 텍스트(긴 블로그 포스트 등)

다시 코드로 돌아와서, 현재는 Firestore에 다운로드받은 URL 그대로 저장하고 있는다. 하지만 다음과 같은 상황에서는 avatarUrl에 접근할 수 없다.

  • 파일 삭제 후 재 업로드: 기존 URL이 유효하지 않게 된다.
  • 보안 규칙 변경: Firestore의 보안 규칙이 변경되면, 이전에 생성된 URL이 차단될 수 있다.
  • Firebase 프로젝트 설정 변경: 인증 구성이나 보안 설정에 변화가 생기면 URL 접근에 제한이 생길 수 있다.

getDownloadUrl로 생성된 URL(avatarUrl)은 해당 파일에 대한 일시적 접근 경로이기 때문이다. 또한 같은 이름의 파일을 업로드하면, Storage에 있던 기존 파일은 덮어 씌워진다. Firestore에 파일 경로 자체를 저장하고, 필요할 때마다 URL을 생성하는 방법으로 해결할 수 있다.

const filePath = `avatars/${user.uid}`;
const locationRef = ref(storage, filePath);

await uploadBytes(storageRef, file);
await updateProfile(user, { photoURL: filePath });

const avatarUrl = await getDownloadURL(storageRef);
setAvatar(avatarUrl);

onSnapshot과 unsubcribe

Firestore에서 데이터를 조회할 때 SQL과 비슷한 문법을 사용할 수 있다.

query(
collection(db, "tweets"),
where("userId", "==", user?.uid),
orderBy("createdAt", "desc"),
limit(10)
);

그럼에도 몇 가지 다른 부분이 있다.

  • 어떤 종류의 필터링(where 문)을 사용할 것인지 미리 알려야한다.
  • 정렬 기능은 쿼리 1개 당 1번만 가능하다.

이러한 차이점은 Firebase가 유연한 스키마를 제공해 데이터 모델의 빠른 변경과 확장을 가능하게 하는 특징과 맞물린다. 또한 모든 단일 필드는 기본적으로 인덱싱되어 있어 RDBMS보다 덜 복잡하다. 이벤트를 감지해야만 데이터를 가져오는 RDBMS와 달리 Firebase는 실시간으로 데이터를 가져올 수 있다.

useEffect(() => {
const fetchData = async () => {};

fetchData();

// cleanUp
return () => {};
}, [])

useEffect 내에 onSnapshot을 선언하여 실시간으로 이벤트를 감지하여 데이터를 불러온다. onSnapshot 호출이 잦아질수록 비용이 발생하므로 컴포넌트가 언마운트될 때는 이벤트를 unsubscribe하는 클린업 함수를 사용한다.

클린업 함수는 컴포넌트가 언마운트되거나 리렌더링되기 전에 사이드 이펙트를 정리하는 함수다. useEffect의 마지막에 반환되는 함수로 정의하여 불필요한 리소스 사용과 의도하지 않은 동작을 방지한다. 주로 비동기 작업을 중단하거나 이벤트 리스너 제거, 타이머 정리, 외부 리소스 구독 해제 등에서 사용한다.

배포

배포를 위해서는 firebase-tools 패키지가 필요하다.

npm install -g firebase-tools
firebase login

# create .firebaserc and firebase.json
firebase init

초기 설정 질문 중, What do you want to use as your public directory? 는 Firebase가 어떤 폴더(public repository)의 콘텐츠를 사용자에게 제공할지 지정하라는 것이다.

deploy는 predeploy — build — deploy 순으로 진행된다.
# package.json
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"predeploy": "npm run build",
"deploy": "firebase deploy"
}

predeploy는 Firebase CLI 명령어로 deploy 명령어 실행할 때 자동으로 호출된다.

scripts 필드에서 명령어를 정의시, 명령어 앞에 ‘pre’를 붙이면 해당 명령어 실행 전 자동으로 ‘pre’ 명령어가 먼저 실행된다.

build 명령어로 생성된, 애플리케이션을 프로덕션 배포용으로 번들링한 파일은 dist 폴더에 저장된다. dist 내 최적화된 HTML, CSS, Js 파일을 바탕으로 deploy를 시작한다. deploy가 끝나면 Hosting URL로 기본 URL이 생성되는데, 이는 바꿀 수 있다.

이렇게 Firebase가 무엇이고 인증은 어떻게 하는지, 데이터를 어떻게 저장하고 가져오는지에 대해 알아보았다. Firebase를 찍먹해봤으니 조만간 프로젝트에 적용해봐야겠다.

--

--

Yeshin Lee
Yeshin Lee

No responses yet