본문으로 건너뛰기
버전: 11.x

프로시저 정의하기

프로시저는 클라이언트에 노출되는 함수이며, 다음 중 하나일 수 있습니다:

  • Query - 데이터를 가져오는 데 사용되며, 일반적으로 데이터를 변경하지 않습니다
  • Mutation - 데이터를 전송하는 데 사용되며, 주로 생성/수정/삭제 목적으로 사용됩니다
  • Subscription - 실시간 이벤트 스트림이 필요할 때 사용합니다. 자세한 내용은 구독 문서를 참조하세요

tRPC의 프로시저는 백엔드 함수를 생성하기 위한 매우 유연한 기본 요소입니다. 불변 빌더 패턴을 사용하므로, 여러 프로시저 간에 기능을 공유하는 재사용 가능한 기본 프로시저를 생성할 수 있습니다.

프로시저 작성하기

tRPC 설정 중에 생성하는 t 객체는 모든 다른 프로시저의 기반이 되는 초기 t.procedure를 반환합니다:

ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
 
const t = initTRPC.context<{ signGuestBook: () => Promise<void> }>().create();
 
export const router = t.router;
export const publicProcedure = t.procedure;
 
const appRouter = router({
// Queries are the best place to fetch data
hello: publicProcedure.query(() => {
return {
message: 'hello world',
};
}),
 
// Mutations are the best place to do things like updating a database
goodbye: publicProcedure.mutation(async (opts) => {
await opts.ctx.signGuestBook();
 
return {
message: 'goodbye!',
};
}),
});
ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
 
const t = initTRPC.context<{ signGuestBook: () => Promise<void> }>().create();
 
export const router = t.router;
export const publicProcedure = t.procedure;
 
const appRouter = router({
// Queries are the best place to fetch data
hello: publicProcedure.query(() => {
return {
message: 'hello world',
};
}),
 
// Mutations are the best place to do things like updating a database
goodbye: publicProcedure.mutation(async (opts) => {
await opts.ctx.signGuestBook();
 
return {
message: 'goodbye!',
};
}),
});

재사용 가능한 "기본 프로시저"

일반적인 패턴으로, t.procedurepublicProcedure로 이름 변경하고 내보내는 것을 권장합니다. 이렇게 하면 특정 사용 사례를 위한 다른 이름의 프로시저를 생성하고 그것들도 내보낼 수 있는 공간을 확보할 수 있습니다. 이 패턴은 "기본 프로시저"라고 불리며, tRPC에서 코드 및 동작 재사용을 위한 핵심 패턴입니다. 모든 애플리케이션은 이를 필요로 할 가능성이 높습니다.

아래 코드에서는 재사용 가능한 기본 프로시저를 사용하여 앱의 일반적인 사용 사례를 구축하고 있습니다. 로그인한 사용자(authedProcedure)를 위한 재사용 가능한 기본 프로시저와, organizationId를 받아 사용자가 해당 조직의 구성원인지 검증하는 또 다른 기본 프로시저를 만들고 있습니다.

이는 단순화된 예시입니다. 실제 환경에서는 Headers, Context, Middleware, Metadata의 조합을 사용하여 사용자를 인증하고 인가하는 것이 좋습니다.

ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
 
type Organization = {
id: string;
name: string;
};
type Membership = {
role: 'ADMIN' | 'MEMBER';
Organization: Organization;
};
type User = {
id: string;
memberships: Membership[];
};
type Context = {
/**
* User is nullable
*/
user: User | null;
};
 
const t = initTRPC.context<Context>().create();
 
export const publicProcedure = t.procedure;
 
// procedure that asserts that the user is logged in
export const authedProcedure = t.procedure.use(async function isAuthed(opts) {
const { ctx } = opts;
// `ctx.user` is nullable
if (!ctx.user) {
(property) user: User | null
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
 
return opts.next({
ctx: {
// ✅ user value is known to be non-null now
user: ctx.user,
},
});
});
 
// procedure that asserts a user is a member of a specific organization
export const organizationProcedure = authedProcedure
.input(z.object({ organizationId: z.string() }))
.use(function isMemberOfOrganization(opts) {
const membership = opts.ctx.user.memberships.find(
(m) => m.Organization.id === opts.input.organizationId,
);
if (!membership) {
throw new TRPCError({
code: 'FORBIDDEN',
});
}
return opts.next({
ctx: {
Organization: membership.Organization,
},
});
});
 
export const appRouter = t.router({
whoami: authedProcedure.query(async (opts) => {
// user is non-nullable here
const { ctx } = opts;
const ctx: { user: User; }
return ctx.user;
}),
addMember: organizationProcedure
.input(
z.object({
email: z.string().email(),
}),
)
.mutation((opts) => {
// ctx contains the non-nullable user & the organization being queried
const { ctx } = opts;
const ctx: { user: User; Organization: Organization; }
 
// input includes the validated email of the user being invited & the validated organizationId
const { input } = opts;
const input: { organizationId: string; email: string; }
 
return '...';
}),
});
ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
 
type Organization = {
id: string;
name: string;
};
type Membership = {
role: 'ADMIN' | 'MEMBER';
Organization: Organization;
};
type User = {
id: string;
memberships: Membership[];
};
type Context = {
/**
* User is nullable
*/
user: User | null;
};
 
const t = initTRPC.context<Context>().create();
 
export const publicProcedure = t.procedure;
 
// procedure that asserts that the user is logged in
export const authedProcedure = t.procedure.use(async function isAuthed(opts) {
const { ctx } = opts;
// `ctx.user` is nullable
if (!ctx.user) {
(property) user: User | null
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
 
return opts.next({
ctx: {
// ✅ user value is known to be non-null now
user: ctx.user,
},
});
});
 
// procedure that asserts a user is a member of a specific organization
export const organizationProcedure = authedProcedure
.input(z.object({ organizationId: z.string() }))
.use(function isMemberOfOrganization(opts) {
const membership = opts.ctx.user.memberships.find(
(m) => m.Organization.id === opts.input.organizationId,
);
if (!membership) {
throw new TRPCError({
code: 'FORBIDDEN',
});
}
return opts.next({
ctx: {
Organization: membership.Organization,
},
});
});
 
export const appRouter = t.router({
whoami: authedProcedure.query(async (opts) => {
// user is non-nullable here
const { ctx } = opts;
const ctx: { user: User; }
return ctx.user;
}),
addMember: organizationProcedure
.input(
z.object({
email: z.string().email(),
}),
)
.mutation((opts) => {
// ctx contains the non-nullable user & the organization being queried
const { ctx } = opts;
const ctx: { user: User; Organization: Organization; }
 
// input includes the validated email of the user being invited & the validated organizationId
const { input } = opts;
const input: { organizationId: string; email: string; }
 
return '...';
}),
});

"기본 프로시저"의 옵션 타입 추론

프로시저의 입력 및 출력 타입을 추론할 수 있을 뿐 아니라, inferProcedureBuilderResolverOptions 헬퍼로 특정 프로시저 빌더(또는 기본 프로시저)의 옵션 타입도 추론할 수 있습니다.

이 타입 헬퍼는 함수의 매개변수에 타입을 선언하는 데 유용합니다. 예를 들어, 프로시저의 핸들러(주 실행 코드)를 라우터의 정의와 분리하거나, 여러 프로시저와 함께 작동하는 헬퍼 함수를 생성하는 경우에 사용할 수 있습니다.

ts
async function getMembersOfOrganization(
opts: inferProcedureBuilderResolverOptions<typeof organizationProcedure>,
) {
// input and ctx are now correctly typed!
const { ctx, input } = opts;
 
return await prisma.user.findMany({
where: {
membership: {
organizationId: ctx.Organization.id,
},
},
});
}
export const appRouter = t.router({
listMembers: organizationProcedure.query(async (opts) => {
// use helper function!
const members = await getMembersOfOrganization(opts);
 
return members;
}),
});
ts
async function getMembersOfOrganization(
opts: inferProcedureBuilderResolverOptions<typeof organizationProcedure>,
) {
// input and ctx are now correctly typed!
const { ctx, input } = opts;
 
return await prisma.user.findMany({
where: {
membership: {
organizationId: ctx.Organization.id,
},
},
});
}
export const appRouter = t.router({
listMembers: organizationProcedure.query(async (opts) => {
// use helper function!
const members = await getMembersOfOrganization(opts);
 
return members;
}),
});

구독

구독에 대한 정보는 구독 가이드를 참고하세요.