2편. Fastify를 위한 TypeBox(legacy) 기본 문법 이해하기
📚 목차
1. 기본 타입과 Object 스키마
2. 선택 필드와 컬렉션 (Optional & Collections) 타입
3. 조합 타입 (Union, Literal, Enum, Nullable)
4. 스키마 유틸리티 (Partial, Pick, Omit)
5. TypeScript 타입 추출 (Static)

1. 기본 타입과 Object 스키마
가장 기본이 되는 Primitive 타입과 데이터의 뼈대가 되는 Object 구조를 정의합니다.
🔷 1. 기본 타입 정의 및 제약 조건
TypeBox는 JSON Schema의 표준 타입을 제공하며, 두 번째 인자로 상세한 유효성 옵션을 전달할 수 있습니다.
이 옵션들은 Fastify의 Ajv 엔진을 통해 강력한 런타임 검증을 수행합니다.
| 타입 | 옵션 | 설명 |
| String | minLength, maxLength | 문자열 최소/최대 길이 제한 |
| String | pattern | 정규표현식 검증 (예: ^[a-z]+$) |
| String | format | 특정 형식 검증 (예: email, date-time, uuid 등) |
| Number / Integer | minimum, maximum | 최소/최대값 범위 제한 |
| Number / Integer | exclusiveMinimum | 최소값 초과 여부 지정 (boolean 또는 값 사용) |
| Number / Integer | multipleOf | 특정 수의 배수인지 확인 |
✔️ 자주 사용하는 Format (String 전용)
Fastify(내부적으로 Ajv 사용)에서 String 타입에 자주 사용하는 format 옵션을 정리하면 다음과 같습니다.
| format | 예시 | 설명 |
| user@example.com | RFC 기반 이메일 주소 형식 검증 | |
| date-time | 2023-10-27T10:00:00Z | ISO 8601 날짜-시간 형식 |
| date | 2023-10-27 | ISO 8601 날짜 형식 |
| time | 10:30:00 | ISO 8601 시간 형식 |
| uuid | 550e8400-e29b-41d4-a716-446655440000 | UUID 형식 검증 |
| ipv4 | 192.168.0.1 | IPv4 주소 형식 |
| ipv6 | 2001:0db8:85a3:0000:0000:8a2e:0370:7334 | IPv6 주소 형식 |
| uri | https://example.com/file | RFC 기반 URI 형식 검증 |
| uri-reference | /api/v1/users | 상대 경로 포함 URI 검증 |
| hostname | api.example.com | 호스트명 형식 검증 |
| regex | ^[a-z]+$ | 정규표현식 자체가 유효한지 검증 |
| json-pointer | /data/attributes/name | JSON Pointer 형식 검증 |
| password (custom 권장) | P@ssw0rd! | 기본 format 없음 → pattern으로 구현 |
✔️ 기본 타입 정의 예시
// 기본 타입 예시
const Name = Type.String({ minLength: 2 });
const Age = Type.Integer({ minimum: 0, maximum: 120 });
const Score = Type.Number({ minimum: 0, maximum: 100 });
const IsActive = Type.Boolean({ default: true });
// 실무형 유효성 검증 적용
const UserEmail = Type.String({
format: 'email',
description: '사용자 주 이메일'
});
const Password = Type.String({
minLength: 8,
maxLength: 20,
pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$' // 대소문자, 숫자 포함 정규식
});
const CreatedAt = Type.String({ format: 'date-time' });
🔷 2. 객체(Object) 스키마 : Type.Object()
대부분의 API는 객체 형태의 데이터를 주고받습니다.
Type.Object는 속성 이름과 그에 해당하는 TypeBox 스키마를 매핑하여 정의합니다.
const UserSchema = Type.Object({
id: Type.Integer({ description: '사용자 고유 ID' }),
name: Type.String(),
email: Type.String({ format: 'email' }),
// 중첩 객체(Nested Object) 정의
// Profile 정보를 별도의 객체 구조로 계층화합니다.
profile: Type.Object({
bio: Type.String({ maxLength: 200 }),
avatarUrl: Type.String({ format: 'uri' }),
settings: Type.Object({
theme: Type.Union([Type.Literal('light'), Type.Literal('dark')]),
notifications: Type.Boolean()
})
})
}, {
additionalProperties: false, // 정의되지 않은 속성 허용 안 함 (보안 강화)
description: '사용자 정보 통합 스키마'
});
2. 선택 필드와 컬렉션 (Optional & Collections) 타입
API를 설계하다 보면 특정 데이터가 들어오지 않아도 되는 경우나, 여러 개의 데이터를 묶어서 처리해야 하는 경우가 생깁니다.
🔷 1. 선택적 필드 (Optional)
TypeBox의 객체 내 모든 필드는 기본적으로 필수(Required)입니다. 데이터가 없을 수도 있는 필드는 Type.Optional()로 감싸주어야 합니다.
이는 TypeScript의 ? 연산자와 동일하게 작동하며, 런타임에서도 해당 필드가 없어도 검증을 통과하게 합니다.
const UserSchema = Type.Object({
username: Type.String(),
// nickname은 전달되지 않아도 검증 에러가 발생하지 않습니다.
nickname: Type.Optional(Type.String()) // 타입: string | undefined
});
🔷 2. 배열 (Array)
동일한 타입의 데이터가 여러 개 나열되는 형태를 정의합니다. 최소/최대 아이템 개수나 중복 허용 여부 등을 옵션으로 제어할 수 있습니다.
// 최소 1개 이상의 태그를 포함해야 하는 문자열 배열
const Tags = Type.Array(Type.String(), {
minItems: 1,
uniqueItems: true // 중복된 값 허용 안 함
});
// Object 배열
const UsersSchema = Type.Array(
Type.Object({
id: Type.Integer(),
name: Type.String()
})
)
🔷 3. 레코드 (Record)
키(Key)의 이름을 미리 알 수 없지만 값(Value)의 타입은 고정되어 있는 경우에 사용합니다.
사전(Dictionary) 형태의 데이터를 정의할 때 유용합니다.
// Key는 문자열이고, Value는 정수인 구조
const Inventory = Type.Record(Type.String(), Type.Integer());
// 예시 데이터: { "apple": 10, "banana": 25 }
3. 조합 타입 (Union, Literal, Enum, Nullable)
단순한 타입을 넘어, 여러 조건을 결합하거나 특정 값으로 범위를 제한해야 할 때 사용합니다.
🔷 1. Literal & Union
▸ Literal: 단 하나의 구체적인 값(문자열, 숫자 등)만 허용합니다. 정확히 일치해야 합니다.
▸ Union: 여러 타입 중 하나를 선택할 수 있게 합니다. TypeScript의 | (OR) 연산자와 매칭됩니다.
// 나열된 값 중 '단 하나'의 값만 허용하는 스키마 (상태값 관리에 유용)
// PENDING, COMPLETED, FAILED 중 오직 한 가지만 가질 수 있으며,
// 그 외의 값이나 중복 선택은 허용되지 않습니다.
const Status = Type.Union([
Type.Literal('PENDING'), // 정확히 'PENDING' 문자열만 허용
Type.Literal('COMPLETED'), // 정확히 'COMPLETED' 문자열만 허용
Type.Literal('FAILED') // 정확히 'FAILED' 문자열만 허용
]);
// 서로 다른 타입을 동시에 허용해야 할 때 (다중 타입 지원)
// 예: ID 값이 '123'(문자열)이거나 123(숫자)인 경우를 모두 허용
const IdSchema = Type.Union([
Type.String(),
Type.Integer()
]);
🔷 2. Enum (열거형 데이터 매핑)
TypeScript의 enum을 정의하고 이를 런타임 검증에 사용하고 싶을 때 매우 유용합니다.
코드의 가독성을 높이고 상숫값을 중앙 관리하기 좋습니다. 4.1의 Union 예제와 동일한 로직을 Enum으로 구현하면 다음과 같습니다.
// 1. TypeScript Enum 정의
enum TaskStatus {
Pending = 'PENDING',
Completed = 'COMPLETED',
Failed = 'FAILED'
}
// 2. Enum을 이용한 TypeBox 스키마 생성
// 런타임에 'PENDING', 'COMPLETED', 'FAILED' 중 하나인지 검증합니다.
const StatusSchema = Type.Enum(TaskStatus);
// 3. 타입 추출
type StatusType = Static<typeof StatusSchema>; // TaskStatus 타입으로 추출됨
🔷 3. Nullable 표현 (데이터 부재 허용)
데이터베이스에서 NULL을 허용하는 필드를 처리할 때 필요합니다.
TypeBox에서는 별도의 키워드 대신 Union에 Type.Null()을 포함하는 방식을 권장합니다.
// 값이 문자열이거나 명시적으로 null일 수 있음을 정의
const NullableString = Type.Union([Type.String(), Type.Null()]);
// 사용 예시: 프로필 사진이 등록되지 않은 경우 등
const Profile = Type.Object({
photoUrl: Type.Union([Type.String({ format: 'uri' }), Type.Null()])
});
4. 스키마 유틸리티 (Partial, Pick, Omit)
이미 만들어진 스키마를 바탕으로 상황에 맞는 새로운 스키마를 빠르고 안전하게 생성합니다.
이는 코드 중복을 줄이고 데이터 전송 객체(DTO)를 설계할 때 핵심적인 역할을 합니다.
| 유틸리티 | 설명 | 활용 사례 |
| Type.Partial | 기존 객체의 모든 필드를 Optional로 변경합니다. | 정보 수정(PATCH) API: 일부 필드만 선택적으로 업데이트할 때 사용합니다. 예: 사용자 프로필 수정 |
| Type.Pick | 기존 객체에서 원하는 필드만 골라 새로운 스키마를 생성합니다. | 특정 정보 조회: 전체 사용자 정보 중 id, nickname 등 일부 필드만 응답해야 할 때 사용합니다. |
| Type.Omit | 특정 필드를 제외한 나머지 필드로 새로운 스키마를 생성합니다. | 민감 정보 제외: 사용자 정보를 응답할 때 password, refreshToken 등의 보안 필드를 제거할 때 사용합니다. |
// 모든 정보가 담긴 기본 사용자 스키마
const UserBaseSchema = Type.Object({
id: Type.Integer(),
email: Type.String(),
password: Type.String(),
createdAt: Type.String()
});
// 1. Partial: 모든 필드를 선택사항(Optional)으로 변경
// 실제 타입: { id?: number; email?: string; password?: string; createdAt?: string; }
const UserUpdateSchema = Type.Partial(UserBaseSchema);
// 2. Omit: 'password' 필드만 제거하고 나머지 필드로 구성
// 주로 사용자 정보를 클라이언트에 반환할 때 보안상 비밀번호를 숨기는 스키마로 사용합니다.
const UserPublicSchema = Type.Omit(UserBaseSchema, ['password']);
// 3. Pick: 'id'와 'email' 필드만 선택해서 구성
// 로그인 여부 확인이나 최소한의 정보만 검증할 때 유용합니다.
const UserSimpleSchema = Type.Pick(UserBaseSchema, ['id', 'email']);
5. TypeScript 타입 추출 (Static)
TypeBox의 가장 큰 장점은 "한 번의 정의로 스키마와 타입을 동시에 얻는 것"입니다.
정의한 스키마(런타임용)로부터 TypeScript 정적 타입(컴파일 타임용)을 자동으로 생성할 수 있습니다.
// 1. 런타임 검증을 위한 스키마 정의 (JavaScript 객체)
const UserSchema = Type.Object({
id: Type.Number(),
name: Type.String(),
roles: Type.Array(Type.Literal('admin', 'user'))
});
// 2. 스키마로부터 정적 타입 추출 (TypeScript 타입)
// 이제 'User'는 아래와 같은 순수 TS 타입으로 동작합니다.
type User = Static<typeof UserSchema>;
/* 추출된 User 타입의 모습:
type User = {
id: number;
name: string;
roles: ('admin' | 'user')[];
}
*/
// 3. 실제 코드에서의 활용
const newUser: User = {
id: 1,
name: '홍길동',
roles: ['admin'] // 타입 자동 완성 및 체크 지원
};
Fastify 실무 적용 예시
import { FastifyPluginAsync } from 'fastify';
const userRoute: FastifyPluginAsync = async (fastify) => {
// 제네릭을 통해 request.body의 타입을 자동으로 추론합니다.
fastify.post<{ Body: Static<typeof UserSchema> }>(
'/register',
{
schema: {
body: UserSchema, // API 요청 시 실시간 유효성 검증 수행
}
},
async (request, reply) => {
// request.body는 이미 'User' 타입으로 완벽하게 추론됩니다.
const { name } = request.body;
return reply.status(201).send({ message: `${name}님 환영합니다.` });
}
);
};
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'4.Node.js > Vitest&TypeBox' 카테고리의 다른 글
| [TypeBox] 4편. Fastify에서 null,undefined,optional의 계층별 처리 예시 (1) | 2026.02.26 |
|---|---|
| [TypeBox] 3편. Fastify TypeBox(legacy) 스키마 패턴 모음 (0) | 2026.02.24 |
| [TypeBox] 1편. Fastify에서 TypeBox를 사용하는 이유와 적용 방법 (0) | 2026.02.19 |
| [Vitest] 8편. Vitest + V8 기반 테스트 커버리지 : @vitest/coverage-v8 (0) | 2026.02.11 |
| [Vitest] 7편. Vitest Mocking 이해하기 : vi.mock()과 vi.fn() (0) | 2026.02.10 |