[Typescript] Zod에서 Superstruct로의 전환기
서론
이번 포스팅에선, 필자가 CHINGOO.BE 프로젝트(GitHub repo.)를 진행하면서 여러가지 최적화들을 시도하다가 번들 사이즈 관련 최적화의 일환으로 기존 프로젝트에서 사용중이던 zod 라이브러리를 superstruct로 전환하게 된 과정을 소개한다.
Zod를 다른 라이브러리로 전환하려는 이유
Zod에서 다른 라이브러리로의 전환을 통해 달성하려던 목표는 "번들 사이즈" 절감이었다.
CHINGOO.BE는 Next.js 기반의 프로젝트라서 next-bundle-analyzer를 돌려보았었다. 여러가지 눈에 띄는 큼직큼직한 패키지들이 보인다. react-dom에나 next관련 패키지는 대체 가능한 패키지가 없으니 건너뛰고, 그 다음으로 가장 눈에 띄는 패키지는 위 이미지에서 빨간 박스가 쳐진 Zod 패키지였다.
Zod가 제공하는 기능이 많긴하지만... validation 하나를 위해서 혼자 12.37kB를 차지하고 있었다... next.config.js에서 optimizePackageImports를 Zod에 적용했음에도 불구하고, react-hook-form와 hookform/resolver를 합친것보다 보다 Zod의 번들 사이즈가 더 컸다.
그래서 validation 라이브러리는 다양한 종류가 있으니, 좀 더 번들 사이즈가 작고 zod를 통해 이용중이던 기능들을 모두 제공하는 라이브러리를 찾아보게 되었다.
optimizePackageImports에 대한 참고자료:
Zod보다 번들 사이즈가 작은 라이브러리 찾기
Zod보다 번들 사이즈가 작으면서 비슷한 기능을 제공하는 라이브러리를 찾기 위해 BundlePhobia를 이용했다.
zod를 검색해보니 Zod와 비슷한 여러 라이브러리들의 리스트가 나타났다.
그 중, Zod보다 무려 74% 작다는 Superstruct가 있었다.
Zod의 용량과 Superstruct의 용량을 비교해보니
Zod: 13.2kB, Superstruct: 3.4kB 로, 9.8kB 더 작았다. 그래서 필자는 Superstruct를 이용해 Zod를 대체할 수 있는지 검토를 시작했다.
Superstruct, 과연 쓸만한가?
필자가 그동안 많고 많은 Validation 라이브러리들 중, Zod를 이용했던 이유는 다음과 같다.
- Type safety를 제공하는 점 -> https://docs.superstructjs.org/guides/06-using-typescript
- Invalid 타입별 오류 메시지 지정이 간편한 점 -> https://blog.betaman.kr/130
- react-hook-form과의 연계가 간편한 점 -> https://github.com/react-hook-form/resolvers?tab=readme-ov-file#superstruct
- yup과 달리 엄격한 검사 규칙을 제공하는 점 -> https://docs.superstructjs.org/guides/02-validating-data
필자가 Zod를 대체하기 위해선 위 4가지 요소가 충족되어야 하고, 다행히도 대부분 Superstruct는 충족하는 것으로 확인되었다.
다만, 문제가 하나 있었던 것이 Invalid 타입별 오류 메시지 지정이 간편한 점이었다. 기본적으로 Superstruct는 오류 메시지 커스텀이 불가능한 점이 문제가 되었다.
하지만 이 또한 해결책은 있었고, 이를 위한 해결법은 아래 포스팅으로 남겨두었다.
그리고 Zod에 비해 조금 아쉬운 점이 하나 더 있었다면, 비동기를 지원하지 않는다는 것이었다.
하지만 필자는 validation에서 비동기를 이용할 일이 없어서 이 사실이 큰 문제가 되진 않을 것으로 예상했다.
일단 Zod로의 전환 가능성은 확인이 완료됐다.
Zod를 Superstruct로 전환하기
Zod와 Superstruct의 Validation Schema
Zod와 Superstruct의 validation schema를 선언하는 방식이 조금 모양이 다르긴하지만... 사실 크게 다르진 않다.
Superstruct를 사용하기 전에 확인해야할 프로젝트 세팅이 있다.
tsconfig.json에서 strict나 strictNullChecks 옵션이 true로 지정되어있는지 확인해야한다. Superstruct의 optional 함수를 엄격하게 이용하기 위해선 해당 옵션이 활성화 되어야 한다.
Zod는 method chaining을 제공하지만, Superstruct는 이를 제공하진 않는다. 그래서 Zod의 schema와 Superstruct의 schema를 비교하면 다음과 같다.
예제에서 사용된 message 함수는 Superstruct에서 공식적으로 제공하는 함수는 아니다. 이 함수에 대한 정보는 이전 포스팅을 참고하길 바란다.
Zod에서 parse 메서드에 해당하는 함수로 Superstruct는 assert와 validate 함수를 제공한다.
다만 조금 차이점이 있다면, 기존 parse 메서드의 기능을 반반(?) 쪼게서 assert와 validate 함수가 기능을 제공하는게 차이점이다.
assert 함수는 첫번째 인자로 전달된 value값이 두번째 인자로 전달된 schema에 충족하는지 검사하고, 충족하지 않을 경우 Throw를 하는 함수이다. 단, 별도의 return 값은 반환하지 않는다.
validate 함수는 assert와 달리 배열을 반환하는 함수이다. 반환되는 배열의 첫번째 요소는 isInvalid로, 값 검증을 실패했을때 true가 되는 값이다. 그리고 두번째 요소가 validate된 결과값으로, 값 검증을 성공했을때 검증된 값을 넘겨주며 실패했을땐 undefined가 된다. 첫번째 인자로 전달된 value값이 두번째 인자로 전달된 schema에 충족하는지 검사하지만, 충족하지 않을 경우 Throw 대신 return 배열값의 첫번째 인자를 false로 반환한다는 점이 assert와 차이가 있다.
Superstruct와 react-hook-form의 연동
다행히 react-hook-form에서 @hookform/resolvers를 통해 공식적으로 Superstruct의 resolver를 지원하고 있다. Zod를 통해 사용했던 방법과 큰 차이는 없다.
resolver만 @hookform/resolvers/supersturct의 superstructResolver로 교체해주면 바로 Zod+react-hook-form과 동일하게 이용 가능하다.
Superstruct로의 전환 결과
CHINGOO.BE 프로젝트 내 모든 Zod 관련 코드를 Superstruct로 마이그레이션 했다. 다행히 미리 검토했던대로 기능 상 차이점은 발생하지 않았다.
빌드를 해보니 Zod를 사용하던 기존 페이지들의 용량이 13kB씩 절감되었다.
번들 분석 결과를 보아도, 왼쪽 이미지에 큼지막하게 있던 Zod가 사라진 모습도 확인할 수 있다.
개별적인 Zod와 Superstruct의 용량을 비교해보아도
Zod는 Gzip된 용량이 12.37kB였던 반면, Superstruct는 1.8kB이다.