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

v9에서 v10으로 마이그레이션

비공식 베타 번역

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

tRPC v10에 오신 것을 환영합니다! 탁월한 개발자 경험(DX)과 완벽한 종단간 타입 안전성을 위한 여정을 계속 이어갈 수 있는 새로운 메이저 버전을 제공하게 되어 기쁩니다.

v10 내부에서는 성능 개선을 구현하고, 편의성 향상을 제공하며, 향후 새로운 기능을 구축할 수 있는 여지를 마련했습니다.

tRPC v10은 v9에서 전환하는 사용자를 위한 호환성 레이어를 제공합니다. .interop()을 사용하면 v10을 점진적으로 도입할 수 있어 프로젝트의 나머지 부분을 계속 개발하면서도 v10의 새로운 기능을 활용할 수 있습니다.

주요 변경 사항 요약

Initializing your server
/src/server/trpc.ts
ts
/**
* This is your entry point to setup the root configuration for tRPC on the server.
* - `initTRPC` should only be used once per app.
* - We export only the functionality that we use so we can enforce which base procedures should be used
*
* Learn how to create protected base procedures and other things below:
* @see https://trpc.io/docs/v10/router
* @see https://trpc.io/docs/v10/procedures
*/
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
import { Context } from './context';
const t = initTRPC.context<Context>().create({
/**
* @see https://trpc.io/docs/v10/data-transformers
*/
transformer: superjson,
/**
* @see https://trpc.io/docs/v10/error-formatting
*/
errorFormatter(opts) {
return opts.shape;
},
});
/**
* Create a router
* @see https://trpc.io/docs/v10/router
*/
export const router = t.router;
/**
* Create an unprotected procedure
* @see https://trpc.io/docs/v10/procedures
**/
export const publicProcedure = t.procedure;
/**
* @see https://trpc.io/docs/v10/merging-routers
*/
export const mergeRouters = t.mergeRouters;
/src/server/trpc.ts
ts
/**
* This is your entry point to setup the root configuration for tRPC on the server.
* - `initTRPC` should only be used once per app.
* - We export only the functionality that we use so we can enforce which base procedures should be used
*
* Learn how to create protected base procedures and other things below:
* @see https://trpc.io/docs/v10/router
* @see https://trpc.io/docs/v10/procedures
*/
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
import { Context } from './context';
const t = initTRPC.context<Context>().create({
/**
* @see https://trpc.io/docs/v10/data-transformers
*/
transformer: superjson,
/**
* @see https://trpc.io/docs/v10/error-formatting
*/
errorFormatter(opts) {
return opts.shape;
},
});
/**
* Create a router
* @see https://trpc.io/docs/v10/router
*/
export const router = t.router;
/**
* Create an unprotected procedure
* @see https://trpc.io/docs/v10/procedures
**/
export const publicProcedure = t.procedure;
/**
* @see https://trpc.io/docs/v10/merging-routers
*/
export const mergeRouters = t.mergeRouters;
Defining routers & procedures
ts
// v9:
const appRouter = trpc.router()
.query('greeting', {
input: z.string(),
resolve(opts) {
return `hello ${opts.input}!`;
},
});
// v10:
const appRouter = router({
greeting: publicProcedure
.input(z.string())
.query((opts) => `hello ${opts.input}!`),
});
ts
// v9:
const appRouter = trpc.router()
.query('greeting', {
input: z.string(),
resolve(opts) {
return `hello ${opts.input}!`;
},
});
// v10:
const appRouter = router({
greeting: publicProcedure
.input(z.string())
.query((opts) => `hello ${opts.input}!`),
});
Calling procedures
ts
// v9
client.query('greeting', 'KATT');
trpc.useQuery(['greeting', 'KATT']);
// v10
// You can now CMD+click `greeting` to jump straight to your server code.
client.greeting.query('KATT');
trpc.greeting.useQuery('KATT');
ts
// v9
client.query('greeting', 'KATT');
trpc.useQuery(['greeting', 'KATT']);
// v10
// You can now CMD+click `greeting` to jump straight to your server code.
client.greeting.query('KATT');
trpc.greeting.useQuery('KATT');
Inferring types

v9

ts
// Building multiple complex helper types yourself. Yuck!
export type TQuery = keyof AppRouter['_def']['queries'];
export type InferQueryInput<TRouteKey extends TQuery> = inferProcedureInput<
AppRouter['_def']['queries'][TRouteKey]
>;
type GreetingInput = InferQueryInput<'greeting'>;
ts
// Building multiple complex helper types yourself. Yuck!
export type TQuery = keyof AppRouter['_def']['queries'];
export type InferQueryInput<TRouteKey extends TQuery> = inferProcedureInput<
AppRouter['_def']['queries'][TRouteKey]
>;
type GreetingInput = InferQueryInput<'greeting'>;

v10

Inference helpers

ts
// Inference helpers are now shipped out of the box.
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from './server';
type RouterInput = inferRouterInputs<AppRouter>;
type RouterOutput = inferRouterOutputs<AppRouter>;
type PostCreateInput = RouterInput['post']['create'];
// ^?
type PostCreateOutput = RouterOutput['post']['create'];
// ^?
ts
// Inference helpers are now shipped out of the box.
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from './server';
type RouterInput = inferRouterInputs<AppRouter>;
type RouterOutput = inferRouterOutputs<AppRouter>;
type PostCreateInput = RouterInput['post']['create'];
// ^?
type PostCreateOutput = RouterOutput['post']['create'];
// ^?

See Inferring types for more.

Middlewares

Middlewares are now reusable and can be chained, see the middleware docs for more.

ts
// v9
const appRouter = trpc
.router()
.middleware((opts) => {
const { ctx } = opts;
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx: {
...ctx,
user: ctx.user,
},
});
})
.query('greeting', {
resolve(opts) {
return `hello ${opts.ctx.user.name}!`;
},
});
// v10
const protectedProcedure = t.procedure.use((opts) => {
const { ctx } = opts;
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx: {
// Old context will automatically be spread.
// Only modify what's changed.
user: ctx.user,
},
});
});
const appRouter = t.router({
greeting: protectedProcedure.query((opts) => {
return `Hello ${opts.ctx.user.name}!`
}),
});
ts
// v9
const appRouter = trpc
.router()
.middleware((opts) => {
const { ctx } = opts;
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx: {
...ctx,
user: ctx.user,
},
});
})
.query('greeting', {
resolve(opts) {
return `hello ${opts.ctx.user.name}!`;
},
});
// v10
const protectedProcedure = t.procedure.use((opts) => {
const { ctx } = opts;
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return opts.next({
ctx: {
// Old context will automatically be spread.
// Only modify what's changed.
user: ctx.user,
},
});
});
const appRouter = t.router({
greeting: protectedProcedure.query((opts) => {
return `Hello ${opts.ctx.user.name}!`
}),
});
Full example with data transformer, OpenAPI metadata, and error formatter
/src/server/trpc.ts
ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
// Context is usually inferred,
// but we will need it here for this example.
interface Context {
user?: {
id: string;
name: string;
};
}
interface Meta {
openapi: {
enabled: boolean;
method: string;
path: string;
};
}
export const t = initTRPC
.context<Context>()
.meta<Meta>()
.create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
transformer: superjson,
});
/src/server/trpc.ts
ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
// Context is usually inferred,
// but we will need it here for this example.
interface Context {
user?: {
id: string;
name: string;
};
}
interface Meta {
openapi: {
enabled: boolean;
method: string;
path: string;
};
}
export const t = initTRPC
.context<Context>()
.meta<Meta>()
.create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
transformer: superjson,
});

v9에서 마이그레이션하기

코드베이스 업그레이드를 **시작(그리고 완료!)**하기 위해 두 가지 전략을 추천합니다.

Codemod 사용하기

@sachinraja가 이 메이저 업그레이드를 위한 훌륭한 codemod를 개발했습니다. 스크립트를 실행하면 95%의 작업이 순식간에 완료됩니다.

정보
  • Codemod을 사용하더라도 전체 마이그레이션 전에 아래 1-3단계를 수행하여 작동을 확인해야 합니다.
  • 이 codemod는 완벽하지 않지만 많은 어려운 작업을 대신 처리해 줍니다.

.interop() 사용하기

기존 v9 라우트를 모두 재작성하는 작업이 팀에 부담스러울 수 있습니다. 대신 v9 프로시저를 유지한 채 v10의 interop() 메서드를 활용해 점진적으로 v10을 도입해 보세요.

1. v9 라우터에 interop() 활성화

v9 라우터를 v10 라우터로 전환하는 데 10글자면 충분합니다. v9 라우터 끝에 .interop()을 추가하기만 하면... 서버 코드 작업이 완료됩니다!

src/server/routers/_app.ts
diff
const appRouter = trpc
.router<Context>()
/* ... */
+ .interop();
export type AppRouter = typeof appRouter;
src/server/routers/_app.ts
diff
const appRouter = trpc
.router<Context>()
/* ... */
+ .interop();
export type AppRouter = typeof appRouter;
정보

.interop()에서 지원하지 않는 몇 가지 기능이 있습니다. 대부분의 사용자가 .interop()을 통해 서버 측 코드를 단 몇 분 만에 마이그레이션할 수 있을 것으로 기대합니다. .interop()이 제대로 작동하지 않는다면 여기를 확인하세요.

2. t 객체 생성하기

이제 v10 라우터를 초기화하여 새로 작성할 라우트에 v10을 사용해 보겠습니다.

src/server/trpc.ts
ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
import { Context } from './context';
const t = initTRPC.context<Context>().create({
// Optional:
transformer: superjson,
// Optional:
errorFormatter(opts) {
const { shape } = opts;
return {
...shape,
data: {
...shape.data,
},
};
},
});
/**
* We recommend only exporting the functionality that we
* use so we can enforce which base procedures should be used
**/
export const router = t.router;
export const mergeRouters = t.mergeRouters;
export const publicProcedure = t.procedure;
src/server/trpc.ts
ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
import { Context } from './context';
const t = initTRPC.context<Context>().create({
// Optional:
transformer: superjson,
// Optional:
errorFormatter(opts) {
const { shape } = opts;
return {
...shape,
data: {
...shape.data,
},
};
},
});
/**
* We recommend only exporting the functionality that we
* use so we can enforce which base procedures should be used
**/
export const router = t.router;
export const mergeRouters = t.mergeRouters;
export const publicProcedure = t.procedure;

3. 새로운 appRouter 생성

  1. 기존 appRouterlegacyRouter로 이름 변경

  2. 새 애플리케이션 라우터 생성:

ts
import { mergeRouters, publicProcedure, router } from './trpc';
// Renamed from `appRouter`
const legacyRouter = trpc
.router()
/* ... */
.interop();
const mainRouter = router({
greeting: publicProcedure.query(() => 'hello from tRPC v10!'),
});
// Merge v9 router with v10 router
export const appRouter = mergeRouters(legacyRouter, mainRouter);
export type AppRouter = typeof appRouter;
ts
import { mergeRouters, publicProcedure, router } from './trpc';
// Renamed from `appRouter`
const legacyRouter = trpc
.router()
/* ... */
.interop();
const mainRouter = router({
greeting: publicProcedure.query(() => 'hello from tRPC v10!'),
});
// Merge v9 router with v10 router
export const appRouter = mergeRouters(legacyRouter, mainRouter);
export type AppRouter = typeof appRouter;

동일한 caller 이름을 가지게 될 프로시저 사용에 주의하세요! 레거시 라우터의 경로가 새 라우터의 경로와 일치하면 문제가 발생할 수 있습니다.

4. 클라이언트에서 사용하기

이제 두 세트의 프로시저가 v10 caller로 클라이언트에서 사용 가능합니다. 클라이언트 코드를 방문하여 caller를 v10 문법으로 업데이트해야 합니다.

ts
// Vanilla JS v10 client caller:
client.proxy.greeting.query();
// React v10 client caller:
trpc.proxy.greeting.useQuery();
ts
// Vanilla JS v10 client caller:
client.proxy.greeting.query();
// React v10 client caller:
trpc.proxy.greeting.useQuery();

interop의 제한 사항

Subscriptions

Subscriptions의 API가 변경되어 구독은 observable 인스턴스를 반환해야 합니다. 자세한 내용은 subscriptions 문서를 참조하세요.

🚧 이 섹션 개선에 기여해 주세요

사용자 정의 HTTP 옵션

HTTP 전용 옵션이 TRPCClient에서 links로 이동을 참조하세요.

사용자 정의 링크

v10에서는 링크 아키텍처가 완전히 개편되었습니다. 따라서 v9용으로 제작된 사용자 정의 링크는 v10이나 interop 중에 작동하지 않습니다. v10용 사용자 정의 링크 생성 방법은 링크 문서를 참조하세요.

클라이언트 패키지 변경 사항

v10은 애플리케이션 클라이언트 측에도 변경 사항을 가져옵니다. 몇 가지 주요 변경 후에는 다음과 같은 개선 사항을 활용할 수 있습니다:

  • 클라이언트에서 직접 서버 정의로 이동
  • 클라이언트에서 라우터나 프로시저 이름 변경

@trpc/react-query

@trpc/react에서 @trpc/react-query로 패키지명 변경

@trpc/react 패키지가 @trpc/react-query로 이름이 변경되었습니다. 이는 react-query를 감싼 얇은 레이어임을 반영하고, 향후 React Server Components(RSCs)나 다른 데이터 가져오기 라이브러리 어댑터와 함께 사용될 수 있도록 하기 위함입니다. @trpc/react를 사용 중이라면 제거하고 @trpc/react-query를 설치한 후 import를 업데이트해야 합니다:

diff
- import { createReactQueryHooks } from '@trpc/react';
+ import { createReactQueryHooks } from '@trpc/react-query';
diff
- import { createReactQueryHooks } from '@trpc/react';
+ import { createReactQueryHooks } from '@trpc/react-query';

react-query 메이저 버전 업그레이드

peerDependenciesreact-query@^3에서 @tanstack/react-query@^4로 업그레이드되었습니다. 클라이언트 훅은 react-query를 감싼 얇은 레이어이므로, 새로운 React 훅 구현에 대한 자세한 내용은 공식 마이그레이션 가이드를 참조하세요.

훅의 tRPC 전용 옵션이 trpc 네임스페이스로 이동

기본 react-query 속성과의 충돌을 피하기 위해 모든 tRPC 옵션을 trpc 속성으로 이동했습니다. 이는 tRPC 전용 옵션을 명확히 구분하며 향후 react-query와의 충돌을 방지합니다:

tsx
// Before
useQuery(['post.byId', '1'], {
context: {
batching: false,
},
});
// After:
useQuery(['post.byId', '1'], {
trpc: {
context: {
batching: false,
},
},
});
// or:
trpc.post.byId.useQuery('1', {
trpc: {
batching: false,
},
});
tsx
// Before
useQuery(['post.byId', '1'], {
context: {
batching: false,
},
});
// After:
useQuery(['post.byId', '1'], {
trpc: {
context: {
batching: false,
},
},
});
// or:
trpc.post.byId.useQuery('1', {
trpc: {
batching: false,
},
});

쿼리 키 변경 사항

참고

앱에서 tRPC 제공 API만 사용하는 경우 마이그레이션에 문제가 없습니다 👍
그러나 tanstack query client를 직접 사용하여 queryClient.setQueriesData를 통해 여러 tRPC 생성 쿼리 데이터를 업데이트하는 경우 주의가 필요합니다!

전체 라우터 무효화 같은 고급 기능을 구현하기 위해 내부적으로 tanstack 쿼리 키 사용 방식을 변경했습니다.

저희는 프로시저 경로를 나타내기 위해 .으로 연결된 문자열을 사용하는 대신 요소들의 하위 배열로 쿼리 키를 변경했습니다. 또한 캐시에 저장될 때 queryinfinite 쿼리를 구분하도록 개선했으며, 쿼리 type과 입력값(input)을 명명된 속성을 가진 객체로 이동시켰습니다.

아래의 간단한 라우터를 예로 들면:

tsx
export const appRouter = router({
user: router({
byId: publicProcedure
.input(z.object({ id: z.number() }))
.query((opts) => ({ user: { id: opts.input.id } })),
}),
});
tsx
export const appRouter = router({
user: router({
byId: publicProcedure
.input(z.object({ id: z.number() }))
.query((opts) => ({ user: { id: opts.input.id } })),
}),
});

trpc.user.byId.useQuery({ id: 10 })에 사용되는 쿼리 키는 다음과 같이 변경됩니다:

  • V9에서의 키: ["user.byId", { id: 10 }]

  • v10에서의 키: [["user", "byId"],{ input: { id:10 }, type: 'query' }]

대부분의 개발자는 이 변경을 눈치채지 못할 것이지만, tanstack queryClient를 직접 사용해 tRPC 생성 쿼리를 조작하는 소수의 개발자는 필터링에 사용하는 키를 변경해야 합니다!

@trpc/client

프로시저 중단(Aborting)

v9에서는 프로시저 중단에 .cancel() 메서드를 사용했습니다.

v10에서는 웹 표준에 더 잘 부합하도록 AbortController Web API로 전환했습니다. .cancel()을 호출하는 대신 쿼리에 AbortSignal을 제공하고 상위 AbortController에서 .abort()를 호출하면 됩니다.

tsx
const ac = new AbortController();
const helloQuery = client.greeting.query('KATT', { signal: ac.signal });
// Aborting
ac.abort();
tsx
const ac = new AbortController();
const helloQuery = client.greeting.query('KATT', { signal: ac.signal });
// Aborting
ac.abort();

HTTP 관련 옵션을 TRPCClient에서 링크로 이동

이전에는 HTTP 옵션(헤더 등)이 createTRPCClient()에 직접 배치되었습니다. 하지만 tRPC는 기술적으로 HTTP에 종속되지 않으므로, 이 옵션들을 TRPCClient에서 httpLinkhttpBatchLink로 이동시켰습니다.

ts
// Before:
import { createTRPCClient } from '@trpc/client';
const client = createTRPCClient({
url: '...',
fetch: myFetchPonyfill,
AbortController: myAbortControllerPonyfill,
headers() {
return {
'x-foo': 'bar',
};
},
});
// After:
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
const client = createTRPCProxyClient({
links: [
httpBatchLink({
url: '...',
fetch: myFetchPonyfill,
AbortController: myAbortControllerPonyfill,
headers() {
return {
'x-foo': 'bar',
};
},
})
]
});
ts
// Before:
import { createTRPCClient } from '@trpc/client';
const client = createTRPCClient({
url: '...',
fetch: myFetchPonyfill,
AbortController: myAbortControllerPonyfill,
headers() {
return {
'x-foo': 'bar',
};
},
});
// After:
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
const client = createTRPCProxyClient({
links: [
httpBatchLink({
url: '...',
fetch: myFetchPonyfill,
AbortController: myAbortControllerPonyfill,
headers() {
return {
'x-foo': 'bar',
};
},
})
]
});

이 변경 사항은 @trpc/server 패키지에도 반영되어 있으며, 이전에는 메인 진입점(entrypoint)에서 내보내던 http 관련 익스포트(export)가 이제 별도의 @trpc/server/http 진입점으로 이동되었습니다.

추가 사항

teardown 옵션 제거

teardown 옵션은 제거되었으며 더 이상 사용할 수 없습니다.

createContext 반환 타입

createContext 함수는 더 이상 null이나 undefined를 반환할 수 없습니다. 커스텀 컨텍스트를 사용하지 않았다면 빈 객체를 반환해야 합니다:

diff
- createContext: () => null,
+ createContext: () => ({}),
diff
- createContext: () => null,
+ createContext: () => ({}),

queryClient가 더 이상 tRPC 컨텍스트로 노출되지 않음

tRPC는 더 이상 trpc.useContext()를 통해 queryClient 인스턴스를 노출하지 않습니다. queryClient의 일부 메서드를 사용해야 한다면, 여기에서 trpc.useContext()가 해당 메서드를 래핑하는지 확인하세요. 아직 tRPC에서 해당 메서드를 래핑하지 않았다면 @tanstack/react-query에서 queryClient를 임포트하여 사용할 수 있습니다:

tsx
import { useQueryClient } from '@tanstack/react-query';
const MyComponent = () => {
const queryClient = useQueryClient();
// ...
};
tsx
import { useQueryClient } from '@tanstack/react-query';
const MyComponent = () => {
const queryClient = useQueryClient();
// ...
};

커스텀 에러 포매터 마이그레이션

formatError()의 내용을 루트 t 라우터로 이동시켜야 합니다. 자세한 내용은 에러 포매팅 문서를 참조하세요.