<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>tRPC Blog</title>
        <link>https://trpc.io/ko/blog</link>
        <description>tRPC Blog</description>
        <lastBuildDate>Fri, 21 Mar 2025 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>ko</language>
        <item>
            <title><![CDATA[tRPC v11 발표]]></title>
            <link>https://trpc.io/ko/blog/announcing-trpc-v11</link>
            <guid>https://trpc.io/ko/blog/announcing-trpc-v11</guid>
            <pubDate>Fri, 21 Mar 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>비공식 베타 번역</div><div class="admonitionContent_e2NW"><p>이 페이지는 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.
오류를 발견하셨나요? <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2025-03-21-announcing-trpc-11.mdx&amp;locale=ko" target="_blank" rel="noopener noreferrer">문제 신고 →</a></p></div></div>
<!-- -->
<p>tRPC v11은 오랫동안 <code>@next</code> 태그를 통해 프로덕션 준비 완료 상태였지만, 저희는 시맨틱 버저닝에 지나치게 얽매이지 않고 새로운 기능을 계속 추가해 왔습니다. 오늘, 마침내 tRPC v11의 공식 릴리즈를 발표하게 되어 매우 기쁩니다!</p>
<!-- -->
<p>2022년 11월 마지막 메이저 버전 릴리즈 이후, tRPC 커뮤니티는 상당한 성장을 이루었습니다:</p>
<ul>
<li>
<p>현재 GitHub에서 <a href="https://github.com/trpc/trpc" target="_blank" rel="noopener noreferrer">35,000개 이상의 스타</a>를 보유하고 있습니다</p>
</li>
<li>
<p>5,000명 이상의 멤버가 있는 <a href="https://trpc.io/discord" target="_blank" rel="noopener noreferrer">Discord 커뮤니티</a></p>
</li>
<li>
<p><a href="https://www.npmjs.com/package/@trpc/server" target="_blank" rel="noopener noreferrer">주간 700k+ npm 다운로드</a></p>
</li>
<li>
<p><a href="https://github.com/trpc/trpc/graphs/contributors" target="_blank" rel="noopener noreferrer">수백 명의 기여자</a></p>
</li>
<li>
<p><a href="https://trpc.io/awesome" target="_blank" rel="noopener noreferrer">확장, 예제 및 콘텐츠로 구성된 <em>훌륭한</em> 생태계</a></p>
</li>
</ul>
<p>tRPC v11의 출시와 함께, <code>@next</code> 채널에서의 안정적인 발전 덕분에 v11이 이미 많은 대형 TypeScript 프로젝트에서 프로덕션 환경에서 사용되고 있음을 알리게 되어 기쁩니다.</p>
<p>새 프로젝트의 경우 tRPC v11에 대해 알아보기 위해 <a href="https://trpc.io/awesome#-starting-points-example-projects-etc" target="_blank" rel="noopener noreferrer">예제 애플리케이션</a>을 통해 시작할 수 있습니다. 아직 tRPC v10을 사용 중인 프로젝트의 경우 <a href="https://trpc.io/docs/v11/migrate-from-v10-to-v11" target="_blank" rel="noopener noreferrer">v11 마이그레이션 가이드</a>를 참조하세요.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="변경-사항-개요">변경 사항 개요<a href="https://trpc.io/ko/blog/announcing-trpc-v11#%EB%B3%80%EA%B2%BD-%EC%82%AC%ED%95%AD-%EA%B0%9C%EC%9A%94" class="hash-link" aria-label="변경 사항 개요 바로가기 링크" title="변경 사항 개요 바로가기 링크">​</a></h2>
<p>v11은 v10과 대체로 하위 호환성을 유지하면서도 많은 새로운 기능과 개선 사항을 도입했습니다. 주요 내용은 다음과 같습니다:</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="tanstack-query-v5-지원">TanStack Query v5 지원<a href="https://trpc.io/ko/blog/announcing-trpc-v11#tanstack-query-v5-%EC%A7%80%EC%9B%90" class="hash-link" aria-label="TanStack Query v5 지원 바로가기 링크" title="TanStack Query v5 지원 바로가기 링크">​</a></h3>
<p><a href="https://tanstack.com/blog/announcing-tanstack-query-v5" target="_blank" rel="noopener noreferrer">TanStack Query v5</a>가 출시되면서 tRPC의 react-query 통합에 몇 가지 주요 변경 사항이 필요해졌습니다. 이는 <code>@next</code> 태그를 통해 조기에 사용 가능했으며, 이제 공식적으로 릴리즈되었습니다. 많은 프로젝트에서 이미 업그레이드를 진행했으며, 전체 React Suspense 지원 및 다양한 개선 사항의 이점을 누리고 있습니다. tRPC React 클라이언트 코드 마이그레이션 지침은 <a href="https://tanstack.com/query/v5/docs/react/guides/migrating-to-v5" target="_blank" rel="noopener noreferrer">TanStack Query의 공식 마이그레이션 가이드</a>를 참조하세요.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="새로운-tanstack-react-query-통합">새로운 TanStack React Query 통합<a href="https://trpc.io/ko/blog/announcing-trpc-v11#%EC%83%88%EB%A1%9C%EC%9A%B4-tanstack-react-query-%ED%86%B5%ED%95%A9" class="hash-link" aria-label="새로운 TanStack React Query 통합 바로가기 링크" title="새로운 TanStack React Query 통합 바로가기 링크">​</a></h3>
<p><a href="https://trpc.io/ko/blog/introducing-tanstack-react-query-client">관련 블로그 포스트 참조</a></p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="formdata비-json-콘텐츠-타입-지원">FormData/비 JSON 콘텐츠 타입 지원<a href="https://trpc.io/ko/blog/announcing-trpc-v11#formdata%EB%B9%84-json-%EC%BD%98%ED%85%90%EC%B8%A0-%ED%83%80%EC%9E%85-%EC%A7%80%EC%9B%90" class="hash-link" aria-label="FormData/비 JSON 콘텐츠 타입 지원 바로가기 링크" title="FormData/비 JSON 콘텐츠 타입 지원 바로가기 링크">​</a></h3>
<p>가장 많이 요청된 기능 중 하나인 JSON 이외의 데이터 전송이 이제 가능해졌습니다. tRPC v11에서는 다양한 <a href="https://trpc.io/ko/docs/server/non-json-content-types">비 JSON 콘텐츠 타입</a>, <code>FormData</code>, 그리고 <code>Blob</code>, <code>File</code>, <code>Uint8Array</code> 같은 바이너리 타입을 주고받을 수 있습니다. 사용 예시는 <a href="https://github.com/trpc/trpc/tree/next/examples/minimal-content-types" target="_blank" rel="noopener noreferrer">여기</a>에서 확인할 수 있습니다.</p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
import router">router</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./trpc'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>
import octetInputParser">octetInputParser</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/http'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'zod'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>>">appRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) router<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>(_: {
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>>
import router">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) formData: MutationProcedure<{
    input: FormData;
    output: void;
    meta: object;
}>">formData</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodCustom<FormData, FormData>>(schema: z.ZodCustom<FormData, FormData>): ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(alias) instanceof<{
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}>(cls: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}, params?: {
    when?: ((payload: z.core.ParsePayload) => boolean) | undefined | undefined;
    error?: string | z.core.$ZodErrorMap<z.core.$ZodIssueCustom> | undefined;
    message?: string | undefined | undefined;
}): z.ZodCustom<FormData, FormData>
export instanceof">instanceof</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="var FormData: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}">FormData</data-lsp>))</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false>.mutation<void>(resolver: ProcedureResolver<object, object, object, FormData, UnsetMarker, void>): MutationProcedure<{
    input: FormData;
    output: void;
    meta: object;
}>">mutation</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) input: FormData" style="border-bottom: solid 2px lightgrey;">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="meta-line"><span class="popover-prefix">                        </span><span class="popover"><div class="arrow"></div>(parameter) input: FormData</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) file: MutationProcedure<{
    input: OctetInput;
    output: void;
    meta: object;
}>">file</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<UtilityParser<OctetInput, ReadableStream<any>>>(schema: UtilityParser<OctetInput, ReadableStream<any>>): ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(alias) const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>
import octetInputParser">octetInputParser</data-lsp>)</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false>.mutation<void>(resolver: ProcedureResolver<object, object, object, ReadableStream<any>, UnsetMarker, void>): MutationProcedure<{
    input: OctetInput;
    output: void;
    meta: object;
}>">mutation</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) input: ReadableStream<any>" style="border-bottom: solid 2px lightgrey;">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="meta-line"><span class="popover-prefix">                        </span><span class="popover"><div class="arrow"></div>(parameter) input: ReadableStream&lt;any&gt;</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp>, <data-lsp lsp="(alias) const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
import router">router</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>
import octetInputParser">octetInputParser</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/http'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'zod'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>>">appRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) router<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>(_: {
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>>
import router">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) formData: MutationProcedure<{
    input: FormData;
    output: void;
    meta: object;
}>">formData</data-lsp>: <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">    .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodCustom<FormData, FormData>>(schema: z.ZodCustom<FormData, FormData>): ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(alias) instanceof<{
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}>(cls: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}, params?: {
    when?: ((payload: z.core.ParsePayload) => boolean) | undefined | undefined;
    error?: string | z.core.$ZodErrorMap<z.core.$ZodIssueCustom> | undefined;
    message?: string | undefined | undefined;
}): z.ZodCustom<FormData, FormData>
export instanceof">instanceof</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="var FormData: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}">FormData</data-lsp>))</span></div><div class="line"><span style="color: #C9D1D9">    .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false>.mutation<void>(resolver: ProcedureResolver<object, object, object, FormData, UnsetMarker, void>): MutationProcedure<{
    input: FormData;
    output: void;
    meta: object;
}>">mutation</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: FormData" style="border-bottom: solid 2px lightgrey;">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="meta-line"><span class="popover-prefix">                        </span><span class="popover"><div class="arrow"></div>(parameter) input: FormData</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) file: MutationProcedure<{
    input: OctetInput;
    output: void;
    meta: object;
}>">file</data-lsp>: <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">    .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<UtilityParser<OctetInput, ReadableStream<any>>>(schema: UtilityParser<OctetInput, ReadableStream<any>>): ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="(alias) const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>
import octetInputParser">octetInputParser</data-lsp>)</span></div><div class="line"><span style="color: #C9D1D9">    .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false>.mutation<void>(resolver: ProcedureResolver<object, object, object, ReadableStream<any>, UnsetMarker, void>): MutationProcedure<{
    input: OctetInput;
    output: void;
    meta: object;
}>">mutation</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: ReadableStream<any>" style="border-bottom: solid 2px lightgrey;">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="meta-line"><span class="popover-prefix">                        </span><span class="popover"><div class="arrow"></div>(parameter) input: ReadableStream&lt;any&gt;</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="react-server-componentsnextjs-app-router">React Server Components/Next.js App Router<a href="https://trpc.io/ko/blog/announcing-trpc-v11#react-server-componentsnextjs-app-router" class="hash-link" aria-label="React Server Components/Next.js App Router 바로가기 링크" title="React Server Components/Next.js App Router 바로가기 링크">​</a></h3>
<p>Next.js App Router와의 통합은 처음부터 가능했으며, 다음 두 가지 접근 방식 중 하나를 선택할 수 있습니다:</p>
<ul>
<li>
<p>서버 중심 접근 방식: <a href="https://trpc.io/ko/docs/server/server-side-calls"><code>createCaller</code></a> 또는 <a href="https://trpc.io/ko/docs/client/vanilla"><code>createTRPCClient</code></a>와 함께 async/await 사용</p>
</li>
<li>
<p>클라이언트 중심 접근 방식: <a href="https://trpc.io/ko/docs/client/react">React Query 통합</a>과 클라이언트 사이드 훅 사용</p>
</li>
</ul>
<p>하지만 두 방식을 혼용할 때 재검증 패턴이 일관되지 않아 개발자 경험이 tRPC의 기준에 미치지 못하는 문제가 있었습니다.</p>
<p>이를 해결하기 위해 React Server Components(RSC) 지원을 개선하고 프리페치 헬퍼를 추가했습니다. 이제 서버 측 RSC에서 프로시저를 실행하고, 클라이언트 측에서 보류 중인 프로미스를 이어받아 React Query 캐시를 자동으로 하이드레이트할 수 있습니다. 이를 통해 서버-클라이언트 간 워터폴 현상 없이 동적 애플리케이션을 구축할 수 있습니다. 자세한 내용은 <a href="https://trpc.io/ko/docs/client/react/server-components">서버 컴포넌트 문서</a>를 참조하세요.</p>
<p>이 새로운 프리페치 패턴 외에도 서버 함수에 대한 실험적 지원이 추가되었습니다. 자세한 내용은 <a href="https://trpc.io/ko/blog/trpc-actions">관련 블로그 포스트</a>에서 확인할 수 있습니다. 서버 함수가 생태계에서 더 확립될수록 이 기능을 계속 발전시킬 계획입니다.</p>
<p>또한 <a href="https://x.com/tannerlinsley/status/1844500352655335522" target="_blank" rel="noopener noreferrer">TanStack 팀과 협력하여 서버 함수 API 설계를 지원</a>했습니다. 목표는 tRPC의 강력한 미들웨어 시스템 일부를 분리하여 생태계 전반에서 사용할 수 있는 별도 패키지로 제공하는 것입니다.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="쿼리-및-뮤테이션의-스트리밍-응답">쿼리 및 뮤테이션의 스트리밍 응답<a href="https://trpc.io/ko/blog/announcing-trpc-v11#%EC%BF%BC%EB%A6%AC-%EB%B0%8F-%EB%AE%A4%ED%85%8C%EC%9D%B4%EC%85%98%EC%9D%98-%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D-%EC%9D%91%EB%8B%B5" class="hash-link" aria-label="쿼리 및 뮤테이션의 스트리밍 응답 바로가기 링크" title="쿼리 및 뮤테이션의 스트리밍 응답 바로가기 링크">​</a></h3>
<p>쿼리 응답을 스트리밍할 수 있는 <a href="https://trpc.io/ko/docs/client/links/httpBatchStreamLink">httpBatchStreamLink</a>를 도입했습니다. 대규모 데이터셋을 다루거나 실시간으로 데이터를 처리하며 프론트엔드에 전달해야 할 때 유용합니다. 이는 구독(subscription)을 대체하는 것이 아니라 도구 상자에 추가되는 또 다른 옵션입니다.</p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #C2C3C5">// @filename: server.ts</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
import router">router</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./trpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>">appRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) router<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>(_: {
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>
import router">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) examples: {
    iterable: QueryProcedure<{
        input: void;
        output: AsyncGenerator<number, never, unknown>;
        meta: object;
    }>;
}">examples</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) iterable: QueryProcedure<{
    input: void;
    output: AsyncGenerator<number, never, unknown>;
    meta: object;
}>">iterable</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<AsyncGenerator<number, never, unknown>>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, AsyncGenerator<number, never, unknown>>): QueryProcedure<{
    input: void;
    output: AsyncGenerator<number, never, unknown>;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">function*</span><span style="color: #24292EFF"> () {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">let</span><span style="color: #24292EFF"> <data-lsp lsp="let i: number">i</data-lsp> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">0</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">while</span><span style="color: #24292EFF"> (</span><span style="color: #1976D2">true</span><span style="color: #24292EFF">) {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>">Promise</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) resolve: (value: unknown) => void">resolve</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function setTimeout(callback: (_: void) => void, delay?: number): NodeJS.Timeout (+2 overloads)
namespace setTimeout">setTimeout</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) resolve: (value: unknown) => void">resolve</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">500</span><span style="color: #24292EFF">));</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">yield</span><span style="color: #24292EFF"> <data-lsp lsp="let i: number">i</data-lsp></span><span style="color: #D32F2F">++</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">      }</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>">AppRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">typeof</span><span style="color: #24292EFF"> <data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>">appRouter</data-lsp>;</span></div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line"><span style="color: #C2C3C5">// @filename: client.ts</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>
import createTRPCClient">createTRPCClient</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) function httpBatchStreamLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter[&quot;_def&quot;][&quot;_config&quot;][&quot;$types&quot;]>): TRPCLink<TRouter>
import httpBatchStreamLink">httpBatchStreamLink</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/client'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>
import AppRouter">AppRouter</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./server'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const trpc: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>">trpc</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>): TRPCClient<...>
import createTRPCClient">createTRPCClient</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>
import AppRouter">AppRouter</data-lsp></span><span style="color: #24292EFF">&gt;({</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>[]">links</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> [</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1"><data-lsp lsp="(alias) httpBatchStreamLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>(opts: HTTPBatchLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>>
import httpBatchStreamLink">httpBatchStreamLink</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) url: string | URL">url</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'http://localhost:3000'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  ]</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const iterable: AsyncIterable<number, never, unknown>" style="border-bottom: solid 2px lightgrey;">iterable</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const trpc: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>">trpc</data-lsp></span><span style="color: #6F42C1">.</span><span style="color: #1976D2"><data-lsp lsp="(property) examples: DecoratedProcedureRecord<{
    transformer: false;
    errorShape: DefaultErrorShape;
}, DecorateCreateRouterOptions<{
    iterable: QueryProcedure<{
        input: void;
        output: AsyncGenerator<number, never, unknown>;
        meta: object;
    }>;
}>>">examples</data-lsp></span><span style="color: #6F42C1">.</span><span style="color: #1976D2"><data-lsp lsp="(property) iterable: {
    query: Resolver<{
        input: void;
        output: AsyncIterable<number, never, unknown>;
        errorShape: DefaultErrorShape;
        transformer: false;
    }>;
}">iterable</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) query: (input: void, opts?: TRPCProcedureOptions) => Promise<AsyncIterable<number, never, unknown>>">query</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="meta-line"><span class="popover-prefix">         </span><span class="popover"><div class="arrow"></div>const iterable: AsyncIterable&lt;number, never, unknown&gt;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">for</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const value: number">value</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">of</span><span style="color: #24292EFF"> <data-lsp lsp="const iterable: AsyncIterable<number, never, unknown>">iterable</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #1976D2"><data-lsp lsp="namespace console
var console: Console">console</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)">log</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #22863A">'Iterable:'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="const value: number" style="border-bottom: solid 2px lightgrey;">value</data-lsp>);</span></div><div class="meta-line"><span class="popover-prefix">                            </span><span class="popover"><div class="arrow"></div>const value: number</span></div><div class="line"><span style="color: #24292EFF">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #8B949E">// @filename: server.ts</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp>, <data-lsp lsp="(alias) const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
import router">router</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>">appRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) router<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>(_: {
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>
import router">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) examples: {
    iterable: QueryProcedure<{
        input: void;
        output: AsyncGenerator<number, never, unknown>;
        meta: object;
    }>;
}">examples</data-lsp>: {</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) iterable: QueryProcedure<{
    input: void;
    output: AsyncGenerator<number, never, unknown>;
    meta: object;
}>">iterable</data-lsp>: <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<AsyncGenerator<number, never, unknown>>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, AsyncGenerator<number, never, unknown>>): QueryProcedure<{
    input: void;
    output: AsyncGenerator<number, never, unknown>;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">function*</span><span style="color: #C9D1D9"> () {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">let</span><span style="color: #C9D1D9"> <data-lsp lsp="let i: number">i</data-lsp> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">0</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">while</span><span style="color: #C9D1D9"> (</span><span style="color: #79C0FF">true</span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>">Promise</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) resolve: (value: unknown) => void">resolve</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="function setTimeout(callback: (_: void) => void, delay?: number): NodeJS.Timeout (+2 overloads)
namespace setTimeout">setTimeout</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="(parameter) resolve: (value: unknown) => void">resolve</data-lsp>, </span><span style="color: #79C0FF">500</span><span style="color: #C9D1D9">));</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">yield</span><span style="color: #C9D1D9"> <data-lsp lsp="let i: number">i</data-lsp></span><span style="color: #FF7B72">++</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">      }</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  },</span></div><div class="line"><span style="color: #C9D1D9">});</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>">AppRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">typeof</span><span style="color: #C9D1D9"> <data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>">appRouter</data-lsp>;</span></div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line"><span style="color: #8B949E">// @filename: client.ts</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>
import createTRPCClient">createTRPCClient</data-lsp>, <data-lsp lsp="(alias) function httpBatchStreamLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter[&quot;_def&quot;][&quot;_config&quot;][&quot;$types&quot;]>): TRPCLink<TRouter>
import httpBatchStreamLink">httpBatchStreamLink</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/client'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>
import AppRouter">AppRouter</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./server'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const trpc: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>">trpc</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>): TRPCClient<...>
import createTRPCClient">createTRPCClient</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(alias) type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>
import AppRouter">AppRouter</data-lsp></span><span style="color: #C9D1D9">&gt;({</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>[]">links</data-lsp>: [</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) httpBatchStreamLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>(opts: HTTPBatchLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>>
import httpBatchStreamLink">httpBatchStreamLink</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) url: string | URL">url</data-lsp>: </span><span style="color: #A5D6FF">'http://localhost:3000'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  ],</span></div><div class="line"><span style="color: #C9D1D9">});</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const iterable: AsyncIterable<number, never, unknown>" style="border-bottom: solid 2px lightgrey;">iterable</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> <data-lsp lsp="const trpc: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>">trpc</data-lsp>.<data-lsp lsp="(property) examples: DecoratedProcedureRecord<{
    transformer: false;
    errorShape: DefaultErrorShape;
}, DecorateCreateRouterOptions<{
    iterable: QueryProcedure<{
        input: void;
        output: AsyncGenerator<number, never, unknown>;
        meta: object;
    }>;
}>>">examples</data-lsp>.<data-lsp lsp="(property) iterable: {
    query: Resolver<{
        input: void;
        output: AsyncIterable<number, never, unknown>;
        errorShape: DefaultErrorShape;
        transformer: false;
    }>;
}">iterable</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) query: (input: void, opts?: TRPCProcedureOptions) => Promise<AsyncIterable<number, never, unknown>>">query</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="meta-line"><span class="popover-prefix">         </span><span class="popover"><div class="arrow"></div>const iterable: AsyncIterable&lt;number, never, unknown&gt;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">for</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const value: number">value</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">of</span><span style="color: #C9D1D9"> <data-lsp lsp="const iterable: AsyncIterable<number, never, unknown>">iterable</data-lsp>) {</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="namespace console
var console: Console">console</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)">log</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'Iterable:'</span><span style="color: #C9D1D9">, <data-lsp lsp="const value: number" style="border-bottom: solid 2px lightgrey;">value</data-lsp>);</span></div><div class="meta-line"><span class="popover-prefix">                            </span><span class="popover"><div class="arrow"></div>const value: number</span></div><div class="line"><span style="color: #C9D1D9">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="라우터-정의-단축-표현">라우터 정의 단축 표현<a href="https://trpc.io/ko/blog/announcing-trpc-v11#%EB%9D%BC%EC%9A%B0%ED%84%B0-%EC%A0%95%EC%9D%98-%EB%8B%A8%EC%B6%95-%ED%91%9C%ED%98%84" class="hash-link" aria-label="라우터 정의 단축 표현 바로가기 링크" title="라우터 정의 단축 표현 바로가기 링크">​</a></h3>
<p>라우트 정의 과정을 간소화하기 위해 새로운 라우터 정의 단축 문법을 도입했습니다. <a href="https://trpc.io/ko/docs/server/routers#inline-sub-router">문서</a></p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}>>">appRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const router: RouterBuilder
<{
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}>(_: {
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// Shorthand plain object for creating a sub-router</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) nested1: {
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}">nested1</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) proc: QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">proc</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<string>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, string>): QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(() </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'...'</span><span style="color: #24292EFF">)</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// Equivalent of:</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) nested2: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>>">nested2</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const router: RouterBuilder
<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>(_: {
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}) => BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>>">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) proc: QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">proc</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<string>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, string>): QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(() </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'...'</span><span style="color: #24292EFF">)</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}>>">appRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="const router: RouterBuilder
<{
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}>(_: {
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// Shorthand plain object for creating a sub-router</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) nested1: {
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}">nested1</data-lsp>: {</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) proc: QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">proc</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<string>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, string>): QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(() </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'...'</span><span style="color: #C9D1D9">),</span></div><div class="line"><span style="color: #C9D1D9">  },</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// Equivalent of:</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) nested2: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>>">nested2</data-lsp>: </span><span style="color: #D2A8FF"><data-lsp lsp="const router: RouterBuilder
<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>(_: {
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}) => BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>>">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) proc: QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">proc</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<string>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, string>): QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(() </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'...'</span><span style="color: #C9D1D9">),</span></div><div class="line"><span style="color: #C9D1D9">  }),</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="구독-서버-발송-이벤트-및-기타-개선">구독: 서버 발송 이벤트 및 기타 개선<a href="https://trpc.io/ko/blog/announcing-trpc-v11#%EA%B5%AC%EB%8F%85-%EC%84%9C%EB%B2%84-%EB%B0%9C%EC%86%A1-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B0%8F-%EA%B8%B0%ED%83%80-%EA%B0%9C%EC%84%A0" class="hash-link" aria-label="구독: 서버 발송 이벤트 및 기타 개선 바로가기 링크" title="구독: 서버 발송 이벤트 및 기타 개선 바로가기 링크">​</a></h3>
<ul>
<li>
<p>tRPC v11은 <a href="https://trpc.io/ko/docs/server/subscriptions#websockets-or-server-sent-events">서버 발송 이벤트(SSE)</a>를 사용한 구독 처리 방식을 새롭게 도입했습니다. 복잡한 WebSocket 없이 실시간 업데이트를 처리하는 훌륭한 방법으로, 향후 우선적으로 사용할 것을 권장합니다.</p>
</li>
<li>
<p>구독에서 <a href="https://trpc.io/ko/docs/server/subscriptions#cleanup-of-side-effects">자바스크립트 제너레이터 사용</a>을 지원합니다. 이를 통해 시간 경과에 따라 여러 값을 생성하고 완료 시 정리 작업을 수행할 수 있는 복잡한 구독 핸들러를 매우 JS 스타일로 작성할 수 있습니다.</p>
</li>
<li>
<p>구독이 <a href="https://trpc.io/ko/docs/server/subscriptions#output-validation">출력 유효성 검사</a>를 지원하여 구독 핸들러의 타입 안전성이 개선되었습니다.</p>
</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="v9-interop-모드-지원-종료">v9 <code>.interop()</code> 모드 지원 종료<a href="https://trpc.io/ko/blog/announcing-trpc-v11#v9-interop-%EB%AA%A8%EB%93%9C-%EC%A7%80%EC%9B%90-%EC%A2%85%EB%A3%8C" class="hash-link" aria-label="v9-interop-모드-지원-종료 바로가기 링크" title="v9-interop-모드-지원-종료 바로가기 링크">​</a></h3>
<p>tRPC v10에서는 v9 사용자의 원활한 마이그레이션을 위해 <code>.interop()</code> 모드를 도입했습니다. tRPC v11에서는 <code>.interop()</code> 모드를 제거했습니다. 여전히 <code>.interop()</code> 모드를 사용 중이라면 <a href="https://trpc.io/docs/v10/migrate-from-v9-to-v10" target="_blank" rel="noopener noreferrer">v10 마이그레이션 가이드</a>를 참조하여 현행 tRPC API로 전환을 완료하세요.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="v11로-마이그레이션">v11로 마이그레이션<a href="https://trpc.io/ko/blog/announcing-trpc-v11#v11%EB%A1%9C-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98" class="hash-link" aria-label="v11로 마이그레이션 바로가기 링크" title="v11로 마이그레이션 바로가기 링크">​</a></h2>
<p>현재 tRPC v10을 사용 중이라면 <a href="https://trpc.io/ko/docs/migrate-from-v10-to-v11">마이그레이션 가이드</a>를 따라 v11로 업그레이드할 수 있습니다. 마이그레이션 가이드에는 v11의 모든 주요 변경 사항과 신규 기능이 포함되어 있습니다.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="감사합니다">감사합니다!<a href="https://trpc.io/ko/blog/announcing-trpc-v11#%EA%B0%90%EC%82%AC%ED%95%A9%EB%8B%88%EB%8B%A4" class="hash-link" aria-label="감사합니다! 바로가기 링크" title="감사합니다! 바로가기 링크">​</a></h2>
<p>tRPC 코어 팀을 대표하여 tRPC를 사용하고 지원해 주신 모든 분께 감사드립니다.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="slug-typescript-performance-lessonstitle-v10-리팩토링-과정에서-얻은-typescript-성능-교훈authors-sachinraja">slug: typescript-performance-lessons
title: v10 리팩토링 과정에서 얻은 TypeScript 성능 교훈
authors: [sachinraja]<a href="https://trpc.io/ko/blog/announcing-trpc-v11#slug-typescript-performance-lessonstitle-v10-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EA%B3%BC%EC%A0%95%EC%97%90%EC%84%9C-%EC%96%BB%EC%9D%80-typescript-%EC%84%B1%EB%8A%A5-%EA%B5%90%ED%9B%88authors-sachinraja" class="hash-link" aria-label="slug: typescript-performance-lessons
title: v10 리팩토링 과정에서 얻은 TypeScript 성능 교훈
authors: [sachinraja] 바로가기 링크" title="slug: typescript-performance-lessons
title: v10 리팩토링 과정에서 얻은 TypeScript 성능 교훈
authors: [sachinraja] 바로가기 링크">​</a></h2>
<ul>
<li>
<p>트위터에서 <a href="https://twitter.com/trpcio" target="_blank" rel="noopener noreferrer">@trpcio</a> 팔로우</p>
</li>
<li>
<p><a href="https://trpc.io/discord" target="_blank" rel="noopener noreferrer">Discord 커뮤니티</a> 참여</p>
</li>
<li>
<p><a href="https://trpc.io/#try-it-out" target="_blank" rel="noopener noreferrer">브라우저에서 tRPC 체험</a></p>
</li>
</ul>
<a id="sponsor-button" href="https://trpc.io/sponsor" class="group flex h-12 w-max items-center gap-4 rounded-lg border-2 bg-zinc-200 px-4 py-2 transition hover:bg-zinc-100 dark:border-zinc-900 dark:bg-zinc-800 hover:dark:border-zinc-700 hover:dark:bg-zinc-900"><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="aspect-square h-6 fill-pink-500 transition-transform duration-200 ease-in group-hover:scale-110"><path d="M17.625 1.499c-2.32 0-4.354 1.203-5.625 3.03-1.271-1.827-3.305-3.03-5.625-3.03C3.129 1.499 0 4.253 0 8.249c0 4.275 3.068 7.847 5.828 10.227a33.14 33.14 0 0 0 5.616 3.876l.028.017.008.003-.001.003c.163.085.342.126.521.125.179.001.358-.041.521-.125l-.001-.003.008-.003.028-.017a33.14 33.14 0 0 0 5.616-3.876C20.932 16.096 24 12.524 24 8.249c0-3.996-3.129-6.75-6.375-6.75zm-.919 15.275a30.766 30.766 0 0 1-4.703 3.316l-.004-.002-.004.002a30.955 30.955 0 0 1-4.703-3.316c-2.677-2.307-5.047-5.298-5.047-8.523 0-2.754 2.121-4.5 4.125-4.5 2.06 0 3.914 1.479 4.544 3.684.143.495.596.797 1.086.796.49.001.943-.302 1.085-.796.63-2.205 2.484-3.684 4.544-3.684 2.004 0 4.125 1.746 4.125 4.5 0 3.225-2.37 6.216-5.048 8.523z"></path></svg><span class="font-semibold text-zinc-900 no-underline dark:text-zinc-300">스폰서 되기!</span></a>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[새로운 TanStack React Query 통합 기능 소개]]></title>
            <link>https://trpc.io/ko/blog/introducing-tanstack-react-query-client</link>
            <guid>https://trpc.io/ko/blog/introducing-tanstack-react-query-client</guid>
            <pubDate>Mon, 17 Feb 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>비공식 베타 번역</div><div class="admonitionContent_e2NW"><p>이 페이지는 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.
오류를 발견하셨나요? <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2025-02-17-new-tanstack-react-query-integration.mdx&amp;locale=ko" target="_blank" rel="noopener noreferrer">문제 신고 →</a></p></div></div>
<p>tRPC의 <code>next</code> 릴리스에서 이제 새로운 TanStack React Query 통합 기능을 사용할 수 있게 되어 기쁘게 생각합니다. 기존의 <a href="https://trpc.io/ko/docs/client/react">React Query 통합</a>과 비교했을 때, 이번 통합은 더 간결하고 TanStack Query의 본질에 가까우며, 자체 클라이언트로 <code>useQuery</code> 및 <code>useMutation</code>을 래핑하는 대신 TanStack React Query의 기본 인터페이스인 <a href="https://tanstack.com/query/latest/docs/framework/react/guides/query-options" target="_blank" rel="noopener noreferrer">QueryOptions</a>와 <a href="https://tanstack.com/query/latest/docs/framework/react/guides/mutations" target="_blank" rel="noopener noreferrer">MutationOptions</a>를 직접 활용합니다.</p>
<!-- -->
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="greeting.tsx"><div class="code-title">greeting.tsx</div><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function useQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): DefinedUseQueryResult<NoInfer<TData>, TError> (+2 overloads)
import useQuery">useQuery</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@tanstack/react-query'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>
import useTRPC">useTRPC</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./trpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function Greeting(): null">Greeting</data-lsp></span><span style="color: #24292EFF">() {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>">trpc</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) useTRPC(): TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>
import useTRPC">useTRPC</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const greetingQuery: UseQueryResult<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>>">greetingQuery</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) useQuery<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, string, TRPCQueryKeyWithoutPrefix>(options: UndefinedInitialDataOptions<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, string, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseQueryResult<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>> (+2 overloads)
import useQuery">useQuery</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>">trpc</data-lsp></span><span style="color: #6F42C1">.</span><span style="color: #1976D2"><data-lsp lsp="(property) greeting: DecorateQueryProcedure<{
    input: {
        name: string;
    };
    output: string;
    transformer: false;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}> &amp; Record<string, never>">greeting</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) DecorateQueryProcedure<{ input: { name: string; }; output: string; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryOptions: TRPCQueryOptions
<string, string>(input: {
    name: string;
}, opts?: UnusedSkipTokenTRPCQueryOptionsIn<string, string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> | undefined) => UnusedSkipTokenTRPCQueryOptionsOut<string, string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> (+2 overloads)">queryOptions</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) name: string">name</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'Jerry'</span><span style="color: #24292EFF"> }));</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// greetingQuery.data === 'Hello Jerry'</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">null</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="greeting.tsx"><div class="code-title">greeting.tsx</div><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function useQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): DefinedUseQueryResult<NoInfer<TData>, TError> (+2 overloads)
import useQuery">useQuery</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@tanstack/react-query'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>
import useTRPC">useTRPC</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">function</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="function Greeting(): null">Greeting</data-lsp></span><span style="color: #C9D1D9">() {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>">trpc</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) useTRPC(): TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>
import useTRPC">useTRPC</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const greetingQuery: UseQueryResult<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>>">greetingQuery</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) useQuery<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, string, TRPCQueryKeyWithoutPrefix>(options: UndefinedInitialDataOptions<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, string, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseQueryResult<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>> (+2 overloads)
import useQuery">useQuery</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>">trpc</data-lsp>.<data-lsp lsp="(property) greeting: DecorateQueryProcedure<{
    input: {
        name: string;
    };
    output: string;
    transformer: false;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}> &amp; Record<string, never>">greeting</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) DecorateQueryProcedure<{ input: { name: string; }; output: string; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryOptions: TRPCQueryOptions
<string, string>(input: {
    name: string;
}, opts?: UnusedSkipTokenTRPCQueryOptionsIn<string, string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> | undefined) => UnusedSkipTokenTRPCQueryOptionsOut<string, string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> (+2 overloads)">queryOptions</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) name: string">name</data-lsp>: </span><span style="color: #A5D6FF">'Jerry'</span><span style="color: #C9D1D9"> }));</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// greetingQuery.data === 'Hello Jerry'</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">null</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>이 새로운 클라이언트는 신규 사용자들에게 흔히 혼란을 주는 추상화 계층을 제거하고, TanStack의 공식 <a href="https://tanstack.com/query/latest" target="_blank" rel="noopener noreferrer">문서</a>를 따르는 분들에게 즉시 익숙하게 느껴질 직관적인 작업 방식을 제공합니다. 또한 tRPC 문서로 설명해야 할 내용이 줄어들었으며, 시작을 돕기 위한 <a href="https://trpc.io/ko/docs/client/tanstack-react-query/setup">설명서</a>도 마련되어 있습니다.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="변경-배경">변경 배경<a href="https://trpc.io/ko/blog/introducing-tanstack-react-query-client#%EB%B3%80%EA%B2%BD-%EB%B0%B0%EA%B2%BD" class="hash-link" aria-label="변경 배경 바로가기 링크" title="변경 배경 바로가기 링크">​</a></h2>
<p>상세한 내용은 이 변경의 근간이 된 원본 RFC를 <a href="https://github.com/trpc/trpc/discussions/6240" target="_blank" rel="noopener noreferrer">여기</a>에서 확인하실 수 있습니다. 주요 이유는 다음과 같습니다:</p>
<ul>
<li>
<p><strong>간결성</strong>: 새로운 클라이언트는 더 단순하고 TanStack Query에 가깝게 설계되었으며, QueryKeys, QueryOptions, MutationOptions와 같은 일반적인 인터페이스를 위한 팩토리를 제공합니다. 이로 인해 <a href="https://tanstack.com/query/latest" target="_blank" rel="noopener noreferrer">공식 TanStack Query 문서</a>를 그대로 따라갈 수 있어 학습 곡선이 낮아집니다.</p>
</li>
<li>
<p><strong>친숙성</strong>: 이미 TanStack Query를 사용 중인 개발자들에게 더 익숙한 방식으로, 애플리케이션 내 다른 작업에 TanStack Query를 사용할 때 tRPC를 위해 별도의 문법을 강제하지 않습니다.</p>
</li>
<li>
<p><strong>React 적합성</strong>: 기존 React Query 통합은 실제로 훅 규칙을 위반합니다. <a href="https://github.com/trpc/trpc/issues/2330" target="_blank" rel="noopener noreferrer">린팅이 정확히 불가능</a>하며, 훅을 props로 전달하는 등 React 컴파일러에서 문제를 일으킬 수 있는 패턴을 유도합니다. 이 측면에서 새로운 클라이언트는 더 React의 관용적 방식에 부합합니다.</p>
</li>
<li>
<p><strong>유지보수성</strong>: 버전 관리에서의 어려움은 tRPC가 TanStack Query의 변화(특히 QueryClient에 주기적으로 추가되는 신기능)와 동기화를 유지하는 데 있었습니다. 기본 인터페이스의 작은 접점을 활용함으로써 React Query 지원이 훨씬 수월해졌으며, <a href="https://bsky.app/profile/tkdodo.eu/post/3lgizrcvjmc24" target="_blank" rel="noopener noreferrer">TanStack 유지보수자가 권장하는 모범 사례</a>를 따르게 되었습니다.</p>
</li>
<li>
<p><strong>피드백</strong>: 기존 클라이언트가 신규 사용자에게 흔히 어려움을 주는 원인이었던 만큼, <a href="https://github.com/trpc/trpc/discussions/6240" target="_blank" rel="noopener noreferrer">이 RFC</a>에 대한 피드백은 압도적으로 긍정적이었으며, 대다수의 사용자가 이 클라이언트 사용을 기대한다는 의견을 남겼습니다. 물론 모든 사용자가 현재 이 클라이언트에 동의하는 것은 아니므로, 기존 클라이언트도 계속 유지할 예정입니다.</p>
</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="기존-trpc-react-query-통합-기능의-향후-계획">기존 tRPC React Query 통합 기능의 향후 계획<a href="https://trpc.io/ko/blog/introducing-tanstack-react-query-client#%EA%B8%B0%EC%A1%B4-trpc-react-query-%ED%86%B5%ED%95%A9-%EA%B8%B0%EB%8A%A5%EC%9D%98-%ED%96%A5%ED%9B%84-%EA%B3%84%ED%9A%8D" class="hash-link" aria-label="기존 tRPC React Query 통합 기능의 향후 계획 바로가기 링크" title="기존 tRPC React Query 통합 기능의 향후 계획 바로가기 링크">​</a></h2>
<p>당분간 사라지지 않을 예정입니다! 오랜 기간 유지보수를 약속드리지만, 중대한 신기능은 추가되지 않을 것이며 안정화된 상태로 간주될 것입니다.</p>
<p>신규 프로젝트는 새로운 TanStack React Query 통합 기능으로 시작하고, 기존 프로젝트는 점진적 마이그레이션을 고려해 보시길 권장합니다.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="마이그레이션-방법">마이그레이션 방법<a href="https://trpc.io/ko/blog/introducing-tanstack-react-query-client#%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EB%B0%A9%EB%B2%95" class="hash-link" aria-label="마이그레이션 방법 바로가기 링크" title="마이그레이션 방법 바로가기 링크">​</a></h2>
<p>기존 클라이언트는 오랜 기간 유지보수될 예정이지만, 신규 프로젝트는 새 클라이언트로 시작하고 활성 프로젝트는 점진적 전환을 고려해 보시길 권장합니다.</p>
<p>두 클라이언트는 상호 호환되며 동일 애플리케이션 내 공존이 가능하므로, 본인의 페이스대로 마이그레이션할 수 있습니다. 또한 커뮤니티 기여를 <strong>간절히 환영하는</strong> 코드모드 작업도 진행 중입니다. 지금까지 코드모드에 기여해 주신 <a href="https://bsky.app/profile/reaper.is" target="_blank" rel="noopener noreferrer">@reaper</a> 님께 감사드립니다!</p>
<p>👉 <a href="https://trpc.io/ko/docs/client/tanstack-react-query/migrating">마이그레이션 문서 읽기</a></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[tRPC와 함께 Server Actions 사용하기]]></title>
            <link>https://trpc.io/ko/blog/trpc-actions</link>
            <guid>https://trpc.io/ko/blog/trpc-actions</guid>
            <pubDate>Thu, 23 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>비공식 베타 번역</div><div class="admonitionContent_e2NW"><p>이 페이지는 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.
오류를 발견하셨나요? <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2024-05-23-trpc-actions.mdx&amp;locale=ko" target="_blank" rel="noopener noreferrer">문제 신고 →</a></p></div></div>
<!-- -->
<p>tRPC v10에서 도입된 프로시저 생성용 빌더 패턴은 커뮤니티로부터 폭넓은 호응을 받았으며, 많은 라이브러리가 유사한 패턴을 채택했습니다.
이 패턴의 인기가 높아짐에 따라 <code>tRPC like XYZ</code>라는 용어까지 생겨났을 정도입니다. 실제로 최근 <a href="https://x.com/localhost_5173/status/1793259910723215835" target="_blank" rel="noopener noreferrer">누군가가 tRPC와 유사한 API로 CLI 애플리케이션을 작성할 수 있는 방법이 있는지 궁금해하는 글</a>을 보았습니다.
사족이지만, <a href="https://github.com/mmkal/trpc-cli" target="_blank" rel="noopener noreferrer">tRPC를 직접 사용하여 이 작업을 수행할 수도 있습니다</a>. 하지만 오늘 주제는 이게 아닙니다.
오늘은 Next.js의 server actions와 함께 tRPC를 사용하는 방법에 대해 이야기해보겠습니다.</p>
<!-- -->
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="server-action이란-무엇인가요">Server Action이란 무엇인가요?<a href="https://trpc.io/ko/blog/trpc-actions#server-action%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94" class="hash-link" aria-label="Server Action이란 무엇인가요? 바로가기 링크" title="Server Action이란 무엇인가요? 바로가기 링크">​</a></h2>
<p>최신 <a href="https://react.dev/reference/rsc/server-actions" target="_blank" rel="noopener noreferrer">React</a> 및 <a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations" target="_blank" rel="noopener noreferrer">Next.js</a> 기능을 따라가지 못했다면, server actions를 통해 서버에서 실행되는 일반 함수를 작성하고 클라이언트에서 가져와 마치 일반 함수처럼 호출할 수 있습니다.
이것이 tRPC와 비슷하게 들릴 수 있으며 사실입니다. <a href="https://x.com/dan_abramov2" target="_blank" rel="noopener noreferrer">Dan Abramov</a>에 따르면, server actions는 번들러 기능으로서의 tRPC입니다:</p>
<div class="mb-4 flex w-full justify-center"><div class="react-tweet-theme root_gzUe root_OzMz"><article class="article_zVA9"><span class="skeleton_XJfA" style="height:3rem;margin-bottom:0.75rem"></span><span class="skeleton_XJfA" style="height:6rem;margin:0.5rem 0"></span><div style="border-top:var(--tweet-border);margin:0.5rem 0"></div><span class="skeleton_XJfA" style="height:2rem"></span><span class="skeleton_XJfA" style="height:2rem;border-radius:9999px;margin-top:0.5rem"></span></article></div></div>
<p>이 설명은 완전히 정확합니다. server actions는 tRPC와 유사하며, 결국 둘 다 <a href="https://en.wikipedia.org/wiki/Remote_procedure_call" target="_blank" rel="noopener noreferrer">RPC</a>입니다. 둘 다 백엔드에 함수를 작성하고 네트워크 계층을 추상화한 상태로 프론트엔드에서 완전한 타입 안전성을 갖고 호출할 수 있게 해줍니다.</p>
<p>그렇다면 tRPC는 어디서 등장할까요? 왜 tRPC와 server actions를 함께 사용해야 할까요? Server actions는 프리미티브(primitive)이며, 모든 프리미티브가 그렇듯 상당히 기본적이라 API 구축에 필요한 근본적인 측면이 부족합니다. 네트워크를 통해 노출되는 모든 API 엔드포인트는 악의적인 사용을 방지하기 위해 요청의 유효성 검증 및 인가가 필요합니다. 앞서 언급했듯이, tRPC의 API는 커뮤니티로부터 인정받고 있으므로, tRPC를 사용해 server actions를 정의하고 입력 검증, 미들웨어를 통한 인증 및 인가, 출력 검증, 데이터 변환기 등 tRPC에 내장된 모든 훌륭한 기능을 활용할 수 있다면 좋지 않을까요? 저는 그렇게 생각합니다. 자세히 알아보겠습니다.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="trpc로-server-actions-정의하기">tRPC로 Server Actions 정의하기<a href="https://trpc.io/ko/blog/trpc-actions#trpc%EB%A1%9C-server-actions-%EC%A0%95%EC%9D%98%ED%95%98%EA%B8%B0" class="hash-link" aria-label="tRPC로 Server Actions 정의하기 바로가기 링크" title="tRPC로 Server Actions 정의하기 바로가기 링크">​</a></h2>
<div class="theme-admonition theme-admonition-note admonition_v5Ag alert alert--secondary"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>참고</div><div class="admonitionContent_e2NW"><p><strong>필수 조건:</strong> Server actions를 사용하려면 Next.js App Router를 사용해야 합니다. 또한 우리가 사용할 모든 tRPC 기능은 tRPC v11에서만 사용 가능하므로, tRPC의 베타 릴리스 채널을 사용 중인지 확인하세요:</p><div class="tabs-container tabList_mG0D"><ul role="tablist" aria-orientation="horizontal" class="tabs"><li role="tab" tabindex="0" aria-selected="true" class="tabs__item tabItem_F3SZ tabs__item--active">npm</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_F3SZ">yarn</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_F3SZ">pnpm</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_F3SZ">bun</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_F3SZ">deno</li></ul><div class="margin-top--md"><div role="tabpanel" class="tabItem_f_Dr"><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">npm install @trpc/server<button type="button" aria-label="클립보드에 코드 복사" class="copy-button">복사</button></pre></div><div role="tabpanel" class="tabItem_f_Dr" hidden=""><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">yarn add @trpc/server<button type="button" aria-label="클립보드에 코드 복사" class="copy-button">복사</button></pre></div><div role="tabpanel" class="tabItem_f_Dr" hidden=""><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">pnpm add @trpc/server<button type="button" aria-label="클립보드에 코드 복사" class="copy-button">복사</button></pre></div><div role="tabpanel" class="tabItem_f_Dr" hidden=""><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">bun add @trpc/server<button type="button" aria-label="클립보드에 코드 복사" class="copy-button">복사</button></pre></div><div role="tabpanel" class="tabItem_f_Dr" hidden=""><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">deno add npm:@trpc/server<button type="button" aria-label="클립보드에 코드 복사" class="copy-button">복사</button></pre></div></div></div></div></div>
<p>먼저 tRPC를 초기화하고 기본 server actions 프로시저를 정의해봅시다.
프로시저 빌더의 <code>experimental_caller</code> 메서드를 사용할 것입니다. 이는 함수가 호출될 때 프로시저 실행 방식을 사용자 정의할 수 있게 해주는 새로운 메서드입니다. 또한 Next.js와 호환되도록 <code>experimental_nextAppDirCaller</code> 어댑터를 사용할 것입니다. 이 어댑터는 클라이언트에서 server action이 <code>useActionState</code>로 래핑된 경우를 처리하며, 이는 <a href="https://react.dev/reference/react/useActionState#my-action-can-no-longer-read-the-submitted-form-data" target="_blank" rel="noopener noreferrer">server action의 호출 시그니처를 변경합니다</a>.</p>
<p>라우터를 사용할 때(<code>user.byId</code> 등)와 달리 일반 경로가 없으므로 <a href="https://trpc.io/ko/docs/server/metadata">메타데이터</a>로 <code>span</code> 속성을 사용할 것입니다. span 속성은 로깅이나 관측 가능성(observability) 도중에 프로시저를 구분하는 데 사용할 수 있습니다.</p>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/adapters/next-app-dir'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">interface</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">&gt;()</span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #6F42C1">.</span><span style="color: #1976D2"><data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">);</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>, <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/adapters/next-app-dir'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">interface</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">&gt;().</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp>.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #D2A8FF"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #C9D1D9">: ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) meta: unknown">meta</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  }),</span></div><div class="line"><span style="color: #C9D1D9">);</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>다음으로 <a href="https://trpc.io/ko/docs/server/context">컨텍스트(context)</a>를 추가해봅시다. 일반 HTTP 어댑터를 사용해 라우터를 호스팅하지 않을 것이므로, 어댑터의 <code>createContext</code> 메서드를 통해 주입되는 컨텍스트가 없을 것입니다. 대신 미들웨어를 사용해 컨텍스트를 주입할 것입니다. 이 예시에서는 세션에서 현재 사용자를 가져와 컨텍스트에 주입해보겠습니다.</p>
<div></div>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/adapters/next-app-dir'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./auth'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">interface</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">&gt;()</span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User | null;
}>(fn: MiddlewareBuilder<{}, Meta, {
    user: User | null;
}, UnsetMarker> | MiddlewareFunction<object, Meta, object, {
    user: User | null;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// Inject user into context</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const user: User | null">user</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) next: <{
    user: User | null;
}>(opts: {
    ctx?: {
        user: User | null;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User | null;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) ctx?: {
    user: User | null;
} | undefined">ctx</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> { <data-lsp lsp="(property) user: User | null">user</data-lsp> } });</span></div><div class="line"><span style="color: #24292EFF">  });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>, <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/adapters/next-app-dir'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./auth'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">interface</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">&gt;().</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp>.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #D2A8FF"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #C9D1D9">: ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) meta: unknown">meta</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  )</span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User | null;
}>(fn: MiddlewareBuilder<{}, Meta, {
    user: User | null;
}, UnsetMarker> | MiddlewareFunction<object, Meta, object, {
    user: User | null;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// Inject user into context</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const user: User | null">user</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) next: <{
    user: User | null;
}>(opts: {
    ctx?: {
        user: User | null;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User | null;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) ctx?: {
    user: User | null;
} | undefined">ctx</data-lsp>: { <data-lsp lsp="(property) user: User | null">user</data-lsp> } });</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>마지막으로, 인증되지 않은 사용자로부터 액션을 보호하는 <code>protectedAction</code> 절차를 생성합니다. 이미 이를 수행하는 미들웨어가 있다면 사용하면 되지만, 이 예제에서는 인라인으로 정의하겠습니다.</p>
<div></div>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/adapters/next-app-dir'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./auth'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">interface</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">&gt;()</span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User | null;
}>(fn: MiddlewareBuilder<{}, Meta, {
    user: User | null;
}, UnsetMarker> | MiddlewareFunction<object, Meta, object, {
    user: User | null;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// Inject user into context</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const user: User | null" style="border-bottom: solid 2px lightgrey;">user</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="meta-line"><span class="popover-prefix">           </span><span class="popover"><div class="arrow"></div>const user: User | null</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) next: <{
    user: User | null;
}>(opts: {
    ctx?: {
        user: User | null;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User | null;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) ctx?: {
    user: User | null;
} | undefined">ctx</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> { <data-lsp lsp="(property) user: User | null">user</data-lsp> } });</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">protectedAction</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User | null; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User;
}>(fn: MiddlewareBuilder<{
    user: User | null;
}, Meta, {
    user: User;
}, UnsetMarker> | MiddlewareFunction<object, Meta, {
    user: User | null;
}, {
    user: User;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">!</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) user: User | null">user</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) code: &quot;UNAUTHORIZED&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;NOT_FOUND&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'UNAUTHORIZED'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    });</span></div><div class="line"><span style="color: #24292EFF">  }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) next: <{
    user: User;
}>(opts: {
    ctx?: {
        user: User;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) ctx?: {
    user: User;
} | undefined">ctx</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">...</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) user: User" style="border-bottom: solid 2px lightgrey;">user</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) user: User" style="border-bottom: solid 2px lightgrey;">user</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #C2C3C5">// &lt;-- ensures type is non-nullable</span></div><div class="meta-line"><span class="popover-prefix">       </span><span class="popover"><div class="arrow"></div>(property) user: User</span></div><div class="line"><span style="color: #24292EFF">    }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>, <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/adapters/next-app-dir'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./auth'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">interface</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">&gt;().</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp>.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #D2A8FF"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #C9D1D9">: ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) meta: unknown">meta</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  )</span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User | null;
}>(fn: MiddlewareBuilder<{}, Meta, {
    user: User | null;
}, UnsetMarker> | MiddlewareFunction<object, Meta, object, {
    user: User | null;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// Inject user into context</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const user: User | null" style="border-bottom: solid 2px lightgrey;">user</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="meta-line"><span class="popover-prefix">           </span><span class="popover"><div class="arrow"></div>const user: User | null</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) next: <{
    user: User | null;
}>(opts: {
    ctx?: {
        user: User | null;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User | null;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) ctx?: {
    user: User | null;
} | undefined">ctx</data-lsp>: { <data-lsp lsp="(property) user: User | null">user</data-lsp> } });</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">protectedAction</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User | null; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User;
}>(fn: MiddlewareBuilder<{
    user: User | null;
}, Meta, {
    user: User;
}, UnsetMarker> | MiddlewareFunction<object, Meta, {
    user: User | null;
}, {
    user: User;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.<data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp>.<data-lsp lsp="(property) user: User | null">user</data-lsp>) {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) code: &quot;UNAUTHORIZED&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;NOT_FOUND&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp>: </span><span style="color: #A5D6FF">'UNAUTHORIZED'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    });</span></div><div class="line"><span style="color: #C9D1D9">  }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) next: <{
    user: User;
}>(opts: {
    ctx?: {
        user: User;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) ctx?: {
    user: User;
} | undefined">ctx</data-lsp>: {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">...</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.<data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) user: User" style="border-bottom: solid 2px lightgrey;">user</data-lsp>: <data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.<data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp>.<data-lsp lsp="(property) user: User" style="border-bottom: solid 2px lightgrey;">user</data-lsp>, </span><span style="color: #8B949E">// &lt;-- ensures type is non-nullable</span></div><div class="meta-line"><span class="popover-prefix">       </span><span class="popover"><div class="arrow"></div>(property) user: User</span></div><div class="line"><span style="color: #C9D1D9">    },</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>자, 이제 실제 서버 액션을 작성해 보겠습니다. <code>_actions.ts</code> 파일을 생성하고 <code>"use server"</code> 지시문을 추가한 후 액션을 정의합니다.</p>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="app/_actions.ts"><div class="code-title">app/_actions.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #22863A">'use server'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'zod'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import protectedAction">protectedAction</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'../server/trpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const createPost: (input: {
    title: string;
}) => Promise<void>">createPost</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import protectedAction">protectedAction</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, Meta, {
    user: User;
}, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, true>">input</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) title: z.ZodString">title</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, true>.mutation<void>(resolver: ProcedureResolver<object, Meta, {
    user: User;
}, {
    title: string;
}, UnsetMarker, void>): (input: {
    title: string;
}) => Promise<void>">mutation</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    title: string;
}>">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// Do something with the input</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C2C3C5">// Since we're using the `experimental_caller`,</span></div><div class="line"><span style="color: #C2C3C5">// our procedure is now just an ordinary function:</span></div><div class="line"><span style="color: #24292EFF"><data-lsp lsp="const createPost: (input: {
    title: string;
}) => Promise<void>" style="border-bottom: solid 2px lightgrey;">createPost</data-lsp>;</span></div><div class="meta-line"><span class="popover-prefix">    </span><span class="popover"><div class="arrow"></div>const createPost: (input: {
    title: string;
}) =&gt; Promise&lt;void&gt;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="app/_actions.ts"><div class="code-title">app/_actions.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #A5D6FF">'use server'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'zod'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import protectedAction">protectedAction</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'../server/trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const createPost: (input: {
    title: string;
}) => Promise<void>">createPost</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import protectedAction">protectedAction</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, Meta, {
    user: User;
}, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, true>">input</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) title: z.ZodString">title</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  )</span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, true>.mutation<void>(resolver: ProcedureResolver<object, Meta, {
    user: User;
}, {
    title: string;
}, UnsetMarker, void>): (input: {
    title: string;
}) => Promise<void>">mutation</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    title: string;
}>">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// Do something with the input</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #8B949E">// Since we're using the `experimental_caller`,</span></div><div class="line"><span style="color: #8B949E">// our procedure is now just an ordinary function:</span></div><div class="line"><span style="color: #C9D1D9"><data-lsp lsp="const createPost: (input: {
    title: string;
}) => Promise<void>" style="border-bottom: solid 2px lightgrey;">createPost</data-lsp>;</span></div><div class="meta-line"><span class="popover-prefix">    </span><span class="popover"><div class="arrow"></div>const createPost: (input: {
    title: string;
}) =&gt; Promise&lt;void&gt;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>와우! 인증되지 않은 사용자로부터 보호되고 SQL 인젝션 같은 공격을 방지하는 입력 검증이 적용된 서버 액션을 정의하는 게 이렇게 쉽습니다. 이제 클라이언트에서 이 함수를 불러와 호출해 보세요.</p>
<div><pre class="shiki min-light with-title" style="background-color: #ffffff; color: #24292eff" title="app/post-form.tsx"><div class="code-title">app/post-form.tsx</div><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #22863A">'use client'</span><span style="color: #24292EFF">;</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { createPost } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./_actions'</span><span style="color: #24292EFF">;</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">PostForm</span><span style="color: #24292EFF">() {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> (</span></div><div class="line"><span style="color: #24292EFF">    &lt;</span><span style="color: #22863A">form</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// Use `action` to make form progressively enhanced</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">action</span><span style="color: #D32F2F">=</span><span style="color: #24292EFF">{createPost}</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// `Using `onSubmit` allows building rich interactive</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// forms once JavaScript has loaded</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">onSubmit</span><span style="color: #D32F2F">=</span><span style="color: #24292EFF">{</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (e) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #1976D2">e</span><span style="color: #6F42C1">.preventDefault</span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">title</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">FormData</span><span style="color: #24292EFF">(</span><span style="color: #1976D2">e</span><span style="color: #24292EFF">.currentTarget)</span><span style="color: #6F42C1">.get</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'title'</span><span style="color: #24292EFF">) </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #C2C3C5">// Maybe show loading toast, etc etc. Endless possibilities</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createPost</span><span style="color: #24292EFF">({ title });</span></div><div class="line"><span style="color: #24292EFF">      }}</span></div><div class="line"><span style="color: #24292EFF">    &gt;</span></div><div class="line"><span style="color: #24292EFF">      &lt;</span><span style="color: #22863A">input</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">type</span><span style="color: #D32F2F">=</span><span style="color: #22863A">"text"</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">name</span><span style="color: #D32F2F">=</span><span style="color: #22863A">"title"</span><span style="color: #24292EFF"> /&gt;</span></div><div class="line"><span style="color: #24292EFF">      &lt;</span><span style="color: #22863A">button</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">type</span><span style="color: #D32F2F">=</span><span style="color: #22863A">"submit"</span><span style="color: #24292EFF">&gt;Create Post&lt;/</span><span style="color: #22863A">button</span><span style="color: #24292EFF">&gt;</span></div><div class="line"><span style="color: #24292EFF">    &lt;/</span><span style="color: #22863A">form</span><span style="color: #24292EFF">&gt;</span></div><div class="line"><span style="color: #24292EFF">  );</span></div><div class="line"><span style="color: #24292EFF">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title" style="background-color: #0d1117; color: #c9d1d9" title="app/post-form.tsx"><div class="code-title">app/post-form.tsx</div><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #A5D6FF">'use client'</span><span style="color: #C9D1D9">;</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { createPost } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./_actions'</span><span style="color: #C9D1D9">;</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">function</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">PostForm</span><span style="color: #C9D1D9">() {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> (</span></div><div class="line"><span style="color: #C9D1D9">    &lt;</span><span style="color: #7EE787">form</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// Use `action` to make form progressively enhanced</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #79C0FF">action</span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9">{createPost}</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// `Using `onSubmit` allows building rich interactive</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// forms once JavaScript has loaded</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #79C0FF">onSubmit</span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9">{</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657">e</span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        e.</span><span style="color: #D2A8FF">preventDefault</span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">title</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">FormData</span><span style="color: #C9D1D9">(e.currentTarget).</span><span style="color: #D2A8FF">get</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'title'</span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #8B949E">// Maybe show loading toast, etc etc. Endless possibilities</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createPost</span><span style="color: #C9D1D9">({ title });</span></div><div class="line"><span style="color: #C9D1D9">      }}</span></div><div class="line"><span style="color: #C9D1D9">    &gt;</span></div><div class="line"><span style="color: #C9D1D9">      &lt;</span><span style="color: #7EE787">input</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">type</span><span style="color: #FF7B72">=</span><span style="color: #A5D6FF">"text"</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">name</span><span style="color: #FF7B72">=</span><span style="color: #A5D6FF">"title"</span><span style="color: #C9D1D9"> /&gt;</span></div><div class="line"><span style="color: #C9D1D9">      &lt;</span><span style="color: #7EE787">button</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">type</span><span style="color: #FF7B72">=</span><span style="color: #A5D6FF">"submit"</span><span style="color: #C9D1D9">&gt;Create Post&lt;/</span><span style="color: #7EE787">button</span><span style="color: #C9D1D9">&gt;</span></div><div class="line"><span style="color: #C9D1D9">    &lt;/</span><span style="color: #7EE787">form</span><span style="color: #C9D1D9">&gt;</span></div><div class="line"><span style="color: #C9D1D9">  );</span></div><div class="line"><span style="color: #C9D1D9">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="더-나아가기">더 나아가기<a href="https://trpc.io/ko/blog/trpc-actions#%EB%8D%94-%EB%82%98%EC%95%84%EA%B0%80%EA%B8%B0" class="hash-link" aria-label="더 나아가기 바로가기 링크" title="더 나아가기 바로가기 링크">​</a></h2>
<p>tRPC 빌더와 재사용 가능한 절차를 정의하는 합성 방식을 활용하면 더 복잡한 서버 액션도 쉽게 구축할 수 있습니다. 몇 가지 예시를 살펴보겠습니다:</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="관측-가능성observability">관측 가능성(Observability)<a href="https://trpc.io/ko/blog/trpc-actions#%EA%B4%80%EC%B8%A1-%EA%B0%80%EB%8A%A5%EC%84%B1observability" class="hash-link" aria-label="관측 가능성(Observability) 바로가기 링크" title="관측 가능성(Observability) 바로가기 링크">​</a></h3>
<p><code>@baselime/node-opentelemtry</code>의 tRPC 플러그인을 사용하면 단 몇 줄의 코드로 관측 가능성을 추가할 수 있습니다:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">diff</div><div class="code-container"><code><div class="line"><span style="color: #24292EFF">--- server/trpc.ts</span></div><div class="line"><span style="color: #24292EFF">+++ server/trpc.ts</span></div><div class="line"><span style="color: #24292EFF">+ import { tracing } from '@baselime/node-opentelemetry/trpc';</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">  export const serverActionProcedure = t.procedure</span></div><div class="line"><span style="color: #24292EFF">    .experimental_caller(</span></div><div class="line"><span style="color: #24292EFF">      experimental_nextAppDirCaller({</span></div><div class="line"><span style="color: #24292EFF">        pathExtractor: (meta: Meta) =&gt; meta.span,</span></div><div class="line"><span style="color: #24292EFF">      }),</span></div><div class="line"><span style="color: #24292EFF">    )</span></div><div class="line"><span style="color: #24292EFF">    .use(async (opts) =&gt; {</span></div><div class="line"><span style="color: #24292EFF">      // Inject user into context</span></div><div class="line"><span style="color: #24292EFF">      const user = await currentUser();</span></div><div class="line"><span style="color: #24292EFF">      return opts.next({ ctx: { user } });</span></div><div class="line"><span style="color: #24292EFF">    })</span></div><div class="line"><span style="color: #24292EFF">+  .use(tracing());</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">--- app/_actions.ts</span></div><div class="line"><span style="color: #24292EFF">+++ app/_actions.ts</span></div><div class="line"><span style="color: #24292EFF">  export const createPost = protectedAction</span></div><div class="line"><span style="color: #24292EFF">+   .meta({ span: 'create-post' })</span></div><div class="line"><span style="color: #24292EFF">    .input(</span></div><div class="line"><span style="color: #24292EFF">      z.object({</span></div><div class="line"><span style="color: #24292EFF">        title: z.string(),</span></div><div class="line"><span style="color: #24292EFF">      }),</span></div><div class="line"><span style="color: #24292EFF">    )</span></div><div class="line"><span style="color: #24292EFF">    .mutation(async (opts) =&gt; {</span></div><div class="line"><span style="color: #24292EFF">      // Do something with the input</span></div><div class="line"><span style="color: #24292EFF">    });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">diff</div><div class="code-container"><code><div class="line"><span style="color: #FFA198">--- server/trpc.ts</span></div><div class="line"><span style="color: #7EE787">+++ server/trpc.ts</span></div><div class="line"><span style="color: #7EE787">+ import { tracing } from '@baselime/node-opentelemetry/trpc';</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">  export const serverActionProcedure = t.procedure</span></div><div class="line"><span style="color: #C9D1D9">    .experimental_caller(</span></div><div class="line"><span style="color: #C9D1D9">      experimental_nextAppDirCaller({</span></div><div class="line"><span style="color: #C9D1D9">        pathExtractor: (meta: Meta) =&gt; meta.span,</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">    )</span></div><div class="line"><span style="color: #C9D1D9">    .use(async (opts) =&gt; {</span></div><div class="line"><span style="color: #C9D1D9">      // Inject user into context</span></div><div class="line"><span style="color: #C9D1D9">      const user = await currentUser();</span></div><div class="line"><span style="color: #C9D1D9">      return opts.next({ ctx: { user } });</span></div><div class="line"><span style="color: #C9D1D9">    })</span></div><div class="line"><span style="color: #7EE787">+  .use(tracing());</span></div><div class="line"></div><div class="line"><span style="color: #FFA198">--- app/_actions.ts</span></div><div class="line"><span style="color: #7EE787">+++ app/_actions.ts</span></div><div class="line"><span style="color: #C9D1D9">  export const createPost = protectedAction</span></div><div class="line"><span style="color: #7EE787">+   .meta({ span: 'create-post' })</span></div><div class="line"><span style="color: #C9D1D9">    .input(</span></div><div class="line"><span style="color: #C9D1D9">      z.object({</span></div><div class="line"><span style="color: #C9D1D9">        title: z.string(),</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">    )</span></div><div class="line"><span style="color: #C9D1D9">    .mutation(async (opts) =&gt; {</span></div><div class="line"><span style="color: #C9D1D9">      // Do something with the input</span></div><div class="line"><span style="color: #C9D1D9">    });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>자세한 내용은 <a href="https://github.com/baselime/node-opentelemetry/blob/main/TRPC.md" target="_blank" rel="noopener noreferrer">Baselime tRPC 통합 문서</a>를 참고하세요. 사용 중인 관측 가능성 플랫폼에 따라 유사한 패턴을 적용할 수 있습니다.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="속도-제한rate-limiting">속도 제한(Rate Limiting)<a href="https://trpc.io/ko/blog/trpc-actions#%EC%86%8D%EB%8F%84-%EC%A0%9C%ED%95%9Crate-limiting" class="hash-link" aria-label="속도 제한(Rate Limiting) 바로가기 링크" title="속도 제한(Rate Limiting) 바로가기 링크">​</a></h3>
<p>Unkey 같은 서비스를 사용해 서버 액션의 요청 속도를 제한할 수 있습니다. 다음은 사용자별 요청 수를 제한하는 Unkey를 사용하는 보호된 서버 액션 예시입니다:</p>
<div></div>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) class Ratelimit
import Ratelimit">Ratelimit</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@unkey/ratelimit'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">rateLimitedAction</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">protectedAction</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User;
}>(fn: MiddlewareBuilder<{
    user: User;
}, Meta, {
    user: User;
}, UnsetMarker> | MiddlewareFunction<object, Meta, {
    user: User;
}, {
    user: User;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const unkey: Ratelimit">unkey</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new Ratelimit(config: RatelimitConfig): Ratelimit
import Ratelimit">Ratelimit</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) rootKey: string">rootKey</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var process: NodeJS.Process">process</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) NodeJS.Process.env: NodeJS.ProcessEnv">env</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="string | undefined">UNKEY_ROOT_KEY</data-lsp></span><span style="color: #D32F2F">!</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) async?: boolean | undefined">async</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">true</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) duration: number | Duration">duration</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'10s'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) limit: number">limit</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">5</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) namespace: string">namespace</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`trpc_</span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) path: string">path</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const unkey: Ratelimit">unkey</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Ratelimit.limit(identifier: string, opts?: LimitOptions): Promise<RatelimitResponse>">limit</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ctx: {
    user: User;
}">ctx</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) user: User">user</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) User.id: string">id</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">!</span><span style="color: #1976D2"><data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) success: boolean">success</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) code: &quot;UNAUTHORIZED&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;NOT_FOUND&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'TOO_MANY_REQUESTS'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) message?: string | undefined">message</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var JSON: JSON">JSON</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)">stringify</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp>)</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    });</span></div><div class="line"><span style="color: #24292EFF">  }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) next: () => Promise<MiddlewareResult<{
    user: User;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) class Ratelimit
import Ratelimit">Ratelimit</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@unkey/ratelimit'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">rateLimitedAction</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">protectedAction</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User;
}>(fn: MiddlewareBuilder<{
    user: User;
}, Meta, {
    user: User;
}, UnsetMarker> | MiddlewareFunction<object, Meta, {
    user: User;
}, {
    user: User;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const unkey: Ratelimit">unkey</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new Ratelimit(config: RatelimitConfig): Ratelimit
import Ratelimit">Ratelimit</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) rootKey: string">rootKey</data-lsp>: <data-lsp lsp="var process: NodeJS.Process">process</data-lsp>.<data-lsp lsp="(property) NodeJS.Process.env: NodeJS.ProcessEnv">env</data-lsp>.</span><span style="color: #79C0FF"><data-lsp lsp="string | undefined">UNKEY_ROOT_KEY</data-lsp></span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) async?: boolean | undefined">async</data-lsp>: </span><span style="color: #79C0FF">true</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) duration: number | Duration">duration</data-lsp>: </span><span style="color: #A5D6FF">'10s'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) limit: number">limit</data-lsp>: </span><span style="color: #79C0FF">5</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) namespace: string">namespace</data-lsp>: </span><span style="color: #A5D6FF">`trpc_${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) path: string">path</data-lsp></span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> <data-lsp lsp="const unkey: Ratelimit">unkey</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Ratelimit.limit(identifier: string, opts?: LimitOptions): Promise<RatelimitResponse>">limit</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.<data-lsp lsp="(property) ctx: {
    user: User;
}">ctx</data-lsp>.<data-lsp lsp="(property) user: User">user</data-lsp>.<data-lsp lsp="(property) User.id: string">id</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"><data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp>.<data-lsp lsp="(property) success: boolean">success</data-lsp>) {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) code: &quot;UNAUTHORIZED&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;NOT_FOUND&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp>: </span><span style="color: #A5D6FF">'TOO_MANY_REQUESTS'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) message?: string | undefined">message</data-lsp>: </span><span style="color: #79C0FF"><data-lsp lsp="var JSON: JSON">JSON</data-lsp></span><span style="color: #C9D1D9">.</span><span style="color: #79C0FF"><data-lsp lsp="(method) JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)">stringify</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp>),</span></div><div class="line"><span style="color: #C9D1D9">    });</span></div><div class="line"><span style="color: #C9D1D9">  }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) next: () => Promise<MiddlewareResult<{
    user: User;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="app/_actions.ts"><div class="code-title">app/_actions.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #22863A">'use server'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'zod'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import rateLimitedAction">rateLimitedAction</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'../server/trpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const commentOnPost: (input: {
    postId: string;
    content: string;
}) => Promise<void>">commentOnPost</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import rateLimitedAction">rateLimitedAction</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.input<z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}, {
    postId: string;
    content: string;
}, UnsetMarker, UnsetMarker, true>">input</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    postId: z.ZodString;
    content: z.ZodString;
}>(shape?: {
    postId: z.ZodString;
    content: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) postId: z.ZodString">postId</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) content: z.ZodString">content</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, { postId: string; content: string; }, { postId: string; content: string; }, UnsetMarker, UnsetMarker, true>.mutation<void>(resolver: ProcedureResolver<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}, UnsetMarker, void>): (input: {
    postId: string;
    content: string;
}) => Promise<void>">mutation</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #1976D2"><data-lsp lsp="namespace console
var console: Console">console</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)">log</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #22863A">`</span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.ctx: {
    user: User;
}">ctx</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) user: User">user</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) User.name: string">name</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A"> commented on </span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.input: {
    postId: string;
    content: string;
}">input</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) postId: string">postId</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A"> saying </span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.input: {
    postId: string;
    content: string;
}">input</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) content: string">content</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    );</span></div><div class="line"><span style="color: #24292EFF">  });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="app/_actions.ts"><div class="code-title">app/_actions.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #A5D6FF">'use server'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'zod'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import rateLimitedAction">rateLimitedAction</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'../server/trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const commentOnPost: (input: {
    postId: string;
    content: string;
}) => Promise<void>">commentOnPost</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import rateLimitedAction">rateLimitedAction</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.input<z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}, {
    postId: string;
    content: string;
}, UnsetMarker, UnsetMarker, true>">input</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    postId: z.ZodString;
    content: z.ZodString;
}>(shape?: {
    postId: z.ZodString;
    content: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) postId: z.ZodString">postId</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) content: z.ZodString">content</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  )</span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, { postId: string; content: string; }, { postId: string; content: string; }, UnsetMarker, UnsetMarker, true>.mutation<void>(resolver: ProcedureResolver<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}, UnsetMarker, void>): (input: {
    postId: string;
    content: string;
}) => Promise<void>">mutation</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="namespace console
var console: Console">console</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)">log</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #A5D6FF">`${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.ctx: {
    user: User;
}">ctx</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) user: User">user</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) User.name: string">name</data-lsp></span><span style="color: #A5D6FF">} commented on ${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.input: {
    postId: string;
    content: string;
}">input</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) postId: string">postId</data-lsp></span><span style="color: #A5D6FF">} saying ${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.input: {
    postId: string;
    content: string;
}">input</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) content: string">content</data-lsp></span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    );</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>tRPC 절차 속도 제한에 대한 자세한 내용은 <a href="https://www.unkey.com/blog/ratelimit-trpc-routes" target="_blank" rel="noopener noreferrer">Unkey 팀의 게시물</a>에서 확인하세요.</p>
<p>가능성은 무궁무진하며, 여러분은 이미 현재 tRPC 애플리케이션에서 유용한 유틸리티 미들웨어를 사용 중일 겁니다. 아직 없다면 <code>npm install</code>로 설치할 수 있는 것들을 찾아보세요!</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="마무리">마무리<a href="https://trpc.io/ko/blog/trpc-actions#%EB%A7%88%EB%AC%B4%EB%A6%AC" class="hash-link" aria-label="마무리 바로가기 링크" title="마무리 바로가기 링크">​</a></h2>
<p>서버 액션은 만능 해결책이 아닙니다. 더 동적인 데이터가 필요한 경우 클라이언트 측 React Query 캐시에 데이터를 유지하고 <code>useMutation</code>을 사용하는 것이 더 적합할 수 있습니다. 이는 완전히 타당한 접근 방식입니다. 이러한 새로운 기본 요소는 점진적으로 도입하기 쉬우므로, 기존 tRPC API의 개별 절차를 서버 액션으로 전환할 수 있습니다. 전체 API를 재작성할 필요는 없습니다.</p>
<p>tRPC를 사용해 서버 액션을 정의하면 현재 사용 중인 로직을 대부분 공유할 수 있으며, 변이를 서버 액션으로 노출할지 전통적인 변이로 노출할지 선택할 수 있습니다. 개발자에게 애플리케이션에 가장 적합한 패턴을 선택할 수 있는 권한이 주어집니다. 현재 tRPC를 사용하지 않는다면 타입 안전성과 입력 검증이 적용된 서버 액션을 정의할 수 있는 패키지(<a href="https://github.com/TheEdoRan/next-safe-action" target="_blank" rel="noopener noreferrer">next-safe-action</a>이나 <a href="https://github.com/IdoPesok/zsa" target="_blank" rel="noopener noreferrer">zsa</a> 등)도 살펴볼 가치가 있습니다.</p>
<p>실제로 이를 적용한 앱을 확인하려면 제가 최근에 만든 <a href="https://trellix-trpc.vercel.app/" target="_blank" rel="noopener noreferrer">Trellix tRPC</a>를 참고하세요.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="의견을-남겨주세요">의견을 남겨주세요<a href="https://trpc.io/ko/blog/trpc-actions#%EC%9D%98%EA%B2%AC%EC%9D%84-%EB%82%A8%EA%B2%A8%EC%A3%BC%EC%84%B8%EC%9A%94" class="hash-link" aria-label="의견을 남겨주세요 바로가기 링크" title="의견을 남겨주세요 바로가기 링크">​</a></h3>
<p>어떻게 생각하시나요? <a href="https://github.com/trpc/trpc/discussions/5737" target="_blank" rel="noopener noreferrer">Github</a>에서 의견을 공유해 주시면 이 기본 요소들을 안정화하는 데 도움이 됩니다.</p>
<p>오류 처리와 관련해 특히 해결해야 할 과제가 남아있습니다. Next.js는 오류 반환을 권장하며, 이를 최대한 타입 안전하게 만들고자 합니다. <a href="https://github.com/trpc/trpc/pull/5554" target="_blank" rel="noopener noreferrer">Alex의 WIP PR</a>에서 초기 작업을 확인할 수 있습니다.</p>
<p>그럼 다음에 뵙겠습니다. 즐거운 코딩 되세요!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[작은 tRPC 클라이언트 작성하기]]></title>
            <link>https://trpc.io/ko/blog/tinyrpc-client</link>
            <guid>https://trpc.io/ko/blog/tinyrpc-client</guid>
            <pubDate>Tue, 17 Jan 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>비공식 베타 번역</div><div class="admonitionContent_e2NW"><p>이 페이지는 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.
오류를 발견하셨나요? <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2023-01-17-tinyrpc-client.mdx&amp;locale=ko" target="_blank" rel="noopener noreferrer">문제 신고 →</a></p></div></div>
<p>tRPC가 어떻게 동작하는지 궁금한 적 있으신가요? 프로젝트에 기여하고 싶지만 내부 구조가 두려우셨나요? 이 글은 tRPC의 핵심 작동 방식을 다루는 최소한의 클라이언트를 작성하며 내부 구조를 익히는 데 목적을 두고 있습니다.</p>
<!-- -->
<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>정보</div><div class="admonitionContent_e2NW"><p>TypeScript의 핵심 개념인 제네릭, 조건부 타입, <code>extends</code> 키워드, 재귀를 이해하는 것이 권장됩니다. 해당 개념이 익숙하지 않다면, 계속 읽기 전에 <a href="https://twitter.com/mattpocockuk" target="_blank" rel="noopener noreferrer">Matt Pocock</a>의 <a href="https://github.com/total-typescript/beginners-typescript-tutorial" target="_blank" rel="noopener noreferrer">초급 TypeScript</a> 튜토리얼을 통해 개념을 익히시길 바랍니다.</p></div></div>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="개요">개요<a href="https://trpc.io/ko/blog/tinyrpc-client#%EA%B0%9C%EC%9A%94" class="hash-link" aria-label="개요 바로가기 링크" title="개요 바로가기 링크">​</a></h2>
<p>다음과 같은 세 가지 프로시저를 가진 간단한 tRPC 라우터가 있다고 가정해 보겠습니다:</p>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Post = {
    id: string;
    title: string;
}">Post</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> { <data-lsp lsp="(property) id: string">id</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">; <data-lsp lsp="(property) title: string">title</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF"> };</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Post = {
    id: string;
    title: string;
}">Post</data-lsp></span><span style="color: #24292EFF">[] </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [];</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}>>">appRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const router: RouterBuilder
<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}>(_: {
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) post: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}>>">post</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const router: RouterBuilder
<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}>(_: {
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) byId: QueryProcedure<{
    input: {
        id: string;
    };
    output: Post;
    meta: object;
}>">byId</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    id: string;
}, {
    id: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    id: z.ZodString;
}>(shape?: {
    id: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) id: z.ZodString">id</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">() }))</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, { id: string; }, { id: string; }, UnsetMarker, UnsetMarker, false>.query<Post>(resolver: ProcedureResolver<object, object, object, {
    id: string;
}, UnsetMarker, Post>): QueryProcedure<{
    input: {
        id: string;
    };
    output: Post;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(({ <data-lsp lsp="(parameter) input: {
    id: string;
}">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const post: Post | undefined">post</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<Post>.find(predicate: (value: Post, index: number, obj: Post[]) => unknown, thisArg?: any): Post | undefined (+1 overload)">find</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) p: Post">p</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) p: Post">p</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) id: string">id</data-lsp> </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) input: {
    id: string;
}">input</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) id: string">id</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">!</span><span style="color: #24292EFF"><data-lsp lsp="const post: Post | undefined">post</data-lsp>) </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) code: &quot;NOT_FOUND&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;UNAUTHORIZED&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">"NOT_FOUND"</span><span style="color: #24292EFF"> });</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> <data-lsp lsp="const post: Post">post</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">      })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) byTitle: QueryProcedure<{
    input: {
        title: string;
    };
    output: Post;
    meta: object;
}>">byTitle</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) title: z.ZodString">title</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">() }))</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, false>.query<Post>(resolver: ProcedureResolver<object, object, object, {
    title: string;
}, UnsetMarker, Post>): QueryProcedure<{
    input: {
        title: string;
    };
    output: Post;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(({ <data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const post: Post | undefined">post</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<Post>.find(predicate: (value: Post, index: number, obj: Post[]) => unknown, thisArg?: any): Post | undefined (+1 overload)">find</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) p: Post">p</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) p: Post">p</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) title: string">title</data-lsp> </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) title: string">title</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">!</span><span style="color: #24292EFF"><data-lsp lsp="const post: Post | undefined">post</data-lsp>) </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) code: &quot;NOT_FOUND&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;UNAUTHORIZED&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">"NOT_FOUND"</span><span style="color: #24292EFF"> });</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> <data-lsp lsp="const post: Post">post</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">      })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) create: MutationProcedure<{
    input: {
        title: string;
    };
    output: {
        title: string;
        id: string;
    };
    meta: object;
}>">create</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) title: z.ZodString">title</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">() }))</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, false>.mutation<{
    title: string;
    id: string;
}>(resolver: ProcedureResolver<object, object, object, {
    title: string;
}, UnsetMarker, {
    title: string;
    id: string;
}>): MutationProcedure<{
    input: {
        title: string;
    };
    output: {
        title: string;
        id: string;
    };
    meta: object;
}>">mutation</data-lsp></span><span style="color: #24292EFF">(({ <data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> { <data-lsp lsp="(property) id: string">id</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const uuid: () => string">uuid</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">...</span><span style="color: #24292EFF"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp> };</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #1976D2"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<Post>.push(...items: Post[]): number">push</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> <data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">      })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Post = {
    id: string;
    title: string;
}">Post</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> { </span><span style="color: #FFA657"><data-lsp lsp="(property) id: string">id</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">; </span><span style="color: #FFA657"><data-lsp lsp="(property) title: string">title</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9"> };</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Post = {
    id: string;
    title: string;
}">Post</data-lsp></span><span style="color: #C9D1D9">[] </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> [];</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}>>">appRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="const router: RouterBuilder
<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}>(_: {
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) post: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}>>">post</data-lsp>: </span><span style="color: #D2A8FF"><data-lsp lsp="const router: RouterBuilder
<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}>(_: {
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) byId: QueryProcedure<{
    input: {
        id: string;
    };
    output: Post;
    meta: object;
}>">byId</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    id: string;
}, {
    id: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    id: z.ZodString;
}>(shape?: {
    id: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) id: z.ZodString">id</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">() }))</span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, { id: string; }, { id: string; }, UnsetMarker, UnsetMarker, false>.query<Post>(resolver: ProcedureResolver<object, object, object, {
    id: string;
}, UnsetMarker, Post>): QueryProcedure<{
    input: {
        id: string;
    };
    output: Post;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: {
    id: string;
}">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const post: Post | undefined">post</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const posts: Post[]">posts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<Post>.find(predicate: (value: Post, index: number, obj: Post[]) => unknown, thisArg?: any): Post | undefined (+1 overload)">find</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) p: Post">p</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) p: Post">p</data-lsp>.<data-lsp lsp="(property) id: string">id</data-lsp> </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) input: {
    id: string;
}">input</data-lsp>.<data-lsp lsp="(property) id: string">id</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"><data-lsp lsp="const post: Post | undefined">post</data-lsp>) </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) code: &quot;NOT_FOUND&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;UNAUTHORIZED&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp>: </span><span style="color: #A5D6FF">"NOT_FOUND"</span><span style="color: #C9D1D9"> });</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const post: Post">post</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) byTitle: QueryProcedure<{
    input: {
        title: string;
    };
    output: Post;
    meta: object;
}>">byTitle</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) title: z.ZodString">title</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">() }))</span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, false>.query<Post>(resolver: ProcedureResolver<object, object, object, {
    title: string;
}, UnsetMarker, Post>): QueryProcedure<{
    input: {
        title: string;
    };
    output: Post;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const post: Post | undefined">post</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const posts: Post[]">posts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<Post>.find(predicate: (value: Post, index: number, obj: Post[]) => unknown, thisArg?: any): Post | undefined (+1 overload)">find</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) p: Post">p</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) p: Post">p</data-lsp>.<data-lsp lsp="(property) title: string">title</data-lsp> </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp>.<data-lsp lsp="(property) title: string">title</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"><data-lsp lsp="const post: Post | undefined">post</data-lsp>) </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) code: &quot;NOT_FOUND&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;UNAUTHORIZED&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp>: </span><span style="color: #A5D6FF">"NOT_FOUND"</span><span style="color: #C9D1D9"> });</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const post: Post">post</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) create: MutationProcedure<{
    input: {
        title: string;
    };
    output: {
        title: string;
        id: string;
    };
    meta: object;
}>">create</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) title: z.ZodString">title</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">() }))</span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, false>.mutation<{
    title: string;
    id: string;
}>(resolver: ProcedureResolver<object, object, object, {
    title: string;
}, UnsetMarker, {
    title: string;
    id: string;
}>): MutationProcedure<{
    input: {
        title: string;
    };
    output: {
        title: string;
        id: string;
    };
    meta: object;
}>">mutation</data-lsp></span><span style="color: #C9D1D9">(({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> { <data-lsp lsp="(property) id: string">id</data-lsp>: </span><span style="color: #D2A8FF"><data-lsp lsp="const uuid: () => string">uuid</data-lsp></span><span style="color: #C9D1D9">(), </span><span style="color: #FF7B72">...</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp> };</span></div><div class="line"><span style="color: #C9D1D9">        <data-lsp lsp="const posts: Post[]">posts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<Post>.push(...items: Post[]): number">push</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">  }),</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>우리 클라이언트의 목표는 다음과 같은 방식으로 프로시저를 호출할 수 있도록 클라이언트 측에서 이 객체 구조를 재현하는 것입니다:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">post1</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">post</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">byId</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">({ id</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'123'</span><span style="color: #24292EFF"> });</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">post2</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">post</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">byTitle</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">({ title</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'Hello world'</span><span style="color: #24292EFF"> });</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">newPost</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">post</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">create</span><span style="color: #6F42C1">.mutate</span><span style="color: #24292EFF">({ title</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'Foo'</span><span style="color: #24292EFF"> });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">post1</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.post.byId.</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">({ id: </span><span style="color: #A5D6FF">'123'</span><span style="color: #C9D1D9"> });</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">post2</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.post.byTitle.</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">({ title: </span><span style="color: #A5D6FF">'Hello world'</span><span style="color: #C9D1D9"> });</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">newPost</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.post.create.</span><span style="color: #D2A8FF">mutate</span><span style="color: #C9D1D9">({ title: </span><span style="color: #A5D6FF">'Foo'</span><span style="color: #C9D1D9"> });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>이를 위해 tRPC는 <a href="https://mdn.io/proxy" target="_blank" rel="noopener noreferrer"><code>Proxy</code></a> 객체와 TypeScript 마법을 조합하여 객체 구조에 <code>.query</code> 및 <code>.mutate</code> 메서드를 추가합니다. 즉, 탁월한 개발자 경험을 제공하기 위해 실제로 수행하는 작업에 대해 '거짓말'을 하는 셈입니다(자세한 내용은 후술).</p>
<p>상위 수준에서 우리가 원하는 것은 <code>post.byId.query()</code>를 서버로의 GET 요청으로, <code>post.create.mutate()</code>를 POST 요청으로 매핑하는 것이며, 모든 타입이 백엔드에서 프론트엔드로 전파되어야 합니다. 그렇다면 어떻게 구현할 수 있을까요?</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="작은-trpc-클라이언트-구현하기">작은 tRPC 클라이언트 구현하기<a href="https://trpc.io/ko/blog/tinyrpc-client#%EC%9E%91%EC%9D%80-trpc-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0" class="hash-link" aria-label="작은 tRPC 클라이언트 구현하기 바로가기 링크" title="작은 tRPC 클라이언트 구현하기 바로가기 링크">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="️-typescript-마법">🧙‍♀️ TypeScript 마법<a href="https://trpc.io/ko/blog/tinyrpc-client#%EF%B8%8F-typescript-%EB%A7%88%EB%B2%95" class="hash-link" aria-label="🧙‍♀️ TypeScript 마법 바로가기 링크" title="🧙‍♀️ TypeScript 마법 바로가기 링크">​</a></h3>
<p>tRPC 사용 시 누리는 멋진 자동 완성과 타입 안전성을 해제하는 재미있는 TypeScript 마법부터 시작해 보겠습니다.</p>
<p>임의 깊이의 라우터 구조를 추론하기 위해 재귀 타입이 필요합니다. 또한 <code>post.byId</code> 및 <code>post.create</code> 같은 프로시저에 각각 <code>.query</code>와 <code>.mutate</code> 메서드를 추가하고 싶습니다. tRPC에서는 이를 프로시저 장식(decorating)이라고 부릅니다. <code>@trpc/server</code>에는 해결된 메서드로 프로시저의 입출력 타입을 추론하는 도우미가 있어, 이 함수들의 타입을 추론하는 데 활용할 것입니다. 이제 코드를 작성해 봅시다!</p>
<p>경로에 대한 자동 완성과 프로시저 입출력 타입 추론을 제공하기 위해 달성하고자 하는 목표를 고려해 보겠습니다:</p>
<ul>
<li>
<p>라우터에 있을 때는 해당 하위 라우터와 프로시저에 접근할 수 있어야 합니다(곧 다룰 예정).</p>
</li>
<li>
<p>쿼리 프로시저에 있을 때는 <code>.query</code>를 호출할 수 있어야 합니다.</p>
</li>
<li>
<p>변형(mutation) 프로시저에 있을 때는 <code>.mutate</code>를 호출할 수 있어야 합니다.</p>
</li>
<li>
<p>그 외 항목에 접근하려고 할 때는 백엔드에 존재하지 않는 프로시저임을 알리는 타입 오류가 발생해야 합니다.</p>
</li>
</ul>
<p>이를 수행할 타입을 생성해 봅시다:</p>
<div></div>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateProcedure<TProcedure> = TProcedure extends AnyTRPCQueryProcedure ? {
    query: Resolver<TProcedure>;
} : TProcedure extends AnyTRPCMutationProcedure ? {
    mutate: Resolver<TProcedure>;
} : never">DecorateProcedure</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCQueryProcedure = QueryProcedure<any>
import AnyTRPCQueryProcedure">AnyTRPCQueryProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) query: Resolver<TProcedure>">query</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt;;</span></div><div class="line"><span style="color: #24292EFF">    }</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCMutationProcedure = MutationProcedure<any>
import AnyTRPCMutationProcedure">AnyTRPCMutationProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) mutate: Resolver<TProcedure>">mutate</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt;;</span></div><div class="line"><span style="color: #24292EFF">    }</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span><span style="color: #24292EFF">;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateProcedure<TProcedure> = TProcedure extends AnyTRPCQueryProcedure ? {
    query: Resolver<TProcedure>;
} : TProcedure extends AnyTRPCMutationProcedure ? {
    mutate: Resolver<TProcedure>;
} : never">DecorateProcedure</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCQueryProcedure = QueryProcedure<any>
import AnyTRPCQueryProcedure">AnyTRPCQueryProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FFA657"><data-lsp lsp="(property) query: Resolver<TProcedure>">query</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt;;</span></div><div class="line"><span style="color: #C9D1D9">    }</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCMutationProcedure = MutationProcedure<any>
import AnyTRPCMutationProcedure">AnyTRPCMutationProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FFA657"><data-lsp lsp="(property) mutate: Resolver<TProcedure>">mutate</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt;;</span></div><div class="line"><span style="color: #C9D1D9">    }</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span><span style="color: #C9D1D9">;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>tRPC의 내장 추론 도우미를 활용해 프로시저의 입출력 타입을 추론하여 <code>Resolver</code> 타입을 정의하겠습니다.</p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type inferProcedureInput<TProcedure extends AnyProcedure> = undefined extends inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] ? void | inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] : inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;]
import inferProcedureInput">inferProcedureInput</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type inferProcedureOutput<TProcedure> = inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;output&quot;]
import inferProcedureOutput">inferProcedureOutput</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type AnyTRPCQueryProcedure = QueryProcedure<any>
import AnyTRPCQueryProcedure">AnyTRPCQueryProcedure</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type AnyTRPCMutationProcedure = MutationProcedure<any>
import AnyTRPCMutationProcedure">AnyTRPCMutationProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">} </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> (</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(parameter) input: inferProcedureInput<TProcedure>">input</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type inferProcedureInput<TProcedure extends AnyProcedure> = undefined extends inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] ? void | inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] : inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;]
import inferProcedureInput">inferProcedureInput</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt;</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Promise<T>">Promise</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(alias) type inferProcedureOutput<TProcedure> = inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;output&quot;]
import inferProcedureOutput">inferProcedureOutput</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt;&gt;;</span></div><div class="line">&nbsp;</div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type inferProcedureInput<TProcedure extends AnyProcedure> = undefined extends inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] ? void | inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] : inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;]
import inferProcedureInput">inferProcedureInput</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type inferProcedureOutput<TProcedure> = inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;output&quot;]
import inferProcedureOutput">inferProcedureOutput</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type AnyTRPCQueryProcedure = QueryProcedure<any>
import AnyTRPCQueryProcedure">AnyTRPCQueryProcedure</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type AnyTRPCMutationProcedure = MutationProcedure<any>
import AnyTRPCMutationProcedure">AnyTRPCMutationProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">} </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> (</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: inferProcedureInput<TProcedure>">input</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type inferProcedureInput<TProcedure extends AnyProcedure> = undefined extends inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] ? void | inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] : inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;]
import inferProcedureInput">inferProcedureInput</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt;,</span></div><div class="line"><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Promise<T>">Promise</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(alias) type inferProcedureOutput<TProcedure> = inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;output&quot;]
import inferProcedureOutput">inferProcedureOutput</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt;&gt;;</span></div><div class="line">&nbsp;</div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p><code>post.byId</code> 프로시저에서 이 타입을 테스트해 보겠습니다:</p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type PostById = (input: {
    id: string;
}) => Promise<Post>" style="border-bottom: solid 2px lightgrey;">PostById</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
    }>>;
}>> &amp; DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
    }>>;
}>">AppRouter</data-lsp></span><span style="color: #24292EFF">[</span><span style="color: #22863A">'post'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'byId'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="meta-line"><span class="popover-prefix">        </span><span class="popover"><div class="arrow"></div>type PostById = (input: {
    id: string;
}) =&gt; Promise&lt;Post&gt;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type PostById = (input: {
    id: string;
}) => Promise<Post>" style="border-bottom: solid 2px lightgrey;">PostById</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
    }>>;
}>> &amp; DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
    }>>;
}>">AppRouter</data-lsp></span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'post'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'byId'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="meta-line"><span class="popover-prefix">        </span><span class="popover"><div class="arrow"></div>type PostById = (input: {
    id: string;
}) =&gt; Promise&lt;Post&gt;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>좋습니다! 예상대로 프로시저에서 <code>.query</code>를 호출하고 정확한 입출력 타입이 추론됩니다!</p>
<p>마지막으로 라우터를 재귀적으로 순회하며 경로의 모든 프로시저를 장식하는 타입을 생성하겠습니다:</p>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">"@trpc/server"</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) type AnyTRPCRouter = Router<any, any>
import AnyTRPCRouter">AnyTRPCRouter</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">"@trpc/server"</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp></span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  [</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TKey">TKey</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">keyof</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #24292EFF">]</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #24292EFF">[</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TKey">TKey</data-lsp></span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">infer</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #24292EFF">&gt;</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateProcedure<TProcedure> = TProcedure extends AnyTRPCQueryProcedure ? {
    query: Resolver<TProcedure>;
} : TProcedure extends AnyTRPCMutationProcedure ? {
    mutate: Resolver<TProcedure>;
} : never">DecorateProcedure</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #24292EFF">&gt;</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">};</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">"@trpc/server"</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) type AnyTRPCRouter = Router<any, any>
import AnyTRPCRouter">AnyTRPCRouter</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">"@trpc/server"</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp></span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  [</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TKey">TKey</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">in</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">keyof</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #C9D1D9">]</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #C9D1D9">[</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TKey">TKey</data-lsp></span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">infer</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #C9D1D9">&gt;</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateProcedure<TProcedure> = TProcedure extends AnyTRPCQueryProcedure ? {
    query: Resolver<TProcedure>;
} : TProcedure extends AnyTRPCMutationProcedure ? {
    mutate: Resolver<TProcedure>;
} : never">DecorateProcedure</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #C9D1D9">&gt;</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">};</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>이 타입을 조금 분석해 보겠습니다:</p>
<ol>
<li>
<p>제네릭으로 <code>TRPCRouterRecord</code>를 타입에 전달합니다. 이는 tRPC 라우터에 존재하는 모든 프로시저와 하위 라우터를 포함하는 타입입니다.</p>
</li>
<li>
<p>레코드의 키(프로시저 또는 라우터 이름)를 순회하며 다음을 수행합니다:</p>
<ul>
<li>키가 라우터를 가리키는 경우, 해당 라우터의 프로시저 레코드에 대해 재귀적으로 타입을 적용하여 모든 프로시저를 데코레이팅합니다. 이를 통해 경로 탐색 시 자동완성이 제공됩니다.</li>
<li>키가 프로시저를 가리키는 경우, 앞서 생성한 <code>DecorateProcedure</code> 타입을 사용해 프로시저를 데코레이팅합니다.</li>
<li>키가 프로시저나 라우터가 아닌 경우 <code>never</code> 타입을 할당하여 "이 키는 존재하지 않음"을 의미하게 합니다. 이는 접근 시도 시 타입 오류를 발생시킵니다.</li>
</ul>
</li>
</ol>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="-프록시-재매핑-구현하기">🤯 프록시 재매핑 구현하기<a href="https://trpc.io/ko/blog/tinyrpc-client#-%ED%94%84%EB%A1%9D%EC%8B%9C-%EC%9E%AC%EB%A7%A4%ED%95%91-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0" class="hash-link" aria-label="🤯 프록시 재매핑 구현하기 바로가기 링크" title="🤯 프록시 재매핑 구현하기 바로가기 링크">​</a></h3>
<p>이제 타입 설정을 완료했으므로, 실제로 클라이언트에서 서버의 라우터 정의를 확장해 프로시저를 일반 함수처럼 호출할 수 있도록 하는 기능을 구현해야 합니다.</p>
<p>먼저 재귀적 프록시 생성을 위한 헬퍼 함수 <code>createRecursiveProxy</code>를 생성합니다:</p>
<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>정보</div><div class="admonitionContent_e2NW"><p>이 구현은 일부 에지 케이스를 처리하지 않는다는 점을 제외하면 실제 프로덕션 코드와 거의 동일합니다. <a href="https://github.com/trpc/trpc/blob/main/packages/server/src/shared/createProxy/index.ts" target="_blank" rel="noopener noreferrer">직접 확인해 보세요</a>!</p></div></div>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">interface</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface ProxyCallbackOptions">ProxyCallbackOptions</data-lsp></span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">readonly</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">[];</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">readonly</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">unknown</span><span style="color: #24292EFF">[];</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type ProxyCallback = (opts: ProxyCallbackOptions) => unknown">ProxyCallback</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface ProxyCallbackOptions">ProxyCallbackOptions</data-lsp></span><span style="color: #24292EFF">) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">unknown</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) callback: ProxyCallback">callback</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type ProxyCallback = (opts: ProxyCallbackOptions) => unknown">ProxyCallback</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) path: readonly string[]">path</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">readonly</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">[]) {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const proxy: unknown">proxy</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">unknown</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var Proxy: ProxyConstructor
new <() => void>(target: () => void, handler: ProxyHandler<() => void>) => () => void">Proxy</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    () </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// dummy no-op function since we don't have any</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// client-side target we want to remap to</span></div><div class="line"><span style="color: #24292EFF">    }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1"><data-lsp lsp="(method) ProxyHandler<() => void>.get?(target: () => void, p: string | symbol, receiver: any): any">get</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) _obj: () => void">_obj</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) key: string | symbol">key</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">typeof</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) key: string | symbol">key</data-lsp> </span><span style="color: #D32F2F">!==</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'string'</span><span style="color: #24292EFF">) </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #C2C3C5">// Recursively compose the full path until a function is invoked</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) callback: ProxyCallback">callback</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">...</span><span style="color: #24292EFF"><data-lsp lsp="(parameter) path: readonly string[]">path</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) key: string">key</data-lsp>]);</span></div><div class="line"><span style="color: #24292EFF">      }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1"><data-lsp lsp="(method) ProxyHandler<() => void>.apply?(target: () => void, thisArg: any, argArray: any[]): any">apply</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) _1: () => void">_1</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) _2: any">_2</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) args: any[]">args</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #C2C3C5">// Call the callback function with the entire path we</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #C2C3C5">// recursively created and forward the arguments</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(parameter) callback: (opts: ProxyCallbackOptions) => unknown">callback</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">          <data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">          <data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">        });</span></div><div class="line"><span style="color: #24292EFF">      }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  );</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> <data-lsp lsp="const proxy: unknown">proxy</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">interface</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface ProxyCallbackOptions">ProxyCallbackOptions</data-lsp></span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">readonly</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">[];</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">readonly</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">unknown</span><span style="color: #C9D1D9">[];</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type ProxyCallback = (opts: ProxyCallbackOptions) => unknown">ProxyCallback</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface ProxyCallbackOptions">ProxyCallbackOptions</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">unknown</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">function</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FFA657"><data-lsp lsp="(parameter) callback: ProxyCallback">callback</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type ProxyCallback = (opts: ProxyCallbackOptions) => unknown">ProxyCallback</data-lsp></span><span style="color: #C9D1D9">, </span><span style="color: #FFA657"><data-lsp lsp="(parameter) path: readonly string[]">path</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">readonly</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">[]) {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const proxy: unknown">proxy</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">unknown</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var Proxy: ProxyConstructor
new <() => void>(target: () => void, handler: ProxyHandler<() => void>) => () => void">Proxy</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    () </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// dummy no-op function since we don't have any</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// client-side target we want to remap to</span></div><div class="line"><span style="color: #C9D1D9">    },</span></div><div class="line"><span style="color: #C9D1D9">    {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProxyHandler<() => void>.get?(target: () => void, p: string | symbol, receiver: any): any">get</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FFA657"><data-lsp lsp="(parameter) _obj: () => void">_obj</data-lsp></span><span style="color: #C9D1D9">, </span><span style="color: #FFA657"><data-lsp lsp="(parameter) key: string | symbol">key</data-lsp></span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">typeof</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) key: string | symbol">key</data-lsp> </span><span style="color: #FF7B72">!==</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'string'</span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #8B949E">// Recursively compose the full path until a function is invoked</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="(parameter) callback: ProxyCallback">callback</data-lsp>, [</span><span style="color: #FF7B72">...</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) path: readonly string[]">path</data-lsp>, <data-lsp lsp="(parameter) key: string">key</data-lsp>]);</span></div><div class="line"><span style="color: #C9D1D9">      },</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProxyHandler<() => void>.apply?(target: () => void, thisArg: any, argArray: any[]): any">apply</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FFA657"><data-lsp lsp="(parameter) _1: () => void">_1</data-lsp></span><span style="color: #C9D1D9">, </span><span style="color: #FFA657"><data-lsp lsp="(parameter) _2: any">_2</data-lsp></span><span style="color: #C9D1D9">, </span><span style="color: #FFA657"><data-lsp lsp="(parameter) args: any[]">args</data-lsp></span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #8B949E">// Call the callback function with the entire path we</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #8B949E">// recursively created and forward the arguments</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(parameter) callback: (opts: ProxyCallbackOptions) => unknown">callback</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">          <data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">          <data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">        });</span></div><div class="line"><span style="color: #C9D1D9">      },</span></div><div class="line"><span style="color: #C9D1D9">    },</span></div><div class="line"><span style="color: #C9D1D9">  );</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const proxy: unknown">proxy</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>이 코드가 어떻게 동작하는지 꽤 신비롭게 보일 수 있습니다. 어떤 원리일까요?</p>
<ul>
<li>
<p><code>get</code> 메서드는 <code>post.byId</code>와 같은 프로퍼티 접근을 처리합니다. 키는 접근하는 프로퍼티 이름으로, <code>post</code>를 입력하면 <code>key</code>는 <code>post</code>가 되고, <code>post.byId</code>를 입력하면 <code>key</code>는 <code>byId</code>가 됩니다. 재귀적 프록시는 이러한 키들을 최종 경로(예: ["post", "byId", "query"])로 결합하여 요청을 보낼 URL을 결정합니다.</p>
</li>
<li>
<p><code>apply</code> 메서드는 프록시에서 <code>.query(args)</code>와 같은 함수를 호출할 때 실행됩니다. <code>args</code>는 함수에 전달한 인자로, <code>post.byId.query(args)</code>를 호출하면 <code>args</code>는 입력값이 됩니다. 이 입력값은 프로시저 유형에 따라 쿼리 파라미터 또는 요청 본문으로 제공됩니다. <code>createRecursiveProxy</code>는 콜백 함수를 입력받으며, 이 콜백 함수에 경로와 인자를 사용하여 <code>apply</code> 메서드를 매핑합니다.</p>
</li>
</ul>
<p>아래는 <code>trpc.post.byId.query({ id: 1 })</code> 호출 시 프록시가 동작하는 방식을 시각적으로 표현한 것입니다:</p>
<p><img decoding="async" loading="lazy" src="https://assets.trpc.io/www/blog/2023-01-17-tinyrpc-client/proxy.png" alt="프록시" class="img_Njog"></p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="-모든-요소-통합하기">🧩 모든 요소 통합하기<a href="https://trpc.io/ko/blog/tinyrpc-client#-%EB%AA%A8%EB%93%A0-%EC%9A%94%EC%86%8C-%ED%86%B5%ED%95%A9%ED%95%98%EA%B8%B0" class="hash-link" aria-label="🧩 모든 요소 통합하기 바로가기 링크" title="🧩 모든 요소 통합하기 바로가기 링크">​</a></h3>
<p>이제 헬퍼 함수의 동작을 이해했으니, 이를 사용해 클라이언트를 생성해 보겠습니다. <code>createRecursiveProxy</code>에 경로와 인자를 받아 <code>fetch</code>를 사용해 서버에 요청하는 콜백을 제공합니다. 함수에 tRPC 라우터 타입(<code>AnyTRPCRouter</code>)을 허용하는 제네릭을 추가하고, 반환 타입을 앞서 생성한 <code>DecorateRouterRecord</code> 타입으로 캐스팅합니다:</p>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) type TRPCResponse<TData = unknown, TError extends TRPCErrorShape = TRPCErrorShape<object>> = TRPCErrorResponse<TError> | TRPCSuccessResponse<TData>
import TRPCResponse">TRPCResponse</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/rpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const createTinyRPCClient: <TRouter extends AnyTRPCRouter>(baseUrl: string) => DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">createTinyRPCClient</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> &lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRouter in <TRouter extends AnyTRPCRouter>(baseUrl: string): DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">TRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCRouter = Router<any, any>
import AnyTRPCRouter">AnyTRPCRouter</data-lsp></span><span style="color: #24292EFF">&gt;(</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(parameter) baseUrl: string">baseUrl</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">) </span><span style="color: #D32F2F">=&gt;</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const path: string[]">path</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">...</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp>]; </span><span style="color: #C2C3C5">// e.g. ["post", "byId", "query"]</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const path: string[]">path</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<string>.pop(): string | undefined">pop</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #D32F2F">!</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'query'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'mutate'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const dotPath: string">dotPath</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const path: string[]">path</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<string>.join(separator?: string): string">join</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #22863A">'.'</span><span style="color: #24292EFF">); </span><span style="color: #C2C3C5">// "post.byId" - this is the path procedures have on the backend</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">let</span><span style="color: #24292EFF"> <data-lsp lsp="let uri: string">uri</data-lsp> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`</span><span style="color: #D32F2F">${</span><span style="color: #24292EFF"><data-lsp lsp="(parameter) baseUrl: string">baseUrl</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">/</span><span style="color: #D32F2F">${</span><span style="color: #24292EFF"><data-lsp lsp="const dotPath: string">dotPath</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> [</span><span style="color: #1976D2"><data-lsp lsp="const input: unknown">input</data-lsp></span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const stringifiedInput: string | false">stringifiedInput</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> <data-lsp lsp="const input: unknown">input</data-lsp> </span><span style="color: #D32F2F">!==</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">&amp;&amp;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var JSON: JSON">JSON</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)">stringify</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="const input: {} | null">input</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">let</span><span style="color: #24292EFF"> <data-lsp lsp="let body: string | undefined">body</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">undefined</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (<data-lsp lsp="const stringifiedInput: string | false">stringifiedInput</data-lsp> </span><span style="color: #D32F2F">!==</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">false</span><span style="color: #24292EFF">) {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (<data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp> </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'query'</span><span style="color: #24292EFF">) {</span></div><div class="line"><span style="color: #24292EFF">        <data-lsp lsp="let uri: string">uri</data-lsp> </span><span style="color: #D32F2F">+=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`?input=</span><span style="color: #D32F2F">${</span><span style="color: #6F42C1"><data-lsp lsp="function encodeURIComponent(uriComponent: string | number | boolean): string">encodeURIComponent</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="const stringifiedInput: string">stringifiedInput</data-lsp>)</span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">      } </span><span style="color: #D32F2F">else</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        <data-lsp lsp="let body: string | undefined">body</data-lsp> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> <data-lsp lsp="const stringifiedInput: string">stringifiedInput</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">      }</span></div><div class="line"><span style="color: #24292EFF">    }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const json: TRPCResponse">json</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type TRPCResponse<TData = unknown, TError extends TRPCErrorShape = TRPCErrorShape<object>> = TRPCErrorResponse<TError> | TRPCSuccessResponse<TData>
import TRPCResponse">TRPCResponse</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>">fetch</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="let uri: string">uri</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) RequestInit.method?: string | undefined">method</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp> </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'query'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'GET'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'POST'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) RequestInit.headers?: HeadersInit | undefined">headers</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #22863A">'Content-Type'</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'application/json'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) RequestInit.body?: BodyInit | null | undefined">body</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #6F42C1">.<data-lsp lsp="(method) Promise<Response>.then<any, TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown>>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown> | PromiseLike<TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown>>) | null | undefined): Promise<...>">then</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) res: Response">res</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) res: Response">res</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Body.json(): Promise<any>">json</data-lsp></span><span style="color: #24292EFF">());</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #22863A">'error'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> <data-lsp lsp="const json: TRPCResponse">json</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var Error: ErrorConstructor
new (message?: string) => Error">Error</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #22863A">`Error: </span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="const json: TRPCErrorResponse<TRPCErrorShape<object>>">json</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) JSONRPC2.ErrorResponse<TRPCErrorShape<object>>.error: TRPCErrorShape<object>">error</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) TRPCErrorShape<object>.message: string">message</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #24292EFF">);</span></div><div class="line"><span style="color: #24292EFF">    }</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// No error - all good. Return the data.</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const json: TRPCSuccessResponse<unknown>">json</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) JSONRPC2.ResultResponse<TRPCResult<unknown>>.result: TRPCResult<unknown>">result</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) TRPCResult<unknown>.data: unknown">data</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span><span style="color: #24292EFF"> []) </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRouter in <TRouter extends AnyTRPCRouter>(baseUrl: string): DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">TRouter</data-lsp></span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="line"><span style="color: #C2C3C5">//   ^? provide empty array as path to begin with</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) type TRPCResponse<TData = unknown, TError extends TRPCErrorShape = TRPCErrorShape<object>> = TRPCErrorResponse<TError> | TRPCSuccessResponse<TData>
import TRPCResponse">TRPCResponse</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/rpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="const createTinyRPCClient: <TRouter extends AnyTRPCRouter>(baseUrl: string) => DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">createTinyRPCClient</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> &lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRouter in <TRouter extends AnyTRPCRouter>(baseUrl: string): DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">TRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCRouter = Router<any, any>
import AnyTRPCRouter">AnyTRPCRouter</data-lsp></span><span style="color: #C9D1D9">&gt;(</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(parameter) baseUrl: string">baseUrl</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #D2A8FF"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const path: string[]">path</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> [</span><span style="color: #FF7B72">...</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp>.<data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp>]; </span><span style="color: #8B949E">// e.g. ["post", "byId", "query"]</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const path: string[]">path</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<string>.pop(): string | undefined">pop</data-lsp></span><span style="color: #C9D1D9">()</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'query'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">|</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'mutate'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const dotPath: string">dotPath</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const path: string[]">path</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<string>.join(separator?: string): string">join</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'.'</span><span style="color: #C9D1D9">); </span><span style="color: #8B949E">// "post.byId" - this is the path procedures have on the backend</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">let</span><span style="color: #C9D1D9"> <data-lsp lsp="let uri: string">uri</data-lsp> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">`${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) baseUrl: string">baseUrl</data-lsp></span><span style="color: #A5D6FF">}/${</span><span style="color: #C9D1D9"><data-lsp lsp="const dotPath: string">dotPath</data-lsp></span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> [</span><span style="color: #79C0FF"><data-lsp lsp="const input: unknown">input</data-lsp></span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp>.<data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const stringifiedInput: string | false">stringifiedInput</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const input: unknown">input</data-lsp> </span><span style="color: #FF7B72">!==</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">&amp;&amp;</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var JSON: JSON">JSON</data-lsp></span><span style="color: #C9D1D9">.</span><span style="color: #79C0FF"><data-lsp lsp="(method) JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)">stringify</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="const input: {} | null">input</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">let</span><span style="color: #C9D1D9"> <data-lsp lsp="let body: string | undefined">body</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">undefined</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">|</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (<data-lsp lsp="const stringifiedInput: string | false">stringifiedInput</data-lsp> </span><span style="color: #FF7B72">!==</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">false</span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (<data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp> </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'query'</span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">        <data-lsp lsp="let uri: string">uri</data-lsp> </span><span style="color: #FF7B72">+=</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">`?input=${</span><span style="color: #79C0FF"><data-lsp lsp="function encodeURIComponent(uriComponent: string | number | boolean): string">encodeURIComponent</data-lsp></span><span style="color: #A5D6FF">(</span><span style="color: #C9D1D9"><data-lsp lsp="const stringifiedInput: string">stringifiedInput</data-lsp></span><span style="color: #A5D6FF">)</span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">      } </span><span style="color: #FF7B72">else</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        <data-lsp lsp="let body: string | undefined">body</data-lsp> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const stringifiedInput: string">stringifiedInput</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">      }</span></div><div class="line"><span style="color: #C9D1D9">    }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const json: TRPCResponse">json</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type TRPCResponse<TData = unknown, TError extends TRPCErrorShape = TRPCErrorShape<object>> = TRPCErrorResponse<TError> | TRPCSuccessResponse<TData>
import TRPCResponse">TRPCResponse</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>">fetch</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="let uri: string">uri</data-lsp>, {</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) RequestInit.method?: string | undefined">method</data-lsp>: <data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp> </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'query'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'GET'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'POST'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) RequestInit.headers?: HeadersInit | undefined">headers</data-lsp>: {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #A5D6FF">'Content-Type'</span><span style="color: #C9D1D9">: </span><span style="color: #A5D6FF">'application/json'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">      },</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) RequestInit.body?: BodyInit | null | undefined">body</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">    }).</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Promise<Response>.then<any, TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown>>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown> | PromiseLike<TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown>>) | null | undefined): Promise<...>">then</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) res: Response">res</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) res: Response">res</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Body.json(): Promise<any>">json</data-lsp></span><span style="color: #C9D1D9">());</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #A5D6FF">'error'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">in</span><span style="color: #C9D1D9"> <data-lsp lsp="const json: TRPCResponse">json</data-lsp>) {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var Error: ErrorConstructor
new (message?: string) => Error">Error</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">`Error: ${</span><span style="color: #C9D1D9"><data-lsp lsp="const json: TRPCErrorResponse<TRPCErrorShape<object>>">json</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) JSONRPC2.ErrorResponse<TRPCErrorShape<object>>.error: TRPCErrorShape<object>">error</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) TRPCErrorShape<object>.message: string">message</data-lsp></span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">);</span></div><div class="line"><span style="color: #C9D1D9">    }</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// No error - all good. Return the data.</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const json: TRPCSuccessResponse<unknown>">json</data-lsp>.<data-lsp lsp="(property) JSONRPC2.ResultResponse<TRPCResult<unknown>>.result: TRPCResult<unknown>">result</data-lsp>.<data-lsp lsp="(property) TRPCResult<unknown>.data: unknown">data</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">  }, []) </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRouter in <TRouter extends AnyTRPCRouter>(baseUrl: string): DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">TRouter</data-lsp></span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="line"><span style="color: #8B949E">//   ^? provide empty array as path to begin with</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>특히 주목할 점은 경로가 <code>/</code>가 아닌 <code>.</code>으로 구분된다는 것입니다. 이를 통해 서버에서 각 프로시저마다 별도의 핸들러를 두지 않고 단일 API 핸들러가 모든 요청을 처리할 수 있습니다. Next.js처럼 파일 기반 라우팅을 사용하는 프레임워크를 사용 중이라면, 모든 프로시저 경로를 처리하는 캐치올(catchall) 파일 <code>/api/trpc/[trpc].ts</code>를 떠올릴 수 있을 것입니다.</p>
<p>또한 <code>fetch</code> 요청에는 <code>TRPCResponse</code> 타입 어노테이션이 추가됩니다. 이는 서버가 응답하는 JSONRPC 호환 응답 형식을 정의합니다. 자세한 내용은 <a href="https://trpc.io/docs/rpc" target="_blank" rel="noopener noreferrer">여기</a>에서 확인할 수 있습니다. 간단히 말해, 응답에는 <code>result</code> 또는 <code>error</code> 객체가 포함되어 요청 성공 여부를 판단하고 오류 발생 시 적절한 처리를 할 수 있습니다.</p>
<p>이게 전부입니다! 이 코드만으로도 클라이언트에서 tRPC 프로시저를 로컬 함수처럼 호출할 수 있습니다. 표면적으로는 단순히 <code>publicProcedure.query / mutation</code>의 리졸버 함수를 일반 프로퍼티 접근으로 호출하는 것처럼 보이지만, 실제로는 네트워크 경계를 넘어 동작하므로 Prisma 같은 서버 사이드 라이브러리를 사용하면서 데이터베이스 자격 증명이 유출되지 않습니다.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="직접-해보기">직접 해보기!<a href="https://trpc.io/ko/blog/tinyrpc-client#%EC%A7%81%EC%A0%91-%ED%95%B4%EB%B3%B4%EA%B8%B0" class="hash-link" aria-label="직접 해보기! 바로가기 링크" title="직접 해보기! 바로가기 링크">​</a></h2>
<p>이제 클라이언트를 생성하고 서버 URL을 제공하면, 프로시저를 호출할 때 완벽한 자동완성과 타입 안전성을 얻을 수 있습니다!</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">url</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'http://localhost:3000/api/trpc'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createTinyRPCClient</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">AppRouter</span><span style="color: #24292EFF">&gt;(url);</span></div><div class="line"></div><div class="line"><span style="color: #C2C3C5">// 🧙‍♀️ magic autocompletion</span></div><div class="line"></div><div class="line"><span style="color: #1976D2">client</span><span style="color: #24292EFF">.</span><span style="color: #1976D2">post</span><span style="color: #24292EFF">.b;</span></div><div class="line"></div><div class="line"><span style="color: #C2C3C5">// 👀 fully typesafe</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">post</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">post</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">byId</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">({ id</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'123'</span><span style="color: #24292EFF"> });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">url</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'http://localhost:3000/api/trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">client</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createTinyRPCClient</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">AppRouter</span><span style="color: #C9D1D9">&gt;(url);</span></div><div class="line"></div><div class="line"><span style="color: #8B949E">// 🧙‍♀️ magic autocompletion</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">client.post.b;</span></div><div class="line"></div><div class="line"><span style="color: #8B949E">// 👀 fully typesafe</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">post</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.post.byId.</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">({ id: </span><span style="color: #A5D6FF">'123'</span><span style="color: #C9D1D9"> });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>전체 클라이언트 코드는 <a href="https://github.com/trpc/trpc/blob/main/packages/tests/showcase/tinyrpc.ts" target="_blank" rel="noopener noreferrer">여기</a>에서 확인할 수 있으며, 사용 예시 테스트는 <a href="https://github.com/trpc/trpc/blob/main/packages/tests/showcase/tinyrpc.test.ts" target="_blank" rel="noopener noreferrer">여기</a>에서 볼 수 있습니다.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="결론">결론<a href="https://trpc.io/ko/blog/tinyrpc-client#%EA%B2%B0%EB%A1%A0" class="hash-link" aria-label="결론 바로가기 링크" title="결론 바로가기 링크">​</a></h2>
<p>이 글이 tRPC의 작동 방식을 이해하는 데 도움이 되었기를 바랍니다. 실제 프로덕션에서는 @trpc/client를 사용하시길 권장합니다. 몇 KB 더 큰 용량이지만 우리가 보여준 것보다 훨씬 많은 유연성을 제공합니다:</p>
<ul>
<li>
<p>중단 신호(abort signals), SSR 등을 위한 쿼리 옵션</p>
</li>
<li>
<p>링크(Link) 시스템</p>
</li>
<li>
<p>프로시저 일괄 처리(batching)</p>
</li>
<li>
<p>WebSocket/구독(subscriptions) 지원</p>
</li>
<li>
<p>체계적인 오류 처리</p>
</li>
<li>
<p>데이터 변환기(transformers)</p>
</li>
<li>
<p>tRPC 규격 응답이 아닌 경우를 포함한 에지 케이스 처리</p>
</li>
</ul>
<p>오늘은 서버 측 구현을 다루지 않았는데, 아마도 다음 기사에서 다룰 예정입니다. 궁금한 점이 있다면 <a href="https://twitter.com/jullerino" target="_blank" rel="noopener noreferrer">트위터</a>로 문의해주세요.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[v10 리팩터링 중 얻은 TypeScript 성능 교훈]]></title>
            <link>https://trpc.io/ko/blog/typescript-performance-lessons</link>
            <guid>https://trpc.io/ko/blog/typescript-performance-lessons</guid>
            <pubDate>Sat, 14 Jan 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>비공식 베타 번역</div><div class="admonitionContent_e2NW"><p>이 페이지는 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.
오류를 발견하셨나요? <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2023-01-14-typescript-performance-lessons.mdx&amp;locale=ko" target="_blank" rel="noopener noreferrer">문제 신고 →</a></p></div></div>
<p>TypeScript가 tRPC의 탁월한 DX를 가능하게 하는 핵심 동력이라는 것은 공공연한 비밀이 아닙니다. TypeScript 채택은 현대적인 JavaScript 기반 경험을 제공하는 표준이 되었지만, 이러한 타입 안정성 향상에는 일정한 트레이드오프가 존재합니다.</p>
<!-- -->
<p>TypeScript가 tRPC가 놀라운 개발자 경험(DX)을 제공하는 데 있어 원동력이라는 것은 비밀이 아닙니다. 오늘날 TypeScript 채택은 뛰어난 JavaScript 기반 경험을 제공하는 현대적 표준이지만, 이러한 타입에 대한 개선된 확실성에는 일부 트레이드오프가 따릅니다.</p>
<p>오늘날 TypeScript 타입 검사기는 속도가 느려지기 쉬운 경향이 있습니다(비록 <a href="https://devblogs.microsoft.com/typescript/announcing-typescript-4-9/#performance-improvements" target="_blank" rel="noopener noreferrer">TS 4.9</a>와 같은 릴리스가 개선을 약속하지만요!). 라이브러리는 거의 항상 코드베이스에서 가장 정교한 TypeScript 기법을 포함하고 있어, TS 컴파일러를 한계까지 밀어붙입니다. 이러한 이유로 우리 같은 라이브러리 개발자는 이 부하에 기여하는 정도를 인지하고 여러분의 IDE가 최대한 빠르게 작동하도록 최선을 다해야 합니다.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="라이브러리-성능-자동화">라이브러리 성능 자동화<a href="https://trpc.io/ko/blog/typescript-performance-lessons#%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%84%B1%EB%8A%A5-%EC%9E%90%EB%8F%99%ED%99%94" class="hash-link" aria-label="라이브러리 성능 자동화 바로가기 링크" title="라이브러리 성능 자동화 바로가기 링크">​</a></h2>
<p>tRPC가 <code>v9</code>에 있을 때, 개발자들로부터 대규모 tRPC 라우터가 타입 검사기에 악영향을 미치기 시작했다는 보고를 받기 시작했습니다. 이는 tRPC 개발의 <code>v9</code> 단계에서 엄청난 채택을 경험하면서 처음 마주한 문제였습니다. 더 많은 개발자들이 tRPC로 더 크고 복잡한 제품을 만들면서 일부 한계점이 드러나기 시작한 것입니다.</p>
<p>현재 여러분의 라이브러리가 느리지 않을 수 있지만, 라이브러리가 성장하고 변화함에 따라 성능을 계속 주시하는 것이 중요합니다. 자동화된 테스트는 커밋마다 라이브러리 코드를 <em>프로그래매틱하게</em> 테스트함으로써 라이브러리 개발(및 애플리케이션 구축!)에서 엄청난 부담을 덜어줍니다.</p>
<p>tRPC의 경우, 3,500개의 프로시저와 1,000개의 라우터를 가진 라우터를 <a href="https://github.com/trpc/trpc/blob/9fc2d06a8924da73e10b9d4497f3a1f53de706ed/scripts/generateBigBoi.ts" target="_blank" rel="noopener noreferrer">생성</a>하고 <a href="https://github.com/trpc/trpc/blob/9fc2d06a8924da73e10b9d4497f3a1f53de706ed/packages/tests/server/react/bigBoi.test.tsx" target="_blank" rel="noopener noreferrer">테스트</a>함으로써 이를 보장하기 위해 노력합니다. 하지만 이는 TS 컴파일러가 고장 나기 전까지 어디까지 밀어붙일 수 있는지 테스트할 뿐, 타입 검사 시간을 측정하지는 않습니다. 서버, 바닐라 클라이언트, React 클라이언트 등 라이브러리의 세 가지 부분을 모두 테스트하는데, 각각 다른 코드 경로를 가지기 때문입니다. 과거에는 라이브러리의 한 부분에만 고립된 퇴행 현상이 발생한 적이 있으며, 이러한 예상치 못한 동작이 발생할 때 테스트가 이를 보여주도록 의존하고 있습니다. (아직 컴파일 시간 측정을 위해 <a href="https://github.com/trpc/trpc/issues/2892" target="_blank" rel="noopener noreferrer">더 많은 작업</a>을 하고 싶긴 합니다)</p>
<p>tRPC는 런타임 부하가 큰 라이브러리가 아니므로 성능 지표는 타입 검사를 중심으로 합니다. 따라서 우리는 다음과 같은 사항을 염두에 둡니다:</p>
<ul>
<li><code>tsc</code>를 사용한 타입 검사 속도 저하</li>
<li>초기 로딩 시간이 긺</li>
<li>TypeScript 언어 서버가 변경 사항에 응답하는 데 오래 걸림</li>
</ul>
<p>마지막 사항은 tRPC가 가장 주의해야 할 부분입니다. 개발자가 변경 후 언어 서버 업데이트를 기다려야 하는 상황은 <strong>절대</strong> 원하지 않습니다. 이것이 바로 tRPC가 탁월한 DX를 누릴 수 있도록 성능을 유지<strong>해야만</strong> 하는 영역입니다.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="trpc에서-성능-개선-기회를-발견한-방법">tRPC에서 성능 개선 기회를 발견한 방법<a href="https://trpc.io/ko/blog/typescript-performance-lessons#trpc%EC%97%90%EC%84%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-%EA%B8%B0%ED%9A%8C%EB%A5%BC-%EB%B0%9C%EA%B2%AC%ED%95%9C-%EB%B0%A9%EB%B2%95" class="hash-link" aria-label="tRPC에서 성능 개선 기회를 발견한 방법 바로가기 링크" title="tRPC에서 성능 개선 기회를 발견한 방법 바로가기 링크">​</a></h2>
<p>TypeScript 정확성과 컴파일러 성능 사이에는 항상 트레이드오프가 존재합니다. 둘 다 다른 개발자들에게 중요한 문제이므로 타입을 작성하는 방식에 대해 극도로 신중해야 합니다. 특정 타입이 "너무 느슨해서" 애플리케이션이 심각한 오류를 만날 가능성이 있을까요? 성능 향상이 그만한 가치가 있을까요?</p>
<p>과연 의미 있는 성능 향상이 있을까요? 아주 좋은 질문입니다.</p>
<p>TypeScript 정확성과 컴파일러 성능 사이에는 항상 트레이드오프가 존재합니다. 둘 다 다른 개발자들에게 중요한 관심사이므로, 타입 작성 방식에 각별히 주의해야 합니다. 특정 타입이 '너무 느슨해서' 애플리케이션이 심각한 오류를 만날 가능성이 있을까요? 성능 향상이 그만한 가치가 있을까요?</p>
<p>실질적인 성능 향상이 과연 있을까요? 좋은 질문입니다.</p>
<p>TypeScript 코드에서 성능 개선을 위한 순간을 찾는 _방법_을 살펴보겠습니다. <a href="https://github.com/trpc/trpc/pull/2716" target="_blank" rel="noopener noreferrer">PR #2716</a>을 생성하며 거친 과정을 설명하고, 그 결과 TS 컴파일 시간이 59% 감소한 성과를 확인해 보겠습니다.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="slug-typescript-performance-lessonstitle-v10-리팩토링-과정에서-얻은-typescript-성능-교훈authors-sachinraja">slug: typescript-performance-lessons
title: v10 리팩토링 과정에서 얻은 TypeScript 성능 교훈
authors: [sachinraja]<a href="https://trpc.io/ko/blog/typescript-performance-lessons#slug-typescript-performance-lessonstitle-v10-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EA%B3%BC%EC%A0%95%EC%97%90%EC%84%9C-%EC%96%BB%EC%9D%80-typescript-%EC%84%B1%EB%8A%A5-%EA%B5%90%ED%9B%88authors-sachinraja" class="hash-link" aria-label="slug: typescript-performance-lessons
title: v10 리팩토링 과정에서 얻은 TypeScript 성능 교훈
authors: [sachinraja] 바로가기 링크" title="slug: typescript-performance-lessons
title: v10 리팩토링 과정에서 얻은 TypeScript 성능 교훈
authors: [sachinraja] 바로가기 링크">​</a></h2>
<p>TypeScript에는 타입에서 병목 현상을 찾는 데 도움이 되는 내장 <a href="https://github.com/microsoft/TypeScript/wiki/Performance-Tracing" target="_blank" rel="noopener noreferrer">추적 도구</a>가 있습니다. 완벽하지는 않지만, 사용 가능한 최상의 도구입니다.</p>
<p>라이브러리가 실제 개발자에게 미치는 영향을 시뮬레이션하려면 실제 애플리케이션에서 테스트하는 것이 이상적입니다. tRPC의 경우, 많은 사용자가 사용하는 환경을 모방한 기본 <a href="https://create.t3.gg/" target="_blank" rel="noopener noreferrer">T3 앱</a>을 생성했습니다.</p>
<p>tRPC를 트레이싱하기 위해 따랐던 단계는 다음과 같습니다:</p>
<ol>
<li>
<p>예제 앱에 라이브러리를 <a href="https://docs.npmjs.com/cli/commands/npm-link" target="_blank" rel="noopener noreferrer">로컬로 링크</a>합니다. 이렇게 하면 라이브러리 코드를 변경하고 즉시 로컬에서 변경 사항을 테스트할 수 있습니다.</p>
</li>
<li>
<p>예제 앱에서 다음 명령어를 실행합니다:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">sh</div><div class="code-container"><code><div class="line"><span style="color: #24292EFF">tsc --generateTrace ./trace --incremental </span><span style="color: #6F42C1">false</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">sh</div><div class="code-container"><code><div class="line"><span style="color: #C9D1D9">tsc --generateTrace ./trace --incremental </span><span style="color: #79C0FF">false</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
</li>
<li>
<p>컴퓨터에 <code>trace/trace.json</code> 파일이 생성됩니다. 이 파일을 트레이스 분석 앱(<a href="https://ui.perfetto.dev/" target="_blank" rel="noopener noreferrer">Perfetto</a> 사용) 또는 <code>chrome://tracing</code>에서 열 수 있습니다.</p>
</li>
</ol>
<p>여기서부터 흥미로워지며 애플리케이션 내 타입의 성능 프로파일을 파악할 수 있습니다. 첫 번째 트레이스는 다음과 같았습니다:
<img decoding="async" loading="lazy" src="https://assets.trpc.io/www/blog/2023-01-14-typescript-performance-lessons/trace-1.png" alt="트레이스 바에서 src/pages/index.ts가 타입 검사에 332ms 소요된 모습" class="img_Njog"></p>
<p>막대가 길수록 해당 프로세스에 더 많은 시간이 소요되었음을 의미합니다. 이 스크린샷에서는 가장 상단의 녹색 막대를 선택했는데, 이는 <code>src/pages/index.ts</code>가 병목 현상임을 나타냅니다. <code>Duration</code> 필드 아래에는 332ms가 소요된 것으로 표시됩니다. 타입 검사에 엄청난 시간을 소비한 셈입니다! 파란색 <code>checkVariableDeclaration</code> 막대는 컴파일러가 대부분의 시간을 하나의 변수에 소비했음을 알려줍니다. 해당 막대를 클릭하면 어떤 변수인지 확인할 수 있습니다:
<img decoding="async" loading="lazy" src="https://assets.trpc.io/www/blog/2023-01-14-typescript-performance-lessons/trace-2.png" alt="변수 위치가 275임을 보여주는 트레이스 정보" class="img_Njog">
<code>pos</code> 필드는 파일 텍스트 내 변수의 위치를 나타냅니다. <code>src/pages/index.ts</code>에서 해당 위치로 이동하면 문제의 원인이 <code>utils = trpc.useContext()</code>임을 알 수 있습니다!</p>
<p>하지만 어떻게 이런 일이? 단순한 훅을 사용하는데! 코드를 살펴봅시다:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { AppRouter } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'~/server/trpc'</span><span style="color: #24292EFF">;</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createTRPCReact</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">AppRouter</span><span style="color: #24292EFF">&gt;();</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Home</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">NextPage</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> () </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> { </span><span style="color: #1976D2">data</span><span style="color: #24292EFF"> } </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">r0</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">greeting</span><span style="color: #6F42C1">.useQuery</span><span style="color: #24292EFF">({ who</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'from tRPC'</span><span style="color: #24292EFF"> });</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">utils</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.useContext</span><span style="color: #24292EFF">();</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #1976D2">utils</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">r49</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">greeting</span><span style="color: #6F42C1">.invalidate</span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">};</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">default</span><span style="color: #24292EFF"> Home;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { AppRouter } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'~/server/trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">trpc</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createTRPCReact</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">AppRouter</span><span style="color: #C9D1D9">&gt;();</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">Home</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">NextPage</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> () </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> { </span><span style="color: #79C0FF">data</span><span style="color: #C9D1D9"> } </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.r0.greeting.</span><span style="color: #D2A8FF">useQuery</span><span style="color: #C9D1D9">({ who: </span><span style="color: #A5D6FF">'from tRPC'</span><span style="color: #C9D1D9"> });</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">utils</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.</span><span style="color: #D2A8FF">useContext</span><span style="color: #C9D1D9">();</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">  utils.r49.greeting.</span><span style="color: #D2A8FF">invalidate</span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">};</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #FFA657"> </span><span style="color: #FF7B72">default</span><span style="color: #FFA657"> </span><span style="color: #C9D1D9">Home;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>자, 여기서는 특별한 게 없습니다. 단일 <code>useContext</code>와 쿼리 무효화만 보입니다. 표면적으로는 TypeScript에 부하를 줄 만한 요소가 없어 문제가 스택 더 깊은 곳에 있음을 암시합니다. 이 변수 뒤에 있는 타입을 살펴봅시다:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">DecorateProcedure</span><span style="color: #24292EFF">&lt;</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyRouter</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">TProcedure</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Procedure</span><span style="color: #24292EFF">&lt;</span><span style="color: #1976D2">any</span><span style="color: #24292EFF">&gt;</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">TProcedure</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyQueryProcedure</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">/**</span></div><div class="line"><span style="color: #C2C3C5">   * </span><span style="color: #D32F2F">@see</span><span style="color: #C2C3C5"> https://tanstack.com/query/v4/docs/framework/react/guides/query-invalidation</span></div><div class="line"><span style="color: #C2C3C5">   */</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">invalidate</span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    input</span><span style="color: #D32F2F">?:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">inferProcedureInput</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TProcedure</span><span style="color: #24292EFF">&gt;</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    filters</span><span style="color: #D32F2F">?:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">InvalidateQueryFilters</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    options</span><span style="color: #D32F2F">?:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">InvalidateOptions</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Promise</span><span style="color: #24292EFF">&lt;</span><span style="color: #1976D2">void</span><span style="color: #24292EFF">&gt;;</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// ... and so on for all the other React Query utilities</span></div><div class="line"><span style="color: #24292EFF">};</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">DecoratedProcedureUtilsRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyRouter</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">OmitNeverKeys</span><span style="color: #24292EFF">&lt;{</span></div><div class="line"><span style="color: #24292EFF">    [</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">keyof</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">]]</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">LegacyV9ProcedureTag</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyRouter</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">DecoratedProcedureUtilsRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">]&gt;</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyQueryProcedure</span></div><div class="line"><span style="color: #24292EFF">          </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">DecorateProcedure</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TRouter</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">]&gt;</span></div><div class="line"><span style="color: #24292EFF">          </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">  }&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">DecorateProcedure</span><span style="color: #C9D1D9">&lt;</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyRouter</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">TProcedure</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">Procedure</span><span style="color: #C9D1D9">&lt;</span><span style="color: #79C0FF">any</span><span style="color: #C9D1D9">&gt;,</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">TProcedure</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyQueryProcedure</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">/**</span></div><div class="line"><span style="color: #8B949E">   * </span><span style="color: #FF7B72">@see</span><span style="color: #8B949E"> </span><span style="color: #C9D1D9">https://tanstack.com/query/v4/docs/framework/react/guides/query-invalidation</span></div><div class="line"><span style="color: #8B949E">   */</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #D2A8FF">invalidate</span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">input</span><span style="color: #FF7B72">?:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">inferProcedureInput</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TProcedure</span><span style="color: #C9D1D9">&gt;,</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">filters</span><span style="color: #FF7B72">?:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">InvalidateQueryFilters</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">options</span><span style="color: #FF7B72">?:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">InvalidateOptions</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">  )</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">Promise</span><span style="color: #C9D1D9">&lt;</span><span style="color: #79C0FF">void</span><span style="color: #C9D1D9">&gt;;</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// ... and so on for all the other React Query utilities</span></div><div class="line"><span style="color: #C9D1D9">};</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">DecoratedProcedureUtilsRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyRouter</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">OmitNeverKeys</span><span style="color: #C9D1D9">&lt;{</span></div><div class="line"><span style="color: #C9D1D9">    [</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">in</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">keyof</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">]]</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">LegacyV9ProcedureTag</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyRouter</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">DecoratedProcedureUtilsRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">]&gt;</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyQueryProcedure</span></div><div class="line"><span style="color: #C9D1D9">          </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">DecorateProcedure</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">, </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">]&gt;</span></div><div class="line"><span style="color: #C9D1D9">          </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">  }&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>좋습니다, 이제 분석하고 배울 것들이 생겼습니다. 먼저 이 코드가 무엇을 하는지 파악해 봅시다.</p>
<p>라우터의 모든 프로시저를 순회하며 React Query의 <a href="https://tanstack.com/query/v4/docs/framework/react/guides/query-invalidation" target="_blank" rel="noopener noreferrer"><code>invalidateQueries</code></a>와 같은 유틸리티로 "데코레이팅"(메서드 추가)하는 재귀 타입 <code>DecoratedProcedureUtilsRecord</code>이 있습니다.</p>
<p>tRPC v10에서는 이전 <code>v9</code> 라우터를 여전히 지원하지만, <code>v10</code> 클라이언트는 <code>v9</code> 라우터의 프로시저를 호출할 수 없습니다. 따라서 각 프로시저에 대해 <code>v9</code> 프로시저인지(<code>extends LegacyV9ProcedureTag</code>) 확인하고, 해당하면 제거합니다. TypeScript가 처리해야 할 작업이 상당합니다...<strong>지연 평가(lazy evaluation)되지 않는다면</strong> 말이죠.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="지연-평가">지연 평가<a href="https://trpc.io/ko/blog/typescript-performance-lessons#%EC%A7%80%EC%97%B0-%ED%8F%89%EA%B0%80" class="hash-link" aria-label="지연 평가 바로가기 링크" title="지연 평가 바로가기 링크">​</a></h3>
<p>여기서 문제는 TypeScript가 즉시 사용되지 않는 코드까지도 타입 시스템에서 평가한다는 점입니다. 코드에서는 <code>utils.r49.greeting.invalidate</code>만 사용하고 있으므로, TypeScript는 <code>r49</code> 프로퍼티(라우터)를 언래핑한 다음 <code>greeting</code> 프로퍼티(프로시저), 마지막으로 해당 프로시저의 <code>invalidate</code> 함수 타입만 찾으면 충분합니다. 다른 타입들은 필요하지 않으며, 모든 tRPC 프로시저에 대한 React Query 유틸리티 메서드의 타입을 즉시 찾는 것은 TypeScript를 불필요하게 느리게 만듭니다. TypeScript는 <strong>객체</strong>의 프로퍼티 타입 평가를 직접 사용될 때까지 지연시키므로, 이론적으로 위 타입은 지연 평가(lazy evaluation)를 적용할 수 있어야 합니다... 맞을까요?</p>
<p>사실 이 타입은 정확히 객체가 아닙니다. 전체를 감싸는 <code>OmitNeverKeys</code> 타입이 존재합니다. 이 유틸리티 타입은 값이 <code>never</code>인 키를 객체에서 제거합니다. 여기서 v9 프로시저를 제거해 Intellisense에 표시되지 않도록 하는 부분입니다.</p>
<p>하지만 이로 인해 심각한 성능 문제가 발생합니다. TypeScript가 모든 타입의 값을 평가하여 <code>never</code>인지 확인하도록 강제하게 됩니다.</p>
<p>이를 어떻게 해결할까요? 타입이 <strong>덜 작업하도록</strong> 변경해야 합니다.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="지연-평가-적용하기">지연 평가 적용하기<a href="https://trpc.io/ko/blog/typescript-performance-lessons#%EC%A7%80%EC%97%B0-%ED%8F%89%EA%B0%80-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0" class="hash-link" aria-label="지연 평가 적용하기 바로가기 링크" title="지연 평가 적용하기 바로가기 링크">​</a></h3>
<p><code>v10</code> API가 레거시 <code>v9</code> 라우터를 더 우아하게 처리할 방법을 찾아야 합니다. 새로운 tRPC 프로젝트는 <a href="https://trpc.io/ko/docs/v10/migrate-from-v9-to-v10#using-interop">interop 모드</a>의 TypeScript 성능 저하를 겪어서는 안 됩니다.</p>
<p>핵심 아이디어는 내부 타입 구조를 재조정하는 것입니다. <code>v9</code> 프로시저와 <code>v10</code> 프로시저는 서로 다른 엔티티이므로 라이브러리 코드에서 동일한 공간을 공유하지 않아야 합니다. tRPC 서버 측에서는 라우터의 단일 <code>record</code> 필드 대신 별도 필드에 타입을 저장하도록 변경해야 했습니다(앞서 언급한 <code>DecoratedProcedureUtilsRecord</code> 참조).</p>
<p>변경 사항: <code>v9</code> 라우터가 <code>v10</code> 라우터로 변환될 때 프로시저를 <code>legacy</code> 필드에 주입하도록 수정했습니다.</p>
<p>기존 타입:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V10Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TProcedureRecord</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  record</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TProcedureRecord</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">};</span></div><div class="line"></div><div class="line"><span style="color: #C2C3C5">// convert a v9 interop router to a v10 router</span></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateV9Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V9Router</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V10Router</span><span style="color: #24292EFF">&lt;{</span></div><div class="line"><span style="color: #24292EFF">  [</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">keyof</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'procedures'</span><span style="color: #24292EFF">]]</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateProcedure</span><span style="color: #24292EFF">&lt;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'procedures'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">]</span></div><div class="line"><span style="color: #24292EFF">  &gt; </span><span style="color: #D32F2F">&amp;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">LegacyV9ProcedureTag</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V10Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TProcedureRecord</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">record</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TProcedureRecord</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">};</span></div><div class="line"></div><div class="line"><span style="color: #8B949E">// convert a v9 interop router to a v10 router</span></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateV9Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V9Router</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V10Router</span><span style="color: #C9D1D9">&lt;{</span></div><div class="line"><span style="color: #C9D1D9">  [</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">in</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">keyof</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'procedures'</span><span style="color: #C9D1D9">]]</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateProcedure</span><span style="color: #C9D1D9">&lt;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'procedures'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">]</span></div><div class="line"><span style="color: #C9D1D9">  &gt; </span><span style="color: #FF7B72">&amp;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">LegacyV9ProcedureTag</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>앞서 본 <code>DecoratedProcedureUtilsRecord</code> 타입에서 <code>LegacyV9ProcedureTag</code>를 첨부해 <code>v9</code>와 <code>v10</code> 프로시저를 타입 수준에서 구분하고, <code>v9</code> 프로시저가 <code>v10</code> 클라이언트에서 호출되지 않도록 강제했습니다.</p>
<p>새로운 타입:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V10Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TProcedureRecord</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  record</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TProcedureRecord</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// by default, no legacy procedures</span></div><div class="line"><span style="color: #24292EFF">  legacy</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {};</span></div><div class="line"><span style="color: #24292EFF">};</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateV9Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V9Router</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// v9 routers inject their procedures into a `legacy` field</span></div><div class="line"><span style="color: #24292EFF">  legacy</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// v9 clients require that we filter queries, mutations, subscriptions at the top-level</span></div><div class="line"><span style="color: #24292EFF">    queries</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateProcedureRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'queries'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="line"><span style="color: #24292EFF">    mutations</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateProcedureRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'mutations'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="line"><span style="color: #24292EFF">    subscriptions</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateProcedureRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'subscriptions'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="line"><span style="color: #24292EFF">  };</span></div><div class="line"><span style="color: #24292EFF">} </span><span style="color: #D32F2F">&amp;</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V10Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #C2C3C5">/* empty object, v9 routers have no v10 procedures to pass */</span><span style="color: #24292EFF"> {}&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V10Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TProcedureRecord</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">record</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TProcedureRecord</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// by default, no legacy procedures</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">legacy</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> {};</span></div><div class="line"><span style="color: #C9D1D9">};</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateV9Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V9Router</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// v9 routers inject their procedures into a `legacy` field</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">legacy</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// v9 clients require that we filter queries, mutations, subscriptions at the top-level</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">queries</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateProcedureRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'queries'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">mutations</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateProcedureRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'mutations'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">subscriptions</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateProcedureRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'subscriptions'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="line"><span style="color: #C9D1D9">  };</span></div><div class="line"><span style="color: #C9D1D9">} </span><span style="color: #FF7B72">&amp;</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V10Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #8B949E">/* empty object, v9 routers have no v10 procedures to pass */</span><span style="color: #C9D1D9"> {}&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>이제 <code>OmitNeverKeys</code>를 제거할 수 있습니다. 프로시저가 사전 분류되어 라우터의 <code>record</code> 프로퍼티 타입은 모든 <code>v10</code> 프로시저를 포함하고, <code>legacy</code> 프로퍼티 타입은 모든 <code>v9</code> 프로시저를 포함합니다. 더 이상 TypeScript가 거대한 <code>DecoratedProcedureUtilsRecord</code> 타입을 완전히 평가하도록 강제하지 않습니다. <code>LegacyV9ProcedureTag</code>로 <code>v9</code> 프로시저를 필터링하는 로직도 제거됩니다.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="결과는">결과는?<a href="https://trpc.io/ko/blog/typescript-performance-lessons#%EA%B2%B0%EA%B3%BC%EB%8A%94" class="hash-link" aria-label="결과는? 바로가기 링크" title="결과는? 바로가기 링크">​</a></h2>
<p>새 추적 결과에서 병목 현상이 사라진 것을 확인할 수 있습니다:
<img decoding="async" loading="lazy" src="https://assets.trpc.io/www/blog/2023-01-14-typescript-performance-lessons/trace-3.png" alt="src/pages/index.ts 타입 검사 시간 136ms로 표시된 트레이스 바" class="img_Njog"></p>
<p>상당한 개선입니다! 타입 검사 시간이 332ms에서 136ms로 줄었습니다 🤯! 전체적으로는 작은 차이로 보일 수 있지만, 이는 큰 성과입니다. 200ms는 한 번이라면 작은 시간이지만 다음을 고려해보세요:</p>
<ul>
<li>
<p>프로젝트 내 다른 TS 라이브러리 수</p>
</li>
<li>
<p>현재 tRPC를 사용하는 개발자 수</p>
</li>
<li>
<p>작업 세션에서 타입이 재평가되는 횟수</p>
</li>
</ul>
<p>이 모든 요소가 누적되면 200ms는 매우 큰 숫자로 변환됩니다.</p>
<p>tRPC든 다른 TS 기반 프로젝트든, TypeScript 개발자 경험 개선 기회를 계속 모색하고 있습니다. TypeScript 관련 주제로 논의하고 싶다면 <a href="https://twitter.com/s4chinraja" target="_blank" rel="noopener noreferrer">트위터</a>에서 저를 멘션해주세요.</p>
<p>이 글을 작성하는 데 도움을 주신 <a href="https://twitter.com/anthonysheww" target="_blank" rel="noopener noreferrer">Anthony Shew</a>님과 검토해 주신 <a href="https://twitter.com/alexdotjs" target="_blank" rel="noopener noreferrer">Alex</a>님께 감사드립니다!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[tRPC v10 발표]]></title>
            <link>https://trpc.io/ko/blog/announcing-trpc-10</link>
            <guid>https://trpc.io/ko/blog/announcing-trpc-10</guid>
            <pubDate>Mon, 21 Nov 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>비공식 베타 번역</div><div class="admonitionContent_e2NW"><p>이 페이지는 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.
오류를 발견하셨나요? <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2022-11-21-announcing-trpc-10.mdx&amp;locale=ko" target="_blank" rel="noopener noreferrer">문제 신고 →</a></p></div></div>
<!-- -->
<p>tRPC는 TypeScript의 강력한 기능을 통해 엄격한 풀스택 타입 바인딩을 강제함으로써 뛰어난 개발자 경험을 제공합니다. API 계약 이탈 없음, 코드 생성 없음.</p>
<!-- -->
<p>2021년 8월 마지막 메이저 버전 출시 이후, tRPC 커뮤니티는 상당한 성장을 이루었습니다:</p>
<ul>
<li>
<p>현재 <a href="https://github.com/trpc/trpc" target="_blank" rel="noopener noreferrer">GitHub에서 15,000개 이상의 스타</a>를 보유</p>
</li>
<li>
<p><a href="https://trpc.io/discord" target="_blank" rel="noopener noreferrer">2,000명 이상의 멤버가 있는 Discord 커뮤니티</a></p>
</li>
<li>
<p><a href="https://www.npmjs.com/package/@trpc/server" target="_blank" rel="noopener noreferrer">주간 npm 다운로드 100k+</a></p>
</li>
<li>
<p><a href="https://github.com/trpc/trpc/graphs/contributors" target="_blank" rel="noopener noreferrer">거의 200명의 기여자</a></p>
</li>
<li>
<p><a href="https://trpc.io/awesome" target="_blank" rel="noopener noreferrer">확장 기능, 예제, 콘텐츠로 구성된 성장 중인 생태계</a></p>
</li>
</ul>
<p><strong>오늘, 우리는 tRPC v10을 출시합니다</strong>. v10이 이미 많은 대규모 TypeScript 프로젝트에서 프로덕션 환경에서 사용되고 있다는 점을 알리게 되어 기쁩니다. 이번 공식 출시는 더 넓은 커뮤니티에 일반 사용 가능성을 알립니다.</p>
<p>신규 프로젝트는 <a href="https://trpc.io/awesome#-starting-points-example-projects-etc" target="_blank" rel="noopener noreferrer">예제 애플리케이션</a>으로 tRPC v10을 학습하고 시작할 수 있습니다. 기존 tRPC v9 사용 프로젝트는 <a href="https://trpc.io/docs/v10/migrate-from-v9-to-v10" target="_blank" rel="noopener noreferrer">v10 마이그레이션 가이드</a>를 참조하세요.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="변경-사항-개요">변경 사항 개요<a href="https://trpc.io/ko/blog/announcing-trpc-10#%EB%B3%80%EA%B2%BD-%EC%82%AC%ED%95%AD-%EA%B0%9C%EC%9A%94" class="hash-link" aria-label="변경 사항 개요 바로가기 링크" title="변경 사항 개요 바로가기 링크">​</a></h2>
<p>v10은 tRPC 역사상 가장 큰 출시입니다. 처음으로 tRPC 구조에 근본적인 변경을 가했으며, 이러한 변경으로 인해 첨단 애플리케이션을 개발하는 신속한 팀들에게 새로운 가능성이 열릴 것이라고 믿습니다.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="개선된-개발자-경험">개선된 개발자 경험<a href="https://trpc.io/ko/blog/announcing-trpc-10#%EA%B0%9C%EC%84%A0%EB%90%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EA%B2%BD%ED%97%98" class="hash-link" aria-label="개선된 개발자 경험 바로가기 링크" title="개선된 개발자 경험 바로가기 링크">​</a></h3>
<p>tRPC v10은 여러분의 IDE를 적극적으로 수용합니다. 타입 통합뿐만 아니라 이번 버전에서 프론트엔드, 백엔드, 편집 경험을 통합했습니다.</p>
<p>v10에서는 다음이 가능합니다:</p>
<ul>
<li>
<p>프론트엔드 소비자에서 백엔드 프로시저로 바로 이동하는 <em>"Go to Definition"</em> 사용</p>
</li>
<li>
<p>입력 인수나 프로시저 이름을 전체 애플리케이션에서 변경하는 <em>"Rename Symbol"</em> 사용</p>
</li>
<li>
<p>tRPC 타입을 애플리케이션에서 수동으로 사용할 때 <a href="https://trpc.io/docs/v10/infer-types" target="_blank" rel="noopener noreferrer">타입 추론 용이화</a></p>
</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="강력한-백엔드-프레임워크">강력한 백엔드 프레임워크<a href="https://trpc.io/ko/blog/announcing-trpc-10#%EA%B0%95%EB%A0%A5%ED%95%9C-%EB%B0%B1%EC%97%94%EB%93%9C-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC" class="hash-link" aria-label="강력한 백엔드 프레임워크 바로가기 링크" title="강력한 백엔드 프레임워크 바로가기 링크">​</a></h3>
<p>v10에서는 백엔드 프로시저 정의 구문을 재검토하여 원하는 로직을 건전한 방식으로 도입할 수 있는 기회를 확장했습니다. 이번 tRPC 버전의 특징:</p>
<ul>
<li>
<p><a href="https://trpc.io/docs/v10/middlewares#context-extension" target="_blank" rel="noopener noreferrer">컨텍스트 확장</a>을 통한 <a href="https://trpc.io/docs/v10/middlewares" target="_blank" rel="noopener noreferrer">재사용 가능한 미들웨어</a></p>
</li>
<li>
<p><a href="https://trpc.io/docs/v10/procedures#multiple-input-parsers" target="_blank" rel="noopener noreferrer">다중 입력 파서</a> 사용이 가능한 <a href="https://trpc.io/docs/v10/procedures#reusable-base-publicprocedures" target="_blank" rel="noopener noreferrer">체인 가능 &amp; 재사용 가능 프로시저</a></p>
</li>
<li>
<p><a href="https://trpc.io/docs/v10/error-formatting" target="_blank" rel="noopener noreferrer">커스텀 오류 포맷팅</a>을 통한 유연한 <a href="https://trpc.io/docs/v10/error-handling" target="_blank" rel="noopener noreferrer">오류 처리</a></p>
</li>
<li>
<p>프로시저에 추가 정보를 부여하는 <a href="https://trpc.io/docs/v10/metadata" target="_blank" rel="noopener noreferrer">프로시저 메타데이터</a></p>
</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="대폭-개선된-typescript-성능">대폭 개선된 TypeScript 성능<a href="https://trpc.io/ko/blog/announcing-trpc-10#%EB%8C%80%ED%8F%AD-%EA%B0%9C%EC%84%A0%EB%90%9C-typescript-%EC%84%B1%EB%8A%A5" class="hash-link" aria-label="대폭 개선된 TypeScript 성능 바로가기 링크" title="대폭 개선된 TypeScript 성능 바로가기 링크">​</a></h3>
<p>TypeScript는 개발자에게 놀라운 작업을 가능하게 하지만 대가가 따릅니다. 타입을 엄격하게 유지하기 위해 사용하는 많은 기법은 TypeScript 컴파일러에 부하를 줍니다. 커뮤니티 피드백에 따르면 tRPC v9를 사용하는 대규모 애플리케이션에서 이 컴파일러 부하로 인해 개발자 IDE 성능 저하가 시작되고 있었습니다.</p>
<p>우리의 목표는 모든 규모의 애플리케이션에서 개발자 경험을 향상시키는 것입니다. v10에서는 TypeScript 성능을 획기적으로 개선하여(특히 TS 증분 컴파일 시) 에디터가 계속 반응성을 유지하도록 했습니다.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="점진적-마이그레이션">점진적 마이그레이션<a href="https://trpc.io/ko/blog/announcing-trpc-10#%EC%A0%90%EC%A7%84%EC%A0%81-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98" class="hash-link" aria-label="점진적 마이그레이션 바로가기 링크" title="점진적 마이그레이션 바로가기 링크">​</a></h2>
<p>마이그레이션 경험을 최대한 간편하게 만들기 위해 많은 노력을 기울였으며, v9 라우터와 (거의) 완전한 하위 호환성을 제공하는 <code>interop()</code> 메서드를 포함하고 있습니다. 자세한 내용은 <a href="https://trpc.io/docs/v10/migrate-from-v9-to-v10" target="_blank" rel="noopener noreferrer">마이그레이션 가이드</a>를 참조하세요.</p>
<p>코어 팀의 <a href="https://twitter.com/s4chinraja" target="_blank" rel="noopener noreferrer">Sachin</a>은 마이그레이션 작업의 상당 부분을 자동화해주는 <a href="https://github.com/sachinraja/trpc-v10-migrate-codemod" target="_blank" rel="noopener noreferrer">코드모드</a>도 개발했습니다.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="성장하는-생태계">성장하는 생태계<a href="https://trpc.io/ko/blog/announcing-trpc-10#%EC%84%B1%EC%9E%A5%ED%95%98%EB%8A%94-%EC%83%9D%ED%83%9C%EA%B3%84" class="hash-link" aria-label="성장하는 생태계 바로가기 링크" title="성장하는 생태계 바로가기 링크">​</a></h2>
<p>tRPC를 중심으로 다양한 서브 라이브러리 생태계가 지속적으로 성장하고 있습니다. 대표적인 예시는 다음과 같습니다:</p>
<ul>
<li>
<p><a href="https://github.com/jlalmes/trpc-openapi" target="_blank" rel="noopener noreferrer">trpc-openapi</a>: REST 호환 엔드포인트 손쉽게 생성</p>
</li>
<li>
<p><a href="https://github.com/t3-oss/create-t3-app" target="_blank" rel="noopener noreferrer">create-t3-app</a>: tRPC 기반 Next.js 풀스택 애플리케이션 부트스트랩</p>
</li>
<li>
<p><a href="https://github.com/t3-oss/create-t3-turbo" target="_blank" rel="noopener noreferrer">create-t3-turbo</a>: tRPC 기반 React Native 앱 신속 시작</p>
</li>
<li>
<p><a href="https://github.com/jlalmes/trpc-chrome" target="_blank" rel="noopener noreferrer">trpc-chrome</a>: tRPC를 이용한 Chrome 확장 프로그램 개발</p>
</li>
<li>
<p><a href="https://github.com/OrJDev/solid-trpc" target="_blank" rel="noopener noreferrer">Solid</a>, <a href="https://github.com/icflorescu/trpc-sveltekit-example" target="_blank" rel="noopener noreferrer">Svelte</a>, <a href="https://github.com/michealroberts/usetrpc" target="_blank" rel="noopener noreferrer">Vue</a> 등 프레임워크 어댑터</p>
</li>
</ul>
<p>더 많은 플러그인, 예제, 어댑터는 <a href="https://trpc.io/awesome" target="_blank" rel="noopener noreferrer">Awesome tRPC 컬렉션</a>에서 확인하세요.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="감사합니다">감사합니다!<a href="https://trpc.io/ko/blog/announcing-trpc-10#%EA%B0%90%EC%82%AC%ED%95%A9%EB%8B%88%EB%8B%A4" class="hash-link" aria-label="감사합니다! 바로가기 링크" title="감사합니다! 바로가기 링크">​</a></h2>
<p>코어 팀과 저는 여러분께 알려드리고 싶습니다: 이제 시작일 뿐입니다. 이미 <a href="https://github.com/reactjs/rfcs/pull/229" target="_blank" rel="noopener noreferrer">React Server Components</a>와 Next.js 13에 대한 실험을 진행 중입니다.</p>
<p>특별히 <a href="https://twitter.com/s4chinraja" target="_blank" rel="noopener noreferrer">Sachin</a>, <a href="https://twitter.com/jullerino" target="_blank" rel="noopener noreferrer">Julius</a>, <a href="https://twitter.com/jlalmes" target="_blank" rel="noopener noreferrer">James</a>, <a href="https://twitter.com/ixahmedxii" target="_blank" rel="noopener noreferrer">Ahmed</a>, <a href="https://twitter.com/trashh_dev" target="_blank" rel="noopener noreferrer">Chris</a>, <a href="https://twitter.com/theo" target="_blank" rel="noopener noreferrer">Theo</a>, <a href="https://twitter.com/shewDev" target="_blank" rel="noopener noreferrer">Anthony</a>, 그리고 <a href="https://github.com/trpc/trpc#all-contributors" target="_blank" rel="noopener noreferrer">이번 릴리스에 기여한 모든 분들</a>께 큰 감사를 드립니다.</p>
<p>tRPC를 사용하고 지원해 주셔서 감사합니다.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="slug-typescript-performance-lessonstitle-v10-리팩토링-과정에서-얻은-typescript-성능-교훈authors-sachinraja">slug: typescript-performance-lessons
title: v10 리팩토링 과정에서 얻은 TypeScript 성능 교훈
authors: [sachinraja]<a href="https://trpc.io/ko/blog/announcing-trpc-10#slug-typescript-performance-lessonstitle-v10-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EA%B3%BC%EC%A0%95%EC%97%90%EC%84%9C-%EC%96%BB%EC%9D%80-typescript-%EC%84%B1%EB%8A%A5-%EA%B5%90%ED%9B%88authors-sachinraja" class="hash-link" aria-label="slug: typescript-performance-lessons
title: v10 리팩토링 과정에서 얻은 TypeScript 성능 교훈
authors: [sachinraja] 바로가기 링크" title="slug: typescript-performance-lessons
title: v10 리팩토링 과정에서 얻은 TypeScript 성능 교훈
authors: [sachinraja] 바로가기 링크">​</a></h2>
<ul>
<li>
<p>트위터에서 <a href="https://twitter.com/trpcio" target="_blank" rel="noopener noreferrer">@trpcio</a> 팔로우</p>
</li>
<li>
<p><a href="https://trpc.io/discord" target="_blank" rel="noopener noreferrer">Discord 커뮤니티</a> 참여</p>
</li>
<li>
<p><a href="https://trpc.io/#try-it-out" target="_blank" rel="noopener noreferrer">브라우저에서 tRPC 체험</a></p>
</li>
</ul>
<a id="sponsor-button" href="https://trpc.io/sponsor" class="group flex h-12 w-max items-center gap-4 rounded-lg border-2 bg-zinc-200 px-4 py-2 transition hover:bg-zinc-100 dark:border-zinc-900 dark:bg-zinc-800 hover:dark:border-zinc-700 hover:dark:bg-zinc-900"><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="aspect-square h-6 fill-pink-500 transition-transform duration-200 ease-in group-hover:scale-110"><path d="M17.625 1.499c-2.32 0-4.354 1.203-5.625 3.03-1.271-1.827-3.305-3.03-5.625-3.03C3.129 1.499 0 4.253 0 8.249c0 4.275 3.068 7.847 5.828 10.227a33.14 33.14 0 0 0 5.616 3.876l.028.017.008.003-.001.003c.163.085.342.126.521.125.179.001.358-.041.521-.125l-.001-.003.008-.003.028-.017a33.14 33.14 0 0 0 5.616-3.876C20.932 16.096 24 12.524 24 8.249c0-3.996-3.129-6.75-6.375-6.75zm-.919 15.275a30.766 30.766 0 0 1-4.703 3.316l-.004-.002-.004.002a30.955 30.955 0 0 1-4.703-3.316c-2.677-2.307-5.047-5.298-5.047-8.523 0-2.754 2.121-4.5 4.125-4.5 2.06 0 3.914 1.479 4.544 3.684.143.495.596.797 1.086.796.49.001.943-.302 1.085-.796.63-2.205 2.484-3.684 4.544-3.684 2.004 0 4.125 1.746 4.125 4.5 0 3.225-2.37 6.216-5.048 8.523z"></path></svg><span class="font-semibold text-zinc-900 no-underline dark:text-zinc-300">스폰서 되기!</span></a>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[tRPC 소개]]></title>
            <link>https://trpc.io/ko/blog/introducing-trpc</link>
            <guid>https://trpc.io/ko/blog/introducing-trpc</guid>
            <pubDate>Wed, 05 May 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.]]></description>
            <content:encoded><![CDATA[<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>비공식 베타 번역</div><div class="admonitionContent_e2NW"><p>이 페이지는 <a href="https://page-turner.com/" target="_blank" rel="noopener noreferrer"><strong>PageTurner</strong></a> AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다.
오류를 발견하셨나요? <a href="https://feedback.page-turner.com/?repo_id=683d130a-1828-4b22-91cd-ef2d269ef3f5&amp;file_path=blog%2F2021-05-05-hello-world.mdx&amp;locale=ko" target="_blank" rel="noopener noreferrer">문제 신고 →</a></p></div></div>
<p>tRPC는 타입을 선언하지 않고도 (Node) 서버부터 클라이언트까지 종단간 타입 안전성을 제공합니다. 백엔드에서는 단순히 함수에서 데이터를 반환하기만 하면, 프론트엔드에서는 엔드포인트 이름을 기반으로 해당 데이터를 사용할 수 있습니다.</p>
<!-- -->
<p>👋 저는 GitHub에서 "KATT"로 알려진 Alex입니다. <a href="https://trpc.io/" target="_blank" rel="noopener noreferrer">tRPC</a>라는 라이브러리를 소개하려 합니다. 아직 관련 글을 발행한 적은 없지만(이미 GitHub에서 &gt;530 🌟를 달성했습니다), 시작을 알리는 간단한 소개를 작성합니다. 향후 문서와 동영상 소개가 예정되어 있으니 기대해 주세요! 최신 소식을 받거나 질문이 있다면 트위터 <a href="https://twitter.com/alexdotjs" target="_blank" rel="noopener noreferrer">@alexdotjs</a>를 팔로우해 주세요.</p>
<p><strong>tRPC 엔드포인트 및 클라이언트 호출 시 모습 예시:</strong>
<img decoding="async" loading="lazy" src="https://assets.trpc.io/www/v9/trpcgif.gif" alt="Alt Text" class="img_Njog"></p>
<p>React 전용 라이브러리(<code>@trpc/react</code>)를 제작했으며, 이는 훌륭한 react-query 위에서 동작합니다. 클라이언트 라이브러리(<code>@trpc/client</code>)는 React 없이도 작동합니다(Svelte/Vue/Angular/[..] 전용 라이브러리를 구축하고 싶다면 문의 주세요!).</p>
<p>코드 생성이 전혀 필요하지 않으며, 기존 Next.js/CRA/Express 프로젝트에 쉽게 통합할 수 있습니다.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="예시">예시<a href="https://trpc.io/ko/blog/introducing-trpc#%EC%98%88%EC%8B%9C" class="hash-link" aria-label="예시 바로가기 링크" title="예시 바로가기 링크">​</a></h2>
<p>다음은 <code>string</code> 인수를 받는 <code>hello</code>라는 tRPC 프로시저(엔드포인트) 예시입니다.</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">appRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.router</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'hello'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  input</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.string</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.optional</span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">resolve</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> ({ input }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      text</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`hello </span><span style="color: #D32F2F">${</span><span style="color: #24292EFF">input </span><span style="color: #D32F2F">??</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'world'</span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    };</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AppRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">typeof</span><span style="color: #24292EFF"> appRouter;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">appRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.</span><span style="color: #D2A8FF">router</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'hello'</span><span style="color: #C9D1D9">, {</span></div><div class="line"><span style="color: #C9D1D9">  input: z.</span><span style="color: #D2A8FF">string</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">optional</span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #D2A8FF">resolve</span><span style="color: #C9D1D9">: ({ </span><span style="color: #FFA657">input</span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">      text: </span><span style="color: #A5D6FF">`hello ${</span><span style="color: #C9D1D9">input</span><span style="color: #A5D6FF"> </span><span style="color: #FF7B72">??</span><span style="color: #A5D6FF"> </span><span style="color: #A5D6FF">'world'}`</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    };</span></div><div class="line"><span style="color: #C9D1D9">  },</span></div><div class="line"><span style="color: #C9D1D9">});</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AppRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">typeof</span><span style="color: #C9D1D9"> appRouter;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>다음은 해당 데이터를 사용하는 타입 안전 클라이언트 예시입니다:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { AppRouter } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./server'</span><span style="color: #24292EFF">;</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">main</span><span style="color: #24292EFF">() {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createTRPCClient</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">AppRouter</span><span style="color: #24292EFF">&gt;({</span></div><div class="line"><span style="color: #24292EFF">    url</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`http://localhost:2022`</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">result</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'hello'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@alexdotjs'</span><span style="color: #24292EFF">);</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #1976D2">console</span><span style="color: #6F42C1">.log</span><span style="color: #24292EFF">(result); </span><span style="color: #C2C3C5">// --&gt; { text: "hello @alexdotjs" }</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line"></div><div class="line"><span style="color: #6F42C1">main</span><span style="color: #24292EFF">();</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { AppRouter } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./server'</span><span style="color: #C9D1D9">;</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">function</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">main</span><span style="color: #C9D1D9">() {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">client</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createTRPCClient</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">AppRouter</span><span style="color: #C9D1D9">&gt;({</span></div><div class="line"><span style="color: #C9D1D9">    url: </span><span style="color: #A5D6FF">`http://localhost:2022`</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">result</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'hello'</span><span style="color: #C9D1D9">, </span><span style="color: #A5D6FF">'@alexdotjs'</span><span style="color: #C9D1D9">);</span></div><div class="line"><span style="color: #C9D1D9">  console.</span><span style="color: #D2A8FF">log</span><span style="color: #C9D1D9">(result); </span><span style="color: #8B949E">// --&gt; { text: "hello @alexdotjs" }</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line"></div><div class="line"><span style="color: #D2A8FF">main</span><span style="color: #C9D1D9">();</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p><strong>타입 안전성을 확보하는 데 필요한 전부입니다!</strong> <code>result</code> 타입은 백엔드 함수 반환 값에서 추론됩니다. 입력 데이터 또한 검증기 반환 값에서 추론되므로 데이터를 바로 안전하게 사용할 수 있습니다. 실제로 입력 데이터는 검증기를 통과해야 합니다(tRPC는 zod/yup/커스텀 검증기와 즉시 호환됩니다).</p>
<p>위 예시를 직접 확인할 수 있는 CodeSandbox 링크: <a href="https://githubbox.com/trpc/trpc/tree/main/examples/standalone-server" target="_blank" rel="noopener noreferrer">https://githubbox.com/trpc/trpc/tree/main/examples/standalone-server</a> (미리보기보다 터미널 출력을 확인해 주세요!)</p>
<p><strong><em>잠깐? 백엔드 코드를 클라이언트로 임포트한다고?</em> - 사실 아닙니다</strong></p>
<p>겉보기에는 그렇게 보일 수 있지만, 서버에서 클라이언트로 실제 코드가 공유되지는 않습니다. TypeScript의 <code>import type</code>은 "[...] 타입 주석 및 선언에 사용할 선언만 임포트합니다. 항상 완전히 삭제되어 런타임에 흔적이 남지 않습니다." - TypeScript 3.8에 추가된 기능 - <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#:~:text=import%20type%20only%20imports%20declarations,also%20erased%20from%20TypeScript's%20output." target="_blank" rel="noopener noreferrer">TypeScript 문서 참조</a>.</p>
<p>코드 생성이 필요 없으며, 서버에서 클라이언트로 타입을 공유할 수 있는 환경만 있다면(모노레포 사용을 권장합니다) 바로 적용 가능합니다.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="시작에-불과합니다">시작에 불과합니다!<a href="https://trpc.io/ko/blog/introducing-trpc#%EC%8B%9C%EC%9E%91%EC%97%90-%EB%B6%88%EA%B3%BC%ED%95%A9%EB%8B%88%EB%8B%A4" class="hash-link" aria-label="시작에 불과합니다! 바로가기 링크" title="시작에 불과합니다! 바로가기 링크">​</a></h2>
<p>앞서 React 라이브러리가 있다고 언급했는데, React에서 위 데이터를 사용하는 방법은 다음과 같습니다:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> { </span><span style="color: #1976D2">data</span><span style="color: #24292EFF"> } </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.useQuery</span><span style="color: #24292EFF">([</span><span style="color: #22863A">'hello'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@alexdotjs'</span><span style="color: #24292EFF">]);</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> { </span><span style="color: #79C0FF">data</span><span style="color: #C9D1D9"> } </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.</span><span style="color: #D2A8FF">useQuery</span><span style="color: #C9D1D9">([</span><span style="color: #A5D6FF">'hello'</span><span style="color: #C9D1D9">, </span><span style="color: #A5D6FF">'@alexdotjs'</span><span style="color: #C9D1D9">]);</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>.. 이제 클라이언트에서 타입 안전 데이터를 얻을 수 있습니다.</p>
<p>기존 브라운필드 프로젝트(Express/Next.js용 어댑터 제공)에 바로 tRPC를 추가할 수 있으며, CRA와 호환되고 React Native에서도 작동해야 합니다. React에 종속되지 않으므로 Svelte 또는 Vue 라이브러리를 개발하려면 연락주세요.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="데이터-변경은-어떻게-하나요">데이터 변경은 어떻게 하나요?<a href="https://trpc.io/ko/blog/introducing-trpc#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%80%EA%B2%BD%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%98%EB%82%98%EC%9A%94" class="hash-link" aria-label="데이터 변경은 어떻게 하나요? 바로가기 링크" title="데이터 변경은 어떻게 하나요? 바로가기 링크">​</a></h2>
<p>변경(mutation)은 쿼리만큼 간단합니다. 내부적으로는 동일하지만 문법적 편의를 위해 다르게 노출되며, GET 요청 대신 HTTP POST 요청을 생성합니다.</p>
<p>다음은 데이터베이스를 사용한 조금 더 복잡한 예시로, todomvc.trpc.io / <a href="https://github.com/trpc/trpc/tree/main/examples/next-prisma-todomvc" target="_blank" rel="noopener noreferrer">https://github.com/trpc/trpc/tree/main/examples/next-prisma-todomvc</a> 에 있는 TodoMVC 예제에서 가져온 것입니다.</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">todoRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createRouter</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.mutation</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'add'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  input</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.object</span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    id</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.string</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.uuid</span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    data</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.object</span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      completed</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.boolean</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.optional</span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      text</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.string</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.min</span><span style="color: #24292EFF">(</span><span style="color: #1976D2">1</span><span style="color: #24292EFF">)</span><span style="color: #6F42C1">.optional</span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">resolve</span><span style="color: #24292EFF">({ ctx</span><span style="color: #212121">,</span><span style="color: #24292EFF"> input }) {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> { </span><span style="color: #1976D2">id</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">data</span><span style="color: #24292EFF"> } </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> input;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">todo</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">ctx</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">task</span><span style="color: #6F42C1">.update</span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      where</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> { id }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      data</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    });</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> todo;</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">todoRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createRouter</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">mutation</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'add'</span><span style="color: #C9D1D9">, {</span></div><div class="line"><span style="color: #C9D1D9">  input: z.</span><span style="color: #D2A8FF">object</span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    id: z.</span><span style="color: #D2A8FF">string</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">uuid</span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">    data: z.</span><span style="color: #D2A8FF">object</span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      completed: z.</span><span style="color: #D2A8FF">boolean</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">optional</span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">      text: z.</span><span style="color: #D2A8FF">string</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">min</span><span style="color: #C9D1D9">(</span><span style="color: #79C0FF">1</span><span style="color: #C9D1D9">).</span><span style="color: #D2A8FF">optional</span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  }),</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">resolve</span><span style="color: #C9D1D9">({ </span><span style="color: #FFA657">ctx</span><span style="color: #C9D1D9">, </span><span style="color: #FFA657">input</span><span style="color: #C9D1D9"> }) {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> { </span><span style="color: #79C0FF">id</span><span style="color: #C9D1D9">, </span><span style="color: #79C0FF">data</span><span style="color: #C9D1D9"> } </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> input;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">todo</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> ctx.task.</span><span style="color: #D2A8FF">update</span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      where: { id },</span></div><div class="line"><span style="color: #C9D1D9">      data,</span></div><div class="line"><span style="color: #C9D1D9">    });</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> todo;</span></div><div class="line"><span style="color: #C9D1D9">  },</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p><strong>React 사용 예시</strong>는 다음과 같습니다:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">addTask</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.useMutation</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'todos.add'</span><span style="color: #24292EFF">);</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> (</span></div><div class="line"><span style="color: #24292EFF">  &lt;&gt;</span></div><div class="line"><span style="color: #24292EFF">    &lt;</span><span style="color: #22863A">input</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">placeholder</span><span style="color: #D32F2F">=</span><span style="color: #22863A">"What needs to be done?"</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">onKeyDown</span><span style="color: #D32F2F">=</span><span style="color: #24292EFF">{(e) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">text</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">e</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">currentTarget</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">value</span><span style="color: #6F42C1">.trim</span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #1976D2">e</span><span style="color: #24292EFF">.key </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'Enter'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">&amp;&amp;</span><span style="color: #24292EFF"> text) {</span></div><div class="line"><span style="color: #24292EFF">          </span><span style="color: #1976D2">addTask</span><span style="color: #6F42C1">.mutate</span><span style="color: #24292EFF">({ text });</span></div><div class="line"><span style="color: #24292EFF">          </span><span style="color: #1976D2">e</span><span style="color: #24292EFF">.</span><span style="color: #1976D2">currentTarget</span><span style="color: #24292EFF">.value </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">''</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">        }</span></div><div class="line"><span style="color: #24292EFF">      }}</span></div><div class="line"><span style="color: #24292EFF">    /&gt;</span></div><div class="line"><span style="color: #24292EFF">  &lt;/&gt;</span></div><div class="line"><span style="color: #24292EFF">)</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">addTask</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.</span><span style="color: #D2A8FF">useMutation</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'todos.add'</span><span style="color: #C9D1D9">);</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> (</span></div><div class="line"><span style="color: #C9D1D9">  &lt;&gt;</span></div><div class="line"><span style="color: #C9D1D9">    &lt;</span><span style="color: #7EE787">input</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #79C0FF">placeholder</span><span style="color: #FF7B72">=</span><span style="color: #A5D6FF">"What needs to be done?"</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #79C0FF">onKeyDown</span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9">{(</span><span style="color: #FFA657">e</span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">text</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> e.currentTarget.value.</span><span style="color: #D2A8FF">trim</span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (e.key </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'Enter'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">&amp;&amp;</span><span style="color: #C9D1D9"> text) {</span></div><div class="line"><span style="color: #C9D1D9">          addTask.</span><span style="color: #D2A8FF">mutate</span><span style="color: #C9D1D9">({ text });</span></div><div class="line"><span style="color: #C9D1D9">          e.currentTarget.value </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">''</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">        }</span></div><div class="line"><span style="color: #C9D1D9">      }}</span></div><div class="line"><span style="color: #C9D1D9">    /&gt;</span></div><div class="line"><span style="color: #C9D1D9">  &lt;/&gt;</span></div><div class="line"><span style="color: #C9D1D9">)</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="지금은-여기까지">지금은 여기까지.<a href="https://trpc.io/ko/blog/introducing-trpc#%EC%A7%80%EA%B8%88%EC%9D%80-%EC%97%AC%EA%B8%B0%EA%B9%8C%EC%A7%80" class="hash-link" aria-label="지금은 여기까지. 바로가기 링크" title="지금은 여기까지. 바로가기 링크">​</a></h2>
<p>말씀드렸듯이, 시작에 불과합니다. 더 많은 기능들이 있습니다:</p>
<ul>
<li>
<p>사용자별 데이터를 위한 요청 컨텍스트 생성(의존성 주입 방식으로 리졸버에 전달) - <a href="https://trpc.io/ko/docs/v9/context">링크</a></p>
</li>
<li>
<p>라우터용 미들웨어 지원 - <a href="https://trpc.io/ko/docs/v9/middlewares">링크</a></p>
</li>
<li>
<p>라우터 병합(모든 백엔드 로직을 단일 파일에 두지 않으려는 경우) - <a href="https://trpc.io/ko/docs/v9/merging-routers">링크</a></p>
</li>
<li>
<p><code>@trpc/next</code> 어댑터를 이용한 React 생태계 최간단 서버 사이드 렌더링 - <a href="https://trpc.io/ko/docs/v9/">링크</a></p>
</li>
<li>
<p>타입 안전 오류 포매팅 - <a href="https://trpc.io/ko/docs/v9/error-formatting">링크</a></p>
</li>
<li>
<p>데이터 변환기(Date/Map/Set 객체 직렬화 지원) - <a href="https://trpc.io/ko/docs/v9/data-transformers">링크</a></p>
</li>
<li>
<p>React Query 헬퍼 함수</p>
</li>
</ul>
<p>시작하려면 <a href="https://trpc.io/ko/docs/v9/nextjs">Next.js 시작 가이드</a>에 예제들이 준비되어 있습니다.</p>
<p><a href="https://twitter.com/alexdotjs" target="_blank" rel="noopener noreferrer">업데이트 소식을 받으려면 트위터에서 팔로우하세요!</a></p>]]></content:encoded>
        </item>
    </channel>
</rss>