본문 바로가기
버전: 10.x

입력 및 출력 검증기

비공식 베타 번역

이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →

tRPC 프로시저는 입력 및/또는 출력에 대한 검증 로직을 정의할 수 있으며, 검증기는 입력과 출력의 타입을 추론하는 데에도 사용됩니다. 우리는 많은 인기 있는 검증기에 대해 일류(first-class) 지원을 제공하며, 직접 지원하지 않는 검증기도 통합할 수 있습니다.

입력 검증기​

입력 검증기를 정의하면 tRPC는 프로시저 호출이 올바른지 확인하고, 그렇지 않은 경우 검증 오류를 반환할 수 있습니다.

입력 검증기를 설정하려면 procedure.input() 메서드를 사용하세요:

ts
// @target: esnext
import { initTRPC } from '@trpc/server';
// ---cut---
// Our examples use Zod by default, but usage with other libraries is identical
import { z } from 'zod';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(
z.object({
name: z.string(),
}),
)
.query((opts) => {
const name = opts.input.name;
// ^?
return {
greeting: `Hello ${opts.input.name}`,
};
}),
});
ts
// @target: esnext
import { initTRPC } from '@trpc/server';
// ---cut---
// Our examples use Zod by default, but usage with other libraries is identical
import { z } from 'zod';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(
z.object({
name: z.string(),
}),
)
.query((opts) => {
const name = opts.input.name;
// ^?
return {
greeting: `Hello ${opts.input.name}`,
};
}),
});

입력 병합​

.input()은 더 복잡한 타입을 구성하기 위해 중첩해서 사용할 수 있으며, 이는 특히 미들웨어에서 여러 프로시저에 공통으로 적용되는 입력을 활용하려는 경우에 유용합니다.

ts
// @target: esnext
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
export const t = initTRPC.create();
// ---cut---
const baseProcedure = t.procedure
.input(z.object({ townName: z.string() }))
.use((opts) => {
const input = opts.input;
// ^?
console.log(`Handling request with user from: ${input.townName}`);
return opts.next();
});
export const appRouter = t.router({
hello: baseProcedure
.input(
z.object({
name: z.string(),
}),
)
.query((opts) => {
const input = opts.input;
// ^?
return {
greeting: `Hello ${input.name}, my friend from ${input.townName}`,
};
}),
});
ts
// @target: esnext
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
export const t = initTRPC.create();
// ---cut---
const baseProcedure = t.procedure
.input(z.object({ townName: z.string() }))
.use((opts) => {
const input = opts.input;
// ^?
console.log(`Handling request with user from: ${input.townName}`);
return opts.next();
});
export const appRouter = t.router({
hello: baseProcedure
.input(
z.object({
name: z.string(),
}),
)
.query((opts) => {
const input = opts.input;
// ^?
return {
greeting: `Hello ${input.name}, my friend from ${input.townName}`,
};
}),
});

출력 검증기​

tRPC는 프로시저의 반환 타입을 추론하여 자동 타입 안전성을 제공하므로, 출력 검증은 입력 정의만큼 항상 중요하지는 않습니다. 출력 검증기를 정의해야 하는 이유는 다음과 같습니다:

  • 신뢰할 수 없는 소스에서 반환된 데이터가 올바른지 확인하기 위해

  • 클라이언트에 불필요한 데이터를 반환하지 않도록 보장하기 위해

정보

출력 검증이 실패하면 서버는 INTERNAL_SERVER_ERROR로 응답합니다.

ts
// @target: esnext
import { initTRPC } from '@trpc/server';
// @noErrors
// ---cut---
import { z } from 'zod';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.output(
z.object({
greeting: z.string(),
}),
)
.query((opts) => {
return {
gre,
// ^|
};
}),
});
ts
// @target: esnext
import { initTRPC } from '@trpc/server';
// @noErrors
// ---cut---
import { z } from 'zod';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.output(
z.object({
greeting: z.string(),
}),
)
.query((opts) => {
return {
gre,
// ^|
};
}),
});

가장 기본적인 검증기: 함수​

타사 의존성 없이 함수만으로 검증기를 정의할 수 있습니다.

정보

특별한 필요가 없다면 커스텀 검증기를 만드는 것을 권장하지 않지만, 여기에는 어떤 마법도 없다는 점(단순히 타입스크립트일 뿐!)을 이해하는 것이 중요합니다!

대부분의 경우 검증 라이브러리 사용을 권장합니다.

ts
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input((value): string => {
if (typeof value === 'string') {
return value;
}
throw new Error('Input is not a string');
})
.output((value): string => {
if (typeof value === 'string') {
return value;
}
throw new Error('Output is not a string');
})
.query((opts) => {
const { input } = opts;
// ^?
return `hello ${input}`;
}),
});
export type AppRouter = typeof appRouter;
ts
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input((value): string => {
if (typeof value === 'string') {
return value;
}
throw new Error('Input is not a string');
})
.output((value): string => {
if (typeof value === 'string') {
return value;
}
throw new Error('Output is not a string');
})
.query((opts) => {
const { input } = opts;
// ^?
return `hello ${input}`;
}),
});
export type AppRouter = typeof appRouter;

라이브러리 통합​

tRPC는 여러 인기 있는 검증 및 파싱 라이브러리와 즉시 사용 가능(out-of-the-box)하게 작동합니다. 아래는 공식적으로 지원을 유지하는 검증기 사용 예시입니다.

Zod와 함께 사용하기​

Zod는 우리의 기본 추천 라이브러리입니다. 강력한 생태계를 갖춰 코드베이스의 여러 부분에서 사용하기에 탁월한 선택입니다. 특별한 선호가 없고 향후 요구사항을 제한하지 않는 강력한 라이브러리를 원한다면 Zod가 훌륭한 선택입니다.

ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(
z.object({
name: z.string(),
}),
)
.output(
z.object({
greeting: z.string(),
}),
)
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(
z.object({
name: z.string(),
}),
)
.output(
z.object({
greeting: z.string(),
}),
)
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;

Yup와 함께 사용하기​

ts
import { initTRPC } from '@trpc/server';
import * as yup from 'yup';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(
yup.object({
name: yup.string().required(),
}),
)
.output(
yup.object({
greeting: yup.string().required(),
}),
)
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
ts
import { initTRPC } from '@trpc/server';
import * as yup from 'yup';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(
yup.object({
name: yup.string().required(),
}),
)
.output(
yup.object({
greeting: yup.string().required(),
}),
)
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;

Superstruct와 함께 사용하기​

ts
import { initTRPC } from '@trpc/server';
import { object, string } from 'superstruct';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(object({ name: string() }))
.output(object({ greeting: string() }))
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
ts
import { initTRPC } from '@trpc/server';
import { object, string } from 'superstruct';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(object({ name: string() }))
.output(object({ greeting: string() }))
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;

scale-ts와 함께 사용하기​

ts
import { initTRPC } from '@trpc/server';
import * as $ from 'scale-codec';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input($.object($.field('name', $.str)))
.output($.object($.field('greeting', $.str)))
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
ts
import { initTRPC } from '@trpc/server';
import * as $ from 'scale-codec';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input($.object($.field('name', $.str)))
.output($.object($.field('greeting', $.str)))
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;

Typia와 함께 사용하기​

ts
import { initTRPC } from '@trpc/server';
import typia from 'typia';
import { v4 } from 'uuid';
import { IBbsArticle } from '../structures/IBbsArticle';
const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
store: publicProcedure
.input(typia.createAssert<IBbsArticle.IStore>())
.output(typia.createAssert<IBbsArticle>())
.query(({ input }) => {
return {
id: v4(),
writer: input.writer,
title: input.title,
body: input.body,
created_at: new Date().toString(),
};
}),
});
export type AppRouter = typeof appRouter;
ts
import { initTRPC } from '@trpc/server';
import typia from 'typia';
import { v4 } from 'uuid';
import { IBbsArticle } from '../structures/IBbsArticle';
const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
store: publicProcedure
.input(typia.createAssert<IBbsArticle.IStore>())
.output(typia.createAssert<IBbsArticle>())
.query(({ input }) => {
return {
id: v4(),
writer: input.writer,
title: input.title,
body: input.body,
created_at: new Date().toString(),
};
}),
});
export type AppRouter = typeof appRouter;

ArkType와 함께 사용하기​

ts
import { initTRPC } from '@trpc/server';
import { type } from 'arktype';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(type({ name: 'string' }).assert)
.output(type({ greeting: 'string' }).assert)
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
ts
import { initTRPC } from '@trpc/server';
import { type } from 'arktype';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(type({ name: 'string' }).assert)
.output(type({ greeting: 'string' }).assert)
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;

@effect/schema와 함께 사용하기​

ts
import * as Schema from '@effect/schema/Schema';
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(Schema.decodeUnknownSync(Schema.Struct({ name: Schema.String })))
.output(
Schema.decodeUnknownSync(Schema.Struct({ greeting: Schema.String })),
)
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
ts
import * as Schema from '@effect/schema/Schema';
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(Schema.decodeUnknownSync(Schema.Struct({ name: Schema.String })))
.output(
Schema.decodeUnknownSync(Schema.Struct({ greeting: Schema.String })),
)
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;

runtypes와 함께 사용하기​

ts
import { initTRPC } from '@trpc/server';
import * as T from 'runtypes';
const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(T.Record({ name: T.String }))
.output(T.Record({ greeting: T.String }))
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
ts
import { initTRPC } from '@trpc/server';
import * as T from 'runtypes';
const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(T.Record({ name: T.String }))
.output(T.Record({ greeting: T.String }))
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;

Valibot와 함께 사용하기​

ts
import { wrap } from '@decs/typeschema';
import { initTRPC } from '@trpc/server';
import { object, string } from 'valibot';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(wrap(object({ name: string() })))
.output(wrap(object({ greeting: string() })))
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
ts
import { wrap } from '@decs/typeschema';
import { initTRPC } from '@trpc/server';
import { object, string } from 'valibot';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(wrap(object({ name: string() })))
.output(wrap(object({ greeting: string() })))
.query(({ input }) => {
// ^?
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;

커스텀 유효성 검사 라이브러리 기여하기​

tRPC 사용을 지원하는 유효성 검사 라이브러리를 작업 중이라면, 이 페이지에 다른 예시와 동등한 사용법 및 문서 링크를 포함한 PR(Pull Request)을 자유롭게 열어주세요.

대부분의 경우 tRPC와의 통합은 기존 타입 인터페이스 중 하나를 충족하는 것만큼 간단하지만, 경우에 따라 새로운 지원 인터페이스를 추가하기 위한 PR을 수락할 수도 있습니다. 논의를 위해 이슈를 자유롭게 열어주세요. 기존 지원 인터페이스는 여기에서 확인할 수 있습니다: