오늘의 기록 😄
오늘은 Toy Project 에서 개발한 Validation Decorator 에 대해 기록하려고 합니다.
저는 대규모 프로젝트에 투입했을 때 공통파트에서 개발한 Validation Decorator 를 사용하여 쉽게 유효성 검증 기능을 만든 좋은 경험이 있습니다.
이 경험을 되살려 값 입력 시 실시간으로 유효성 검증되는 Validation Property Decorator 를 Toy Project 에 적용했습니다.
Decorator
Decorator 는 클래스 선언, 메서드, 접근자, 프로퍼티 또는 매개변수에 첨부할 수 있는 특수한 종류의 선언으로 Java Annotation 과 같은 @express 형식을 가집니다.
만약 Validation 기능을 Property Decorator 개발하여 사용할 경우 아래와 같이 쓰입니다.
@Size(6, 100)
adId = ''
@Size(6, 100)
adPwd = ''
reflect-metadata
JavaScript 에 내장되어 있는 Reflect 에 metadata 기능을 확장한 패키지 입니다. 이 패키지를 사용하면 사용자 정의 metadata 를 추가할 수 있습니다.
구현
구현하기 전에 reflect-metadata, Decorator 를 사용하기 위해 패키지 설치와 tsconfig.json 을 수정해줍니다.
Command Line:
npm install reflect-metadata
tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true, // Decorator 에 대한 실험적 지원 활성화
"emitDecoratorMetadata": true // Decorator 가 있는 선언에 대해 특정 타입의 metadata 를 내보내는 실험적 지원 활성화
}
}
먼저 자주 쓰이는 유효성 검증을 Decorator 로 지정하여 method 로 만듭니다. 저는 Size 라는 method 를 만들어 문자열의 길이를 제한하는 Decorator 를 만들었습니다.
vue-class-component 의 createDecorator 는 사용자 정의 Decorator 를 생성하기 위한 도움 역할을 해줍니다.
// validation.ts
import { createDecorator, VueDecorator } from 'vue-class-component'
/**
* @param min 최소 길이
* @param max 최대 길이
* @param errMsg 에러 메세지 (default: 최소 ${min}자 이상 최대 ${max}자 이하 문자를 입력해주세요.)
*/
export function Size(min: number, max: number, errMsg = `최소 ${min}자 이상 최대 ${max}자 이하 문자를 입력해주세요.`): VueDecorator {
return createDecorator((options, key) => {
console.log('size decorator!')
})
}
위와 같은 코드를 작성하면 아래와 같이 Vue Component 에서 Size 를 import 하여 사용할 수 있습니다.
// Login.vue
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { Size } from '@/common/validation'
@Component
export default class Login extends Vue {
@Size(6, 100)
adId = ''
@Size(6, 100)
adPwd = ''
}
</script>
이제 Decorator 함수 안을 구현해줍니다. 정규식과 에러 메세지가 담겨있는 metadata 를 정의합니다.
// validation.ts
import { createDecorator, VueDecorator } from 'vue-class-component'
const validationsKey = Symbol('validationsKey')
/**
* @param min 최소 길이
* @param max 최대 길이
* @param errMsg 에러 메세지 (default: 최소 ${min}자 이상 최대 ${max}자 이하 문자를 입력해주세요.)
*/
export function Size(min: number, max: number, errMsg = `최소 ${min}자 이상 최대 ${max}자 이하 문자를 입력해주세요.`): VueDecorator {
return createDecorator((options, key) => {
let validtions = Reflect.getMetadata(validationsKey, options) // 객체 또는 속성의 프로토타입 체인에 있는 메타데이터 키의 메타데이터 값을 가져옵니다.
if (!validtions) {
validtions = {}
}
validtions[key] = {
regExp: new RegExp(`^\\s*(?:\\S\\s*){${min},${max}}$`),
errMsg: errMsg
}
Reflect.defineMetadata(validationsKey, validtions, options) // options 에 validations 를 validationsKey 로 메타데이터 정의
console.log(Reflect.getMetadata(validationsKey, options))
})
}
metadata 를 console 로 확인하면 정규식과 에러메세지가 담겨져있는 것을 확인할 수 있습니다.
metadata 에 담겨져 있는 정규식과 에러메세지를 이용하여 Validator Method 를 options 의 methods 에 추가해줍니다.
Validator Method 명칭은 ${propertyKey}Validator 로 만들었습니다.
유효성 검증이 실패하면 ${propertyKey}Rules 배열에 접근하여 값을 push 합니다.
// validation.ts
import { createDecorator, VueDecorator } from 'vue-class-component'
const validationsKey = Symbol('validationsKey')
/**
* @param min 최소 길이
* @param max 최대 길이
* @param errMsg 에러 메세지 (default: 최소 ${min}자 이상 최대 ${max}자 이하 문자를 입력해주세요.)
*/
export function Size(min: number, max: number, errMsg = `최소 ${min}자 이상 최대 ${max}자 이하 문자를 입력해주세요.`): VueDecorator {
return createDecorator((options, key) => {
let validtions = Reflect.getMetadata(validationsKey, options) // 객체 또는 속성의 프로토타입 체인에 있는 메타데이터 키의 메타데이터 값을 가져옵니다.
if (!validtions) {
validtions = {}
}
validtions[key] = {
regExp: new RegExp(`^\\s*(?:\\S\\s*){${min},${max}}$`),
errMsg: errMsg
}
Reflect.defineMetadata(validationsKey, validtions, options) // options 에 validations 를 validationsKey 로 메타데이터 정의
createValidator(options)
})
}
// methods 객체에 validator 등록
function createValidator(options: any) {
const metadata = Reflect.getMetadata(validationsKey, options)
const keys = Object.keys(metadata)
const currentKey = keys[keys.length - 1]
options.methods[`${currentKey}Validator`] = function validator(value: any) {
const validtions = metadata[currentKey]
if (!validtions.regExp.test(value) && value) {
this.$data[`${currentKey}Rules`].push(false || validtions.errMsg)
} else {
this.$data[`${currentKey}Rules`] = []
}
}
console.log(options)
}
options 를 확인한 결과 methods 에 등록된 걸 확인할 수 있습니다.
이제 마지막으로 실시간 값 감지를 위한 watch 를 options 에 추가합니다.
// validation.ts
import { createDecorator, VueDecorator } from 'vue-class-component'
const validationsKey = Symbol('validationsKey')
/**
* @param min 최소 길이
* @param max 최대 길이
* @param errMsg 에러 메세지 (default: 최소 ${min}자 이상 최대 ${max}자 이하 문자를 입력해주세요.)
*/
export function Size(min: number, max: number, errMsg = `최소 ${min}자 이상 최대 ${max}자 이하 문자를 입력해주세요.`): VueDecorator {
return createDecorator((options, key) => {
let validtions = Reflect.getMetadata(validationsKey, options) // 객체 또는 속성의 프로토타입 체인에 있는 메타데이터 키의 메타데이터 값을 가져옵니다.
if (!validtions) {
validtions = {}
}
validtions[key] = {
regExp: new RegExp(`^\\s*(?:\\S\\s*){${min},${max}}$`),
errMsg: errMsg
}
Reflect.defineMetadata(validationsKey, validtions, options) // options 에 validations 를 validationsKey 로 메타데이터 정의
createValidator(options)
})
}
// methods 객체에 validator 등록
function createValidator(options: any) {
const metadata = Reflect.getMetadata(validationsKey, options)
const keys = Object.keys(metadata)
const currentKey = keys[keys.length - 1]
options.methods[`${currentKey}Validator`] = function validator(value: any) {
const validtions = metadata[currentKey]
if (!validtions.regExp.test(value) && value) {
this.$data[`${currentKey}Rules`].push(false || validtions.errMsg)
} else {
this.$data[`${currentKey}Rules`] = []
}
}
createWatch(options)
}
// watch 등록
function createWatch(options: any) {
const metadata = Reflect.getMetadata(validationsKey, options)
const keys = Object.keys(metadata)
const currentKey = keys[keys.length - 1]
if (!options.watch) {
options.watch = {}
}
options.watch[currentKey] = [
{
handler: `${currentKey}Validator`,
deep: false,
immediate: false,
user: true
}
]
console.log(options)
}
options 에 watch 가 추가된 것을 확인할 수 있습니다. 그리고 실시간으로 값 유효성을 체크하는 것을 확인할 수 있습니다.
여러 가지 Validation Decorator 를 위해 createValidationDecorator 함수를 만들었습니다.
// validation.ts
import { createDecorator, VueDecorator } from 'vue-class-component'
import 'reflect-metadata'
const validationsKey = Symbol('validationsKey')
// decorator 생성
function createValidationDecorator(regExp: RegExp, errMsg: string): VueDecorator {
return createDecorator((options, key) => {
let validtions = Reflect.getMetadata(validationsKey, options)
if (!validtions) {
validtions = {}
}
validtions[key] = {
regExp: regExp,
errMsg: errMsg
}
Reflect.defineMetadata(validationsKey, validtions, options)
createValidator(options)
})
}
// methods 객체에 validator 등록
function createValidator(options: any) {
const metadata = Reflect.getMetadata(validationsKey, options)
const keys = Object.keys(metadata)
const currentKey = keys[keys.length - 1]
options.methods[`${currentKey}Validator`] = function validator(value: any) {
const validtions = metadata[currentKey]
if (!validtions.regExp.test(value) && value) {
this.$data[`${currentKey}Rules`].push(false || validtions.errMsg)
} else {
this.$data[`${currentKey}Rules`] = []
}
}
createWatch(options)
}
// watch 등록
function createWatch(options: any) {
const metadata = Reflect.getMetadata(validationsKey, options)
const keys = Object.keys(metadata)
const currentKey = keys[keys.length - 1]
if (!options.watch) {
options.watch = {}
}
options.watch[currentKey] = [
{
handler: `${currentKey}Validator`,
deep: false,
immediate: false,
user: true
}
]
}
/**
* @param min 최소 길이
* @param max 최대 길이
* @param errMsg 에러 메세지 (default: 최소 ${min}자 이상 최대 ${max}자 이하 문자를 입력해주세요.)
*/
export function Size(min: number, max: number, errMsg = `최소 ${min}자 이상 최대 ${max}자 이하 문자를 입력해주세요.`): VueDecorator {
return createValidationDecorator(new RegExp(`^\\s*(?:\\S\\s*){${min},${max}}$`), errMsg)
}
/**
* @param errMsg 에러 메세지 (default: '이메일 형식에 맞지 않습니다.')
*/
export function Email(errMsg = '이메일 형식에 맞지 않습니다.'): VueDecorator {
return createValidationDecorator(new RegExp('^[0-9a-zA-Z]([-_\\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\\.]?[0-9a-zA-Z])*\\.[a-zA-Z]{2,3}$'), errMsg)
}
향후 계획
Property Decorator 를 통해 유효성 검증을 만들었지만 유효성 검증할 Property 가 많아질수록 그에 따른 method, watch 가 많아져 그 부분을 보완할 점을 생각해야 할 것 같습니다.
그리고 이러한 @CusmtomValidate(regExp: RegExp, errMsg: string) 직접 정의한 정규식으로 검증할 수 있는 Decorator 를 제공하여 자유도를 높여야 할 것 같습니다.
참고
https://typescript-kr.github.io/pages/decorators.html
https://www.npmjs.com/package/reflect-metadata
https://medium.com/jspoint/introduction-to-reflect-metadata-package-and-its-ecmascript-proposal-8798405d7d88