이전시간에 루트경로에 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를 가져오는 로직도 삭제하도록 하겠습니다.
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
...
bucket.file(image/png)
에서 이미지의 이름으로 nanoid를 호출해줍니다.package.json 의 cors
, express
를 dependence로 옮겨줍니다.
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()
}
Buffer
클래스에 byteLength
라는 메소드를 사용하면 파일의 용량(길이)을 확인할 수 있습니다 const byteLength = Buffer.byteLength(buffer)
//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.
import validator from 'validator
에서 형식정의를 찾을수가 없다는 안내가 뜨고 있습니다.TS가 지원되는지 확인하는 방법
d.ts
파일이 있는지 확인합니다.➕ package.json의 main에 적혀있는 해당하는 파일이 어떤 형식으로 내보내기를 하는지 확인하면 가져오기 방식도 정리할 수 있습니다.
module.exports
로 내보내는 경우import * as
그런데 현재 validator의 패키지의 index.js를 확인해보니 d.ts가 없습니다. 이런경우 직접 만들거나 외부에서 찾아와야합니다. 저희는 외부(@type/node)에서 찾아오도록 하겠습니다.
//타입이 있는지 확인
$ npm info @types/validator
//있다면 설치 : 타입스크립트만 사용하기에 개발의존성으로 설치해줍니다.
$ npm i @types/validator -D
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: '잘못된 양식입니다!' }
}
...
}
//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()
}
fromButter()
을 호출합니다. 그리고 buffer객체를 인수로 넣어줍니다.fromButter()
는 비동기로 동작하기 때문에 await를 붙여줘야합니다.await file.save(buffer)
로 뽑아낸 데이터를 type이라는 변수에 담을 때, type은 객체데이터고 그안에 ext(확장자)라는 속성과 mime(컨텐츠)이라는 속성을 가지고 있습니다.bucket.file(${nanoid()}.png)
의 png부분에 ext를 대신해 넣어줍니다.const { ext } = await FileType.fromBuffer(buffer) as {ext:string}
타입단언을 해줍니다. //허용하고자 하는 확장자를 가지고 있는 배열데이터를 만들어줍니다.
const allowTpyes = ['jpg', 'png','webp']
if(!allowTpyes.includes(ext)){// true가 나오면 허용
throw {status: 400, message: '유효한 타입이 아닙니다!'}
}
//host
$ npm run dev
열린 개발서버에서 사진을 하나 저장한 후 firebase storage에 이동을 해 확인합니다.
프로젝트를 깃허브에 올려보도록 하겠습니다.
먼저, 루트경로의 gitignore 파일에 .env
와 .-debug.log
를 추가해줍니다.
루트 경로를 확인해보면 아래와 같이 github폴더 안에 두개의 파일이 있습니다.
()의 경우 branch의 이름입니다.
이 두개의 파일을 합쳐서 관리를 해보도록 하겠습니다. (들여쓰기를 맞춰줘야합니다.)
$ git init
$ git add *
$ git commit -m "커밋메세지"
$ git branch -M main
$ git remote add origin 깃주소
위와 같이 깃허브의 actions 창에서 노란불이 들어온 것을 확인할 수 있습니다.
()부분은 10-1에서 설정했던 이름 영역입니다.
이제 자동배포가 가능합니다.
firebase-빌드-hosting로 이동합니다.
위 도메인에 접속을 하면 npm run dev시 확인했던 화면이 나오는 것을 확인할 수 있습니다.
참고로 아직 firebase의 functions는 배포를 하지 않은 상황입니다(서버코드).
hosting은 기본적으로 정적 웹사이트를 주로 이야기하며 서버코드는 호스팅이라고 부르지 않습니다.
$ npm run fb:deploy
만약 배포중 문제가 생긴다면(물론, 여러가지 문제가 발생할 수 있기에 100% 해결 방법이 될 수는 없습니다) firebase-functions-로그-google clodue에서 열기 를 통해 조금 더 자세한 내용을 확인할 수 있습니다.
cloud functions
를 검색해 이동을 해보면 아래와 같이 뜨는 것을 확인할 수 있습니다. 여기서 이름 api는 functions 루트 경로의 index.ts의 export const api = functions.https.onRequest(app)
에 명시된 이름입니다.
api를 눌러 접속을 하면 사용내용이 나옵니다. 여기서 권한을 클릭합니다.
- api를 만들어서 프론트엔드에서 사용을 하고 있는 구조이기 때문에 접속 권한이 열러있어야합니다.
- 그 세부적으로 접속 권한을 제외하는 제한하는 것은
index.ts
의cors()
에서 화이트 리스트를 제안할 수 있습니다.
서버 주소로 설정을 변경해보도록 하겠습니다.
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
}
})
설정 후 로컬서버를 열어보면 네크워크 부분에서 정상적으로 파일이 업로드 되는 것을 확인할 수 있습니다.
버켓을 먼저 만든 후 진행을 해야하는데 실수가 생겼네요. 현재 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)
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)
http://localhost:3000
, kdt-test-97b7e.web.app
,kdt-test-97b7e.firebaseapp.com
]vite공식문서를 확인해보면 vite.js에서는 import.meta.env.PROD
과 import.meta.env.DEV
로 환경을 설정할 수 있는 것을 확인할 수 있습니다. 이를 통해 프론트엔드에서 출력이 가능하도록 설계가 되어 있는 것입니다.
APP.vue
created(){
console.log(import.meta.evn)
}
위와 같이 다른 설정을 하지 않았음에도 dev, mode, product등이 설정되어 있는 것을 확인할 수 있습니다.(PROD는 제품모드로 서버로 올라가는 경우 ture가 됩니다.)
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
}
})
위와 같이 삼항 연산자를 통해 개발모드와 로컬모드로 나누어 설정할 수 있습니다.
npm run fb:deploy
npm run deploy
관리자 로그인 설정을 한다면 다음과 같습니다. (실제로는 더 복잡합니다.)
//로그인
router.post('/', async (req, res) => {
const {id, pw} = req.body
if(id === 'adim' && pw === '1234') {
//처리
}
})
실제로는 db에 있는 정보와 일치하는 체크, 비밀번호를 암호화, 로그인 토근(JWT) 등등을 설정 해야합니다.