//emuls
$ npm run fb:emuls//TS:watch function
$ npm run build:watch루트경로의 package.json
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "preview": "vite preview",
    "fb:login": "firebase login",
    "fb:logout": "firebase logout",
    "fb:init": "firebase init",
    "fb:emuls": "firebase emulators:start --import=./exports --export-on-exit",✅
    "fb:deploy": "firebase deplay"
  },"fb:emuls"에 위와 같은 코드를 추가해줍니다.폴더경로는 원하는 경로로 해도 괜찮습니다.
addDelted함수는 더이상 사용되지 않기에 지워도 괜찮습니다.src> jobs폴더를 만들어 index.ts파일로 addDelted함수를 잘라서 붙여 보관을 해도 좋습니다.import * as admin from 'firebase-admin'
const db = admin.firestore()
//job
async function addDeleted() {
  const snaps = await db.collection('Todos').get()
  for(const snap of snaps.docs){
    await snap.ref.update({
      deleted: false
    })
  }
  console.log('끝')
}
하나의 job입니다. 
루트 경로에 있는 src가 vite프로젝트입니다. 여기에 파일을 업로드 하는 기능을 만들어보겠습니다.
hosting이라는 이름으로 설정을 해줍니다.//hosting
$ npm run dev<template>
  <input
    type="file"
    @change="selectFile" />
</template><script lang="ts">
import {defineComponent} from 'vue'
export default defineComponent({
  methods: {
    selectFile(event: Event) {
      const files = (event.target as HTMLInputElement).files as FileList
      for(let i = 0; i<files.length; i++) {
        const file = files[i]
        const reader = new FileReader()
        reader.readAsDataURL(file)
        reader.addEventListener('load', e => {
          console.log((e.target as FileReader).result)
        })
      }
    }
  }
})
</script>
이렇게 파일을 문자화한 데이터를 서버에 전송하고 DB에 저장하는 과정을 해볼 예정입니다. DB에는 많은 정보가 들어갈 수 있지만 무거운 정보를 넣는 것은 권장하지 않습니다. 따라서 이미지는 파일 형태에 따로 스토로지에 저장을 합니다. 그리고 DB에는 스토로지에 저장된 파일의 주소를 저장합니다.
todo.ts
투두 아이템을 만들거나 추가할 때 이미지를 넣을 수 있는 구조를 만들어보겠습니다.
interface Todo {
  id?: string
  title: string,
  image?: string, //✅
  done: boolean
  createdAt: string
  updateAt: string,
  deleted: boolean
}
...
// 투두추가
router.post('/', async (req, res) => {
  const { title, imageBase64 } = req.body
  const date = new Date().toISOString()
  //스토로지에 파일 저장, bucket은 하나의 폴더정도로 이해해도 좋습니다.
  const bucket = admin.storage().bucket('images')
  const [, body] = imageBase64.split(',')
    //실제 파일화를 하려면 buffer라는 객체가 필요합니다.
  const buffer = Buffer.from(body, 'base64')  
  const file = bucket.file('image.png')
  await file.save(buffer)
  //변수로 만들어야 재활용이 가능합니다.
  const todo: Todo = {
    //documnet내용작성
    title,
    image: 'http://localhost:9199/images/image.png', //스토리지 포트번호
    done: false,
    createdAt: date,
    updateAt: date,
    //IOS는 국제표준시로 date를 생성합니다.
    deleted: false
  }
const bucket = admin.storage().bucket('images')bucket()은 하나의 폴더개념으로 이해하면 쉽습니다. images라는 이미지파일을 bucket()이라는 폴더에 담습니다.
 const [, body] = imageBase64.split(',')const buffer = Buffer.from(body, 'base64')const file = bucket.file('image.png')await file.save(buffer) image: 'http://localhost:9199/images/image.png',이미지의 이름과 확장자의 경우 base64코드에서 추출해서 채워넣거나 프론트엔드에서 이미지에 대한 정보를 서버에 전송해 넣어줘야합니다.
App.vue
<script lang="ts">
import {defineComponent} from 'vue'
import axios from 'axios'
export default defineComponent({
  methods: {
    selectFile(event: Event) {
      const files = (event.target as HTMLInputElement).files as FileList
      for(let i = 0; i<files.length; i++) {
        const file = files[i]
        const reader = new FileReader()
        reader.readAsDataURL(file)
        reader.addEventListener('load', async e => {
          console.log((e.target as FileReader).result)
          const {data} = await axios({
            url: 'http://localhost:5001/kdt-test-98de9/us-central1/api/todo',
            method: 'POST',
            data: {
              title: '파일추가!',
              imageBase64: (e.target as FileReader).result
            }
          })
          console.log('투두생성 응답:', data)
        })
      }
    }
  }
})
</script>

필요한 예외처리
- 용량제한
- 보안
- 파일형식
등등이 존재합니다.
스토로지 파일 저장 코드의 경우 반복해서 사용이 되고 있기 때문에 별도로 관리를 하는 것이 좋습니다. 모듈화를 시킨다면 utils폴더의 index.ts로 만들어줍니다.
import * as admin from 'firebase-admin'
export async function saveFile(base64: string, bucketName = 'images') {
  const bucket = admin.storage().bucket(bucketName)
  const [, body] = base64.split(',') //4.
  const buffer = Buffer.from(body, 'base64')
  const file = bucket.file('image.png')
  await file.save(buffer)
  return `http://localhost:9199/${bucketName}/image.png`
}todo.ts
import {saveFile} from '../utils' ✅
router.post('/', async (req, res) => {
  const {title, imageBase64} = req.body //1. images를 꺼내옵니다.
  const date = new Date().toISOString()
  const image= await saveFile(imageBase64) ✅
  const todo = {
    title,
    image, ✅
    done: false,
    createdAt: date,
    updatedAt: date,
    deleted: false
  }
  const ref = await db.collection('Todos').add(todo) 
  //생성된 데이터를 응답
  res.status(200).json({
    id: ref.id,
    ...todo
  })
})
//투두수정
router.put('/:id', async(req, res) => {
  const {title, done, imageBase64✅} = req.body 
  const {id} = req.params
  const snap = await db.collection('Todos').doc(id).get()
  if(!snap.exists) {
    return res.status(404).json('존재하지 않는 정보입니다.')
  }
  const image= await saveFile(imageBase64)✅
  const { createdAt } = snap.data() as Todo
  const updatedAt = new Date().toDateString()
  await snap.ref.update({
    title,
    image,✅
    done,
    updatedAt
  })
  return res.status(200).json({
    id: snap.id,
    title,
    image,✅
    done,
    createdAt,
    updatedAt
  })
})이전에 작성한 데이터들에도 image가 들어갈 수 있도록 해줘야합니다.
utils>index.ts
import * as admin from 'firebase-admin'
const db = admin.firestore()
async function addFields() {
  const snaps = await db.collection('Todos').get()
  for(const snap of snaps.docs){
    const {image} = snap.data() //이미지가 없는 경우에만
    if(!image) {
      await snap.ref.update({
        image: null
      })
    }
  }
  console.log('끝')
}
addFields() //✅한번 실행todo.ts
interface Todo {
  id?: string
  title: string,
  image?: string | null, ✅
  done: boolean,
  createdAt: string,
  updateAt: string,
  deleted: boolean
}src>index.ts
admin.initializeApp()
import * as functions from 'firebase-functions'
import * as express from 'express'
import * as cors from 'cors'
import todo from './routes/todo'
import './jobs' //✅한번 저장 후 바로 주석처리를 해줍니다.현재 우리가 통신하는 서버는 모두 로컬호스트에 맞춰져있습니다. 이제는 실제 파이어베이스 서버에 배포를 할 것이기에 환경변수를 도입해보도록 하겠습니다.
//hosting
$ npm i -D dotenv프로젝트의 루트 경로에 .env파일을 만들어줍니다. 이렇게 만든 .env파일은 프로젝트에서 동작하는 환경에서만 동작합니다. 또한 git에 올라가지 않도록 .gitignore에 명시를 해줍니다.
NODE_ENV=development서버에서의 환경변수 지정
- 대표적으로 넷니파이에서는
Build & deploy에서 지정이 가능합니다.
functions >src > utils> index.ts
import * as admin from 'firebase-admin'
export async function saveFile(base64: string, bucketName = 'images') {
  const bucket = admin.storage().bucket(bucketName)
  const [, body] = base64.split(',') //4.
  const buffer = Buffer.from(body, 'base64')
  const file = bucket.file('image.png')
  await file.save(buffer)
  return process.env.NODE === 'development'
  ? `http://localhost:9199/${bucketName}/image.png` //로컬주소
  : `https://strage.googleapis.com/${bucketName}/image.png` //서버주소
}루트경로의 index.ts에 사용한다고 설정을 해줍니다.
import * as admin from 'firebase-admin'
admin.initializeApp()
import * as dotenv from 'dotenv'✅
dotenv.config() ✅환경변수는 서버리스 함수에서 유용하게 활용을 할 수 있습니다