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

프로시저 정의하기

비공식 베타 번역

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

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

  • 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를 받아 사용자가 해당 조직에 속해 있는지 검증하는 또 다른 기본 프로시저를 만듭니다.

이는 단순화된 예시입니다. 실제로는 헤더, 컨텍스트, 미들웨어, 메타데이터를 조합하여 사용자를 인증권한 부여하는 것이 좋습니다.

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 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 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;
}),
});

구독(Subscriptions)

구독에 대한 정보는 구독 가이드에서 확인할 수 있습니다.