7

z.string() min, max 메서드 구현하기

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가지만 구현하였지만 나머지 비슷한 메서드들도 이와 같이 동작하기 때문에 동작 원리를 이해하기에는 충분할 것이다.