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

서버 액션

서버 액션을 사용하면 서버에서 함수를 정의하고 클라이언트 컴포넌트에서 직접 호출할 수 있으며, 네트워크 계층은 프레임워크에 의해 추상화됩니다.

tRPC 프로시저를 사용하여 서버 액션을 정의하면 입력 검증, 미들웨어를 통한 인증 및 인가, 출력 검증, 데이터 트랜스포머 등 tRPC의 모든 내장 기능을 활용할 수 있습니다.

정보

서버 액션 통합은 experimental_ 접두사를 사용하며 아직 활발한 개발 단계에 있습니다. 향후 릴리스에서 API가 변경될 수 있습니다.

서버 액션 프로시저 설정

1. experimental_caller를 사용하여 기본 프로시저 정의

프로시저 빌더에 experimental_callerexperimental_nextAppDirCaller를 함께 사용하여 일반 함수(서버 액션)로 호출할 수 있는 프로시저를 생성합니다. pathExtractor 옵션을 사용하면 메타데이터로 프로시저를 식별할 수 있으며, user.byId와 같은 라우터 경로가 없는 서버 액션의 경우 로깅 및 관찰 가능성 측면에서 유용합니다.

server/trpc.ts
ts
import { initTRPC, TRPCError } from '@trpc/server';
import { experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir';
 
interface Meta {
span: string;
}
 
export const t = initTRPC.meta<Meta>().create();
 
export const serverActionProcedure = t.procedure.experimental_caller(
experimental_nextAppDirCaller({
pathExtractor: ({ meta }) => (meta as Meta)?.span ?? '',
}),
);
server/trpc.ts
ts
import { initTRPC, TRPCError } from '@trpc/server';
import { experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir';
 
interface Meta {
span: string;
}
 
export const t = initTRPC.meta<Meta>().create();
 
export const serverActionProcedure = t.procedure.experimental_caller(
experimental_nextAppDirCaller({
pathExtractor: ({ meta }) => (meta as Meta)?.span ?? '',
}),
);

2. 미들웨어를 통해 컨텍스트 추가

서버 액션은 HTTP 어댑터를 거치지 않으므로 컨텍스트를 주입할 createContext가 없습니다. 대신 세션 데이터와 같은 컨텍스트를 제공하기 위해 미들웨어를 사용합니다:

server/trpc.ts
ts
import { initTRPC, TRPCError } from '@trpc/server';
import { experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir';
import { currentUser } from '../auth';
 
interface Meta {
span: string;
}
 
export const t = initTRPC.meta<Meta>().create();
 
export const serverActionProcedure = t.procedure
.experimental_caller(
experimental_nextAppDirCaller({
pathExtractor: ({ meta }) => (meta as Meta)?.span ?? '',
}),
)
.use(async (opts) => {
const user = await currentUser();
return opts.next({ ctx: { user } });
});
server/trpc.ts
ts
import { initTRPC, TRPCError } from '@trpc/server';
import { experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir';
import { currentUser } from '../auth';
 
interface Meta {
span: string;
}
 
export const t = initTRPC.meta<Meta>().create();
 
export const serverActionProcedure = t.procedure
.experimental_caller(
experimental_nextAppDirCaller({
pathExtractor: ({ meta }) => (meta as Meta)?.span ?? '',
}),
)
.use(async (opts) => {
const user = await currentUser();
return opts.next({ ctx: { user } });
});

3. 보호된 액션 프로시저 생성

인가 미들웨어를 추가하여 인증이 필요한 액션에 재사용 가능한 기본 설정을 만듭니다:

server/trpc.ts
ts
export const protectedAction = serverActionProcedure.use((opts) => {
if (!opts.ctx.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
});
}
 
return opts.next({
ctx: {
...opts.ctx,
user: opts.ctx.user, // ensures type is non-nullable
},
});
});
server/trpc.ts
ts
export const protectedAction = serverActionProcedure.use((opts) => {
if (!opts.ctx.user) {
throw new TRPCError({
code: 'UNAUTHORIZED',
});
}
 
return opts.next({
ctx: {
...opts.ctx,
user: opts.ctx.user, // ensures type is non-nullable
},
});
});

서버 액션 정의

"use server" 지시문이 포함된 파일을 생성하고 프로시저 빌더를 사용하여 액션을 정의합니다:

app/_actions.ts
ts
'use server';
 
import { z } from 'zod';
import { protectedAction } from '../server/trpc';
 
export const createPost = protectedAction
.input(
z.object({
title: z.string(),
}),
)
.mutation(async (opts) => {
// opts.ctx.user is typed as non-nullable
// opts.input is typed as { title: string }
// Create the post...
});
app/_actions.ts
ts
'use server';
 
import { z } from 'zod';
import { protectedAction } from '../server/trpc';
 
export const createPost = protectedAction
.input(
z.object({
title: z.string(),
}),
)
.mutation(async (opts) => {
// opts.ctx.user is typed as non-nullable
// opts.input is typed as { title: string }
// Create the post...
});

experimental_caller 덕분에 프로시저는 이제 서버 액션으로 사용할 수 있는 일반 비동기 함수가 됩니다.

클라이언트 컴포넌트에서 호출

서버 액션을 가져와 클라이언트 컴포넌트에서 사용합니다. 서버 액션은 점진적 강화용 action 속성과 onSubmit을 통한 프로그래매틱 호출 모두와 함께 작동합니다:

app/post-form.tsx
tsx
'use client';
 
import { createPost } from '../_actions';
 
export function PostForm() {
return (
<form
onSubmit={async (e) => {
e.preventDefault();
const title = new FormData(e.currentTarget).get('title') as string;
await createPost({ title });
}}
>
<input type="text" name="title" />
<button type="submit">Create Post</button>
</form>
);
}
app/post-form.tsx
tsx
'use client';
 
import { createPost } from '../_actions';
 
export function PostForm() {
return (
<form
onSubmit={async (e) => {
e.preventDefault();
const title = new FormData(e.currentTarget).get('title') as string;
await createPost({ title });
}}
>
<input type="text" name="title" />
<button type="submit">Create Post</button>
</form>
);
}

메타데이터를 통한 관찰 가능성 추가

.meta() 메서드를 사용하여 로깅 또는 트레이싱을 위해 액션에 태그를 지정합니다. 메타데이터의 span 속성은 pathExtractor로 전달되므로 관찰 가능성 도구에서 사용할 수 있습니다:

app/_actions.ts
ts
'use server';
 
import { z } from 'zod';
import { protectedAction } from '../server/trpc';
 
export const createPost = protectedAction
.meta({ span: 'create-post' })
.input(
z.object({
title: z.string(),
}),
)
.mutation(async (opts) => {
// ...
});
app/_actions.ts
ts
'use server';
 
import { z } from 'zod';
import { protectedAction } from '../server/trpc';
 
export const createPost = protectedAction
.meta({ span: 'create-post' })
.input(
z.object({
title: z.string(),
}),
)
.mutation(async (opts) => {
// ...
});

서버 액션과 뮤테이션 사용 시점

서버 액션은 모든 tRPC 뮤테이션의 대체재가 아닙니다. 다음 트레이드오프를 고려하세요:

  • 서버 액션 사용: 점진적 강화(JavaScript 없이 작동하는 폼)가 필요하거나, 클라이언트 측 React Query 캐시를 업데이트할 필요가 없는 액션인 경우
  • useMutation 사용: 클라이언트 측 캐시를 업데이트하거나, 낙관적 업데이트를 표시하거나, UI에서 복잡한 로딩/오류 상태를 관리해야 하는 경우

기존 tRPC API와 함께 서버 액션을 점진적으로 도입할 수 있으며, 전체 API를 다시 작성할 필요가 없습니다.