[firebase실강] 5강

youngseo·2022년 7월 8일
0

FireBase

목록 보기
5/9
post-thumbnail

5강

1. 환경변수 => publicUrl()

이전시간에 루트경로에 dot.env를 설정했는데 루트경로에 만드는 경우 루트경로를 찾을 수 있는 프론트엔드 프로젝트에서만 쓸수 있게 됩니다. 따라서 .env를 functions폴더 내부로 옮겨야합니다.

또한 우리가 dot.env를 사용하고 있었던 이유가 develop모드인지 확인하며 서버주소를 사용할 것인지, 로컬주소를 사용할 것인지를 결정짓기 위해서였습니다.

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 file.publicUrl()}

그런데 publiceUrl()을 사용하면 로컬에서는 로컬에 맞는 주소가, 서버에는 서버에 맞는 주소가 설정되기 때문에 env파일을 삭제하겠습니다. 또한 루트의 index.ts의 dot.env를 가져오는 로직도 삭제하도록 하겠습니다.

2. bucket에 고유 아이디 설정(nanoid)

index.ts

⭐버켓(bucket.file('image.png')에 저장할 때는 덮어쓰지 않도록 고유의 아이디가 필요합니다. 따라서 나노아이디를 이용해 관리를 해보겠습니다.

나노아이디가 4.0으로 넘어 오면서 ESM방식으로 바뀌었습니다. 타입스크립트를 구성하는 방식에 따라 달라지겠지만 우리의 타입스크립트는 노드.js에서 동작하는 방식으로 작성된 것이고 module은 commonjs방식을 사용하고 있습니다.

현재 백엔드는 노드.js(최근에는 es모듈도 도입되긴 했습니다. 과도기), 프론트엔드는 ESM방식으로 되어 있기 때문에 백엔드에서는 EMS방식으로 작성된 부분은 사용을 할 수 없습니다. 따라서 3버전을 사용할 예정입니다.

//functions //3버전으로 일반의존성으로 설치

$ npm i nanoid@3
import {nanoid} from 'nanoid' //✅1

export async function saveFile(bas64:string, bucketName = 'images') {
  const bucket = admin.storage().bucket(bucketName)
  const [, body] = bas64.split(',')
    //실제 파일화를 하려면 buffer라는 객체가 필요합니다.
  const buffer = Buffer.from(body, 'base64')  
  const file = bucket.file(`${nanoid()}.png`) //✅1
 ... 
  • 설치된 nanoid를 가져와 기존의 bucket.file(image/png)에서 이미지의 이름으로 nanoid를 호출해줍니다.

3. express , cors를 일반의존성으로 변경

package.jsoncors, express를 dependence로 옮겨줍니다.

4. 파일 용량제한 byteLength

export async function saveFile(bas64:string, bucketName = 'images') {
  const bucket = admin.storage().bucket(bucketName)
  const [, body] = bas64.split(',')
    //실제 파일화를 하려면 buffer라는 객체가 필요합니다.
  const buffer = Buffer.from(body, 'base64')  
  const file = bucket.file(`${nanoid()}.png`) 
  const byteLength = Buffer.byteLength(buffer) //✅1
  //1MB = 1024KB입니다. //아래는 10MB
  if(10 * 1024 * 1024 < byteLength) {  //2.✅
    throw { status: 400, message: '제한 용량 초과' }
  }
  await file.save(buffer)

  
  return file.publicUrl()
}
  • Node.js에서 사용하는 Buffer클래스에 byteLength라는 메소드를 사용하면 파일의 용량(길이)을 확인할 수 있습니다
    • const byteLength = Buffer.byteLength(buffer)
  • if조건으로 조건을 제한해줍니다.
    • 1MB는 1024KB입니다.
    • 10MB 1024 = 10KB 1025 => 10MB입니다.

5. 유효성 검사 (validator)

  • 파일이 들어왔는데 base64가 아닌 다른 문자가 들어오는 경우를 에러처리해보도록 하겠습니다(파일 유효성검사)
  • 노드.js에서 많이 사용되는 유효성검사 패키지로는 validator가 있습니다.
    • 알파벳인지? /아스키코드인지? /base64인지? /이메일양식인지/ Json인지? 등등

5-1 설치 및 실행

//functions

$ npm i validator 
import * as admin from 'firebase-admin'
import { nanoid } from 'nanoid'
import validator from 'validator'

export async function saveFile(base64: string, bucketName = 'images') {
  const bucket = admin.storage().bucket(bucketName)

 if(!validator.isBase64(base64)){
  throw { status: 400, message: '잘못된 양식입니다!' }
}

  const [, body] = base64.split(',') //4.
  • base64가 맞는지를 검사하는 것이기 때문에 base64를 검사하기 전에 사용을 합니다.

5-2 type에러 해결

  • 현재 import validator from 'validator에서 형식정의를 찾을수가 없다는 안내가 뜨고 있습니다.

    TS가 지원되는지 확인하는 방법

  • nodemodules의 해당 패키지에 d.ts파일이 있는지 확인합니다.

➕ package.json의 main에 적혀있는 해당하는 파일이 어떤 형식으로 내보내기를 하는지 확인하면 가져오기 방식도 정리할 수 있습니다.

  • module.exports로 내보내는 경우 import * as

5-3. 타입 찾아오기

그런데 현재 validator의 패키지의 index.js를 확인해보니 d.ts가 없습니다. 이런경우 직접 만들거나 외부에서 찾아와야합니다. 저희는 외부(@type/node)에서 찾아오도록 하겠습니다.

//타입이 있는지 확인
$ npm info @types/validator
 
//있다면 설치 : 타입스크립트만 사용하기에 개발의존성으로 설치해줍니다.
$ npm i @types/validator -D

5-4 코드 작성

import validator from 'validator'export async function saveFile(base64: string, bucketName = 'images') {
  const bucket = admin.storage().bucket(bucketName)

 if(!validator.isBase64(base64)){ //✅ true, false로 반환됩니다.
  throw { status: 400, message: '잘못된 양식입니다!' }
  }
...
}

6. 확장자 추출 (file-type)

  • file-type을 통해 파일의 타입을 알아낼 수 있습니다.
  • 마찬가지로 ESM방식을 사용하기 직전 버전인 16버전을 사용하겠습니다.
//functions 

$ npm i file-type@16
import * as FileType from 'file-type'...
  const {ext} = await FileType.fromBuffer(buffer) as {ext:string}const file = bucket.file(`${nanoid()}.${ext}`)await file.save(buffer)

  return file.publicUrl()
}
  • 용량을 확인하는 코드 아래에 FileType이 가지고 있는 fromButter()을 호출합니다. 그리고 buffer객체를 인수로 넣어줍니다.
    • fromButter()는 비동기로 동작하기 때문에 await를 붙여줘야합니다.
    • await file.save(buffer)로 뽑아낸 데이터를 type이라는 변수에 담을 때, type은 객체데이터고 그안에 ext(확장자)라는 속성과 mime(컨텐츠)이라는 속성을 가지고 있습니다.
    • 따라서 객체구조분해를 통해 ext를 뽑아냅니다.
    • bucket.file(${nanoid()}.png) 의 png부분에 ext를 대신해 넣어줍니다.
  • const { ext } = await FileType.fromBuffer(buffer) as {ext:string} 타입단언을 해줍니다.

7. 확장자를 이용한 필터링

  //허용하고자 하는 확장자를 가지고 있는 배열데이터를 만들어줍니다. 
  const allowTpyes = ['jpg', 'png','webp']
  if(!allowTpyes.includes(ext)){// true가 나오면 허용
    throw {status: 400, message: '유효한 타입이 아닙니다!'}
  }

9. 로컬 테스트

//host

$ npm run dev

열린 개발서버에서 사진을 하나 저장한 후 firebase storage에 이동을 해 확인합니다.

10. 깃허브 업로드

프로젝트를 깃허브에 올려보도록 하겠습니다.

먼저, 루트경로의 gitignore 파일에 .env.-debug.log를 추가해줍니다.

루트 경로를 확인해보면 아래와 같이 github폴더 안에 두개의 파일이 있습니다.

  • merge: 메인브랜치로 뭔가 올라오면 배포한다.
  • pull_requrest: 병합과정을 거치면 배포한다.

10-1 파일 설정

()의 경우 branch의 이름입니다.

이 두개의 파일을 합쳐서 관리를 해보도록 하겠습니다. (들여쓰기를 맞춰줘야합니다.)

10-2 깃 레포와 연결

$ git init
$ git add *
$ git commit -m "커밋메세지"
$ git branch -M main
$ git remote add origin 깃주소

위와 같이 깃허브의 actions 창에서 노란불이 들어온 것을 확인할 수 있습니다.


()부분은 10-1에서 설정했던 이름 영역입니다.

이제 자동배포가 가능합니다.

10-3 firebase

firebase-빌드-hosting로 이동합니다.

위 도메인에 접속을 하면 npm run dev시 확인했던 화면이 나오는 것을 확인할 수 있습니다.

참고로 아직 firebase의 functions는 배포를 하지 않은 상황입니다(서버코드).
hosting은 기본적으로 정적 웹사이트를 주로 이야기하며 서버코드는 호스팅이라고 부르지 않습니다.

11. firebase functions배포

$ npm run fb:deploy
  • 참고로 호스팅도 포함되어 배포되기는 합니다. 만약 functions만 배포를 하는 경우 functions 폴더 내부로 이동을 한 후 명령어를 실행해주시면 됩니다.(functions내부의 package.josn을 확인해보면 deploy명령어가 있는 것을 확인할 수 있습니다.)

만약 배포중 문제가 생긴다면(물론, 여러가지 문제가 발생할 수 있기에 100% 해결 방법이 될 수는 없습니다) firebase-functions-로그-google clodue에서 열기 를 통해 조금 더 자세한 내용을 확인할 수 있습니다.

12. cloud functions에서 권한설정

cloud functions를 검색해 이동을 해보면 아래와 같이 뜨는 것을 확인할 수 있습니다. 여기서 이름 api는 functions 루트 경로의 index.ts의 export const api = functions.https.onRequest(app)에 명시된 이름입니다.

api를 눌러 접속을 하면 사용내용이 나옵니다. 여기서 권한을 클릭합니다.

  • api를 만들어서 프론트엔드에서 사용을 하고 있는 구조이기 때문에 접속 권한이 열러있어야합니다.
  • 그 세부적으로 접속 권한을 제외하는 제한하는 것은 index.tscors()에서 화이트 리스트를 제안할 수 있습니다.

13. App.vue 주소 변경

서버 주소로 설정을 변경해보도록 하겠습니다.

const {data} = await axios({
  url: 'https://us-central1-kdt-test-97b7e.cloudfunctions.net/api/todo',
  method: 'POST',
  data: {
    title: '파일추가!',
    imageBase64: (e.target as FileReader).result
  }
})

설정 후 로컬서버를 열어보면 네크워크 부분에서 정상적으로 파일이 업로드 되는 것을 확인할 수 있습니다.

14. 버켓추가

버켓을 먼저 만든 후 진행을 해야하는데 실수가 생겼네요. 현재 imges라는 폴더가 이미 존재하므로 다른 추천이름-imges로 설정 후 utils의 이름을 변경해보도록 하겠습니다.

현재 functions만 다시 배포하면 되는 상황이기에 functions폴더로 이동을 해 디플로이 명령을 실행합니다.

$ cd functions 
$ npm run deploy

배포가 완료된 후 로컬서버에서 파일을 업로드해보면 storage에 파일이 잘 올라가는 것을 확인할 수 있습니다.

  • 누구나 다 파일을 올릴 수 있는 것이 아니라 제한을 하고자 한다면, 비밀번호를 설정하면 됩니다.
  • 비밀번호 설정은 api header에 요청을 할 때 apikey를 포함시킬 수 있도록 하면 됩니다.
  • 아주 간단하게만 예시를 들자면 아래와 같습니다. (모듈화를 통해 세분화하는 것이 좋습니다.)
//todo추가
router.post('/', async (req, res) => {
  const {apikey, username} = req.headers
  if(apikey !== '1234qwer'||username !== 'heropy-admin') {
    res.status(401).json('유효한 사용자가 아닙니다.')
  }
...

15. 리전 변경

리전 변경의 경우 export const api = functions.https.onRequest(app)dp region을 추가해 설정할 수 있습니다.

export const api = 
    functions
	.region('asia-northeast3')
	.https.onRequest(app)
  • us-centrol-1 이 코어 리전입니다.
  • 코어 리전으로 설정이 되어 있는 경우 최대한 가까운 리전으로 연결해주기도 하지만, 완벽하게 리전을 서울로 설정하는 것보다는 느립니다.
  • 한국에서만 사용을 할 예정이라면 서울리전으로 설정하는 것도 좋습니다.

➕ 추가

runWith()메소드를 통해 cloud functions의 옵션을 선택할 수 있습니다. (단, 요금이 나옵니다!)

export const api = functions
	.region('asia-northeast3')
    .runWith({
    minInstances: 0,
    memory: '1GB'
  })
	.https.onRequest(app)
  • minInstances
    • 사용자들이 서버에 요청을 할 때 이 요청을 처리하는 functions이 하나가 아니라 여러개를 만들 수 있습니다.
    • 인스턴스를 여러개 설정하면 더 빠르지만 요금이 부과됩니다.
  • memory
    • 기본 값은 256MB입니다.

15. cors제한(화이트리스트)

const app = express()
app.use(express.json())
app.use(cors({
  origin: [
    'http://localhost:3000',
    'kdt-test-97b7e.web.app', 
    'kdt-test-97b7e.firebaseapp.com'
  ]
}))
app.use('/todo', todo)
  • origin 옵션을 통해 접속 포트를 제한할 수 있습니다.
    • hosting에서 등록된 도메인으로 설정할 수도 있습니다.
    • ex) origin: [http://localhost:3000, kdt-test-97b7e.web.app,
      kdt-test-97b7e.firebaseapp.com]

16. 환경변수 설정

16-1 vite.js에서의 환경변수 설정

vite공식문서를 확인해보면 vite.js에서는 import.meta.env.PRODimport.meta.env.DEV로 환경을 설정할 수 있는 것을 확인할 수 있습니다. 이를 통해 프론트엔드에서 출력이 가능하도록 설계가 되어 있는 것입니다.

APP.vue

created(){
  console.log(import.meta.evn) 
}

위와 같이 다른 설정을 하지 않았음에도 dev, mode, product등이 설정되어 있는 것을 확인할 수 있습니다.(PROD는 제품모드로 서버로 올라가는 경우 ture가 됩니다.)

16-2 프로젝트에 적용

reader.addEventListener('load', async e => {
  console.log((e.target as FileReader).result)
  const url = import.meta.env.DEV
    ? 'http://localhost:5001/kdt-test-97b7e/us-central1/api/todo'
  : 'https://us-central1-kdt-test-97b7e.cloudfunctions.net/api/todo'

  const {data} = await axios({
    url,
    method: 'POST',
    data: {
      title: '파일추가!',
      imageBase64: (e.target as FileReader).result
    }
  })

위와 같이 삼항 연산자를 통해 개발모드와 로컬모드로 나누어 설정할 수 있습니다.

17. 배포

  • 프론트엔드의 경우 저장소에 올리기만 하면 자동배포가 됩니다.
  • 서버는 따로 배포를 해줘야합니다.
    • 전체 배포의 경우 루트 경로에서 npm run fb:deploy
    • 서버만 배포의 경우 functions에서 npm run deploy

+ 로그인 설정

관리자 로그인 설정을 한다면 다음과 같습니다. (실제로는 더 복잡합니다.)

//로그인
router.post('/', async (req, res) => {
  const {id, pw} = req.body
  if(id === 'adim' && pw === '1234') {
    //처리
  }
})

실제로는 db에 있는 정보와 일치하는 체크, 비밀번호를 암호화, 로그인 토근(JWT) 등등을 설정 해야합니다.

0개의 댓글