z.string() min, max 메서드 구현하기
- #1: z.string() 구현하기
- #3: z.string() min, max 메서드 구현하기
- #3: z.enum(), z.optional() 구현하기
- #4: z.object() 구현하기
Zod를 활용하다 보면 string
인 것을 확인하는 것뿐만 아니라 최소 길이, 최대 길이와 같이 추가적인 검증을 하고 싶은 경우가 있다.
오늘은 지난 글에서 구현한 ZodString
클래스에 검증 로직을 추가해 주는 메서드를 구현해 보자.
min, max 메서드
다음과 같이 min
또는 max
메서드를 사용하면 입력 값이 string
인지 확인 후
길이까지 추가 검증을 해준다.
import { z } from 'zod';
const minFive = z.string().min(5, "5글자 이상이어야 합니다.");
minFive.parse(input); // input.length < 5 => 에러 발생
메서드 체이닝을 통해 두 가지 이상을 검증을 추가하는 것도 가능하다.
import { z } from 'zod';
const minFiveMaxTen = z
.string()
.min(5, "5글자 이상이어야 합니다.")
.max(10, "10글자 이하여야 합니다.");
minFiveMaxTen.parse(input); // input.length < 5 또는 input.length > 10 => 에러 발생
구현해보기
이제 ZodString
을 보면서 어떠한 로직을 추가해야 할지 생각해보자.
class ZodString extends ZodType<string> {
_parse(
data: unknown
): { isValid: true; data: string } | { isValid: false; reason?: string } {
if (typeof data === "string") {
return {
isValid: true,
data,
};
} else {
return {
isValid: false,
reason: `${data}는 string이 아닙니다.`,
};
}
}
}
min
, max
메서드를 호출하면 어떠한 검증이 추가로 필요한지를 변수(checks
라고 하자)에 저장하고 실제 검증 로직은 _parse
메서드에서 해당 변수를 읽어서 구현하면 될 것 같다.
예시에서 봤던 것처럼 여러 검증 로직을 추가할 수 있기 때문에 이를 배열로 관리하면 좋을 것이다.
또한 이러한 검증은 string
에만 존재하기 때문에 변수는 ZodString
클래스 내부에 있어야 한다.
class ZodString extends ZodType<string> {
checks: unknown[]
_parse(data: unknown){
// this.checks 반복문을 통해 추가 검증 로직 구현
}
}
min
, max
의 경우 숫자와 메시지를 함께 제공하면 _parse
에서 검증 로직을 구현할 수 있으므로
다음과 같이 타입을 작성할 수 있을 것이다.
class ZodString extends ZodType<string> {
checks: (
| { kind: "min"; value: number; message?: string }
| { kind: "max"; value: number; message?: string }
)[];
_parse(data: unknown) {
// this.checks 반복문을 통해 추가 검증 로직 구현
}
}
이제 this.checks
에 접근하여 추가 검증 로직을 구현해보자.
class ZodString extends ZodType<string> {
checks: (
| { kind: "min"; value: number; message?: string }
| { kind: "max"; value: number; message?: string }
)[];
_parse(
data: unknown
):
| { isValid: false; reason?: string | undefined }
| { isValid: true; data: string } {
// string이 아니면 더 이상 확인할 필요도 없다.
if (typeof data !== "string") {
return {
isValid: false,
reason: `${data}는 string이 아닙니다.`,
};
}
// 배열을 순회하며 추가 검증을 시도한다.
for (const check of this.checks) {
if (check.kind === "min") {
if (data.length < check.value) {
return {
isValid: false,
reason: check.message,
};
}
} else if (check.kind === "max") {
if (data.length > check.value) {
return {
isValid: false,
reason: check.message,
};
}
}
}
// 위에서 어떠한 케이스에도 걸리지 않았다면 유효한 string이다.
return {
isValid: true,
data,
};
}
}
이제 min
, max
메서드를 구현해 보자.
각각의 메서드가 호출되면 checks
에 무엇이 확인되어야 할지 추가되고
이 checks
를 기반으로 새로운 ZodString
이 생성된다.
type ZodStringCheck =
| { kind: "min"; value: number; message?: string }
| { kind: "max"; value: number; message?: string };
class ZodString extends ZodType<string> {
checks: ZodStringCheck[];
constructor(checks: ZodStringCheck[]) {
super();
this.checks = checks;
}
// _parsed 생략
min(value: number, message?: string) {
// 검증 로직을 this.checks에 추가하고
this.checks.push({ kind: "min", value, message });
// 다시 새로운 ZodString 인스턴스를 생성해 메서드 체이닝이 가능하도록 구현한다.
return new ZodString(this.checks);
}
max(value: number, message?: string) {
this.checks.push({ kind: "max", value, message });
return new ZodString(this.checks);
}
}
이렇게 만들고 확인을 위해 스키마를 만들려고 하면 이전과 다르게 타입 에러가 발생한다.
const mySchema = new ZodString(); // Expected 1 arguments, but got 0.
이유는 생성자에서 checks
를 받도록 하였고 이는 반드시 입력되어야 하도록 했기 때문이다.
세부 구현 사항을 몰라도 편하게 인스턴스를 생성할 수 있도록 create
라는 static 메서드를 생성해 보자.
class ZodString extends ZodType<string> {
// 생략
static create() {
return new ZodString([]);
}
}
const mySchema = ZodString.create(); // z.string()과 동일
이러면 기대했던 대로 길이까지 추가 검증을 할 수 있고
const mySchema = ZodString.create().min(5, "5글자 이상이어야 합니다.");
const result = mySchema.safeParse("hell");
// {
// success: false,
// error: Error: 5글자 이상이어야 합니다. ...
// }
메서드 체이닝까지 할 수 있게 되었다.
const mySchema = ZodString.create()
.min(5, "5글자 이상이어야 합니다.")
.max(10, "10글자 이하여야 합니다.");
const result = mySchema.safeParse("helloworld!");
// {
// success: false,
// error: Error: 10글자 이하여야 합니다. ...
// }
결론
오늘은 Zod의 메서드가 어떻게 동작하는지 ZodString
클래스에 min
, max
를 추가하면서 알아보았다.
2가지만 구현하였지만 나머지 비슷한 메서드들도 이와 같이 동작하기 때문에 동작 원리를 이해하기에는 충분할 것이다.