본문 바로가기

FrontEnd

[TS]유효성 검증(validation) 대신 구문 분석(Parsing)하세요

반응형
io-ts, Runtypes 및 Zod와 같은 라이브러리를 사용하여
모든 타입의  수신 및 발신 데이터를 안전하게 구문 분석하는 방법을 알아봅시다.

TLDR : 검증과 객체 생성을 동시해 해주며 타입스크립트 타입도 만들어 주는 라이브러리를 사용하자

 
 
우리는 링크 계층에서 약간 떨어져 있지만 결국 이것은 우리가 인터넷에서 읽고 쓰는 데이터를 처리하는 방법에 관한 것입니다.
사용자 입력을 검증하는 것이 모든 웹 프로그래밍의 초석이라는 것은 누구나 알고 있습니다.
사실, 소프트웨어로 들어오는 모든 입출력 데이터는 검증되어야 합니다.
그런데 검증 대신 파싱하라는게 무슨말인가요?(parse, don’t validate)

검증 예시

yup과 같이 인기 있는 자바스크립트 유효성 검사 라이브러리를 살펴보겠습니다.
joi와 같은 다른 인기 있는 라이브러리나 ajv와 같은 JSON 밸리데이션 라이브러리를 대신 사용할 수도 있지만
우리가 주장하는 요점은 여전히 ​​동일합니다.
 
Data validation with yup.
import * as yup from "yup";

let schema = yup.object().shape({
  name: yup.string().required(),
  age: yup.number().required().positive().integer(),
  email: yup.string().email(),
  website: yup.string().url(),
  createdOn: yup.date().default(function () {
    return new Date();
  }),
});

// data from an API, from user, etc. Note that the type would be `any` 
// (or perhaps `unkown`) as we don't really know what it looks like 
// if it comes from outside
const data: any = {
  name: 'jimmy',
  age: 24,
}

// check validity
schema
  .isValid(data)
  .then(function (valid) {
    console.log('isValid?', valid); // => true
    // do something with the data, however, it's still `any`/`unkown`....
  });
 
위의 코드는 데이터의 유효성을 검사하고 스키마에 따라 데이터가 유효한지 확인하는 작업을 잘 수행합니다.
정확한 스키마를 생성할 수 있는 좋은 기회를 제공하며, 사용자 지정 유효성 검사 논리를 잘 지원합니다.
유지 관리가 용이하고 재사용, 확장 등이 쉽습니다.
문제가 뭔가요?
 
24-27행에서 우리는 통과/실패 사례를 쉽게 처리할 수 있고
데이터가 유효한 경우 비즈니스 로직을 계속할 수 있습니다.
(그리고 그렇지 않은 경우 400 오류 또는 이와 유사한 오류를 반환할 수도 있음).
하지만 우리에겐 아직 데이터 타입이 없습니다. 아직 any/unknown 입니다.
물론 typecast할 수 있지만 그것은 문제를 야기합니다.
 
우리는 이제 schema와 type을 따로 손으로 유지해야 하고,
그 두 개의 타입이 실제로 일치하는지 확인할 수 있는 방법은 없습니다.
let userSchema = yup.object().shape({
  name: yup.string().required(),
  age: yup.number().required().positive().integer(),
  email: yup.string().email(),
  website: yup.string().url(),
  createdOn: yup.date().default(function () {
    return new Date();
  }),
});

type UserType = {
  name: string
  age: number
  email: string
  website: string
  createdOn: Date,
}

// check validity
schema
  .isValid(data)
  .then(function (valid) {
    const user = data as UserType // it's valid so let's cast!
  });​
 
누군가 직접 `userSchema`와 `UserType`이 동기화 상태를 유지하는지 확인하고 있나요?
실제로 같나요?
 
스키마와 타입이 항상 일치할 수만 있다면 다음과 같이 validator를 선언할 수 있습니다.
function parseUserData(data: any): UserType | Error.

 
'validator'는 실제로 데이터를 단순히 유효성을 검사하는 것이 아니라 UserType으로 파싱합니다.
(실패할 수 있으므로 오류가 발생할 수 있음).
따라서 일반적으로 파싱은 실제로는 function <T>parse(x: any, schema): T | Error. 형태입니다.


위의 예에서 스키마 타입을 지정하지 않았다는 사실을 눈치채셨을 수도 있습니다.
나중에 다시 설명하겠습니다.
그러나 먼저 파싱으로 전환할 수 있는 방법을 살펴보겠습니다.\
 

그래서 파싱이 뭔가요?

반응형
> Parsing, syntax analysis, or syntactic analysis is the process of analyzing a string of symbols, either in natural languagecomputer languages or data structures, conforming to the rules of a formal grammar.

[https://en.wikipedia.org/wiki/Parsing]
 
핵심은 '분석 프로세스'과 '규칙 준수'입니다.
스키마 & 타입규칙을 준수해야 햐며.
이 경우 형식 문법을 구성하는 것으로 간주될 수 있습니다(무슨 뜻인지 모르더라도 걱정하지 마세요).
분석 프로세스는 데이터를 스키마 및 타입에 맞추려고 할 때 코드에서 수행하는 작업입니다.
우리가 "스키마 & 타입"이라고 말하는 이유는
동기화되거나 동기화되지 않을 수 있는 두 개의 개별 항목 대신
어떤 하나로 동일화 하길 원하기 때문입니다.
 
이 예에서는 특정 구조를 알 수 없는 JavaScript 또는 JSON 개체를 구문 분석합니다.
문자열이나 이진 데이터를 구문 분석하는 것은 또 다른 이야기입니다.
다양한 데이터 포맷을 JSON/JavaScript 객체로 쉽게 변환할 수 있으므로
웹 프로그래밍에서 이는 매우 일반적인 문제입니다.
 

어렵게 파싱 하기

타입 가드 및 타입 알리아스 같은 멋진 TypeScript 기능과 함께 이전 예제의 yup을 사용하여
알려지지 않은 데이터를 UserType으로 구문 분석해 보겠습니다.
이것은 "parse, don't validate" 방법을 사용하는 다소 수동적이고 장황한 방법이지만
이미 원래 솔루션의 몇 가지 문제를 수정했습니다.
 
1) typecast와
2) type과 스키마를 별도로 관리하는 것입니다.
 
주 : 파서와 생성자를 통합함.
// Let's use some custom type aliases for readability
type PositiveInteger = number
type Email = string
type URL = string

// Type guards to validate invidiual values, fields
const isPositiveInteger = (x: any): x is PositiveInteger =>
  yup.number().required().positive().integer().isType(x)
const isEmail = (x: any): x is Email =>
  yup.string().required().email().isType(x)
const isURL = (x: any): x is URL => yup.string().required().url().isType(x)
const isDate = (x: any): x is Date => yup.date().required().isType(x)
const isString = (x: any): x is string => yup.string().required().isType(x)

// UserType again, now with our custom type aliases
type UserType = {
  name: string
  age: PositiveInteger
  email?: Email
  website?: URL
  createdOn?: Date
}

/**
  parse, don't validate
  compiler can help us quite a bit here to make sure the parsing
  is actually correct
 */
const parseToUserType = (x: any): UserType | Error => {
  let { name, age, email, website, createdOn } = x

  if (!isString(name)) return new Error("invalid name")
  if (!isPositiveInteger(age)) return new Error("invalid age")
  email = isEmail(email) ? email : undefined // optional, silently drop invalid values
  website = isURL(website) ? website : undefined // optional, silently drop invalid values
  createdOn = isDate(createdOn) ? createdOn : new Date() // optional, use default if invalid

  return { age, name, createdOn, email, website }
}

// Business logic is pretty awesome now!
function myHandler(): Response {
  const userType: UserType | Error = parseToUserType(data)
  if (userType instanceof Error) return { error: "Ohh there was a 400 error" }
  // use UserType normally, do what ever you want
  return { message: `Welcome, ${userType.name}` }
}
 
무엇이 바뀌었죠?
 

이제 비즈니스 로직에서 모든 데이터를 UserType으로 파싱할 수 있습니다.

  • 오류가 있는지 없는지 확인하고 문제가 없는지 확인합니다.

우리는 문자열이나 숫자보다 더 정확한 타입을 사용하고 있습니다.

  • 타입 별칭이 작동하는 방식으로 인해 타입 가드가 더 화려하더라도 이메일 유형은 여전히 ​​문자열일 뿐입니다. 이에 대해서는 나중에 다시 살펴보겠습니다.

오류가 발생하기 쉽고 위험하며 타입 검사가 잘 되지 않는 코드는 타입 가드로 제한됩니다.

  • 그것들을 잘 테스트하고 특정 유형에 대한 파서를 작성하는 것은 쉽습니다 :)

TS 컴파일러는 파서가 실제로 작동하는지 확인합니다.

  • 우리는 안전하게 our(x: any) => UserType | Error 파서 함수에서 잘 지원되는 오류 파서 기능입니다.

파서와 타입 가드를 작성하는 것은 여전히 ​​힘이 많이 듭니다.

  • 그들은 또한 매우 상용구처럼 보입니다. 분명히 미리 빌드된 타입 가드가 있는 일부 라이브러리와 파서를 구축하는 도구가 있겠죠?

깨달음을 얻은 것처럼 파싱

TypeScript용으로 특별히 제작되었으며
제가 여기에서 소개한 아이디어를 사용하는 다양한 종류의 데이터 유효성 검사 라이브러리를 소개할 때입니다.
Runtypes, Zodio-ts는 데이터를 특정 타입으로 파싱하거나 혹은 실패할 수 있는 유사한 "런타임 타입 유효성 검사기"입니다.
어떤 사람들은 이것을 (역)직렬화라고 부르고, 다른 사람들은 그것을 인코딩/디코딩이라고 부르고, 어떤 사람들은 구문 분석이나 유효성 검사에 대해 이야기합니다.
이름 빼고, 그들이 하는 일은 본질적으로 동일합니다.
runtypes를 살펴보겠습니다.
import * as RT from "runtypes"

/*
 TS does not support accessing types during runtime, so instead, 
 we let Runtypes create the type for us using their own Domain
 Specific Language (DSL) to declare the schema & type at the
 same time.
*/
const UserType = RT.Record({
  name: RT.String,
  age: RT.Number.withBrand("PositiveInteger").withConstraint((n) => n > 0),
  email: RT.String.withBrand("Email").withConstraint(isEmail).optional(),
  website: RT.String.withBrand("URL").withConstraint(isURL).optional(),
  createdOn: RT.InstanceOf(Date).optional(),
})

// if you want to get the TS type, eg. for function signatures
type UserType = RT.Static<typeof UserType>

declare const data: any // (just so this example compiles)

try {
  // "If the object doesn't conform to the type specification, check will throw an exception."
  const userType: UserType = UserType.check(data)
  console.log(`The User ${userType.name} has arrived!`)
} catch (error) {
  console.error(`Oof, data wasn't what we want! ${error}`)
 
 
Runtypes는 가장 멋진 대안은 아니지만 그럼에도 불구하고 소개할 만한 가치가 있습니다.
  • 구문 분석이 정말 쉽고 간결해집니다.
    • 거의 우아한 Runtypes는 JS 객체용 파서를 생성하기 위한 기본 도구 상자를 제공합니다.
  • 타입 == 스키마 == 파서
    • 3개를 따로 관리할 필요가 없습니다. Runtypes 객체는 타입, 스키마 및 파서를 하나로 통합한 것입니다!
  • 우리는 여전히 일부 타입 가드(withConstraint)를 사용하고 있습니다
    • Runtypes은 매우 기본적인 타입과 함께 제공되므로 여기에서 사용할 수 있는 중요하고 유용한 유효성 검사 논리가 여전히 포함되어 있습니다.
    • 타입 가드는 TypeScript에서 "유효성을 검사 대신 파싱하는" 라이브러리를 지원하는 언어 기능입니다.
  • .check에서 오류가 발생하므로 이를 catch 하는 것을 잊지 마세요!
    • UserType 대신에 Error가 발생했지만 우리는 이미 여기에서 꽤 좋은 위치에 있습니다. 그러나 Zod 및 Funtypes(Runtypes 포크)는 이 문제를 해결합니다.

결론

"검증 대신 파싱하기"는 수신 데이터를 특정 타입으로 파싱하거나 파싱이 실패할 경우 통제된 방식으로 실패하는 것을 의미합니다.
코드 내에서 신뢰할 수 있고 안전하며 타입이 지정된 데이터 구조를 사용하고
들어오는 모든 데이터가 프로그램의 가장 가장자리에서 처리되도록 하는 것입니다.
들어오는 데이터를 코드 깊숙이 전달하지 말고 즉시 구문 분석하고 필요한 경우 빠르게 실패하십시오.
 
구문 분석을 수행하면 들어오는 모든 데이터를 명시적으로 처리해야 하기 때문에 유효성 검사보다 낫습니다.
타입 안전한 방식 또는 작업을 제공하고 애플리케이션 및 데이터 저장소 주변에 악성 콘텐츠를 전달하기 어렵게 만듭니다.
그러나 구문 분석에 데이터 유효성 검사가 포함되는 경우가 많은 것은 사실입니다.
 
 
지금까지 배운 내용에 만족하신다면 지금이 바로 박수 및 팔로우를 누르고 Runtypes 또는 Zod를 실행해 볼 수 있는 좋은 시간입니다!
그러나 아래의 보너스 섹션을 반드시 확인해야한다고 생각합니다!

주 : 원문은 원래 다른 라이브러리들도 다루고 있지만,
현재 대세는 zod로 굳어진 상황이기에 번역하여 정리하지 않았습니다.

참고 :

https://github.com/colinhacks/zod#brand

 

GitHub - colinhacks/zod: TypeScript-first schema validation with static type inference

TypeScript-first schema validation with static type inference - GitHub - colinhacks/zod: TypeScript-first schema validation with static type inference

github.com

https://www.totaltypescript.com/tutorials/zod

 

Zod Tutorial

Zod is a TypeScript-first schema declaration and validation library. In this tutorial, Matt Pocock has prepared a set of exercises that will help you level up.

www.totaltypescript.com

 
 

 

반응형