03. superstruct을 사용한 유효성 검사
superstruct는 NestJs의 class-validator처럼 요청의 유효성을 검사해주는 라이브러리이다. 이 라이브러리는 타입스크립트와 비슷하게 string, number, object 이외에도 다양한 기본 타입을 지원하며, 추가 검증에 필요한 Refinements나 Utility 타입을 제공하고 있다. 더 자세한 내용은 공식 문서에서 확인할 수 있다. 사실 나도 쓰는 것만 쓰는 탓에 superstruct의 방대한 기능을 다 사용하고 있지는 못하다.
기본 구조
기본적인 코드 형태는 아래와 같다. object 메소드 안에 객체를 넣어두었는데, 이 객체에는 유효성을 검증하고 싶은 필드와 그 필드에 적용할 검증 방식을 정의해두고 있다. 여기에서 알 수 있는 사실은 superstruct의 object 메소드는 인자로 받은 객체의 key값으로 반복문을 돌고, 그에 해당하는 필드만을 정해진 방식으로 처리할 수 있다는 것이다. 따라서 아래의 코드에서 CreateProduct는 key가 name, description, price, tags, images 인 프로퍼티만 검사할 수 있으며, 만약 비교하려는 객체에 id라는 값이 있어도 예외를 던지거나 하지 않는다.
// products.structs.ts
export const CreateProduct = object({
name: string(),
description: string(),
price: min(number(), 0),
tags: nullable(array(string())),
images: nullable(array(string())),
});
export const PatchProduct = partial(CreateProduct);
superstruct에서는 이렇게 만들어진 비교용 객체를 내부적으로 struct라고 하는 듯하다. omit pick partial 등의 유틸리티 타입을 사용하면 이미 만들어진 struct로 부터 새로운 struct를 만들어낼 수도 있다.
assert와 create
이렇게 만들어진 struct는 superstruct에서 제공하는 assert 함수를 통해 특정 값이 유효한지를 검사하게 된다. assert 함수는 첫번째 인자로 유효성을 검사하고 싶은 값을 받고, 두 번째 인자로 struct를 받는다. 앞서 살펴본 것처럼 struct는 있어야 할 게 제대로 들어있는지만 확인한다. 따라서 요청으로부터 각각의 값을 분리할 필요 없이 req.body를 통채로 넣어줘도 문제 없이 동작한다.
만약 비교하려는 값이 struct에 정의된 타입들과 맞지 않는다면 assert 함수는 예외를 던질 것이다. 그런데 유효성 검사를 통과한다면 struct는 아무런 값도 리턴하지 않는다. 따라서 검사 후 '구조에 맞는' 새로운 값을 리턴받고 싶다면 assert 대신 create를 사용하면 된다.
// assert를 쓰면 두 줄이 필요한데
assert(req.body, CreateProduct);
const { ...boardField } = req.body;
// create으로는 한 줄이면 충분하다
const boardField = create(req.body, CreateProduct);
create를 사용함으로써 얻을 수 있는 이점 중 하나는 defaulted 메소드를 사용해 필드에 기본값을 정할 수 있다는 것이다. 비교하려는 값이 존재하지 않는다면, create는 기본값을 포함하는 새로운 값을 리턴해준다.
custom refinement
superstruct는 타입 뿐만 아니라 값의 범위 등을 제한할 수 있도록 refinement 타입을 제공한다. 또한, 제공되는 타입 외에도 원하는 방식으로 값을 검증할 수 있도록 refine 메소드를 제공하고 있다. refine 메소드는 '값의 타입', '구조체의 이름', '검증 함수'를 인자로 받는다. 이 검증 함수는 boolean을 리턴해야 하며, 이를 통해 특정 값이 특정 패턴이나 특정 문자를 포함하는지 등을 원하는 대로 확인할 수 있다.
const email = refine(string(), 'email', (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
});
const password = refine(string(), 'password', (value) => {
const hasLetter = /[a-zA-Z]/.test(value);
const hasNumber = /[0-9]/.test(value);
return hasLetter && hasNumber;
});
export const SignIn = object({
email,
password,
});
블로그의 정보
Ayden's journal
Beard Weard Ayden