4.Node.js/Vitest&TypeBox

[TypeBox] 2편. Fastify를 위한 TypeBox(legacy) 기본 문법 이해하기

쿼드큐브 2026. 2. 23. 12:21
반응형
반응형

 

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 예시 설명
email 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 도구의 도움을 받아 생성되거나 다듬어졌습니다.

반응형

 

반응형