[firebase실강] 2강

youngseo·2022년 7월 4일
0

FireBase

목록 보기
2/9
post-thumbnail

firebase 2강

🐥Functions

firebase페이지 - 빌드 - functions

firebase가 기본적으로 구글클라우드 플랫폼을 기반으로 합니다. (※아마존- AWS, 마이크로-Azure)
그런데 현재 버전2를 앞두고 약간 불안정 합니다.
예시로 functions의 경우 원래는 배포만 하면 되었는데 지금은 조금 바뀌어있어 그 사용법에 대해 오늘 다뤄보도록 하겠습니다.

구글 클라우드 플랫폼으로 이동

빌드 - Functions - 상태 - Cloud Console 로 이동

1. Artifact Registry API 설치

원래는 배포만 하면 되었지만 이제는 Artifact Registry API를 사용해야 사용을 할 수 있습니다. (아마 버전2로 넘어가며 내부적으로 세팅을 해주지 않을까 합니다.) 사용을 눌러놓으면 배포가 가능합니다.

Artifact Registry API 검색 - marketplace 설치

2. 진행할 부분 확인

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라는 이름으로 로컬에서 요청해 사용을 할 수 있습니다.

3. API주소 사용하는 방법

3-1 로컬호스트 체크

UI관련은 localhost:4000에, 백엔드 기능은 5001번에 열린상태입니다.

3-2 프로젝트 ID확인

프로젝트 설정에서 프로젝트 ID를 확인할 수 있습니다.

3-3 API주소 확인

//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~리전도메인 주소로 대체 됩니다.

에서도 확인을 할 수 있습니다.

🐥실습: Todo 프로젝트를 위한 백엔드 코드 만들기

functions폴더 하나가 하나의 백엔드 코드라고 이해해도 좋습니다. 따라서 터미널에서 루트 경로가 아닌 functions내부로 들어가 명령을 실행해야합니다.

1. npm run build:watch

$ cd functions
$ npm run build:watch
  • 타입스크립트를 수정할 때마다 빌드된 내용을 반환합니다.(function에서 실행)

2. express와 cors설치

cors란?

  • 브라우저에서는 보안상의 이유로 cross-origin HTTP요청을 제한합니다. 따라서 cross-origin 요청을 하려면 서버의 동의가 필요합니다. 이러한 교차출력정책에 관련된 내용이 바로 cors으로, 백엔드 요청이 들어오는 것을 서버에서 어떤 사람에게는 허락하고 어떤 사람에게는 제한할 수 있습니다.

express란?

  • express란, NodeJS를 사용하여 서버를 개발하고자 하는 개발자들을 위하여 서버를 쉽게 구성할 수 있게 만든 프레임워크입니다. (NodeJS를 사용하여 쉽게 서버를 구성할 수 있게 만든 클래스와 라이브러리의 집합체)
  • 아래와 같이 사용을 할 수 있습니다.
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}`)
})

프론트엔드와 백엔드에서의 라우터 정의

  • 백엔드에서의 라우터의 개념과 프론트엔드에서의 라우터의 개념은 조금 다릅니다.
  • 프론트엔드에서는 페이지라는 단어와 매칭해서 이해를 하고 각 페이지를 나누어주는 역할 개념으로 라우터를 사용합니다.
  • 그런데 백엔드는 라우터를 페이지 용도로 이해하면 안됩니다. 들어오는 요청을 분산해주는 개념으로 이해해야 합니다.

2-1 설치

$ functions > npm i express cors

2-2 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을 하는 이유

  1. 가져오기, 내보내기 방식이 다릅니다
  • node.js는 기본적으로 commonJS모듈 방식
    • => require() module.exports / default로 내보낸다는 개념이 없습니다.
  • TS는 ESM방식
    • => import, export / export defualt{}도 가능
  • default로 내보낸다는 개념 없기 때문에 모두 가지고 와서 admin으로 쓰겠다라는 선언이 필요합니다.import * as admin from 'firebase-admin'
  1. cors사용 예시
app.use(cors({
   origin: ['https://localhost:3000'] //localhos:3000만 허용
})

우리 프로젝트에서는 모든 요청을 허용하기 위해 빈 값을 넣었습니다.

3. todo만들기

3-1 모듈화하기

src내부에 routes라는 폴더를 만들어 todo.ts를 만들어줍니다.
(내용이 많아질 것 같은경우 routes안에 또다른 폴더를 만들어 관리해도 좋습니다)

3-2 가지고오는 코드 작성

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)

3-3 todo.ts

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', ()=>{}) 

4 get(조회)

각각 요청을 할 때 뭐가 들어오는지를 한번 확인을 해보도록 하겠습니다.

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

4-2 로컬테스트

  • 로컬에서 테스트를 하기 위해서 emuls가 오픈되어 있는지를 확인해줍니다.
  • 주소에 http://localhost:5001/아이디/us-central1/api/todo를 누르면 아래와 같이 뜹니다.
  • 주소는 Emulator UI의 Functions emulator에서도 확인할 수 있습니다.

4-3 headers, body, query

headers와 query의 요청정보를 visual studio의 thunder client를 통해 확인 해보겠습니다.

header정보에 username정보를 포함, queryParameters를 a:1, b:2로 채워서 보내보도록 하겠습니다.

localhost:4000번에 접근하면 확인할 수 있습니다.

5. post(추가)

이번에는 데이터를 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)
})
  1. firebase-admin에서 admin객체를 가져옵니다
  2. firestore의 DB정보를 가져옵니다.
  • firestore는 firebase에서 사용하는 DB의 이름입니다
  1. request.body부분에 사용자가 담은 title의 내용이 들어있을 것입니다. 그 내용으로 db를 생성해 collection을 요청해보겠습니다.
  2. 사용할 컬렉션의 이름을 Todos로 정한 후 add메서드를 이용해 하나의 도큐먼트를 추가할 예정입니다.
  3. 그 도큐먼트에 추가할 내용을 todo라는 변수에 작성합니다.
  4. 만든 객체(todo)를 반환해줍니다.
    • 같은 시간이 설정될 수 있도록 new Date().toISOString()를 변수로 빼 관리해줍니다.

요청 확인

thunder client로 post요청을 해 정상적으로 작동하는지 확인할 수 있습니다.
또한 localhost:4000번의 firestore emulator에서 데이터를 확인할 수 있습니다.

5. POST2 - 반환되는 데이터에 id 추가

  • 응답 받은 결과에 id를 넣어 반환 하고 싶습니다.
  • 아이디는 도큐먼트에 자동으로 생성이 됩니다.
    • await db.collection('Todos').add(todo) 후 반환되는 ref객체에 들어있습니다.
    • firebase에서는 새생성한 도큐먼트를 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)가 켜져있어야합니다.

6. get(todo조회)

모든 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배열을 반환합니다.

7. 타입스크립트 에러 해결

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})전개연산자로 모두 꺼내옵니다.
  • id도 함께 내보낼 수 있도록 추가해줍니다. todos.push({.id: snap.id, ..field})
  • 단, 이 id는 interface에 정의가 되어 있지 않기에 interface에 값을 정의를 해줍니다.

7-2. id 타입추가

방법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안에 id도 정의를 해줍니다.
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

용어정리

  • 컬렉션
  • documentary : 각각의 할일을 document로 만들 것입니다
  • 필드: 각각의 정보를 필드라고 합니다

모든 데이터베이스에서 위 용어를 사용하는 것은 아니며, 대표적으로 몽고DB에서 사용하는 용어입니다.


0개의 댓글