백엔드/express.js

조건쿼리 (conditional query)

syleemomo 2024. 5. 20. 22:29
728x90

 

https://mongoosejs.com/docs/tutorials/query_casting.html

 

Mongoose v8.4.0: Mongoose Tutorials: Query Casting

Query Casting The first parameter to Model.find(), Query#find(), Model.findOne(), etc. is called filter. In older content this parameter is sometimes called query or conditions. For example: const query = Character.find({ name: 'Jean-Luc Picard' }); query.

mongoosejs.com

 

 

* 프론트엔드 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <h1>조건쿼리</h1>
  <script src="app.js"></script>
</body>
</html>
const categories = ['자기계발', '패션']
const done = false
fetch(`http://localhost:5000/category?categories=${encodeURIComponent(categories.join('-'))}&done=${done}`)
.then(res => res.json())
.then(result => {
  console.log(result)
})

 

* 백엔드

app.get('/category', async (req, res) => {
    let query = null 
    let { categories, done } = req.query 
    console.log(req.query)
    
    if(categories){
        categories = decodeURIComponent(categories).split('-')
        console.log(categories)
        if(Array.isArray(categories) && categories.length > 0){
            query = Todo.find({ category: { $in: categories } })
        }
    }
    if(done){
        query = query.find({ isDone: done === 'true' ? true: false })
    }
    
    if(query){
        res.send(await query.exec())
    }else{
        res.send({ msg: '쿼리 실패!' })
    }
})

package.json 파일이 위치한 곳의 index.js 파일에 위 라우트를 추가한다.

const express = require('express')
const app = express()
const cors = require('cors')
const logger = require('morgan')
const mongoose = require('mongoose')
const axios = require('axios')
const Todo = require('./src/models/Todo')
// const user = require('./src/models/User')
const usersRouter = require('./src/routes/users')
const todosRouter = require('./src/routes/todos')
const config = require('./config')
const validator = require('./validator')

const corsOptions = {
    origin: '*',
    credentials: true 
}

mongoose.connect(config.MONGODB_URL)
.then(() => console.log("mongodb connected ..."))
.catch(e => console.log(`failed to connect mongodb: ${e}`))

app.use(cors(corsOptions))
app.use(express.json()) // request body 파싱
app.use(logger('tiny')) // Logger 설정 

app.use('/api/users', usersRouter) // User 라우터
app.use('/api/todos', todosRouter) // Todo 라우터
 
app.get('/hello', (req, res) => {
    res.json('hello world!')
})
app.post('/hello', (req, res) => { // POST 요청 테스트 
    console.log(req.body)
    res.json({ userId: req.body.userId, email: req.body.email })
})
app.get('/error', (req, res) => {
    throw new Error('서버에 치명적인 에러가 발생했습니다.')
})
app.get('/fetch', async (req, res) => {
    const response = await axios.get('https://jsonplaceholder.typicode.com/todos')
    res.send(response.data)
  })

app.get('/category', async (req, res) => {
    let query = null 
    let { categories, done } = req.query 
    console.log(req.query)
    
    if(categories){
        categories = decodeURIComponent(categories).split('-')
        console.log(categories)
        if(Array.isArray(categories) && categories.length > 0){
            query = Todo.find({ category: { $in: categories } })
        }
    }
    if(done){
        query = query.find({ isDone: done === 'true' ? true: false })
    }
    
    if(query){
        res.send(await query.exec())
    }else{
        res.send({ msg: '쿼리 실패!' })
    }
})
app.use( (req, res, next) => {  // 사용자가 요청한 페이지가 없는 경우 에러처리
    res.status(404).send("Sorry can't find page")
})
app.use( (err, req, res, next) => { // 서버 내부 오류 처리
    console.error(err.stack)
    res.status(500).send("something is broken on server !")
})
app.listen(5000, () => {
    console.log('server is running on port 5000...')
})

index.js 파일의 전체코드는 위와 같다. 물론 Todo 스키마도 정의되어 있어야 한다. 

조건에 따라 이전의 쿼리 결과에서 다시 쿼리한다. 실제로 DB에 접근하는 코드는 await query.exec() 이다. find() 함수는 조건에 따라 쿼리조건만 추가한다. 

할일을 끝마치지 못한 TODO 필터링

카테고리가 "자기계발"이나 "패션"인 할일(67개) 중에서 할일을 종료하지 못한 것(23개)만 다시 필터링하였다. 물론 필터링되는 갯수는 랜덤하게 생성한 더미 데이터이므로 다를수 있다. 

 

* 프론트엔드 수정

const categories = ['자기계발', '패션']
const done = ''
fetch(`http://localhost:5000/category?categories=${encodeURIComponent(categories.join('-'))}&done=${done}`)
.then(res => res.json())
.then(result => {
  console.log(result)
})

만약 할일종료 여부에 대한 필터링을 제외하면 결과는 아래와 같다. 

 

 

*최종코드 (프론트엔드/백엔드)

const categories = []
const done = ''
fetch(`http://localhost:5000/category?categories=${encodeURIComponent(categories.join('-'))}&done=${done}`)
.then(res => res.json())
.then(result => {
  console.log(result)
})
app.get('/category', async (req, res) => {
    let query = Todo.find({})
    let { categories, done } = req.query 
    console.log(req.query)
    
    if(categories){
        categories = decodeURIComponent(categories).split('-')
        console.log(categories)
        if(Array.isArray(categories) && categories.length > 0){
            query = query.find({ category: { $in: categories } })
            console.log(query.getFilter())
        }
    }
    if(done){
        query = query.find({ isDone: done === 'true' ? true: false })
        console.log(query.getFilter())
    }
    
    try{
        const result = await query.exec()
        res.send(result)
    }catch(e){
        res.send({ msg: '쿼리 실패!', error: e })
    }
})

맨 처음에 쿼리조건이 없으면 전체목록을 반환한다. query.getFilter() 메서드는 현재 쿼리조건이 내가 원하는대로 제대로 생성되었는지 확인하는 용도이다. 

 

const categories = []
const done = 'false'
fetch(`http://localhost:5000/category?categories=${encodeURIComponent(categories.join('-'))}&done=${done}`)
.then(res => res.json())
.then(result => {
  console.log(result)
})

할일을 종료한 것만 필터링하면 아래와 같다.

 

* 조건쿼리를 사용하기 힘든 경우

조건쿼리는 서로 다른 필드를 쿼리할때는 계속 조건이 추가되면서 필터링되기 때문에 유용하지만, 아래와 같이 동일한 필드에 조건을 추가할때는 사용하기 힘들다. 왜냐하면 이전의 조건을 덮어쓰기 때문이다. 

router.get('/condition', async (req, res, next) => {
    const { c1, c2, c3 } = req.query
    
    let query = Todo.find({})
    if(c1){
        query = query.find({ category: c1 })
        console.log(query.getFilter())
    }
    if(c2){
        query = query.find({ category: c2 })
        console.log(query.getFilter())
    }
    if(c3){
        query = query.find({ category: c3 })
        console.log(query.getFilter())
    }
    try{
        const result = await query.exec()
        res.json(result)
    }catch(e){
        res.json({msg: '쿼리실패!'})
    }
    
})

이렇게 작성하고 API 테스터로 쿼리를 날려보자!

아래와 같이 query.getFilter() 를 출력해보면 동일한 필드이기 때문에 쿼리조건이 추가되서 필터링이 되지 않고 쿼리조건을 덮어써버린다. 

그래서 아래와 같이 맨 마지막 '여행' 이라는 카테고리를 만족하는 도큐먼트가 전부 검색이 된다. 

 

그렇다면 동일한 필드(category 필드를 배열로 가정)에 c1, c2, c3 조건이 각각 존재할때 조건쿼리처럼 계속 조건을 추가해서 찾으려면 어떻게 하면 될까? 예를 들어 c1 = '오락', c3 = '여행' 값이 존재할때 category 필드에 c1과 c3 값을 모두 포함하는 도큐먼트를 찾고 싶은 상황이다.

router.get('/condition', async (req, res, next) => {
    const { c1, c2, c3 } = req.query
    
    const query = []
    if(c1){
        query.push(c1)
    }
    if(c2){
        query.push(c2)
    }
    if(c3){
        query.push(c3)
    }
    try{
        const result = await Todo.find({ category: { $all: query}})
        res.json(result)
    }catch(e){
        res.json({msg: '쿼리실패!'})
    }
    
})

이런 식으로 각 조건에 따라 찾으려는 값이 존재하면 query 배열에 추가한다. 마지막에 find 메서드를 이용하여 한번에 쿼리한다. 

router.get('/condition', async (req, res, next) => {
    const { c1, c2, c3 } = req.query
    
    let query = await Todo.find({})
    if(c1){
        query = query.filter(q => q.category.includes(c1))
    }
    if(c2){
        query = query.filter(q => q.category.includes(c2))
    }
    if(c3){
        query = query.filter(q => q.category.includes(c3))
    }
    if(query.length > 0){ // category 필드 (배열)에 c1, c2, c3 항목이 모두 포함된 도큐먼트 검색
        res.send(query)
    }else{
        res.json({code:404,message:'검색 실패!'})
    }
    
})

또는 전체 도큐먼트를 검색해놓고, 자바스크립트 filter 메서드를 사용해서 조건에 따라 걸러낸다. 

 

728x90