프로젝트/할일목록(TODO) 앱

할일목록(TODO) 앱 7 - Mongoose virtual 과 moment.js 로 현재 시각 기준으로 시간 표시하기

syleemomo 2023. 8. 12. 10:47
728x90

https://jsikim1.tistory.com/195

 

moment.js 사용 방법 - JavaScript 날짜 라이브러리

moment.js 사용 방법 - JavaScript 날짜 라이브러리 moment.js는 JavaScript에서 사용되는 날짜관련 라이브러리 중 가장 많이 사용되었던 라이브러리입니다. 현재는 더이상의 업데이트가 없을 것이라 하였

jsikim1.tistory.com

https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs/mongoose

 

Express Tutorial Part 3: Using a Database (with Mongoose) - Learn web development | MDN

In this article, we've learned a bit about databases and ORMs on Node/Express, and a lot about how Mongoose schema and models are defined. We then used this information to design and implement Book, BookInstance, Author and Genre models for the LocalLibrar

developer.mozilla.org

https://runebook.dev/ko/docs/mongoose/tutorials/virtuals

 

Mongoose Virtuals Mongoose에서 가상은 MongoDB를 저장하지 않는 속성입니다.

Documentation Contributors History

runebook.dev

https://velog.io/@casin/Mongoose-populate-virtual-%EC%82%AC%EC%9A%A9%EB%B0%A9%EB%B2%95

 

Mongoose populate, virtual 사용방법

mongoose에서의 populate, virtual 사용 방법

velog.io

 

* virtual 필드를 사용하는 이유

virtual 필드는 DB에 접근이 가능한 getter, setter 이다. 다시말해, DB의 필드들을 가공해서 다른 형태로 조회할 수도 있고, DB에 저장하기 전에 데이터를 가공해서 여러 필드를 변경할 수도 있다. 

app.get('/user/fullName', (req, res, next) => {
    const user = User.findById('1234567890')
    res.json({userName: `${user.firstName} ${user.lastName}`})
})
app.post('/users/fullName', (req, res, next) => {
    const users = User.find({ name: { $regex: /사/}})
    res.json({ users: users.map(user => user.userName = `${user.firstName} ${user.lastName}`)})
}

라우트 핸들러에서 필드를 가공할 수도 있지만, 위와 같이 여러 라우터에서 동일한 필드들을 동일한 형태로 가공하는 경우에는 코드 중복이 되고, 코드를 변경해야 할때 여러 라우트 핸들러에서 전부 변경해야 하므로 유지보수가 힘들다. 예를 들어, 사용자의 firstName, lastName 필드를 합쳐서 보여줘야 하는 라우트 핸들러가 여러군데 존재하는 경우 라우트 핸들러 내부가 아니라 virtual 로 정의해두면 코드 중복을 피할수 있다. 

userSchema.virtual('fullName').get(function(){
    return `${this.firstName} ${this.lastName}`
})

그래서 공통적으로 필드를 가공해서 보여줘야 한다면, 위와 같이 virtual 필드로 미리 정의해놓고, 여러 라우트 핸들러에서 사용하는 것이 좋다. 이렇게 하면 함수를 재사용하는 것과 같이 코드가 깔끔해지고 유지보수도 쉬워진다.

app.get('/user/fullName', (req, res, next) => {
    const user = User.findById('1234567890')
    res.json({userName: user.fullName})
})
app.post('/users/fullName', (req, res, next) => {
    const users = User.find({ name: { $regex: /사/}})
    res.json({ users: users.map(user => user.userName = user.fullName)})
}

 

* moment 라이브러리 사용하기 

npm install moment

시간을 다양한 방법으로 계산해주는 moment 라이브러리를 설치한다.

server > src > models > User.js 파일에 아래와 같은 코드를 추가한다.

const moment = require("moment")

moment 라이브러리를 사용하기 위하여 임포트한다.

userSchema.virtual('status').get(function () {
  return this.isAdmin ? "관리자" : "사용자"
})
userSchema.virtual('createdAgo').get(function () {
  return moment(this.createdAt).fromNow() // day ago
})
userSchema.virtual('lastModifiedAgo').get(function () {
  return moment(this.lastModifiedAt).fromNow() // day ago
})

mongoose 의 virtual 을 이용하여 가상의 필드를 추가한다. virtual 필드는 데이터베이스에 새로운 필드가 만들어지는 것이 아니라 조회할때만 기존의 필드를 가공하여 보여준다. 

먼저 User 모델에 status 라는 가상의 필드를 추가하고, 해당 필드를 조회할때 콜백함수가 실행되면서 isAdmin 이 true 이면 "관리자"라는 문자열을 출력하고, false 이면 "사용자"라는 문자열을 출력해준다. User 모델에서 createdAgo 필드를 조회하면 콜백함수가 실행되면서 moment 라이브러리를 사용하여 현재 시각으로부터 사용자가 회원가입을 한 날짜를 계산해준다. 즉, 3일전에 회원가입을 했으면 "3 days ago" 라고 알려주는 것이다. User 모델에서 lastModifiedAgo 필드를 조회하면 콜백함수가 실행되면서 moment 라이브러리를 사용하여 현재 시각으로부터 사용자가 자신의 사용자정보를 마지막으로 업데이트한 날짜를 계산해준다. 

 

server > src > models > Todo.js 파일에 아래와 같은 코드를 추가한다.

const moment = require("moment")

moment 라이브러리를 사용하기 위하여 임포트한다.

// 가공해서 보여줄 필드
todoSchema.virtual('status').get(function () {
  return this.isDone ? "종료" : "진행중"
})

todoSchema.virtual('createdAgo').get(function () {
  return moment(this.createdAt).fromNow() // day ago
})
todoSchema.virtual('lastModifiedAgo').get(function () {
  return moment(this.lastModifiedAt).fromNow() // day ago
})
todoSchema.virtual('finishedAgo').get(function (){
  return moment(this.finishedAt).fromNow() // day ago
})

mongoose 의 virtual 을 이용하여 가상의 필드를 추가한다. virtual 필드는 데이터베이스에 새로운 필드가 만들어지는 것이 아니라 조회할때만 기존의 필드를 가공하여 보여준다. 

먼저 Todo 모델에 status 라는 가상의 필드를 추가하고, 해당 필드를 조회할때 콜백함수가 실행되면서 isDone 이 true 이면 "종료" 라는 문자열을 출력하고, false 이면 "진행중" 이라는 문자열을 출력해준다. Todo 모델에서 createdAgo 필드를 조회하면 콜백함수가 실행되면서 moment 라이브러리를 사용하여 현재 시각으로부터 사용자가 새로운 할일을 생성한 날짜를 계산해준다. 즉, 3일전에 할일(TODO)를 생성 했으면 "3 days ago" 라고 알려주는 것이다. Todo 모델에서 lastModifiedAgo 필드를 조회하면 콜백함수가 실행되면서 moment 라이브러리를 사용하여 현재 시각으로부터 사용자가 할일(TODO)를 마지막으로 업데이트한 날짜를 계산해준다. Todo 모델에서 finishedAgo 필드를 조회하면 콜백함수가 실행되면서 moment 라이브러리를 사용하여 현재 시각으로부터 사용자가 할일(TODO)을 종료한 날짜를 계산해준다. 3일전에 할일을 끝마쳤으면 "3 days ago" 라고 출력해준다.

 

* 전체 할일목록 조회시 virtual 필드도 함께 보여주기

// isAuth : 전체 할일목록을 조회할 권한이 있는지 검사하는 미들웨어 
router.get('/', isAuth, expressAsyncHandler(async (req, res, next) => {
  const todos = await Todo.find({ author: req.user._id }).populate('author', ['name', 'userId']) // req.user 는 isAuth 에서 전달된 값
  if(todos.length === 0){
    res.status(404).json({ code: 404, message: 'Fail to find todos !'})
  }else{
    res.json({ code: 200, todos: todos.map(todo => {
      return {...todo._doc, createdAgo: todo.createdAgo, 
             lastModifiedAgo: todo.lastModifiedAgo,
             finishedAgo: todo.finishedAgo}
    }) })
  }
}))

server > src > routes > todos.js 파일에서 전체할일 목록 조회 API를 위와 같이 수정한다. 

virtual 필드 조회하기

 

* 특정 할일 조회시 virtual 필드도 함께 보여주기 

// isAuth : 특정 할일을 조회할 권한이 있는지 검사하는 미들웨어 
router.get('/:id', isAuth, expressAsyncHandler(async (req, res, next) => {
  const todo = await Todo.findOne({ 
    author: req.user._id,  // req.user 는 isAuth 에서 전달된 값
    _id: req.params.id // TODO id 
  })
  if(!todo){
    res.status(404).json({ code: 404, message: 'Todo Not Found '})
  }else{
    res.json({ 
      code: 200, 
      todo: {...todo._doc, createdAgo: todo.createdAgo, 
        lastModifiedAgo: todo.lastModifiedAgo,
        finishedAgo: todo.finishedAgo
      } 
    })
  }
}))

server > src > routes > todos.js 파일에서 특정할일 조회 API를 위와 같이 수정한다. 

virtual 필드 조회하기

 

* 회원가입/로그인/사용자정보 수정시 virtual 필드 함께 보여주기

router.post('/register', [
    validateUserName(), 
    validateUserEmail(),
    validateUserPassword()
], expressAsyncHandler( async (req, res, next) => {
    const errors = validationResult(req)
    if(!errors.isEmpty()){
        console.log(errors.array())
        res.status(400).json({
            code: 400,
            message: 'Invalid Form data for user',
            error: errors.array()
        })
    }else{
        console.log(req.body)
        const user = new User({
            name: req.body.name, 
            email: req.body.email, 
            userId: req.body.userId, 
            password: req.body.password
        })
        const newUser = await user.save() // 사용자정보 DB 저장
        if(!newUser){
            res.status(401).json({ code: 401, message: 'Invalid User Data'})
        }else{
            const { name, email, userId, isAdmin, createdAt } = newUser 
            res.json({
                code: 200,
                token: generateToken(newUser), // 시그니쳐(암호화된 사용자정보)
                name, email, userId, isAdmin, createdAt,
                status: newUser.status,
                createdAgo: newUser.createdAgo,
                lastModifiedAgo: newUser.lastModifiedAgo
            })
        }
    }
}))

server > src > routes > users.js 파일에서 회원가입 API를 위와 같이 수정한다. 위와 같이 해도 되지만,  virtual 필드도 newUser 에서 구조분해로 조회가 가능하다. 

virtual 필드 조회하기
virtual 필드 조회하기

회원가입시에는 방금전에 가입을 하였기 때문에 createdAgo, lastModifiedAgo 는 사실 큰 의미가 없기는 하다. 

router.post('/login', [
    validateUserEmail(),
    validateUserPassword()
], expressAsyncHandler( async (req, res, next) => {
    const errors = validationResult(req)
    if(!errors.isEmpty()){
        res.status(400).json({
            code: 400,
            message: 'Invalid Form data for user',
            error: errors.array()
        })
    }else{
        console.log(req.body)
        const loginUser = await User.findOne({
            email: req.body.email, 
            password: req.body.password 
        })
        if(!loginUser){
            console.log(loginUser)
            res.status(401).json({ code: 401, message: 'Invalid Email or Password'})
        }else{
            const { name, email, userId, isAdmin, createdAt } = loginUser
            res.json({
                code: 200,
                token: generateToken(loginUser),
                name, email, userId, isAdmin, createdAt,
                status: loginUser.status,
                createdAgo: loginUser.createdAgo,
                lastModifiedAgo: loginUser.lastModifiedAgo
            })
        }
    }
}))

server > src > routes > users.js 파일에서 로그인 API를 위와 같이 수정한다. 

virtual 필드 조회하기
virtual 필드 조회하기

로그인시에는 방금 회원가입을 한 것이 아니기 때문에 createdAgo 필드는 조회할 가치가 있다. 마찬가지로 lastModifiedAgo 필드도 로그인하기 훨씬 전에 사용자 정보를 수정했을 가능성도 있기 때문에 조회할 가치가 있다. 

router.put('/', [
    validateUserName(),
    validateUserEmail(),
    validateUserPassword()
],isAuth, expressAsyncHandler( async (req, res, next) => {
    const errors = validationResult(req)
    if(!errors.isEmpty()){
        res.status(400).json({
            code: 400,
            message: 'Invalid Form data for user',
            error: errors.array()
        })
    }else{
        const user = await User.findById(req.user._id)
        if(!user){
            res.status(404).json({ code: 404, message: 'User Not Founded'})
        }else{
            user.name = req.body.name || user.name 
            user.email = req.body.email || user.email
            user.password = req.body.password || user.password
            user.isAdmin = req.body.isAdmin || user.isAdmin 
            user.lastModifiedAt = new Date() // 수정시각 업데이트

            const updatedUser = await user.save()
            const { name, email, userId, isAdmin, createdAt } = updatedUser
            res.json({
            code: 200,
            token: generateToken(updatedUser),
            name, email, userId, isAdmin, createdAt,
            status: updatedUser.status,
                createdAgo: updatedUser.createdAgo,
                lastModifiedAgo: updatedUser.lastModifiedAgo
            })
        }
    }
}))

server > src > routes > users.js 파일에서 사용자정보 변경 API를 위와 같이 수정한다. 

virtual 필드 조회하기

방금 사용자정보를 업데이트하였기 때문에 lastModifiedAgo 필드는 큰 의미가 없다. 하지만 사용자정보 변경시에는 방금 회원가입을 한 것은 아니기 때문에 createdAgo 필드는 조회할 가치가 있다. 또한, isAdmin 필드를 true 로 변경하였기 때문에 status 필드도 조회할 가치가 있다. 

 

* virtual 필드를 보여주는 다른 방법

기본적으로 virtual 필드는 res.json()을 이용하여 JSON 문자열로 보낼때 제외하고 전송된다. virtual 필드도 함께 JSON문자열로 전송하기 위해서는 아래와 같이 설정하면 된다. 

const opts = { toJSON: { virtuals: true } }

server > src > models > User.js 파일 상단에 위와 같은 코드를 추가한다. 이는 JSON문자열로 전송할때 virtual 필드도 함께 전송하도록 하는 설정이다. 

const userSchema = new Schema({
    name: {
        type: String,
        required: true, 
    },
    email: {
        type: String,
        required: true, 
        unique: true,  // unique: 색인(primary key) email은 중복불가
    },
    userId: {
        type: String, 
        required: true, 
    },
    password: {
        type: String, 
        required: true, 
    },
    isAdmin: {
        type: Boolean,
        default: false, 
    },
    createdAt: {
        type: Date, 
        default: Date.now, 
    },
    lastModifiedAt: {
        type: Date, 
        default: Date.now, 
    }
}, opts)

해당 옵션을 실제로 적용하기 위해서는 Schema 생성자 함수의 두번째 인자로 opts 설정을 추가하면 된다. 

router.post('/login', limitUsage, [
    validateUserEmail(),
    validateUserPassword()
], expressAsyncHandler( async (req, res, next) => {
    const errors = validationResult(req)
    if(!errors.isEmpty()){
        res.status(400).json({
            code: 400,
            message: 'Invalid Form data for user',
            error: errors.array()
        })
    }else{
        console.log(req.body)
        const loginUser = await User.findOne({
            email: req.body.email, 
            password: req.body.password 
        })
        if(!loginUser){
            console.log(loginUser)
            res.status(401).json({ code: 401, message: 'Invalid Email or Password'})
        }else{
            const { name, email, userId, isAdmin, createdAt } = loginUser
            res.json({
                code: 200,
                token: generateToken(loginUser),
                loginUser
            })
        }
    }
}))

 

로그인 API에서는 loginUser 를 통째로 전달한다. 

사용자 로그인
virtual 필드가 추가로 전송된 화면

 

* populate 로 ref 의 도큐먼트를 조회할때 virtual 필드도 함께 조회하기

 // isAuth : 전체 할일목록을 조회할 권한이 있는지 검사하는 미들웨어 
  router.get('/', isAuth, expressAsyncHandler(async (req, res, next) => {
    const todos = await Todo.find({ author: req.user._id }).populate('author', ['name', 'email', 'userId', 'createdAt', 'lastModifiedAt']) // req.user 는 isAuth 에서 전달된 값
    if(todos.length === 0){
      res.status(404).json({ code: 404, message: 'Fail to find todos !'})
    }else{
      res.json({ code: 200, todos: todos.map(todo => {
        return {...todo._doc, 
        	  createdAgo: todo.createdAgo, 
              lastModifiedAgo: todo.lastModifiedAgo,
              finishedAgo: todo.finishedAgo, 
              status: todo.status,
              author: {...todo.author._doc, 
                        createdAgo: todo.author.createdAgo, 
                        lastModifiedAgo: todo.author.lastModifiedAgo, 
                        status: todo.author.status
                      }
              }
      }) })
    }
  }))

Todo 모델의 author 필드를 populate 하면 todo.author 에는 할일을 작성한 사용자의 전체정보를 조회할 수 있다. 그러므로 당연히 virtual 필드도 조회가 가능하다.  

728x90