웹 풀사이클 데브코스 TIL [Day 26] - 유효성 검사(Validation) 및 리팩토링

JaeKyung Hwang·2023년 12월 22일
0
post-thumbnail

2023.12.22(금)

✔️유효성 검사(validation)

사용자가 입력한 데이터가 특정 규칙에 맞게 입력되었는지 그 유효성, 타당성을 검증하는 것

💻CODE

어제 코드에 유효성 검사 추가 & 리팩토링

  • 폴더 구조
    .
    │  app.js
    │  mariadb.js
    │  package-lock.json
    │  package.json
    │
    ├─node_modules
    │
    └─routes
            channels.js
            users.js
  • mariadb.js
    // get the client
    const mysql = require('mysql2')
    
    // create the connection to database
    const connection = mysql.createConnection({
        // host: 'localhost',
        // port: 3306,
        user: 'root',
        password: 'root',
        // timezone: 'Asia/Seoul',
        database: 'Youtube',
        dateStrings: true
    })
    
    module.exports = connection;
  • app.js (변경 없음)
    const express = require('express')  // npm install express
    const app = express()
    const port = 8888
    app.listen(port, () => console.log(`> Server is running on http://localhost:${port}/`))
    
    const userRouter = require('./routes/users')
    const channelRouter = require('./routes/channels')
    
    app.use(express.json())
    app.use('/', userRouter)
    app.use('/channels', channelRouter)
  • routes/users.js
    const express = require('express')
    const router = express.Router()
    const conn = require('../mariadb')
    const { body, param, validationResult } = require('express-validator')
    
    const validate = (req, res, next) => {
        const err = validationResult(req)
        if (err.isEmpty()) {
            next()
        } else {
            return res.status(400).json(err.array())
        }
    }
    
    // 로그인
    router.post(
        '/login',
        [
            body('email').notEmpty().isEmail().withMessage('이메일 확인 필요'),
            body('password').notEmpty().isString().withMessage('비밀번호 확인 필요'),
            validate
        ],
        (req, res) => {
            const { email, password } = req.body
            let sql = 'SELECT * FROM `users` WHERE email = ?'
            conn.query(
                sql, email,
                function (err, results) {
                    if (err) {
                        return res.status(400).json(err);
                    }
    
                    const loginUser = results[0]
                    if (loginUser && loginUser.password == password) {
                        res.status(200).json({ message: `${loginUser.name}님 환영합니다.` })
                    } else {
                        res.status(403).json({ message: "이메일 또는 비밀번호가 틀렸습니다." })
                    }
                }
            )
        })
    
    // 회원가입
    router.post(
        '/join',
        [
            body('email').notEmpty().isEmail().withMessage('이메일 확인 필요'),
            body('password').notEmpty().isString().withMessage('비밀번호 확인 필요'),
            body('name').notEmpty().isString().withMessage('이름 확인 필요'),
            body('contact').notEmpty().isString().withMessage('연락처 확인 필요'),
            validate
        ],
        (req, res) => {
            const { email, name, password, contact } = req.body
            let sql = 'INSERT INTO `users` (email, name, password, contact) VALUES (?, ?, ?, ?)'
            let values = [email, name, password, contact]
            conn.query(
                sql, values,
                function (err, results) {
                    if (err) {
                        return res.status(400).json(err);
                    }
    
                    res.status(201).json({ message: `${name}님 환영합니다.`, results })
                }
            )
        })
    
    router.route('/users')
        .get(   // 회원 개별 조회
            [
                body('email').notEmpty().isEmail().withMessage('이메일 확인 필요'),
                validate
            ],
            (req, res) => {
                let { email } = req.body
                let sql = 'SELECT * FROM `users` WHERE email = ?'
                conn.query(
                    sql, email,
                    function (err, results) {
                        if (err) {
                            return res.status(400).json(err);
                        }
    
                        if (results.length) {
                            res.status(200).json(results)
                        } else {
                            res.status(404).json({ message: "존재하지 않는 회원입니다." })
                        }
                    }
                )
            })
        .delete(    // 회원 개별 탈퇴
            [
                body('email').notEmpty().isEmail().withMessage('이메일 확인 필요'),
                validate
            ],
            (req, res) => {
                let { email } = req.body
                let sql = 'DELETE FROM `users` WHERE email = ?'
                conn.query(
                    sql, email,
                    function (err, results) {
                        if (err) {
                            return res.status(400).json(err);
                        }
    
                        if (results.affectedRows != 0) {
                            res.status(200).json({ message: `탈퇴되었습니다.`, results })
                        } else {
                            res.status(404).json({ message: "존재하지 않는 회원입니다." })
                        }
                    }
                )
            })
    
    module.exports = router
  • routes/channels.js
    const express = require('express')
    const router = express.Router()
    const conn = require('../mariadb')
    const { body, param, validationResult } = require('express-validator')
    
    const channelNotFound = (res) => res.status(404).json({ message: "채널이 존재하지 않습니다." })
    const validate = (req, res, next) => {
        const err = validationResult(req)
        if (err.isEmpty()) {
            next()
        } else {
            return res.status(400).json(err.array())
        }
    }
    
    router.route('/')
        .get(   // 채널 전체 조회
            [
                body('userId').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
                validate
            ],
            (req, res) => {
                const { userId } = req.body
    
                let sql = 'SELECT * FROM `channels` WHERE user_id = ?'
                conn.query(
                    sql, userId,
                    function (err, results) {
                        if (err) {
                            return res.status(400).json(err);
                        }
    
                        if (results.length) {
                            res.status(200).json(results)
                        } else {
                            channelNotFound(res)
                        }
                    }
                )
            })
        .post(  // 채널 개별 생성
            [
                body('userId').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
                body('name').notEmpty().isString().withMessage('문자를 입력해주세요.'),
                validate
            ],
            (req, res) => {
                const { name, userId } = req.body
    
                let sql = 'INSERT INTO `channels` (name, user_id) VALUES (?, ?)'
                let values = [name, userId]
                conn.query(
                    sql, values,
                    function (err, results) {
                        if (err) {
                            return res.status(400).json(err);
                        }
    
                        res.status(201).json({ message: `${name} 채널을 응원합니다.`, results })
                    }
                )
            })
    
    router.route('/:id')
        .get(   // 채널 개별 조회
            [
                param('id').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
                validate
            ],
            (req, res) => {
                let { id } = req.params
                id = parseInt(id)
    
                let sql = 'SELECT * FROM `channels` WHERE id = ?'
                conn.query(
                    sql, id,
                    function (err, results) {
                        if (err) {
                            return res.status(400).json(err);
                        }
    
                        if (results.length) {
                            res.status(200).json(results)
                        } else {
                            channelNotFound(res)
                        }
                    }
                )
            })
        .put(   // 채널 개별 수정
            [
                param('id').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
                body('name').notEmpty().isString().withMessage('채널명 오류'),
                validate
            ],
            (req, res) => {
                let { id } = req.params
                id = parseInt(id)
                let { name } = req.body
    
                let sql = 'UPDATE `channels` SET name = ? WHERE id = ?'
                let values = [name, id]
                conn.query(
                    sql, values,
                    function (err, results) {
                        if (err) {
                            return res.status(400).json(err);
                        }
    
                        if (results.affectedRows != 0) {
                            res.status(200).json({ message: `채널명이 ${name}(으)로 성공적으로 수정되었습니다.`, results })
                        } else {
                            channelNotFound(res)
                        }
                    }
                )
            })
        .delete(    // 채널 개별 삭제
            [
                param('id').notEmpty().isInt().withMessage('숫자를 입력해주세요.'),
                validate
            ],
            (req, res) => {
                let { id } = req.params
                id = parseInt(id)
    
                let sql = 'DELETE FROM `channels` WHERE id = ?'
                conn.query(
                    sql, id,
                    function (err, results) {
                        if (err) {
                            return res.status(400).json(err);
                        }
                        if (results.affectedRows != 0) {
                            res.status(200).json({ message: `채널이 삭제되었습니다.`, results })
                        } else {
                            channelNotFound(res)
                        }
                    }
                )
            })
    
    module.exports = router

🌉Middleware

❓ middleware?

  • 요청과 응답 중간(사이)에 위치하여 middleware라고 불림
  • router와 error handler 또한 middleware의 일종이며, 요청과 응답 중간에서 특정 작업을 수행하기 위해 사용됨

📍middleware의 특징

  1. 순차적 실행 (등록된 순서대로 실행)
    • middleware를 등록한 순서(코드 상에서 순서)에 따라 어떤 middleware가 먼저 실행될지 결정됨
  2. 재사용성과 모듈화
    • 모듈로서 구현할 수 있기 때문에, 특정 기능을 담당하는 미들웨어를 여러 부분에서 재사용 가능
  3. next() 함수
    • next라는 method를 호출하여 stack에 있는 다음 middleware로 제어를 넘길 수 있음
    • 이를 통해 여러 middleware가 체인 형태로 연결되어 실행됨 ※ 현재 middleware 함수가 request-response cycle을 끝내지 않으면 무조건 next()를 호출해야하며 그렇지 않으면 request가 hanging됨
    • 제어흐름 벗어나기!
      설명요약
      next('route')호출 시 router middleware stack에서 나머지 middleware 함수들을 skip
      ※ app.METHOD() 또는 router.METHOD() 함수를 사용하여 load된 middleware 함수에서만 작동!
      middleware sub-stack 밖으로 탈출
      = 다음 middleware가 아니라 router로 이동
      next('router')호출 시 router의 나머지 middleware 함수들을 skip하고 router 인스턴스 밖으로 control 전달express.Router()로 만든 router 인스턴스 밖으로 탈출
    • Express 5부터 Promise를 반환하는 middleware 함수는 거절(reject)하거나 error가 발생했을 때 next(value)를 호출함
      • next는 rejected value나 thrown error와 함께 호출됨
      • 문자열 'route''router'를 제외한 아무 값을 next() 함수에 넘기면 Express는 현재 request를 error로 간주하고 나머지 non-error handling routing과 middleware 함수들을 생략함! ⇒ 바로 가장 가까운 error handling middleware로 넘어감
    • 예시
      • Middleware function myLogger
        • 매 request마다 myLogger에 의해 console에 “LOGGED”가 출력되고 next()로 다음 middleware인 app.get을 실행시킴

          const express = require('express')
          const app = express()
          
          const myLogger = function (req, res, next) {
            console.log('LOGGED')
            next()
          }
          
          app.use(myLogger)
          
          app.get('/', (req, res) => {
            res.send('Hello World!')
          })
          
          app.listen(3000)
      • Middleware function requestTime
        • 매 request마다 req 객체에 timestamp 값을 가진 requestTime property를 만들고 next()로 다음 middleware인 app.get을 실행시킴

        • app.get에서는 requestTime middleware에서 만든 req.requestTime property에서 timestamp를 가져와 출력함

          객체에 property를 추가해주면 이후 다른 middleware에서 사용 가능

          const express = require('express')
          const app = express()
          
          const requestTime = function (req, res, next) {
            req.requestTime = Date.now()
            next()
          }
          
          app.use(requestTime)
          
          app.get('/', (req, res) => {
            let responseText = 'Hello World!<br>'
            responseText += `<small>Requested at: ${req.requestTime}</small>`
            res.send(responseText)
          })
          
          app.listen(3000)
      • Middleware function validateCookies
        • cookie-parser middleware를 사용해 req 객체에서 들어오는 cookie를 parse하여 cookieValidator 함수에 전달

        • validateCookies middlware는 Promise를 반환

          • 성공 시 next()로 다음 middleware 실행 (코드에는 없기 때문에 request hanging됨)
          • 실패 시 next()로 rejected value나 thrown error 값을 넘김 → error handler middleware 실행
          async function cookieValidator (cookies) {
            try {
              await externallyValidateCookie(cookies.testCookie)
            } catch {
              throw new Error('Invalid cookies')
            }
          }
          const express = require('express')
          const cookieParser = require('cookie-parser')
          const cookieValidator = require('./cookieValidator')
          
          const app = express()
          
          async function validateCookies (req, res, next) {    // return Promise
            await cookieValidator(req.cookies)
            next()
          }
          
          app.use(cookieParser())
          
          app.use(validateCookies)
          
          // error handler
          app.use((err, req, res, next) => {
            res.status(400).send(err.message)
          })
          
          app.listen(3000)
      • Configurable middleware
        • options 객체나 다른 매개변수들을 허용하는 함수를 만들어야 하는 경우 다음과 같이 함수를 반환하는 함수 작성! (함수형 프로그래밍, 고차 함수)

          // my-middleware.js 파일
          
          module.exports = function (options) {
              return function (req, res, next) {
                  // Implement the middleware function based on the options object
                  next()
              }
          }
          
          // 또는
          
          module.exports = (options) => (req, res, next) => {
              // Implement the middleware function based on the options object
              next()
          }
          const mw = require('./my-middleware.js')
          
          app.use(mw({ option1: '1', option2: '2' }))

📍middleware의 유형

  • Application-level middleware 🔗
    • Express로 만든 web application에 전역으로 적용되는 middleware
    • app.use()app.METHOD() 함수를 사용해서 middleware를 app 객체 인스턴스에 바인딩
      사용 방법설명
      app.use(middleware)모든 요청에서 해당 middleware를 사용
      app.use('/path', middleware)특정 경로로 접근할 때 middleware를 사용
      app.METHOD('/path', middleware)특정 HTTP 메소드에 특정 경로로 접근할 때 middleware를 사용
  • Router-level middleware 🔗
    • 특정 경로로 요청이 들어오면, 실행되는 라우터 middleware
    • express.Router()로 만든 라우터 인스턴스에 바인딩
    • 애플리케이션 레벨 middleware와 같은 방식(router.use()router.METHOD())으로 middleware를 등록하고 사용 가능
  • Error-handling middleware 🔗
    • middleware나 router에서 에러가 발생하면 실행되는 middleware
    • error handling과 관련된 로직을 수행하여 사용자에게 error page 제공 가능
    • 반드시 4개의 인자(err, req, res, next) 사용 (하나라도 빠지면 일반 middleware로 해석됨)
      app.use(function(err, req, res, next) {
        console.error(err.stack);
        res.status(500).send('Something broke!');
      });
  • Built-in middleware 🔗
    • Express 자체에서 제공하는 기본적인 middleware로, application에서 기본적인 기능을 수행
    • 버전 4.x부터 Express는 더 이상 Connect에 의존하지 않고 이전에 Express에 포함되었던 middleware 함수들은 이제 별도의 모듈에 있음 🔗
    • 현재 Express의 built-in middleware 함수들은 다음 6가지임 🔗
      • express.json() (버전 4.16.0+)
      • express.raw() (버전 4.17.0+)
      • express.Router()
      • express.static()
      • express.text() (버전 4.17.0+)
      • express.urlencoded() (버전 4.16.0+)
  • Third-party middleware 🔗
    • Express 외부에서 개발자들이 만든 후 제공하는 middleware package 🔗
    • 추가적인 기능을 제공하거나 특정 작업을 수행하기 위해 사용됨

다음 주에 배울 할 JWT 실습이 기대된다.

profile
이것저것 관심 많은 개발자👩‍💻

0개의 댓글