[TypeBox] 1편. Fastify에서 TypeBox를 사용하는 이유와 적용 방법
📚 목차
1. Fastify에서 Schema가 필요한 이유
2. TypeBox의 역할과 동작 원리
3. Fastify에서 TypeBox 사용을 위한 기본 설정
4. 코드 연결하기: TypeBox 적용 예시

1. Fastify에서 Schema가 필요한 이유
🔷 Fastify에서 Schema의 역할
Fastify는 Schema 기반 프레임워크입니다.
Route를 정의할 때 Request와 Response 구조를 Schema로 명시할 수 있으며, 이 Schema는 다음과 같은 역할을 수행합니다.
스키마를 통해 얻을 수 있는 주요 이점은 다음과 같습니다:
▸ 데이터 검증 (Validation):
서버로 들어오는 요청 데이터가 올바른 형식인지 확인합니다. 잘못된 데이터가 비즈니스 로직(핸들러)까지 도달하지 못하도록 입구에서 자동으로 차단합니다.
▸ 성능 최적화 (Serialization):
응답 데이터를 JSON으로 변환할 때, 미리 정의된 구조를 바탕으로 불필요한 연산을 생략하여 일반적인 방식보다 훨씬 빠르게 결과를 내보냅니다.
▸ 문서화 자동화: 작성한 스키마를 기반으로 Swagger(OpenAPI)와 같은 API 명세서를 별도의 작업 없이 자동으로 생성할 수 있습니다.
▸ 개발 생산성 (Type Safety): TypeScript와 함께 사용하면 코드 작성 단계에서 데이터의 형태를 미리 알 수 있어, 오타나 잘못된 데이터 접근으로 인한 실수를 방지합니다.
예를 들어, 다음과 같이 스키마를 정의하면 Fastify가 자동으로 검증을 수행합니다.
fastify.post('/users', {
schema: {
body: {
type: 'object',
required: ['email'],
properties: {
email: { type: 'string', format: 'email' }
}
}
}
}, async (request, reply) => {
// email이 없거나 형식이 틀리면 핸들러가 실행되기 전 400 Bad Request를 반환합니다.
return { ok: true }
})
🔷 Request Validation과 Response Serialization
Fastify의 성능은 검증과 직렬화를 전담하는 내부 라이브러리에서 나옵니다.
Fastify는 경로(Route)가 등록될 때 제공된 스키마를 분석하여 컴파일합니다.
🔸 Ajv를 이용한 Request Validation 구조
Fastify는 기본 검증기로 Ajv (Another JSON Schema Validator)를 사용합니다.
▸ 서버 시작 시 스키마를 Ajv 검증 함수로 컴파일합니다.
▸ 요청이 들어오면 컴파일된 함수가 즉시 실행됩니다.
▸ 이 과정은 정규표현식이나 단순 조건문보다 훨씬 빠릅니다.
🔸 fast-json-stringify를 이용한 Response 처리 구조
응답 시에는 fast-json-stringify 라이브러리가 동작합니다.
▸ 스키마를 분석하여 특정 구조에 최적화된 직렬화 코드를 생성합니다.
▸ 필요 없는 필드를 제거하고, 데이터 타입을 확정하여 직렬화 속도를 일반적인 방식 대비 최대 2~3배까지 끌어올립니다
처리 흐름은 다음과 같습니다.
Client Request 전송
↓
Schema Validation (Ajv):스키마와 대조하여 적합성 판별
↓
Handler 실행:비즈니스 로직 수행
↓
Response Serialization (fast-json-stringify)
(스키마 기반 고속 JSON 변환)
↓
Client Response 반환
이 구조 덕분에 Fastify는 런타임 오버헤드를 최소화하면서도 높은 안정성을 유지할 수 있습니다.
🔷 TypeScript 타입만으로는 부족한 이유
TypeScript 타입은 컴파일 타임에만 존재하며, 컴파일 후 JavaScript 환경(런타임)에서는 모든 타입 정보가 사라집니다.
실제 환경에서의 문제는 다음과 같습니다:
▸ 런타임 검증 불가능: 외부에서 들어오는 실제 데이터가 string인지 number인지 알 방법이 없습니다.
▸ 중복 정의 문제: 런타임 검증을 위해 JSON Schema를 쓰고, 개발 환경을 위해 TypeScript 인터페이스를 따로 작성해야 합니다.
// 1. 개발용 타입
type CreateUserDto = { email: string }
// 2. 런타임 검증용 스키마 (따로 관리해야 함!)
const CreateUserSchema = { type: 'object', properties: { email: { type: 'string' } } }
이러한 중복 정의와 타입 불일치 문제를 근본적으로 해결하기 위해 우리는 TypeBox를 사용합니다.
2. TypeBox의 역할과 동작 원리
🔷 TypeScript 타입과 JSON Schema 중복 정의 문제
기존의 개발 방식에서는 동일한 데이터 구조를 두 번 작성해야 했습니다.
▸ TypeScript 타입: 코드 작성 시 자동 완성과 타입 체크를 위해 작성 (type UserDto = { email: string })
▸ JSON Schema: 실행 시 데이터 검증을 위해 작성 ({ type: 'object', ... })
구조가 변경될 때마다 두 곳을 모두 수정해야 하며, 한 곳이라도 누락되면 런타임 에러나 타입 불일치 버그로 이어지는 등 유지보수 비용이 크게 증가합니다.
🔷 TypeBox가 해결하는 구조
TypeBox를 사용하면 단 한 번의 정의로 모든 문제를 해결할 수 있습니다.
생성된 타입은 개발 중(컴파일 타임)에 사용되고, 원본 스키마 객체는 서버 실행 중(런타임) 데이터 검증에 사용됩니다.
import { Type, Static } from '@sinclair/typebox'
// 1. 스키마 정의 (JSON Schema 생성)
const UserSchema = Type.Object({
email: Type.String()
})
// 2. 타입 추출 (TypeScript 타입 생성)
type UserDto = Static<typeof UserSchema>
🔷TypeBox가 Fastify와 잘 맞는 이유
Fastify와 TypeBox가 "최상의 궁합"이라 불리는 이유는 두 도구가 지향하는 목표가 완벽히 일치하기 때문입니다.
1. 표준의 일치:
▸ Fastify는 고속 처리를 위해 표준 JSON Schema를 요구합니다.
▸ TypeBox의 모든 출력물은 이 표준을 따르므로, 별도의 변환 과정 없이 Fastify의 핵심 엔진(Ajv)에 즉시 전달됩니다.
2. 타입 추론의 자동화
▸ Fastify는 TypeProvider라는 시스템을 통해 외부 라이브러리의 타입을 수용합니다.
▸ TypeBox는 이 시스템에 가장 최적화되어 있어, 개발자가 별도의 타입을 선언하지 않아도 request.body나 request.query의 타입을 실시간으로 추론해 줍니다.
3. 성능 저하 없는 안전성:
▸ TypeBox는 런타임에 추가적인 변환 오버헤드를 발생시키지 않습니다.
▸ 정의된 스키마는 곧바로 Fastify의 최적화 엔진으로 들어가며, 동시에 TypeScript의 엄격한 타입 체크까지 보장합니다.
3. Fastify에서 TypeBox 사용을 위한 기본 설정
🔷 패키지 설치
Fastify에서 TypeBox를 사용하려면 다음 패키지들을 설치해야 합니다. 여기서 주의할 점은 패키지 명칭과 버전의 차이입니다
npm install @sinclair/typebox
npm install @fastify/type-provider-typebox
💡 참고: @sinclair/typebox vs typebox
NPM 라이브러리 목록에는 typebox와 @sinclair/typebox 두 가지가 존재하여 혼동을 줄 수 있습니다.
구분@sinclair/typeboxtypebox
| 패키지명 | @sinclair/typebox | typebox |
| 버전 범위 | v0.x (Legacy 라인) | v1.x 이상 (Current 라인) |
| 유지보수 상태 | 유지보수는 계속되지만 레거시 라인 성격 | 현재 공식 권장 패키지 |
| 특징 | 초기 TypeBox 패키지 명칭 Fastify, Ajv, TypeScript ecosystem에서 널리 사용된 기존 네임스페이스 |
패키지명이 단순화됨 v1.0부터 구조 및 API 정리, 장기 사용 기준의 공식 패키지 |
| 향후 방향 | 기존 프로젝트 호환성 유지 목적 중심 | 신규 프로젝트 기준 권장 선택 |
💡 어떤 것을 선택해야 할까요?
▸ 안정성 중심: 현재 많은 Fastify 플러그인과 생태계가 @sinclair/typebox 범위를 의존성으로 참조하고 있습니다.
지속적으로 관리되므로 안정적인 프로젝트에 적합합니다.
▸ 최신 트렌드 중심: GitHub 공식 저장소에서는 최신 명칭인 typebox 사용을 권장하고 있습니다. 최신 기능과 간결한 패키지 경로를 원한다면 이를 선택합니다.
4. 코드 연결하기: TypeBox 적용 예시
단순히 라이브러리를 설치한다고 해서 타입이 자동으로 연결되지는 않습니다.
Fastify 인스턴스에 "TypeBox를 사용하여 타입을 추론하겠다"라고 명시적으로 알려주어야 합니다.
🔷 서버 인스턴스 생성 (app.ts)
withTypeProvider를 사용하여 Fastify 엔진에 TypeBox 시스템을 장착합니다.
// src/app.ts
import Fastify from 'fastify';
import type { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
export const createApp = async () => {
const app = Fastify({ logger: true })
// --------------------------------
// 핵심 설정: Type Provider 연결
// --------------------------------
// 이 설정을 통해 이후 정의되는 모든 라우트에서
// 스키마 기반의 자동 타입 추론이 가능해집니다.
.withTypeProvider<TypeBoxTypeProvider>();
// 플러그인 및 라우터 등록...
return app;
};
🔷 라우터에서의 활용 (user.route.ts)
이제 라우터에서는 별도의 타입 선언 없이도 안전하게 데이터를 다룰 수 있습니다.
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox';
import {
CreateUserBodySchema,
UserParamsSchema,
UserQuerySchema,
UserHeaderSchema
} from './user.dto';
// FastifyPluginAsyncTypebox 타입을 사용하여 플러그인을 정의하면
// 내부 핸들러에서 별도의 타입 지정 없이도 스키마 기반 타입 추론이 가능해집니다.
export const userRoutes: FastifyPluginAsyncTypebox = async (fastify) => {
fastify.post('/:id', {
schema: {
body: CreateUserBodySchema, // 요청 본문 검증 등록
params: UserParamsSchema, // 경로 파라미터 검증 등록
querystring: UserQuerySchema, // 쿼리 스트링 검증 등록
headers: UserHeaderSchema, // 헤더 검증 등록
response: {
// 성공 시(201) 반환될 데이터의 구조를 정의하여 직렬화 성능을 높입니다.
201: Type.Object({ ok: Type.Boolean(), userId: Type.String() })
}
}
}, async (request, reply) => {
// [중요] 타입 캐스팅(as UserDto) 없이 모든 필드에 자동 완성이 지원됩니다.
// 스키마에 정의된 형식에 맞춰 TypeScript 타입이 실시간으로 매핑됩니다.
const { id } = request.params; // string (uuid format)
const { email, name } = request.body; // string, string
const { mode } = request.query; // 'async' | 'sync' | undefined
const apiVersion = request.headers['x-api-version'];
// 비즈니스 로직 수행...
// reply.send 시에도 201 응답 스키마에 맞지 않는 데이터를 보내면 필터링됩니다.
return reply.status(201).send({ ok: true, userId: id });
});
};
🔷 설계시 핵심 포인트 : additionalProperties: false (user.dto.ts)
작성된 코드에서 가장 중요한 보안 및 데이터 정합성 설정은 additionalProperties: false입니다.
이 옵션은 스키마에 정의되지 않은 "추가 속성"을 어떻게 처리할지 결정합니다.
1. 요청(Request) 시: 불필요한 필드 차단
클라이언트가 스키마에 정의되지 않은 데이터를 보낼 경우 이를 무시하거나 에러를 발생시켜 서버의 데이터 정합성을 보호합니다.
▸ 스키마 예시
import { Type } from '@sinclair/typebox';
/**
* 회원 가입 Body 스키마
*/
export const CreateUserBodySchema = Type.Object({
email: Type.String({ format: 'email' }), // 이메일 형식 검증
password: Type.String({ minLength: 8 }), // 최소 8자 이상
name: Type.String() // 필수 문자열
});
/**
* 경로 파라미터(Path Params) 스키마
* URL의 /users/:id 부분에서 'id'를 검증합니다.
*/
export const UserParamsSchema = Type.Object({
id: Type.String({ format: 'uuid' }) // UUID 형식 검증
});
/**
* 쿼리 스트링(Query String) 스키마
* URL 뒤의 ?mode=async 등을 검증합니다.
*/
export const UserQuerySchema = Type.Object({
mode: Type.Optional(Type.Enum({ async: 'async', sync: 'sync' })) // 선택적 열거형 값
});
/**
* 요청 헤더(Headers) 스키마
* 커스텀 헤더 등의 포함 여부와 형식을 검증합니다.
*/
export const UserHeaderSchema = Type.Object({
'x-api-version': Type.String() // 필수 헤더 필드
});
▸ 요청문 예시
Fastify는 isAdmin 필드를 감지하고 요청을 차단(400 Bad Request)하거나 해당 필드를 무시합니다.
이를 통해 공격자가 의도치 않은 필드(권한 등)를 조작하는 것을 방지합니다.
{
"email": "test@test.com",
"phoneNumber": "+821012345678",
"isAdmin": true // 정의되지 않은 필드!
}
2. 응답(Response) 시: 민감 정보 유출 방지 (필터링)
서버에서 클라이언트로 데이터를 보낼 때, 스키마에 명시된 필드만 골라내어 응답을 생성합니다.
▸ 스키마 예시
export const UserResponseSchema = Type.Object(
{
id: Type.Integer(),
email: Type.String({ format: 'email' }),
displayName: Type.Union([Type.String(), Type.Null()]),
},
{ additionalProperties: false } // 응답 시에도 엄격하게 제한합니다.
);
▸ DB에서 조회된 원본 데이터 예시
{
"id": 1,
"email": "test@test.com",
"passwordHash": "$2b$10$...", // 절대 노출되면 안 되는 필드
"internalMemo": "관리용 메모", // 클라이언트는 몰라도 되는 필드
"displayName": "홍길동"
}
▸ 응답문 예시(클라이언트가 받는 JSON)
Fastify의 직렬화 엔진은 스키마를 기준으로 데이터를 필터링합니다.
클라이언트에게 전달되는 최종 JSON에는 passwordHash나 internalMemo 같은 필드가 자동으로 제거되어 안전하게 전송됩니다.
{
"id": 1,
"email": "test@test.com",
"displayName": "홍길동"
}
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'4.Node.js > Vitest&TypeBox' 카테고리의 다른 글
| [TypeBox] 3편. Fastify TypeBox(legacy) 스키마 패턴 모음 (0) | 2026.02.24 |
|---|---|
| [TypeBox] 2편. Fastify를 위한 TypeBox(legacy) 기본 문법 이해하기 (0) | 2026.02.23 |
| [Vitest] 8편. Vitest + V8 기반 테스트 커버리지 : @vitest/coverage-v8 (0) | 2026.02.11 |
| [Vitest] 7편. Vitest Mocking 이해하기 : vi.mock()과 vi.fn() (0) | 2026.02.10 |
| [Vitest] 6편. MSW(Mock Service Worker) + Vitest 실무 API 테스트 (0) | 2026.02.06 |