유동

[Express] 프론트에서 온 값을 검증하는 exception 모듈 만들기 본문

node.js/ExpressJS

[Express] 프론트에서 온 값을 검증하는 exception 모듈 만들기

동 선 2023. 8. 2. 13:52

 

백엔드에서 api를 만들때 db연결, sql등 할게 많겠지만

제일중요한건 프론트엔드에서 온 값들이 유효한지 검증해야하는게 제일 중요하다

  • 만약 로그인 api를 작성한다고 하자, 프론트에서 받는 값은 loginId, password 이 두가지 값을 받을것이다.
    우리는 loginId, password 이 두 값을 들고 db에 조회해볼것이다.

하지만 받아온 두 값이 이상한 값이 들어오면 어떻게될까?

  • 당연히 프론트에서 loginId 정규식, password 정규식 검사해서 넘겨주기때문에 그런 일은 거의 없을것이다.

하지만 백엔드가 항상 염두해두는 부분은 프론트엔드 코드는 조작이 가능하다는 점이다.

  • 만약 유저가 불순한 마음을 품고 프론트의 검증하는 로직을 지운 후 loginId나 password를 100만자, 1000만자 보내버리면?
  • 혹은 db 컬럼 자료형에 맞지 않는 값을 보내버린다면??

당연히 데이터베이스에서 에러를 내뱉어주기때문에 db가 터질일은 웬만하면 없을것이다.
하지만 백엔드에서 한번 더 검증해준다면 데이터의 무결성을 보장하고 db io를 줄여주는것에 일조할수 있을것이다...

로그인 api이기때문에 굳이 정규식 검사는 해주지 않고 그냥 값이 undefined 인지, ""(빈 문자열)인지, null인지, 길이 검사 등 만 해줄것임

if (loginId === undefined || loginId === "" || loginId === null ) {
  // ... throw new Error("...")
}

if (loginId.length < 1 || loginId.length > maxLoginIdLength) {
  // ... throw new Error("...")
}
  • 이렇게 해줄수도 있지만?
  • 프로그램이 커질수록 api개수가 수백개가 될수도 있는데 모든 api 하나하나 이렇게 해주면 지옥이 펼처질 것이다

이렇게 해주면 어떨까?

exception(loginId, "loginId").checkInput().checkLength(1, maxLoginIdLength);
exception(password, "password").checkInput().checkLength(1, maxPwLength);

코드를 보자마자 이해가 간다. input값을 check 해주고(undefined인지, ""인지) 길이가 1부터 maxLoginIdLength 까지인지?

일단 해당 모듈을 보자

const { loginIdRegex, pwRegex, nameRegex, phoneNumberRegex, emailRegex } = require("../module/regex");
const badRequestErrorCode = 400;
const errorMessage = {
    invalidInput: "요청값이 잘못되었습니다",
    length: "길이가 비정상적입니다",
    regex: "정규표현식 실패",
    isNumber: "정수가 아닙니다",
}

function Exception(input, name) {
    this.checkInput = () => {
        if (input === undefined) this.setError(errorMessage.invalidInput);
        return this;
    }

    this.checkLength = (min, max) => {
        if (input.length < min || input.length > max) this.setError(errorMessage.length);
        return this;
    }

    this.checkIdRegex = () => {
        if (!loginIdRegex.test(input)) this.setError(errorMessage.regex);
        return this;
    }

    this.checkPwRegex = () => {
        if (!pwRegex.test(input)) this.setError(errorMessage.regex);
        return this;
    }

    this.checkNameRegex = () => {
        if (!nameRegex.test(input)) this.setError(errorMessage.regex);
        return this;
    }

    this.checkPhoneNumberRegex = () => {
        if (!phoneNumberRegex.test(input)) this.setError(errorMessage.regex);
        return this;
    }

    this.checkEmailRegex = () => {
        if (!emailRegex.test(input)) this.setError(errorMessage.regex);
        return this;
    }

    this.isNumber = () => {
        if (isNaN(Number(input))) this.setError(errorMessage.isNumber);
        return this;
    }

    this.setError = (message) => {
        const error = new Error(`${name}: ${message}`);
        error.status = badRequestErrorCode;
        throw error;
    }
}

const exception = (input, name) => {
    const res = new Exception(input, name);
    return res;
}

module.exports = exception;

이렇게 메서드 체이닝 방식을 사용하기 위해 조건을 만족하면 자기 자신을 리턴, 만족하지 못하면 에러 를 생성, 코드를 지정해주고 에러를 던져주는 거임

 

해당 모듈을 아래처럼 try-catch 안에서 사용해준다면?

router.post("/login", async (req, res, next) => {
    const { loginId, password } = req.body;
    const result = {
        message: "",
        token: null
    };
    let client = null;

    try {
        // request값 유효성 검증
        exception(loginId, "loginId").checkInput().checkLength(1, maxLoginIdLength);
        exception(password, "password").checkInput().checkLength(1, maxPwLength);
        if (loginId === process.env.ADMIN_ID && password === process.env.ADMIN_PW) {
            const token = await jwt.adminSign();
            res.cookie('accessToken', token);
            return res.redirect('/admin');
        }

        // db연결
        client = createClient();
        await client.connect();

        const sql = "SELECT id FROM user_TB WHERE login_id = $1 AND password = $2";
        const params = [loginId, password];
        const data = await client.query(sql, params);

        if (data.rows.length !== 0) {
            const userPk = data.rows[0].id;
            const token = await jwt.userSign(userPk, loginId);
            result.token = token;
        } else {
            result.message = "아이디 또는 비밀번호가 올바르지 않습니다";
        }
        res.send(result);

    } catch (error) {
        console.error(error);
        next(error);

    } finally {
        if (client) {
            await client.end();
        }
    }
});

exception 모듈에서 throw 해주는 에러들은 전부다 400 status code를 가지고 던져주기 때문에 catch부분에서 status가 400인 것들만 잡아내서 프론트에 던져주면 된다.

다른 api도 그냥 exception 가져와서 그냥 값 넣어주기만 하면 된다

// 회원가입 api
router.post("/signup", async (req, res, next) => {
  const { loginId, password, name, phoneNumber, email } = req.body;
  const result = {
    data: "",
    message: "",
  };
  let client = null;

  try {
    exception(loginId, "loginId").checkInput().checkIdRegex();
    exception(password, "password").checkInput().checkPwRegex();
    exception(name, "name").checkInput().checkNameRegex();
    exception(phoneNumber, "phoneNumber").checkInput().checkPhoneNumberRegex();
    exception(email, "email").checkInput().checkEmailRegex();

    ....

postman 테스트 결과

200 ok

 

400 bad request

하지만 아직 끝이 아니다

  • catch부분에서 에러를 잡아서 하나하나 처리해주고있는데 이걸 next()를 이용해 에러를 한곳에다 몰빵처리할것이다
  • express에서 지원해주는 에러처리 미들웨어를 이용해서 바꿔볼거임