우리의 코드는 현재 백엔드에서 돌아가고 있습니다.
put
을 날리면 request
로 처리후 반환되는 그 정보를 받습니다.루트경로에 axios를 설치해줍니다.
$ npm i axios
$ npm run fb:emuls
$ functions > npm run build:watch //ts => js
//투두수정
router.put('/:id', async(req, res) => {
const {title, done} = req.body
const {id} = req.params
const snap = await db.collection('Todos').doc(id).get()
const {createdAt} = snap.data() as Todo
const updatedAt = new Date().toDateString
await snap.ref.update({ //이것은 db를 갱신해주는 것이지 snap을 갱신해주는 것은 아닙니다.
title,
done,
updatedAt: new Date().toDateString()
})
res.status(200).json({
id: snap.id,
title,
done,
createdAt,
updatedAt
})
})
req.body
)에서 title, done을 꺼낼 수 있습니다.https://asia-northeast3-heropy-api.cloudfunctions.net/api/todos/:todoId
와 같이 주소 부분에 파라미터로 받아서 어떤 id를 가지는 항목을 수정할 것인지를 설정해줘야합니다.REST API
형태로 하기도 하지만 body부분에 담아서 요청하기도 합니다.(즉, API설계에 따라서 달라집니다.)/:id
로 동적파라미터를 설정해줍니다.db.collection('Todos').doc(id)
doc()
메서드를 이용하면 원하는 정보만을 뽑아올 수 있습니다.get()
메서드로 그렇게 뽑아온 데이터를 가지고 옵니다.snap
으로 받아옵니다. snap
의 ref
속성에는 update
라는 메소드가 있습니다. 이 update
메소드를 통해 db에서 찾은 데이터를 업데이트 해줍니다.snap
에서 id
를 뽑아 반환합니다.data()
메소드를 이용합니다.snap.data()
에서 가져와 사용을 합니다.const { createdAt } = sanp.data()
와 같이 객체구조분해할당을 하는 경우 타입스크립트 에러가 나게 됩니다.
await snap.ref.update
는 DB를 갱신해주는 것이지 snap을 갱신해주는 것은 아닙니다. 즉, snap을 그대로res.status
에서 사용해서는 안됩니다.
따라서 최신에서 사용한 그 값을snap.ref.update
에서 사용한 후res.status
에서 갱신을 해주는 것입니다.
snap : DB에서 찾은 도큐먼트를 활용할 수 있는 객체
snap.data() : snap에서 data메소드를 사용하면 그 객체의 필드들이 다 반환됩니다.(ex, fileds.title, fileds.done, fileds.createdAt 등)
정상적으로 동작하는 것을 확인할 수 있습니다.
//todo 삭제
router.delete('/:id', async(req, res) => {
const { id } = req.params
const snap = await db.collection('Todos').doc(id).get()
await snap.ref.delete()
res.status(200).json(true)
})
export default router
id)
를 req.parmas
에서 뽑아냅니다.DB에서 이렇게 데이터를 날려보내면 데이터 추적이 어렵기 때문에 실제로 서버개발자는 실제로 지우지 않고 지워진 것처럼 만들고 30일후에 지워지는 등 과 같이 만듭니다. 이런 경우 delete를 쓰지 않고 추후 스케줄러를 거는 방식으로 합니다.
//todo 삭제
router.delete('/:id', async(req, res) => {
const {id} = req.params
const snap = await db.collection('Todos').doc(id).get()
await snap.ref.update({
deleted: true //이렇게 있으면 지어진 데이터로 취급
})
res.status(200).json(true)
})
export default router
실제로 데이터를 지우지 않고 delete가 있는 경우 지워진 상태로보기 때문에 조회시 필터를 해줘야합니다,
//todo조회
// https://localhost:5001/kdt-test-9f352/us-central1/api/todo/heropy
router.get('/', async (req, res) => {
const snaps = await db.collection('Todos')
.where('deleted', '!=', true)//✅이부분 추가
.get()
const todos: Todo[] = []
snaps.forEach(snap => {
const fields = snap.data()
todos.push({
id: snap.id,
...fields as Todo
})
})
기존 값을 조회하려면, deleted가 true가 아닌 값이 미리 들어가 있어야 합니다 즉, 이전에 넣은 값들에도 deleted를 설정해줘야합니다.
//job
function addDeleted() {
const snaps = await db.collection('Todos').get()
snaps.forEach(async snap => { // ⭐불가능합니다.
await snap.ref.update({
deleted:false
})
})
console.log('완료')
}
⭐foreach에서 async await를 쓴다고 해서 foreach에서 수행되는 로직을 다 기다리고 console로 넘어가지 않습니다
그래서 필요한 것이 바로 for문입니다
//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('끝')
}
이제는 deleted라는 속성도 필요하기 때문에 POST 메소드에 추가를 해줍니다.
interface Todo {
id?: string
title: string
done: boolean
createdAt: string
updateAt: string,
deleted: boolean✅
}
//투두 추가
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,
deleted: false✅
}
const ref = await db.collection('Todos').add(todo)
//생성된 데이터를 응답
res.status(200).json({
id: ref.id,
...todo
})
})
addDeleted()
addDelted()
메소드를 딱 한번 실행을 하고 저장을 해줍니다. 이후 addDelted()
는 지워줍니다.
잘못된 id에 대해 조회를 하려는 경우 조회가 되지 않도록 예외처리를 해보겠습니다.
router.delete('/:id', async(req, res) => {
const {id} = req.params
const snap = await db.collection('Todos').doc(id).get()
// 예외처리
if(!snap.exists) {
return res.status(404).json('존재하지 않는 정보입니다.') ✅
}
await snap.ref.update({
deleted: true //이렇게 있으면 지어진 데이터로 취급
})
res.status(200).json(true)
return ✅ //ts에러해결
})
snap
의 exists
속성을 이용하면 내용이 존재하는지 아닌지를 판단할 수 있습니다.마찬가지로 id가 필요한 투두 수정에서도 예외처리를 해줍니다.
router.put('/:id', async(req, res) => {
const {title, done} = req.body
const {id} = req.params
const snap = await db.collection('Todos').doc(id).get()
if(!snap.exists) {
return res.status(404).json('존재하지 않는 정보입니다.')✅
}
const { createdAt } = snap.data() as Todo
const updatedAt = new Date().toDateString()
await snap.ref.update({
title,
done,
updatedAt
})
return res.status(200).json({
id: snap.id,
title,
done,
createdAt,
updatedAt
})
})
//투두 조회
router.get('', async (req, res) => {
const snaps = await db.collection('Todos')
.where('deleted', '!=', true)
.get()
const todos: Todo[] = []
snaps.forEach(snap => {
const fields = snap.data()
todos.push({
id: snap.id,
...fields as Todo
})
})
todos.sort((a, b) => { //숫자를 기준으로 비교할 수 있도록 new Date().getTime()
const aTime :number = new Date(a.createdAt).getTime()
const bTime :number = new Date(b.createdAt).getTime()
return bTime-aTime
})
res.status(200).json(todos)
})
firebase.json파일의 emulators에서 어떤 것이 사용되고 있는지 확인할 수 있습니다.
...
"emulators": {
"auth": {
"port": 9099 // FIREBASE에서 제공하는 회원가입, 로그인 관련: 헤로피님 블로그 추천
},
"functions": {
"port": 5001 //넷니파이나 버셀에서 사용하는 서버리스 함수와 같은 개념(서버를 구축하지 않아도 돌릴 수 있는 서버코드
},
"firestore": { //데이터베이스(일반)
"port": 8080
},
"database": { //리얼타임데이터베이스(기능이 도입되기 전, 실시간 통신하면서 서버에서의 정보가 변하면 프론트엔드의 정보도 변함.)
"port": 9000
},
"hosting": { //싱글페이지 어플리케이션, 백엔드가 동작하지 않아도 서버를 업로드할 수 있는 서비스
"port": 5000
},
"pubsub": { //스케줄러(코드를 실행하는 스케줄: 스크랩핑, 크롤링)나 메세지 전송할 때 쓰는 기능
"port": 8085
},
"storage": { // 이미지 파일 저장
"port": 9199
},
"ui": {
"enabled": true
}
},
"remoteconfig": {
"template": "remoteconfig.template.json"
}
}