프로시저 정의하기
프로시저는 클라이언트에 노출되는 함수이며, 다음 중 하나일 수 있습니다:
Query- 데이터를 가져오는 데 사용되며, 일반적으로 데이터를 변경하지 않습니다Mutation- 데이터를 전송하는 데 사용되며, 주로 생성/수정/삭제 목적으로 사용됩니다Subscription- 실시간 이벤트 스트림이 필요할 때 사용합니다. 자세한 내용은 구독 문서를 참조하세요
tRPC의 프로시저는 백엔드 함수를 생성하기 위한 매우 유연한 기본 요소입니다. 불변 빌더 패턴을 사용하므로, 여러 프로시저 간에 기능을 공유하는 재사용 가능한 기본 프로시저를 생성할 수 있습니다.
프로시저 작성하기
tRPC 설정 중에 생성하는 t 객체는 모든 다른 프로시저의 기반이 되는 초기 t.procedure를 반환합니다:
tsimport {initTRPC } from '@trpc/server';import {z } from 'zod';constt =initTRPC .context <{signGuestBook : () =>Promise <void> }>().create ();export constrouter =t .router ;export constpublicProcedure =t .procedure ;constappRouter =router ({// Queries are the best place to fetch datahello :publicProcedure .query (() => {return {message : 'hello world',};}),// Mutations are the best place to do things like updating a databasegoodbye :publicProcedure .mutation (async (opts ) => {awaitopts .ctx .signGuestBook ();return {message : 'goodbye!',};}),});
tsimport {initTRPC } from '@trpc/server';import {z } from 'zod';constt =initTRPC .context <{signGuestBook : () =>Promise <void> }>().create ();export constrouter =t .router ;export constpublicProcedure =t .procedure ;constappRouter =router ({// Queries are the best place to fetch datahello :publicProcedure .query (() => {return {message : 'hello world',};}),// Mutations are the best place to do things like updating a databasegoodbye :publicProcedure .mutation (async (opts ) => {awaitopts .ctx .signGuestBook ();return {message : 'goodbye!',};}),});
재사용 가능한 "기본 프로시저"
일반적인 패턴으로, t.procedure를 publicProcedure로 이름 변경하고 내보내는 것을 권장합니다. 이렇게 하면 특정 사용 사례를 위한 다른 이름의 프로시저를 생성하고 그것들도 내보낼 수 있는 공간을 확보할 수 있습니다. 이 패턴은 "기본 프로시저"라고 불리며, tRPC에서 코드 및 동작 재사용을 위한 핵심 패턴입니다. 모든 애플리케이션은 이를 필요로 할 가능성이 높습니다.
아래 코드에서는 재사용 가능한 기본 프로시저를 사용하여 앱의 일반적인 사용 사례를 구축하고 있습니다. 로그인한 사용자(authedProcedure)를 위한 재사용 가능한 기본 프로시저와, organizationId를 받아 사용자가 해당 조직의 구성원인지 검증하는 또 다른 기본 프로시저를 만들고 있습니다.
이는 단순화된 예시입니다. 실제 환경에서는 Headers, Context, Middleware, Metadata의 조합을 사용하여 사용자를 인증하고 인가하는 것이 좋습니다.
tsimport {initTRPC ,TRPCError } from '@trpc/server';import {z } from 'zod';typeOrganization = {id : string;name : string;};typeMembership = {role : 'ADMIN' | 'MEMBER';Organization :Organization ;};typeUser = {id : string;memberships :Membership [];};typeContext = {/*** User is nullable*/user :User | null;};constt =initTRPC .context <Context >().create ();export constpublicProcedure =t .procedure ;// procedure that asserts that the user is logged inexport constauthedProcedure =t .procedure .use (async functionisAuthed (opts ) {const {ctx } =opts ;// `ctx.user` is nullableif (!ctx .user ) {throw newTRPCError ({code : 'UNAUTHORIZED' });}returnopts .next ({ctx : {// ✅ user value is known to be non-null nowuser :ctx .user ,},});});// procedure that asserts a user is a member of a specific organizationexport constorganizationProcedure =authedProcedure .input (z .object ({organizationId :z .string () })).use (functionisMemberOfOrganization (opts ) {constmembership =opts .ctx .user .memberships .find ((m ) =>m .Organization .id ===opts .input .organizationId ,);if (!membership ) {throw newTRPCError ({code : 'FORBIDDEN',});}returnopts .next ({ctx : {Organization :membership .Organization ,},});});export constappRouter =t .router ({whoami :authedProcedure .query (async (opts ) => {// user is non-nullable hereconst {ctx } =opts ;returnctx .user ;}),addMember :organizationProcedure .input (z .object ({z .string ().}),).mutation ((opts ) => {// ctx contains the non-nullable user & the organization being queriedconst {ctx } =opts ;// input includes the validated email of the user being invited & the validated organizationIdconst {input } =opts ;return '...';}),});
tsimport {initTRPC ,TRPCError } from '@trpc/server';import {z } from 'zod';typeOrganization = {id : string;name : string;};typeMembership = {role : 'ADMIN' | 'MEMBER';Organization :Organization ;};typeUser = {id : string;memberships :Membership [];};typeContext = {/*** User is nullable*/user :User | null;};constt =initTRPC .context <Context >().create ();export constpublicProcedure =t .procedure ;// procedure that asserts that the user is logged inexport constauthedProcedure =t .procedure .use (async functionisAuthed (opts ) {const {ctx } =opts ;// `ctx.user` is nullableif (!ctx .user ) {throw newTRPCError ({code : 'UNAUTHORIZED' });}returnopts .next ({ctx : {// ✅ user value is known to be non-null nowuser :ctx .user ,},});});// procedure that asserts a user is a member of a specific organizationexport constorganizationProcedure =authedProcedure .input (z .object ({organizationId :z .string () })).use (functionisMemberOfOrganization (opts ) {constmembership =opts .ctx .user .memberships .find ((m ) =>m .Organization .id ===opts .input .organizationId ,);if (!membership ) {throw newTRPCError ({code : 'FORBIDDEN',});}returnopts .next ({ctx : {Organization :membership .Organization ,},});});export constappRouter =t .router ({whoami :authedProcedure .query (async (opts ) => {// user is non-nullable hereconst {ctx } =opts ;returnctx .user ;}),addMember :organizationProcedure .input (z .object ({z .string ().}),).mutation ((opts ) => {// ctx contains the non-nullable user & the organization being queriedconst {ctx } =opts ;// input includes the validated email of the user being invited & the validated organizationIdconst {input } =opts ;return '...';}),});
"기본 프로시저"의 옵션 타입 추론
프로시저의 입력 및 출력 타입을 추론할 수 있을 뿐 아니라, inferProcedureBuilderResolverOptions 헬퍼로 특정 프로시저 빌더(또는 기본 프로시저)의 옵션 타입도 추론할 수 있습니다.
이 타입 헬퍼는 함수의 매개변수에 타입을 선언하는 데 유용합니다. 예를 들어, 프로시저의 핸들러(주 실행 코드)를 라우터의 정의와 분리하거나, 여러 프로시저와 함께 작동하는 헬퍼 함수를 생성하는 경우에 사용할 수 있습니다.
tsasync 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;}),});
tsasync 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;}),});
구독
구독에 대한 정보는 구독 가이드를 참고하세요.