firebase페이지 - 빌드 - functions
firebase가 기본적으로 구글클라우드 플랫폼을 기반으로 합니다. (※아마존- AWS, 마이크로-Azure)
그런데 현재 버전2를 앞두고 약간 불안정 합니다.
예시로 functions의 경우 원래는 배포만 하면 되었는데 지금은 조금 바뀌어있어 그 사용법에 대해 오늘 다뤄보도록 하겠습니다.
구글 클라우드 플랫폼으로 이동
빌드 - Functions - 상태 - Cloud Console 로 이동
원래는 배포만 하면 되었지만 이제는 Artifact Registry API
를 사용해야 사용을 할 수 있습니다. (아마 버전2로 넘어가며 내부적으로 세팅을 해주지 않을까 합니다.) 사용을 눌러놓으면 배포가 가능합니다.
Artifact Registry API 검색 - marketplace 설치
functions 폴더가 하나의 npm 프로젝트 입니다. 현재 functions내부의 src >index.ts가 있습니다. 실제로 배포를 하기 위해서는 index.ts에 firebase-functions라는 패키지를 가지고 와 세팅을 해줘야합니다.
import * as functions from 'firebase-functions'
export const helloWorld = functions.https.onRequest((req, res) => {
functions.logger.info("Hello log!", {structuredData:true}
response.send("Hello from Firebase!")
}
helloWorld
라는 이름으로 로컬에서 요청해 사용을 할 수 있습니다.
UI관련은 localhost:4000에, 백엔드 기능은 5001번에 열린상태입니다.
//todoapp에 접근 localhost/id/리전/function의 이름/접근하려는곳
// http://localhost:5001/kdt-test-98de9/asia-northeast3/api/todo - 로컬주소
// http://asia-northeast3-kdt-test-9f352.cloudfuntions.net/api/todo - 서버주소
※서버배포시 https~리전
은 도메인 주소
로 대체 됩니다.
functions폴더 하나가 하나의 백엔드 코드라고 이해해도 좋습니다. 따라서 터미널에서 루트 경로가 아닌 functions내부로 들어가 명령을 실행해야합니다.
$ cd functions
$ npm run build:watch
cross-origin
HTTP요청을 제한합니다. 따라서 cross-origin
요청을 하려면 서버의 동의가 필요합니다. 이러한 교차출력정책에 관련된 내용이 바로 cors으로, 백엔드 요청이 들어오는 것을 서버에서 어떤 사람에게는 허락하고 어떤 사람에게는 제한할 수 있습니다.const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
프론트엔드와 백엔드에서의 라우터 정의
- 백엔드에서의 라우터의 개념과 프론트엔드에서의 라우터의 개념은 조금 다릅니다.
- 프론트엔드에서는 페이지라는 단어와 매칭해서 이해를 하고 각 페이지를 나누어주는 역할 개념으로 라우터를 사용합니다.
- 그런데 백엔드는 라우터를 페이지 용도로 이해하면 안됩니다. 들어오는 요청을 분산해주는 개념으로 이해해야 합니다.
$ functions > npm i express cors
index.ts
//관리자 권한 가져오기
import * as admin from 'firebase-admin'✅
//초기화 (반드시 가장 먼저 해야합니다. 위치가 아래로 내려가면 X)
admin.initializeApp()✅
import * as functions from 'firebase-functions'
import * as express from 'express'✅
import * as cors from 'cors'✅
//내가 사용하고자하는 어플리케이션이 만들어집니다
const app = express() ❇️
app.use(express.json())❇️ //express.json: json문법을 해석할수 있는 내장플러그인
//이 코드가 없으면 요청을 허용할 수가 없습니다. 빈경우 모든 요청을 허용
app.use(cors())❇️
//todo라는 요청페이지를 만들어 페이지에 대한 결과를 객체로 담습니다
app.use('/todo', {})❇️ //⭐우리는 {}안을 만들면 됩니다.
export const heropy = functions.https.onRequest(app)
❓
import * as adimin
을 하는 이유
- 가져오기, 내보내기 방식이 다릅니다
- node.js는 기본적으로 commonJS모듈 방식
- => require() module.exports / default로 내보낸다는 개념이 없습니다.
- TS는 ESM방식
- => import, export / export defualt{}도 가능
- default로 내보낸다는 개념 없기 때문에 모두 가지고 와서 admin으로 쓰겠다라는 선언이 필요합니다.
import * as admin from 'firebase-admin'
- cors사용 예시
app.use(cors({ origin: ['https://localhost:3000'] //localhos:3000만 허용 })
우리 프로젝트에서는 모든 요청을 허용하기 위해 빈 값을 넣었습니다.
src내부에 routes라는 폴더를 만들어 todo.ts를 만들어줍니다.
(내용이 많아질 것 같은경우 routes안에 또다른 폴더를 만들어 관리해도 좋습니다)
index.ts
import * as admin from 'firebase-admin'
admin.initializeApp()
import * as functions from 'firebase-functions'
import * as express from 'express'
import * as cors from 'cors'
import todo from './routes/todo' ✅//todo.ts가지고 오기
const app = express()
app.use(express.json())
app.use(cors())
app.use('/todo', todo) ✅//todo라는 이름으로 연결
export const api = functions.https.onRequest(app)
import * as express from 'express' //프레임워크를 가지고 와서
const router = express.Router() //라우터를 만듭니다.
//각각의 router를 어떤 방식으로 받을 것인지를 설정
router.get('')
router.post('')
router.put('')
router.delete('')
export default router //내보내기
아래 두가지는 같은 요청입니다
//https://localhost:5001/kdt-test-9f352/asia-northeast3/api/todo/✅heropy router.get('/heropy', ()=>{})
각각 요청을 할 때 뭐가 들어오는지를 한번 확인을 해보도록 하겠습니다.
import * as express from 'express'
const router = express.Router()
// https://localhost:5001/kdt-test-9f352/us-central1/api/todo/heropy
router.get('/', (req, res) => {
console.log('req.headers', req.headers )
console.log('req.body', req.body)
console.log('req.params', req.params)
console.log('req.query', req.query)
//처리를 거쳐
//여러가지 방법이 존재하지만 우리에게 익숙한 json확인
res.status(200).json({
name: 'Heropy',
age: 85,
})
})
export default router
http://localhost:5001/아이디/us-central1/api/todo
를 누르면 아래와 같이 뜹니다.Functions emulator
에서도 확인할 수 있습니다.headers와 query의 요청정보를 visual studio의 thunder client를 통해 확인 해보겠습니다.
header정보에 username정보를 포함, queryParameters를 a:1, b:2로 채워서 보내보도록 하겠습니다.
localhost:4000
번에 접근하면 확인할 수 있습니다.
이번에는 데이터를 DB에 저장하고 그 저장된 데이터를 요청할 수 있도록 만들어보겠습니다.
import * as admin from 'firebase-admin'//1. admin객체를 가져옵니다
//firestore은 DB의 이름입니다
const db = admin.firestore()//2. firestore의 DB정보를 가져옵니다.
// 투두추가
router.pos t('/', async (req, res) => {
const {title} = req.body
const date = new Date().toISOString()
const todo = { //4. documnet내용작성
title,
done: false,
//IOS는 국제표준시로 date를 생성합니다.
createdAt: date,
updatedAt: date
}
//3. 사용할 컬렉션의 이름: Todos, add 변수에 담은 도큐먼트를 추가
//컬렉션이 없으면 생성, 있으면 그 컬렉션을 사용
// 5. 생성된 데이터를 기다려야하기때문에 await를 추가해줍니다.
await db.collection('Todos').add(todo)
//생성된 데이터를 응답
res.status(200).json(todo)
})
firebase-admin
에서 admin
객체를 가져옵니다new Date().toISOString()
를 변수로 빼 관리해줍니다.요청 확인
thunder client로 post요청을 해 정상적으로 작동하는지 확인할 수 있습니다.
또한 localhost:4000번의 firestore emulator에서 데이터를 확인할 수 있습니다.
await db.collection('Todos').add(todo)
후 반환되는 ref객체에 들어있습니다.res.status(200).json(todo)
=> res.status(200).json({id: ref.id, ...todo})
router.post('/', async (req, res) => {
const {title} = req.body
const date = new Date().toISOString()
const todo = { //4. documnet내용작성
title,
done: false,
//IOS는 국제표준시로 date를 생성합니다.
createdAt: date,
updatedAt: date
}
//생성된 도큐먼트의 그 ref객체를 받습니다(이객체에 id가 들어있습니다.)
✅ const ref = await db.collection('Todos').add(todo)
await db.collection('Todos').add(todo)
//생성된 데이터를 응답
res.status(200).json({
✅ id: ref.id,
...todo
})
})
id
를 포함한 객체를 반환하는 것을 확인할 수 있습니다.npm run build:watch
)가 켜져있어야합니다.모든 document를 반환할 수 있도록 만들어보도록 하겠습니다.
쿼리
await db.collection('Todos') // done이 false인 모든 아이들을 찾아라. 메소드 체이닝이 가능합니다. //filter개념이기에 모든 정보를 조회할 때는 생략가능합니다. .where("done", "==",fase) .where("title", "==", '아침먹기!') .get() //그렇게 조회한 조회를 다 가져와
get()
만 사용하는 경우 모든 정보를 가져옵니다.
작성
//todo조회
router.get('/', async (req, res) => {
//1. 하나를 찾아도 (유사)배열형태로 반환되기에 forEach사용이 가능합니다.
const snaps = await db.collection('Todos').get()
//4. 반환하기 위해 빈 배열을 만듭니다.
const todos = []
snaps.forEach(snap => { //2. forEach로 반복처리를 합니다.
const {title, done} = snap.data() //3. 필요한 필드를 꺼낼 수 있습니다.
todos.push({ //5. 꺼낸 필드를 반환합니다.
title,
done
})
})
res.status(200).json(todos) //6. 그렇게 담은 todos를 반환합니다
})
.where
은 필터링 개념으로 3개의 인자를 받습니다.- 만약 모든 documnet를 가져오려면
.where
없이.get()
만으로 가져올 수 있습니다.- 그렇게 조회한 값을 snaps로 받습니다.
- 데이터가 1개더라도 유사배열형태로 반환됩니다.
- foreach를 통해 꺼내올 필드를 정해 가져와 반환할 todos배열에 담습니다.
res.status(200).json(todos)
를 통해 json형태로 todos배열을 반환합니다.
import * as admin from 'firebase-admin'
import * as express from 'express'
const db = admin.firestore()
const router = express.Router()
interface Todo { //2. 전역화✅
title: string
done: boolean
createdAt: string
updateAt: string
}
//todo조회
router.get('/', async (req, res) => {
const snaps = await db.collection('Todos').get()
const todos :Todo[] = []✅ //타입을 지정합니다.
snaps.forEach(snap => {
const {title, done} = snap.data()
todos.push({
title,
done,
createAt,
updateAt
})
})
//그렇게 담은 todos를 반환합니다
res.status(200).json(todos)
})
const todos = []
형태는 타입스크립트로 never를 반환하게 됩니다. 따라서 인터페이스를 만들어 그 안에 들어갈 내용의 타입을 정의해줍니다.createAt
, updateAt
로 함께 정의를 해줍니다.createAt
, updateAt
이 필수 속성임으로 todos에 함께 넣어줍니다.{title, done}
과 같은 객체구조분해 할당이 아닌 const fields = sanp.data()
와 같이 변수로 받아 todos.push({...field})
전개연산자로 모두 꺼내옵니다.todos.push({.id: snap.id, ..field})
방법1
Todo & {id: string}
)interface Todo {
title: string
done: boolean
createdAt: string
updateAt: string
}
// 투두 조회
router.get('/', async (req, res) => {
const snaps = await db.collection('Todos').get()
type ResponseTodo = Todo & {id: string} //✅
const todos: ResponseTodo[] = []
snaps.forEach(snap => {
const fields = snap.data()
todos.push({
//반환할 때 id를 넣을 것이기 때문에 Todo interface에 넣는 것이 아니라 인터섹션을 통해 타입을 지정합니다.
id: snap.id,✅
...fields as Todo✅
})
})
//그렇게 담은 todos를 반환합니다
res.status(200).json(todos)
})
export default router
방법2
interface Todo {
id?: string
title: string
done: boolean
createdAt: string
updateAt: string
}
// 투두 조회
router.get('/', async (req, res) => {
const snaps = await db.collection('Todos').get()
const todos: Todo[] = []
snaps.forEach(snap => {
const fields = snap.data()
todos.push({
//반환할 때 id를 넣을 것이기 때문에 Todo interface에 넣는 것이 아니라 인터섹션을 통해 타입을 지정합니다.
id: snap.id,
...fields as Todo
})
})
//그렇게 담은 todos를 반환합니다
res.status(200).json(todos)
})
export default router
모든 데이터베이스에서 위 용어를 사용하는 것은 아니며, 대표적으로 몽고DB에서 사용하는 용어입니다.