본문 바로가기
버전: 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. 기존 appRouter를 legacyRouter로 이름 변경

  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 메이저 버전 업그레이드​

peerDependencies가 react-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 쿼리 키 사용 방식을 변경했습니다.

저희는 프로시저 경로를 나타내기 위해 .으로 연결된 문자열을 사용하는 대신 요소들의 하위 배열로 쿼리 키를 변경했습니다. 또한 캐시에 저장될 때 query와 infinite 쿼리를 구분하도록 개선했으며, 쿼리 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에서 httpLink와 httpBatchLink로 이동시켰습니다.

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 라우터로 이동시켜야 합니다. 자세한 내용은 에러 포매팅 문서를 참조하세요.