0. 들어가며
이번에 회사에서 새 레포지토리를 구성하면서 가장 먼저 고민한 것은 도메인 계층과 UI 계층을 어떻게 분리할 것인가였다.
기존에는 UI 코드 안에 모든 비즈니스 로직이 섞여 있었고, API 응답 객체를 그대로 사용하는 방식이었다. 하지만 이 방법은 다음과 같은 두 가지 문제를 만들었다.
문제 1. API 응답 객체를 그대로 사용
→MER_UUID,USR_NM같은 축약 필드명이 코드 전반에 퍼져 가독성이 떨어졌고, 백엔드 필드명이 변경되면 프론트엔드 전체를 수정해야 했다.문제 2. UI와 비즈니스 로직이 뒤섞임
→ Vue/React 컴포넌트 파일 안에 UI 코드와 비즈니스 플로우가 함께 들어있어, 코드 변경 시 UI까지 영향을 받는 경우가 많았고 책임 분리가 어려웠다.
클린 아키텍처는 “의존성이 안쪽(도메인)으로만 흐르게 하여, 비즈니스 로직을 외부(UI, DB, 프레임워크) 변화로부터 보호하는 구조”이다.
그 과정에서 자연스럽게 데이터 변환 로직이 필요해졌다. 즉, 백엔드에서 내려오는 DTO(Data Transfer Object)를
프론트엔드의 Entity(도메인 모델)로 변환해 다룰 수 있는 장치가 필요했고, 이를 위해 data → domain 변환을 담당하는 Mapper 클래스를 선언했다.
하지만 여기서 또 다른 문제가 드러났다.
Mapper만으로는 구조를 정리할 수 있지만, 타입스크립트는 컴파일 타임에만 타입을 보장한다는 것이다!
즉, 런타임에 잘못된 데이터 타입이 들어와도 검증할 수 없다는 한계가 있었다.
이를 보완하기 위해 런타임 검증을 지원하는 두 가지 방식을 참고했다.
- Zod: 스키마 기반 Validator (런타임에 스키마를 순회하면서 검증)
- typia: 코드 생성 기반 Validator (빌드 타임에 최적화된 검증 코드를 생성 → 런타임에서는 실행만)
물론 라이브러리를 그대로 가져다 쓸 수도 있지만, 보안 요건상 직접 구현해야 했고 동시에 학습 차원에서도 이를 시도해보기로 했다.
이번 글은 “[TS × 클린 아키텍처] 1편 — 타입스크립트 한계와 Mapper” 시리즈의 1차 글이다.
시리즈는 총 2편으로 구성된다.
- 이번 글(1차)에서는 Zod의 스키마 방식을 차용해, Mapper와 스키마 변환 클래스를 직접 구현하며 타입스크립트의 한계를 어떻게 극복하려 했는지 다룬다.
- 다음 글(2차)에서는 typia의 방식을 차용해, 빌드 타임에 Validator 함수를 만들어 성능까지 보완한 결과를 살펴볼 예정이다.
1. Mapper의 필요성과 타입스크립트의 한계
본격적으로 구현 얘기로 들어가기 전에,
먼저 왜 Mapper가 필요했는지, 그리고 타입스크립트만으로는 왜 부족했는지를 짚고 넘어가야 한다.
이 부분을 이해해야 뒤에서 스키마 기반 접근이 왜 등장했는지 자연스럽게 연결된다.
1) Mapper가 필요한 이유
(1) Mapper의 역할
- 프론트엔드에서 백엔드 API를 연동하다 보면, 종종 이런 데이터를 그대로 쓰게 되는 경우가 있다:
// dto/UserDTO.ts
// 백엔드에서 내려오는 원본 데이터 (DTO)
export type UserDTO = {
MER_UUID: string; // 사용자 고유 UUID
USR_NM: string; // 사용자 이름
CST_AGE: number; // 고객 나이
USR_STS?: string; // 상태 (ACTIVE, INACTIVE 등)
};- 얼핏 보면 별문제 없어 보이지만, 실제 코드에서는 두 가지 불편함이 있다.
- MER_UUID, USR_NM 같은 축약 필드가 프론트 전역에 퍼지면 코드 해석에 지연을 준다.
- 또한, 만약 백엔드가 MER_UUID → MERCHANT_ID로 바꾼다면? 프론트 전체에서 해당 필드를 쓰는 코드를 전부 수정해야 한다.
- 이 문제를 해결하기 위해, DTO ↔ Domain 변환을 담당하는 Mapper를 둔다.
- 즉, 외부에서 내려오는 복잡한 데이터는 Mapper 안에서만 다루고, 도메인 계층에서는 항상 깨끗한 Entity만 쓰도록 한다.
- 프론트엔드에서는 다음과 같은 Entity를 선언하면 된다:
// domain/User.ts
// 프론트엔드에서 실제 사용하는 도메인 모델 (Entity)
export type User = {
id: string; // MER_UUID → id
name: string; // USR_NM → name
age: number; // CST_AGE → age
status?: string;
};- 그 다음, Mapper 클래스를 이용해, DTO -> Domain으로 변환한다.
// mapper/UserMapper.ts
import type { UserDTO } from "../dto/UserDTO";
import type { User } from "../domain/User";
export class UserMapper {
// DTO -> Domain (백엔드 → 프론트엔드)
toDomain(dto: UserDTO): User {
return {
id: dto.MER_UUID,
name: dto.USR_NM,
age: dto.CST_AGE,
status: dto.USR_STS,
};
}
// Domain -> DTO (프론트엔드 → 백엔드)
toDTO(entity: User): UserDTO {
return {
MER_UUID: entity.id,
USR_NM: entity.name,
CST_AGE: entity.age,
USR_STS: entity.status,
};
}
}- 결과적으로, 이제 프론트에서는 항상 User Entity만 다루게 되어, 가독성과 더불어 외부 변경에도 안전해진다.
사용 예시
const raw: UserDTO = {
MER_UUID: "ABC-123",
USR_NM: "Alice",
CST_AGE: 30,
USR_STS: "ACTIVE",
};
const user = new UserMapper().toDomain(raw);
console.log(user);
// { id: "ABC-123", name: "Alice", age: 30, status: "ACTIVE" }
(2) Mapper와 타입스크립트 조합의 한계
Mapper를 도입하면 DTO ↔ Entity 변환이 깔끔해지고, 코드 가독성·유지보수성이 좋아진다.
하지만 여전히 한 가지 치명적인 빈틈이 남는다.
바로 타입스크립트의 타입은 컴파일 타임에만 동작한다는 점이다.
타입스크립트의 타입은 어디까지나 개발자가 코드를 작성할 때만 도움을 준다.
즉, 에디터 자동완성이나 컴파일러의 타입 체크 단계에서는 유효성을 보장하지만,
실제로 코드가 런타임에서 실행될 때는 타입 정보가 전부 사라진다.
다시 말해, 개발 중에는 "이 값은 number여야 해요" 라고 알려주지만,
실행 중에는 그냥 자바스크립트 객체로만 존재한다.
👉 그래서 외부에서 들어오는 값(API 응답, 사용자 입력 등) 은 타입스크립트만으로는 막을 수 없다.
- 다음과 같은 예시를 살펴보자.
- 겉보기엔 타입이 맞는 것처럼 보이지만, 실제 실행 시점에는 전혀 다른 값이 들어와도 그대로 통과되는 상황이다.
type User = { id: string; age: number };
// ❌ 잘못된 값
const raw = { id: "123", age: "스물" };
// any로 들어오면 런타임에서는 검증 불가
function printUser(user: User) {
console.log(`${user.id} is ${user.age} years old.`);
}
printUser(raw as any);
// 출력: "123 is 스물 years old."
- 즉, 타입스크립트는 "약속"만 지켜줄 뿐, 실제 값이 올바른지는 보장하지 않는다는 게 문제다.
- 이 때문에 API 응답이 잘못 내려오거나 사용자 입력이 엉뚱하게 들어와도 코드가 그대로 실행되며,
- 결과적으로 잘못된 값이 도메인 로직 안으로 흘러 들어가게 된다.
2) 타입스크립트의 빈틈을 메우는 스키마
(1) 타입 스크립트의 단점, 런타임 데이터 문제
- 앞서 본 것처럼 타입스크립트는 컴파일 타임에는 안전성을 보장해주지만, 실제로 프로그램이 실행되는 런타임 단계에서는 타입 정보가 사라진다.
- 이 말은 곧, API 응답이나 사용자 입력이 잘못 들어와도 그대로 통과할 수 있다는 뜻이다.
- 예를 들어, 서버에서 다음과 같은 데이터가 내려왔다고 하자.
// 서버에서 내려온 잘못된 데이터
const rawUser1 = { id: "123", age: "20" }; // 숫자 대신 문자열
const rawUser2 = { id: "456", age: "스물" }; // 완전히 잘못된 값
function printUser(user: { id: string; age: number }) {
console.log(`${user.id} is ${user.age} years old.`);
}
printUser(rawUser1 as any);
// 출력: 123 is 20 years old. (논리적으로 잘못된 값)
printUser(rawUser2 as any);
// 출력: 456 is 스물 years old. (완전히 잘못된 값)
rawUser1의 경우 문자열"20"이 그대로 출력되지만, 실제로는 숫자로 계산할 수 없는 값이다.rawUser2의 경우"스물"같은 한글 문자열이 들어왔음에도 오류 없이 실행된다.- 이처럼 타입스크립트 타입만으로는 런타임 데이터 유효성 검증을 할 수 없다.
(2) 스키마: 데이터의 설계도
- 이 문제를 해결하기 위해 도입되는 개념이 바로 스키마(schema)다.
- 스키마는 한마디로 “데이터의 설계도”다.
- 어떤 필드가 존재해야 하는지, 타입은 무엇인지, 값의 범위는 어떻게 되는지, 필수/옵션 여부는 무엇인지 등의 규칙을 코드로 선언해둔 것이다.
- 예를 들어, 나이는 0 이상의 정수여야 한다는 스키마를 정의해보자.
const ageSchema = s.number().int().min(0);
const result = ageSchema.safeParse("스물");
if (!result.success) {
console.log(result.error.issues);
// 출력: "Expected number"
}
- 입력값이
"스물"같은 잘못된 값이면 에러가 발생한다. - 올바른 값일 경우에만 변환된 결과를 안전하게 얻을 수 있다.
(3) 타입 vs 스키마
| 구분 | 타입스크립트(Type) | 스키마(Schema) |
|---|---|---|
| 동작 시점 | 컴파일 타임 | 런타임 |
| 역할 | 코드 작성 시 오류 탐지 | 실행 중 실제 값 검증 |
| 보장 범위 | 개발자 코드 내부 | 외부 입력(API, 사용자 입력 등) |
| 한계 | 런타임에서는 타입 정보 사라짐 | 성능 부담(검증 오버헤드 발생) |
- 타입스크립트의 타입 시스템만으로 런타임 안전성을 보장할 수 없는데, 잘못된 데이터가 들어와도 그대로 실행되기 때문이다.
- 그래서 스키마를 도입하면 실제 기대한 규칙을 따르는지 런타임에서 확인 가능하다.
2. 스키마 기반 검증기 구현하기
💡 구현의 목표는 단순했다. (구현한 레포지토리 링크)
- 데이터 구조를 코드로 선언할 수 있어야 하고,
- 런타임에도 타입 검증을 수행할 수 있어야 하며,
- Mapper와 결합했을 때 안전하게 DTO ↔ Entity 변환이 가능해야 했다.
1) 설계 원칙
이를 위해 다음과 같은 원칙으로 설계했다.
- BaseSchema 추상 클래스
- 모든 스키마 클래스의 공통 부모
_parse()메서드를 구현해 실제 검증 로직을 정의safeParse()를 통해 검증 성공/실패 결과를 반환
- 단일 타입별 Schema 클래스
StringSchema,NumberSchema,ObjectSchema등- 각 타입에 맞는 제약 조건(길이, 범위, 정규식 등)을 체이닝 방식으로 추가
- 조합 가능한 구조
OptionalSchema,NullableSchema등을 통해 값이 없거나 null인 경우 처리TransformSchema를 통해 검증 후 변환 로직 적용 (예: 문자열"20"→ 숫자20)
- 에러 관리
ValidationError객체에 발생한 문제를 누적 저장- 어떤 경로(path)에서 어떤 에러가 났는지 명확하게 전달
2) 주요 코드
추상 클래스(
BaseSchema)로 스키마의 뼈대를 정의하고, 이를 상속받은 구체 스키마들이 각자_parse를 구현한다.
모든 결과는safeParse로 감싸 동일한 구조로 반환되므로, 외부에서는 일관된 방식으로 검증 결과를 처리할 수 있다.
(1) BaseSchema: 추상 클래스
- 모든 스키마 클래스의 공통 부모로 추상 클래스
BaseSchema<T>를 정의했다.
export abstract class BaseSchema<T> {
readonly _type!: T;
protected abstract _parse(value: unknown, path: Path): MaybePromise<T>;
safeParse(value: unknown): SafeParseResult<T> {
try {
return { success: true, data: this.parse(value) };
} catch (e) {
return this._makeSafeResultError(e);
}
}
...
}
- 이 클래스는 최소 두 가지를 책임진다.
| 대상 | 역할 |
|---|---|
추상 메서드 _parse |
실제 검증 로직을 하위 클래스에서 구현하도록 강제한다. |
| 검증 결과 포맷 통일 | try/catch로 감싸 검증 성공/실패 여부를 객체 형태로 반환한다.항상 { success: boolean, data?: T, error?: ValidationError } 같은 형태로 리턴하여, 모든 스키마가 동일한 결과 구조를 가진다. |
(2) 구체 스키마 클래스: 상속
- 이제 각 타입별 스키마(
StringSchema,NumberSchema,ObjectSchema등)는BaseSchema를 상속받아_parse메서드를 구현한다. - 이 중
StringSchema구현 예시를 살펴보자.
export class StringSchema extends BaseSchema<string> {
constructor(private readonly config: StringConfig = {}) {
super();
}
protected override _parse(input: unknown, path: Path): string {
// [1] 타입 검증
if (typeof input !== 'string') {
this._fail(path, 'invalid_type', 'Expected string');
}
const { min, max, re } = this.config;
const len = input.length;
// [2] **조건 검증**
if (min != null && len < min) {
this._fail(path, 'too_small', `Min length ${min}`);
}
if (max != null && len > max) {
this._fail(path, 'too_big', `Max length ${max}`);
}
if (re && !re.test(input)) {
this._fail(path, 'invalid_string', 'Regex mismatch');
}
return input;
}
private _fail(
path: Path,
code: 'invalid_type' | 'too_small' | 'too_big' | 'invalid_string',
message: string
): never {
throw new ValidationError([{ path, code, message }]);
}
...
}
| 번호 | 설명 |
|---|---|
| [1] 타입 검증 | 입력값이 string인지 먼저 확인한다. 아니라면 throw로 에러를 던져 즉시 실패 처리한다. |
| [2] 조건 검증 | 생성자에 넘겨받은 설정(min, max, re)을 기준으로 길이와 패턴을 검사한다. 모든 조건을 통과하면 입력값을 그대로 반환한다.• min: 최소 길이• max: 최대 길이• re: 정규식 |
export class StringSchema extends BaseSchema<string> {
...
// [3] 체이닝
min = (n: number) => new StringSchema({ ...this.config, min: n });
max = (n: number) => new StringSchema({ ...this.config, max: n });
regex = (r: RegExp) => new StringSchema({ ...this.config, re: r });
email = () => this.regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
}
| 번호 | 설명 |
|---|---|
| [3] 체이닝 | StringSchema 클래스는 min, max, regex 같은 메서드를 체이닝 방식으로 제공한다. 즉, 기존 설정을 유지하면서 새로운 조건을 추가한 새로운 StringSchema 인스턴스를 반환한다. |
- 체이닝은 다음과 같이 사용할 수 있다.
const schema = new StringSchema()
.min(3) // 최소 길이 3자
.max(10) // 최대 길이 10자
.regex(/^[a-z]+$/); // 알파벳 소문자만 허용
- 이 외에도 객체, enum 등 여러 타입에 대해 BaseSchema를 상속받아, 구현하는 방식으로 진행했다.
(3) Mapper와 결합
- 스키마 클래스는 단독으로도 사용할 수 있지만, Mapper와 함께 쓰면 DTO → Entity 변환 + 런타임 검증을 동시에 처리할 수 있다.
// User 스키마 정의
const UserDTOSchema = new ObjectSchema({
MER_UUID: new StringSchema(),
USR_NM: new StringSchema().min(1),
CST_AGE: new BaseSchema<number>(), // 간단 예시
});
// User 도메인 모델
type User = { id: string; name: string; age: number };
// Mapper with Schema
class UserMapper {
static toDomain(raw: unknown): User {
const result = UserDTOSchema.safeParse(raw);
if (!result.success) throw result.error;
const dto = result.data;
return {
id: dto.MER_UUID,
name: dto.USR_NM,
age: dto.CST_AGE,
};
}
}
- 실제 동작 코드를 살펴보자
// 올바른 데이터
const raw1 = { MER_UUID: "ABC-123", USR_NM: "Alice", CST_AGE: 25 };
console.log(UserMapper.toDomain(raw1));
// { id: "ABC-123", name: "Alice", age: 25 }
// 잘못된 데이터 (age가 문자열)
const raw2 = { MER_UUID: "DEF-456", USR_NM: "Bob", CST_AGE: "스물" };
console.log(UserMapper.toDomain(raw2));
// → Error: Expected number at CST_AGE
- 첫 번째 케이스(
raw1)는CST_AGE가 숫자 타입이라 스키마 검증을 통과하고, Mapper가 DTO → Domain 변환을 정상적으로 수행한다. - 두 번째 케이스(
raw2)는CST_AGE가 문자열"스물"이라NumberSchema검증에서 실패한다. 따라서 스키마가 에러를 던지고, safeParse 결과가 실패로 처리된다.
3) 실제 적용 예시
(1) 상황 설정
- API에서 내려오는 데이터는 종종 프론트엔드에서 바로 쓰기 어려운 형태다.
- 예를 들어 다음처럼 내려올 수 있다:
{
"MER_UUID": "ABC-123",
"USR_NM": "Alice",
"CST_AGE": "25",
"USR_STS": "ACTIVE"
}
- 여기서
CST_AGE는 문자열"25"이므로, 단순 타입스크립트 타입 정의만으로는 안전성을 확보할 수 없다.
(2) User 스키마 정의
- 런타임에서도 타입 안정성을 보장하기 위해, 스키마를 정의한다.
- 우선, 프론트엔드에서 실제로 사용할 도메인 모델(Entity)을 타입으로 정의한다.
- API의 축약 필드명 대신
id,name,age,status처럼 읽기 좋은 camelCase로 정의했다.
// domain/User.ts
export type User = {
id: string;
name: string;
age: number;
status?: string;
};
- 그 다음으로, User에 대한 DTO 스키마(UserDTOSchema)를 정의했다.
// schema/UserSchema.ts
import { StringSchema } from "./StringSchema";
import { TransformSchema } from "./TransformSchema";
import { ObjectSchema } from "./ObjectSchema";
export const UserDTOSchema = new ObjectSchema({
MER_UUID: new StringSchema(),
USR_NM: new StringSchema().min(1),
CST_AGE: new TransformSchema(new StringSchema(), (v) => Number(v)),
USR_STS: new StringSchema().optional(),
});
이 스키마는 API에서 내려오는 원시 데이터를 런타임에서 검증하고, 필요하다면 변환(transform)까지 수행한다.
ObjectSchema는 객체 구조를 검증.MER_UUID: 문자열로만 허용.USR_NM: 문자열 + 최소 길이 1자.CST_AGE: 문자열로 들어오더라도TransformSchema를 통해 숫자로 변환.USR_STS: optional → 값이 없어도 통과.
(3) Mapper 구현
- 앞서 정의한 UserDTOSchema를 Mapper와 결합해,
- DTO → Domain 변환 과정에서 런타임 검증과 변환을 동시에 처리한다.
// mapper/UserMapper.ts
import type { User } from "../domain/User";
import { UserDTOSchema } from "../schema/UserSchema";
export class UserMapper {
static toDomain(raw: unknown): User {
const result = UserDTOSchema.safeParse(raw);
if (!result.success) throw result.error;
const dto = result.data;
return {
id: dto.MER_UUID,
name: dto.USR_NM,
age: dto.CST_AGE,
status: dto.USR_STS,
};
}
}
safeParse를 통해 DTO를 검증한다.- 성공하면
{ success: true, data }구조를 반환하고, - 실패하면
{ success: false, error }객체를 반환한다. - 결과적으로 UI/비즈니스 로직에서는 항상 일관된 User 타입만 다루면 된다.
(4) 실제 동작
- 먼저, 올바른 데이터가 들어온 경우를 보자.
// 올바른 데이터
const raw1 = { MER_UUID: "ABC-123", USR_NM: "Alice", CST_AGE: "25" };
const user1 = UserMapper.toDomain(raw1);
console.log(user1);
// { id: "ABC-123", name: "Alice", age: 25 }
CST_AGE는 문자열"25"였지만,TransformSchema가 숫자25로 변환했다.- Mapper가 변환 결과를 도메인 모델
{ id, name, age, status }로 리턴한다.
- 이번에는 잘못된 데이터가 들어온 경우를 보자.
// 잘못된 데이터 (나이가 '스물')
const raw2 = { MER_UUID: "DEF-456", USR_NM: "Bob", CST_AGE: "스물" };
try {
UserMapper.toDomain(raw2);
} catch (e) {
console.error(e);
// ValidationError: Expected number at CST_AGE
}
CST_AGE값"스물"은 숫자로 변환 불가하다.- 그래서
TransformSchema내부에서 변환을 실패해, ValidationError 발생시켰다.
- 결국 스키마 클래스를 사용하면 런타임에서 DTO를 검증하고,
- Mapper가 안전하게 Domain 모델로 변환한다.
- 또한, 올바른 값은 변환되어 통과, 잘못된 값은 즉시 실패시킬 수 있었다.
3. 마치며
이번 시간에는 클린 아키텍처를 도입하는 과정에서 마주한 타입스크립트의 한계와, 이를 해결하기 위해 시도한 방법을 정리했다.
Mapper는 DTO ↔ Entity 변환(필드명 매핑, 구조 변환)을 하는데, DTO 값 자체가 잘못 들어오면 Mapper만으로는 막을 수 없다.
따라서 런타임 검증은 스키마가 담당하고, Mapper는 이를 호출해 “검증 + 변환”을 한 번에 처리하도록 설계했다.
즉, Mapper와 스키마가 결합되면서 DTO → Entity 변환 과정에서 안전성과 일관성을 동시에 확보할 수 있었다.
하지만 런타임에서 매번 데이터 구조를 순회하며 검증하기에 성능 저하가 발생했다. 실제 벤치마크 결과는 다음과 같다.
검증 조건
- 데이터 크기:
SIZE = 100,000- 검증 시나리오
- 하드 케이스: nullable, optional, enum, strict, invalid 필드를 섞은 복잡한 데이터
- 간단 케이스: 모든 값이 정상, unknown 없음
- 혼합 케이스: 정상 80%, 실패 20% 비율
- 비교 대상
manualToDomain: 수동 매핑 + 조건문 검증UserMapper.toDomain: 스키마 클래스 검증 + 변환
- 결과
| 케이스 | 스키마 적용 전 (수동) | 스키마 적용 후 | 차이 |
|---|---|---|---|
| 하드 데이터셋 | 16.291ms | 4.569s | 약 280배 느림 |
| 간단 데이터셋 | 65.15ms | 3.183s | 약 49배 느림 |
| 혼합 데이터셋 | 51.514ms | 5.197s | 약 100배 느림 |
- 스키마 클래스는 입력값을 받을 때마다 (1) 객체 필드 순회 → (2) 각 조건 검사(min, max, regex 등) → (3) 실패 시 ValidationError 생성 단계를 반복한다.
- 즉, “해석(interpret)” 기반 실행이어서 데이터 크기만 커져도 비용이 급격히 증가한다.
- 반대로 수동 매핑은 이미 “컴파일된 조건문”이므로 단순 분기만 실행해서 빠른 것이다.
- 결국 스키마 클래스 방식이 안정성과 유지보수성은 확보했지만, 성능 면에서는 손해가 있었다.
이 벤치마크를 통해, 스키마를 런타임마다 해석하는 구조는 본질적으로 성능 한계가 있을 수밖에 없다는 점을 확인했다.
추가 리서치를 통해 알게 된 것은, 스키마를 매번 생성·해석하는 방식보다는 typia처럼
타입 정보를 AST로 분석하여 빌드 타임에 최적화된 validator 함수를 자동 생성하는 방식이 성능상 훨씬 유리하다는 점이었다.
typia는 TypeScript Compiler API를 이용해 타입 정보를 AST로 분석하고, 그 결과를 바탕으로 최적화된 validator 함수를 코드 형태로 생성한다.
따라서 런타임에서는 매번 스키마를 해석할 필요 없이, 이미 생성된 조건문 기반 함수를 실행하기만 하면 된다.
이 덕분에 “수동 매핑”과 거의 유사한 성능을 유지하면서도 타입 안정성을 동시에 보장할 수 있다.
👉 다음 글에서는 typia의 아이디어를 응용해, AST 기반 코드 생성 방식으로 개선하는 방법을 다뤄보겠다.
'개발 기술 > 개발 이야기' 카테고리의 다른 글
| [TS × 클린 아키텍처] 2편 — 타입스크립트 한계와 Mapper: AST로 타입 검증하기 (0) | 2025.10.12 |
|---|---|
| Tailwind 없이, PostCSS+PurgeCSS로 유틸리티 클래스 구축하기 (0) | 2025.05.26 |
| [CSS] SPA에서 Global과 Split CSS, 두 장점을 모두 살리는 방법 (0) | 2025.04.27 |
| Vue 3 <script setup> 선언 순서를 자동 정렬하는 ESLint 룰 개발기 (0) | 2025.02.28 |
| 테스트 코드를 관리하는 법 2: 커버리지 감소 검사하기 (0) | 2025.01.24 |
댓글