Logo

타입스크립트를 쓰는데도 유효성 검증이 필요할까?

“타입스크립트로 코드를 짜니까 별도로 유효성 검증은 필요없는 거 아니에요?”

타입스크립트를 공부하고 계시거나 자바스크립트 경험이 많지 않은 개발자들로 부터 종종 받는 질문인데요. 정말 타입스크립트의 타입 검사가 자바스크립트의 유효성 검증을 대신할 수 있을까요?

이번 포스팅에서는 이러한 타입스크립트에 대한 오해를 풀어보는 시간을 갖도록 하겠습니다.

타입스크립트의 역할

이해를 돕기 위해서 타입스크립트로 간단한 코드를 함께 짜볼까요? 사용자 계정을 나타내는 Account 인터페이스를 선언하고 이것을 입력으로 받는 processAccount() 함수를 작성해보겠습니다.

account.ts
interface Account {
  id: string;
  email: string;
  age: number;
  level: "GOLD" | "SILVER" | "BRONZE";
  active: boolean;
  createdAt: Date;
  image?: string | undefined;
  ips?: string[] | undefined;
}

function processAccount(account: Account) {
  // 어떤 로직
}

그 다음 Account 인터페이스와 일치하지 않는 인수로 processAccount() 함수를 호출하려고 하면 아래와 같이 타입 에러가 발생하는데요.

account.test.ts
processAccount({}); // ❌ Argument of type '{}' is not assignable to parameter of type 'Account'.

processAccount({
  id: 101, //  ❌ Type 'number' is not assignable to type 'string'.ts(2322)
  email: "user@test.com",
  age: "35", //  ❌ Type 'string' is not assignable to type 'number'.ts(2322)
  level: "PLATINUM", //  ❌ Type '"PLATINUM"' is not assignable to type '"GOLD" | "SILVER" | "BRONZE"'.ts(2322)
  active: true,
  createdAt: "2020-01-17T16:45:30", //  ❌ Type 'string' is not assignable to type 'Date'.ts(2322)
});

그런데 위에서 작성한 타입스크립트 코드를 타입스크립트 컴파일러(compiler)인 tsc를 통해서 자바스크립트로 변환하면 어떻게 될까요?

$ npx tsc account.ts

그러면 다음과 같이 Account 인터페이스가 없어지고, processAccount() 함수에 붙어있던 타입 정보도 모두 사라지게 되는데요.

account.js
function processAccount(account) {
  // 어떤 로직
}

그러므로 이 함수를 실제로 호출할 때는 어떤 형태의 인수를 넘기더라도 아무런 문제가 발생하지 않습니다.

account.test.js
updateAccount({}); // ✅ 문제없이 실행 됨

updateAccount({
  id: 101,
  email: "user@test.com",
  age: "35",
  level: "PLATINUM",
  active: true,
  createdAt: "2020-01-17T16:45:30",
}); // ✅ 문제없이 실행 됨

이 간단한 예제를 통해서 우리는 무엇을 배울 수 있을까요?

  1. 타입스크립트 코드에서 타입 에러는 컴파일 과정에서만 발생할 수 있습니다.
  2. 실제로 자바스크립트 프로그램이 실행될 때는 타입스크립트는 아무런 역할을 못합니다.

여기서 “컴파일”은 항상 빌드나 배포 과정에서만 일어나는 과정은 아니라는 것에 주의하셔야 하는데요. VSCode와 같은 코드 편집기에서도 흔하게 타입 에러를 확인할 수 있죠? 이것은 우리가 코드를 편집할 때 실시간으로 코드 편집기가 알아서 컴파일을 해주기 때문입니다.

타입스크립트는 코드에 명시되어 있는 타입 구조를 정적으로 분석하여 프로그램을 실행하지 않고도 발생할 가능성이 있는 문제를 예측할 수 있는데요. 결국 타입스크립트는 이렇게 예측 가능한 문제를 개발자에게 알려줌으로써 좀 더 견고한 코드를 짤 수 있도록 도와주는 도구일 뿐입니다.

다시 한 번 정리해보면 타입 검사란 타입스크립트로 작성된 코드의 컴파일 시점에서 일어나며 오직 개발자를 위한 것입니다. 유효성 검증은 컴파일이 끝난 자바스립트 프로그램의 실행 시점에서 일어나며 최종 사용자를 위한 것입니다. 즉, 이 둘은 엄연히 다른 것이며 타입 검사가 유효성 검증을 대신할 수 없습니다.

타입 시스템의 한계

아무리 우리가 타입스크립트를 사용하여 예측할 수 있는 문제를 모두 해결하더라도 프로그램이 실행될 때는 예측할 수 없는 문제들이 너무나도 많습니다. 왜냐하면 프로그램이 실행되는 환경에는 우리가 통제할 수 없는 너무나도 많은 변수가 존재하기 때문인데요.

특히 웹 애플리케이션이나 CLI(커맨드 라인 인터페이스) 도구처럼 사용자로부터 입력을 받는 일이 빈번하거나 외부 API에 크게 의존하는 프로그램의 경우, 원치 않거나 예상치 못한 형태로 해당 애플리케이션에 데이터가 유입될 가능성이 상당히 높습니다.

이렇게 외부에서 들어오는 데이터를 효과적으로 검증하려면 단순히 자료형을 제한하는 것만으로는 부족할 때가 많은데요. 예를 들어, 자바스크립트에서는 숫자형과 문자형을 구분지을 수는 있지만 숫자형 내에서 정수와 실수를 구분할 수는 없고 숫자의 범위를 제한할 수도 없죠. 문자형을 데이터를 입력받을 때도 정해진 선택지 내에서 입력을 받고 싶거나 이메일, URL, IP 등 특정 문자열 형식으로 입력을 받아야 할 수도 있습니다.

account.ts
interface Account {
  id: string;
  email: string;
  age: number;
  level: "GOLD" | "SILVER" | "BRONZE";
  active: boolean;
  createdAt: Date;
  image?: string | undefined;
  ips?: string[] | undefined;
}

function processAccount(account: Account) {
  // 어떤 로직
}

// 자료형이 모두 맞기 때문에 타입 에러가 발생하지 않음... 😞
processAccount({
  id: "", // ⚠️ 빈문자열
  email: "https://www.daleseo.com", // ⚠️ 이메일 형식에 맞지 않음
  age: 9999.123, // ⚠️ 9999살? 나이가 소수?
  level: "GOLD",
  active: true,
  createdAt: new Date("2020-01-17T16:45:30")
});

이렇게 실제로 애플리케이션에 요구하는 유효성 검증은 자바스크립트의 느슨한 타입 시스템으로 충족하기에는 한계가 있죠. 이러한 현실적인 이유로 대부분의 애플리케이션에서는 타입스크립트 사용과 별개로 유효성 검증을 구현하게 됩니다.

유효성 검증 구현의 고통

그럼 우리가 직접 유효성 검증을 구현해보면 어떨까요?

Account 인터페이스로 선언한 8개의 속성 중 4개의 속성에 대해서만 유효성 검증을 구현해보겠습니다.

account.ts
interface Account {
  id: string;
  email: string;
  age: number;
  level: "GOLD" | "SILVER" | "BRONZE";
  active: boolean;
  createdAt: Date;
  image?: string | undefined;
  ips?: string[] | undefined;
}

function updateAccount(account: Account) {
  if (typeof account.id !== "string" || account.id.length < 36)
    throw new Error("Invalid id");

  if (typeof account.age !== "number" || account.age < 0)
    throw new Error("Invalid age");

  if (!["GOLD", "SILVER", "BRONZE"].includes(account.level))
    throw new Error("Invalid level");

  if (!(account.createdAt instanceof Date))
    throw new Error("Invalid createdAt");

  // 어떤 로직
}

어떤가요? 생각보다 작성해야 할 코드가 많고 함수 내부가 상당히 지저분해지죠? 여기에다가 정규식을 이용해서 이메일, URL, IP 검증까지 한다면 코드가 이것보다 훨씬 더 복잡해질 것입니다. 오히려 함수 내에서 비지니스 로직보다 유효성 검증 로직이 차지하는 공간이 더 커질 수도 있고요. 소위 배보다 배꼽이 더 커질 수 있는 상황이 올 수 있거죠.

그런데 이보다 더 큰 문제는 이 손수 작성한 유효성 검증 로직을 Account 인터페이스로 선언한 타입과 항상 일치하도록 관리해줘야 한다는 건데요. 타입 바뀔 때마다 유효성 검증 로직을 수정해준다는게 여간 번거로운 일이 아니며 게다가 까먹기도 참 쉽습니다. 어느 정도 규모가 있는 애플리케이션을 유지보수 해보셨다면 공감하실 거에요.

따라서 개발 생산성이나 유지 보수 측면에서 위와 같은 방식으로 직접 유효성 검증을 구현하는 것은 효과적인 접근 방법이 아닐 것입니다.

유효성 검증 라이브러리

다행히도 자바스크립트 커뮤니티에는 유효성 검증을 도와주는 여러가지 라이브러리가 있는데요.

대표적으로 JoiYup 그리고 Zod를 들 수 있습니다. (공교롭게도 모두 이름이 3글자이네요 😆) 이 유효성 검증 라이브러리들은 각각 특장점이 있지만 공통적으로 타입스크립트가 아닌 자바스크립트로 스키마(schema)를 정의하고 이것을 이용하여 유효성 검증을 가능하게 해줍니다.

예를 들어서, Zod를 통해서 위해서 작성한 Account 인터페이스와 processAccount() 함수를 재작성해보겠습니다.

account.ts
// ✅ 스키마 정의
const Account = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().int().min(18).max(80),
  level: z.enum(["GOLD", "SILVER", "BRONZE"]),
  image: z.string().url().max(200).optional(),
  ips: z.string().ip().array().optional(),
  active: z.boolean().default(false),
  createdAt: z.date().default(new Date()),
});

// ✅ 스키마로 부터 타입 추론
type Account = z.infer<typeof Account>;

function updateAccount(account: Account) {
  // ✅ 스키마로 유효성 검증
  Account.parse(account);

  // 어떤 로직
}

어떤가요? 코드가 정말 깔끔해졌죠! 🎉

더군다나 우리는 더 이상 타입 선언과 유효성 검증을 일치시키야 한다는 걱정할 필요가 없습니다. 동일한 스키마를 통해서 두 가지 부분 모두 해결되고 있으니까요. 😎

마치면서

지금까지 우리가 타입스크립트로 코드를 작성함에도 불구하고 왜 별도의 유효성 검증이 필요한지에 대해서 알아보았습니다. 그리고 직접 유효성 검증을 구현하는 것이 얼마나 비효율적이고 항상 타입과 일치하도록 관리하는 것이 얼마나 힘든지도 살펴보았습니다.

타입스크립트로 컴파일 때 수행되는 타입 검사와 자바스크립트로 프로그램 실행 때 해줘야하는 유효성 검증은 엄연히 다르다는 것을 이해하시는데 도움이 되었으면 좋겠습니다.

다음 포스팅에서는 타입스크립트 친화적인 유효성 검증 라이브러리로 개발자들로부터 최근 많은 인기를 끌고 있는 Zod에 대해서 알아보도록 하겠습니다.

Zod 관련 포스팅은 Zod 태그를 통해서 쉽게 만나보세요!