서버 사이드 렌더링
이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →
SSR을 활성화하려면 createTRPCNext 설정 콜백에서 ssr: true를 설정하기만 하면 됩니다.
서버 사이드 렌더링 단계에서 쿼리를 올바르게 실행하려면 config 내에 추가 로직을 포함해야 합니다. 또한 Response Caching도 고려해 보세요.
utils/trpc.tstsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from './api/trpc/[trpc]';export consttrpc =createTRPCNext <AppRouter >({ssr : true,ssrPrepass ,config (info ) {const {ctx } =info ;if (typeofwindow !== 'undefined') {// during client requestsreturn {links : [httpBatchLink ({url : '/api/trpc',}),],};}return {links : [httpBatchLink ({// The server needs to know your app's full urlurl : `${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 renderingreturn {cookie :ctx .req .headers .cookie ,};},}),],};},});
utils/trpc.tstsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from './api/trpc/[trpc]';export consttrpc =createTRPCNext <AppRouter >({ssr : true,ssrPrepass ,config (info ) {const {ctx } =info ;if (typeofwindow !== 'undefined') {// during client requestsreturn {links : [httpBatchLink ({url : '/api/trpc',}),],};}return {links : [httpBatchLink ({// The server needs to know your app's full urlurl : `${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 renderingreturn {cookie :ctx .req .headers .cookie ,};},}),],};},});
또는 특정 요청에 따라 SSR을 조건부로 적용하려면 ssr에 콜백 함수를 전달할 수 있습니다. 이 콜백은 boolean을 반환하거나 boolean으로 resolve되는 Promise를 반환할 수 있습니다:
utils/trpc.tstsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from './api/trpc/[trpc]';export consttrpc =createTRPCNext <AppRouter >({ssrPrepass ,config (info ) {const {ctx } =info ;if (typeofwindow !== 'undefined') {// during client requestsreturn {links : [httpBatchLink ({url : '/api/trpc',}),],};}return {links : [httpBatchLink ({// The server needs to know your app's full urlurl : `${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 renderingreturn {cookie :ctx .req .headers .cookie ,};},}),],};},ssr (opts ) {// only SSR if the request is coming from a botreturnopts .ctx ?.req ?.headers ['user-agent']?.includes ('bot') ?? false;},});
utils/trpc.tstsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from './api/trpc/[trpc]';export consttrpc =createTRPCNext <AppRouter >({ssrPrepass ,config (info ) {const {ctx } =info ;if (typeofwindow !== 'undefined') {// during client requestsreturn {links : [httpBatchLink ({url : '/api/trpc',}),],};}return {links : [httpBatchLink ({// The server needs to know your app's full urlurl : `${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 renderingreturn {cookie :ctx .req .headers .cookie ,};},}),],};},ssr (opts ) {// only SSR if the request is coming from a botreturnopts .ctx ?.req ?.headers ['user-agent']?.includes ('bot') ?? false;},});
pages/_app.tsxtsximport {trpc } from '../utils/trpc';import type {AppProps } from 'next/app';import type {AppType } from 'next/app';constMyApp :AppType = ({Component ,pageProps }:AppProps ) => {return <Component {...pageProps } />;};export defaulttrpc .withTRPC (MyApp );
pages/_app.tsxtsximport {trpc } from '../utils/trpc';import type {AppProps } from 'next/app';import type {AppType } from 'next/app';constMyApp :AppType = ({Component ,pageProps }:AppProps ) => {return <Component {...pageProps } />;};export defaulttrpc .withTRPC (MyApp );
SSR과 응답 캐싱
앱에서 SSR을 활성화하면 Vercel과 같은 환경에서 앱 로딩이 느려질 수 있습니다. 하지만 SSG를 사용하지 않고도 전체 앱을 정적으로 렌더링할 수 있습니다. 자세한 내용은 이 트위터 스레드를 참조하세요.
SSR 응답에 대한 캐시 헤더를 설정하려면 createTRPCNext의 responseMeta 콜백을 사용할 수 있습니다. responseMeta를 이용한 프레임워크 독립적인 캐싱에 대한 일반적인 내용은 응답 캐싱 문서도 참조하세요.
utils/trpc.tsxtsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from '../server/routers/_app';export consttrpc =createTRPCNext <AppRouter >({config () {if (typeofwindow !== 'undefined') {return {links : [httpBatchLink ({url : '/api/trpc',}),],};}consturl =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 callsreturn {status :clientErrors [0].data ?.httpStatus ?? 500,};}// cache request for 1 day + revalidate once every secondconstONE_DAY_IN_SECONDS = 60 * 60 * 24;return {headers : newHeaders ([['cache-control',`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS }`,],]),};},});
utils/trpc.tsxtsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from '../server/routers/_app';export consttrpc =createTRPCNext <AppRouter >({config () {if (typeofwindow !== 'undefined') {return {links : [httpBatchLink ({url : '/api/trpc',}),],};}consturl =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 callsreturn {status :clientErrors [0].data ?.httpStatus ?? 500,};}// cache request for 1 day + revalidate once every secondconstONE_DAY_IN_SECONDS = 60 * 60 * 24;return {headers : newHeaders ([['cache-control',`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS }`,],]),};},});
Next.js 어댑터를 이용한 API 응답 캐싱
Next.js API 핸들러에서 responseMeta를 사용해 API 응답을 직접 캐싱할 수도 있습니다:
pages/api/trpc/[trpc].tstsximport {initTRPC } from '@trpc/server';import * astrpcNext from '@trpc/server/adapters/next';export constcreateContext = async ({req ,res ,}:trpcNext .CreateNextContextOptions ) => {return {req ,res ,};};typeContext =Awaited <ReturnType <typeofcreateContext >>;export constt =initTRPC .context <Context >().create ();export constappRouter =t .router ({public :t .router ({slowQueryCached :t .procedure .query (async (opts ) => {await newPromise ((resolve ) =>setTimeout (resolve , 5000));return {lastUpdated : newDate ().toJSON (),};}),}),});export typeAppRouter = typeofappRouter ;export defaulttrpcNext .createNextApiHandler ({router :appRouter ,createContext ,responseMeta (opts ) {const {ctx ,paths ,errors ,type } =opts ;// assuming you have all your public routes with the keyword `public` in themconstallPublic =paths &&paths .every ((path ) =>path .includes ('public'));// checking that no procedures erroredconstallOk =errors .length === 0;// checking we're doing a query requestconstisQuery =type === 'query';if (ctx ?.res &&allPublic &&allOk &&isQuery ) {// cache request for 1 day + revalidate once every secondconstONE_DAY_IN_SECONDS = 60 * 60 * 24;return {headers : newHeaders ([['cache-control',`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS }`,],]),};}return {};},});
pages/api/trpc/[trpc].tstsximport {initTRPC } from '@trpc/server';import * astrpcNext from '@trpc/server/adapters/next';export constcreateContext = async ({req ,res ,}:trpcNext .CreateNextContextOptions ) => {return {req ,res ,};};typeContext =Awaited <ReturnType <typeofcreateContext >>;export constt =initTRPC .context <Context >().create ();export constappRouter =t .router ({public :t .router ({slowQueryCached :t .procedure .query (async (opts ) => {await newPromise ((resolve ) =>setTimeout (resolve , 5000));return {lastUpdated : newDate ().toJSON (),};}),}),});export typeAppRouter = typeofappRouter ;export defaulttrpcNext .createNextApiHandler ({router :appRouter ,createContext ,responseMeta (opts ) {const {ctx ,paths ,errors ,type } =opts ;// assuming you have all your public routes with the keyword `public` in themconstallPublic =paths &&paths .every ((path ) =>path .includes ('public'));// checking that no procedures erroredconstallOk =errors .length === 0;// checking we're doing a query requestconstisQuery =type === 'query';if (ctx ?.res &&allPublic &&allOk &&isQuery ) {// cache request for 1 day + revalidate once every secondconstONE_DAY_IN_SECONDS = 60 * 60 * 24;return {headers : newHeaders ([['cache-control',`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS }`,],]),};}return {};},});
FAQ
Q: 클라이언트 헤더를 서버에 수동으로 전달해야 하는 이유는 무엇인가요? tRPC가 자동으로 처리해주지 않는 이유는?
SSR을 수행할 때 클라이언트 헤더를 서버에 전달하지 않는 경우는 드물지만, 헤더에 동적으로 내용을 추가해야 할 수 있습니다. 따라서 tRPC는 헤더 키 충돌 등에 대한 책임을 지지 않기로 했습니다.
Q: Node 18에서 SSR을 사용할 때 connection 헤더를 삭제해야 하는 이유는?
connection 헤더를 제거하지 않으면 데이터 가져오기가 TRPCClientError: fetch failed로 실패합니다. connection은 금지된 헤더 이름이기 때문입니다.
Q: 네트워크 탭에서 여전히 네트워크 요청이 보이는 이유는?
기본적으로 데이터 가져오기 훅에 사용하는 @tanstack/react-query는 SSR을 통해 초기 데이터를 이미 받아온 경우에도 마운트 시와 윈도우 포커스 시 데이터를 재요청합니다. 이는 데이터가 항상 최신 상태임을 보장하기 위함입니다. 이 동작을 비활성화하려면 SSG 페이지를 참조하세요.