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

서버 사이드 렌더링

SSR을 활성화하려면 ssr: truecreateTRPCNext 설정 콜백에 설정하면 됩니다.

정보

SSR을 활성화하면 tRPC는 서버에서 모든 쿼리를 미리 가져오기 위해 getInitialProps를 사용합니다. 이 때문에 getServerSideProps를 사용할 때 이 문제가 발생할 수 있으며, tRPC 측에서는 해결할 수 없습니다.

 
대신, SSR을 비활성화한 상태(기본값)로 두고 서버 사이드 헬퍼를 사용하여 getStaticProps 또는 getServerSideProps에서 쿼리를 사전에 가져올 수 있습니다.

서버 사이드 렌더링 단계에서 쿼리를 올바르게 실행하려면 config 안에 추가 로직을 추가해야 합니다. 또한 Response Caching를 고려해 보세요.

utils/trpc.ts
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from './api/trpc/[trpc]';
 
export const trpc = createTRPCNext<AppRouter>({
ssr: true,
ssrPrepass,
config(info) {
const { ctx } = info;
if (typeof window !== 'undefined') {
// during client requests
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
return {
links: [
httpBatchLink({
// The server needs to know your app's full url
url: `${getBaseUrl()}/api/trpc`,
/**
* Set custom request headers on every request from tRPC
* @see https://trpc.io/docs/client/headers
*/
headers() {
if (!ctx?.req?.headers) {
return {};
}
// To use SSR properly, you need to forward client headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
return {
cookie: ctx.req.headers.cookie,
};
},
}),
],
};
},
});
utils/trpc.ts
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from './api/trpc/[trpc]';
 
export const trpc = createTRPCNext<AppRouter>({
ssr: true,
ssrPrepass,
config(info) {
const { ctx } = info;
if (typeof window !== 'undefined') {
// during client requests
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
return {
links: [
httpBatchLink({
// The server needs to know your app's full url
url: `${getBaseUrl()}/api/trpc`,
/**
* Set custom request headers on every request from tRPC
* @see https://trpc.io/docs/client/headers
*/
headers() {
if (!ctx?.req?.headers) {
return {};
}
// To use SSR properly, you need to forward client headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
return {
cookie: ctx.req.headers.cookie,
};
},
}),
],
};
},
});

또는 특정 요청에 조건부로 SSR을 적용하려면 ssr에 콜백을 전달할 수 있습니다. 이 콜백은 boolean 또는 boolean으로 해석되는 Promise를 반환할 수 있습니다:

utils/trpc.ts
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from './api/trpc/[trpc]';
 
export const trpc = createTRPCNext<AppRouter>({
ssrPrepass,
config(info) {
const { ctx } = info;
if (typeof window !== 'undefined') {
// during client requests
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
return {
links: [
httpBatchLink({
// The server needs to know your app's full url
url: `${getBaseUrl()}/api/trpc`,
/**
* Set custom request headers on every request from tRPC
* @see https://trpc.io/docs/client/headers
*/
headers() {
if (!ctx?.req?.headers) {
return {};
}
// To use SSR properly, you need to forward client headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
return {
cookie: ctx.req.headers.cookie,
};
},
}),
],
};
},
ssr(opts) {
// only SSR if the request is coming from a bot
return opts.ctx?.req?.headers['user-agent']?.includes('bot') ?? false;
},
});
utils/trpc.ts
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from './api/trpc/[trpc]';
 
export const trpc = createTRPCNext<AppRouter>({
ssrPrepass,
config(info) {
const { ctx } = info;
if (typeof window !== 'undefined') {
// during client requests
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
return {
links: [
httpBatchLink({
// The server needs to know your app's full url
url: `${getBaseUrl()}/api/trpc`,
/**
* Set custom request headers on every request from tRPC
* @see https://trpc.io/docs/client/headers
*/
headers() {
if (!ctx?.req?.headers) {
return {};
}
// To use SSR properly, you need to forward client headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
return {
cookie: ctx.req.headers.cookie,
};
},
}),
],
};
},
ssr(opts) {
// only SSR if the request is coming from a bot
return opts.ctx?.req?.headers['user-agent']?.includes('bot') ?? false;
},
});
pages/_app.tsx
tsx
import { trpc } from '../utils/trpc';
import type { AppProps } from 'next/app';
import type { AppType } from 'next/app';
 
const MyApp: AppType = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
};
 
export default trpc.withTRPC(MyApp);
pages/_app.tsx
tsx
import { trpc } from '../utils/trpc';
import type { AppProps } from 'next/app';
import type { AppType } from 'next/app';
 
const MyApp: AppType = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
};
 
export default trpc.withTRPC(MyApp);

SSR을 사용한 응답 캐싱

앱에서 SSR을 활성화하면 Vercel과 같은 환경에서 앱이 느리게 로드되는 것을 발견할 수 있습니다. 하지만 SSG를 사용하지 않고도 전체 앱을 정적으로 렌더링할 수 있습니다. 더 많은 통찰을 위해 이 Twitter 스레드를 읽어 보세요.

createTRPCNextresponseMeta 콜백을 사용하여 SSR 응답의 캐시 헤더를 설정할 수 있습니다. responseMeta를 사용한 프레임워크 비종속적 캐싱에 대한 일반적인 응답 캐싱 문서도 참고하세요.

utils/trpc.tsx
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from '../server/routers/_app';
 
export const trpc = createTRPCNext<AppRouter>({
config() {
if (typeof window !== 'undefined') {
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
const url = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: 'http://localhost:3000/api/trpc';
 
return {
links: [
httpBatchLink({
url,
}),
],
};
},
ssr: true,
ssrPrepass,
responseMeta(opts) {
const { clientErrors } = opts;
 
if (clientErrors.length) {
// propagate http first error from API calls
return {
status: clientErrors[0].data?.httpStatus ?? 500,
};
}
 
// cache request for 1 day + revalidate once every second
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
},
});
utils/trpc.tsx
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from '../server/routers/_app';
 
export const trpc = createTRPCNext<AppRouter>({
config() {
if (typeof window !== 'undefined') {
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
const url = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: 'http://localhost:3000/api/trpc';
 
return {
links: [
httpBatchLink({
url,
}),
],
};
},
ssr: true,
ssrPrepass,
responseMeta(opts) {
const { clientErrors } = opts;
 
if (clientErrors.length) {
// propagate http first error from API calls
return {
status: clientErrors[0].data?.httpStatus ?? 500,
};
}
 
// cache request for 1 day + revalidate once every second
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
},
});

Next.js 어댑터를 사용한 API 응답 캐싱

Next.js API 핸들러에서 responseMeta를 사용하여 API 응답을 직접 캐싱할 수도 있습니다:

pages/api/trpc/[trpc].ts
tsx
import { initTRPC } from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
 
export const createContext = async ({
req,
res,
}: trpcNext.CreateNextContextOptions) => {
return {
req,
res,
};
};
 
type Context = Awaited<ReturnType<typeof createContext>>;
 
export const t = initTRPC.context<Context>().create();
 
export const appRouter = t.router({
public: t.router({
slowQueryCached: t.procedure.query(async (opts) => {
await new Promise((resolve) => setTimeout(resolve, 5000));
 
return {
lastUpdated: new Date().toJSON(),
};
}),
}),
});
 
export type AppRouter = typeof appRouter;
 
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
responseMeta(opts) {
const { ctx, paths, errors, type } = opts;
// assuming you have all your public routes with the keyword `public` in them
const allPublic = paths && paths.every((path) => path.includes('public'));
// checking that no procedures errored
const allOk = errors.length === 0;
// checking we're doing a query request
const isQuery = type === 'query';
 
if (ctx?.res && allPublic && allOk && isQuery) {
// cache request for 1 day + revalidate once every second
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
}
return {};
},
});
pages/api/trpc/[trpc].ts
tsx
import { initTRPC } from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
 
export const createContext = async ({
req,
res,
}: trpcNext.CreateNextContextOptions) => {
return {
req,
res,
};
};
 
type Context = Awaited<ReturnType<typeof createContext>>;
 
export const t = initTRPC.context<Context>().create();
 
export const appRouter = t.router({
public: t.router({
slowQueryCached: t.procedure.query(async (opts) => {
await new Promise((resolve) => setTimeout(resolve, 5000));
 
return {
lastUpdated: new Date().toJSON(),
};
}),
}),
});
 
export type AppRouter = typeof appRouter;
 
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
responseMeta(opts) {
const { ctx, paths, errors, type } = opts;
// assuming you have all your public routes with the keyword `public` in them
const allPublic = paths && paths.every((path) => path.includes('public'));
// checking that no procedures errored
const allOk = errors.length === 0;
// checking we're doing a query request
const isQuery = type === 'query';
 
if (ctx?.res && allPublic && allOk && isQuery) {
// cache request for 1 day + revalidate once every second
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
}
return {};
},
});

자주 묻는 질문

Q: 왜 클라이언트의 헤더를 서버로 수동으로 전달해야 하나요? tRPC가 자동으로 처리하지 않는 이유는 무엇인가요?

SSR을 수행할 때 클라이언트의 헤더를 서버로 전달하지 않으려는 경우는 드물지만, 헤더에 동적으로 항목을 추가하려는 경우가 있을 수 있습니다. 따라서 tRPC는 헤더 키 충돌 등에 대한 책임을 지고 싶어 하지 않습니다.

Q: Node 18에서 SSR을 사용할 때 connection 헤더를 삭제해야 하는 이유는 무엇인가요?

connection 헤더를 제거하지 않으면 connection금지된 헤더 이름이기 때문에 데이터 가져오기가 TRPCClientError: fetch failed로 실패합니다.

Q: 네트워크 탭에서 여전히 네트워크 요청이 이루어지는 것을 볼 수 있는 이유는 무엇인가요?

기본적으로 @tanstack/react-query(데이터 가져오기 훅에 사용)는 SSR을 통해 초기 데이터를 이미 보유하고 있더라도 마운트 및 창 재초점 시 데이터를 다시 가져옵니다. 이를 통해 데이터가 항상 최신 상태임을 보장합니다. 이 동작을 비활성화하려면 SSG 페이지를 참고하세요.