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

서버 액션

비공식 베타 번역

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

서버 액션을 사용하면 서버에서 함수를 정의하고 프레임워크가 네트워크 레이어를 추상화한 상태로 클라이언트 컴포넌트에서 직접 호출할 수 있습니다.

tRPC 프로시저를 사용해 서버 액션을 정의하면 입력 유효성 검사, 미들웨어를 통한 인증 및 권한 부여, 출력 유효성 검사, 데이터 변환기 등 tRPC의 모든 내장 기능을 활용할 수 있습니다.

정보

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

서버 액션 프로시저 설정하기

1. experimental_caller로 기본 프로시저 정의하기

프로시저 빌더에 experimental_nextAppDirCaller와 함께 experimental_caller를 사용하여 일반 함수(서버 액션)로 호출 가능한 프로시저를 생성하세요. 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) => {
// ...
});

서버 액션 vs 뮤테이션 사용 시기

서버 액션은 모든 tRPC 뮤테이션을 대체하지 않습니다. 다음과 같은 장단점을 고려하세요:

  • 서버 액션 사용: 점진적 향상(JavaScript 없이 동작하는 폼)이 필요하거나, 액션이 클라이언트 측 React Query 캐시를 업데이트할 필요가 없는 경우

  • useMutation 사용: 클라이언트 측 캐시 업데이트, 낙관적 업데이트 표시, UI에서 복잡한 로딩/오류 상태 관리가 필요한 경우

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