tRPC
tRPC는 엔드투엔드 타입 안전성을 제공하는 TypeScript API 구축 프레임워크입니다. tRPC를 사용하면 API 작업의 입력과 출력 변경사항이 클라이언트 코드에 즉시 반영되며, 프로젝트 재빌드 없이도 IDE에서 바로 확인할 수 있습니다.
tRPC API 생성기는 AWS CDK 인프라 설정과 함께 새로운 tRPC API를 생성합니다. 생성된 백엔드는 서버리스 배포를 위해 AWS Lambda를 사용하며 Zod를 통한 스키마 검증을 포함합니다. 또한 로깅, AWS X-Ray 추적, CloudWatch 메트릭을 위한 AWS Lambda Powertools를 설정합니다.
사용 방법
섹션 제목: “사용 방법”tRPC API 생성
섹션 제목: “tRPC API 생성”다음 두 가지 방법으로 새로운 tRPC API를 생성할 수 있습니다:
- 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
- VSCode에서 Nx 콘솔 열기
- 클릭
Generate (UI)
"Common Nx Commands" 섹션에서 - 검색
@aws/nx-plugin - ts#trpc-api
- 필수 매개변수 입력
- 클릭
Generate
pnpm nx g @aws/nx-plugin:ts#trpc-api
yarn nx g @aws/nx-plugin:ts#trpc-api
npx nx g @aws/nx-plugin:ts#trpc-api
bunx nx g @aws/nx-plugin:ts#trpc-api
어떤 파일이 변경될지 확인하기 위해 드라이 런을 수행할 수도 있습니다
pnpm nx g @aws/nx-plugin:ts#trpc-api --dry-run
yarn nx g @aws/nx-plugin:ts#trpc-api --dry-run
npx nx g @aws/nx-plugin:ts#trpc-api --dry-run
bunx nx g @aws/nx-plugin:ts#trpc-api --dry-run
매개변수 | 타입 | 기본값 | 설명 |
---|---|---|---|
name 필수 | string | - | The name of the API (required). Used to generate class names and file paths. |
computeType | string | ServerlessApiGatewayRestApi | The type of compute to use to deploy this API. Choose between ServerlessApiGatewayRestApi (default) or ServerlessApiGatewayHttpApi. |
auth | string | IAM | The method used to authenticate with your API. Choose between IAM (default), Cognito or None. |
directory | string | packages | The directory to store the application in. |
생성기 출력 결과
섹션 제목: “생성기 출력 결과”생성기는 <directory>/<api-name>
디렉토리에 다음 프로젝트 구조를 생성합니다:
디렉터리src
- init.ts 백엔드 tRPC 초기화
- router.ts tRPC 라우터 정의 (Lambda 핸들러 API 진입점)
디렉터리schema Zod를 사용한 스키마 정의
- echo.ts “echo” 프로시저의 입력/출력 예제 정의
디렉터리procedures API가 노출하는 프로시저(작업)
- echo.ts 예제 프로시저
디렉터리middleware
- error.ts 오류 처리 미들웨어
- logger.ts AWS Powertools Lambda 로깅 설정 미들웨어
- tracer.ts AWS Powertools Lambda 추적 설정 미들웨어
- metrics.ts AWS Powertools Lambda 메트릭 설정 미들웨어
- local-server.ts 로컬 개발 서버용 tRPC 독립 실행형 어댑터 진입점
디렉터리client
- index.ts 기계 간 API 호출을 위한 타입 안전 클라이언트
- tsconfig.json TypeScript 설정
- project.json 프로젝트 구성 및 빌드 타겟
생성기는 또한 API 배포에 사용할 수 있는 CDK 구성을 packages/common/constructs
디렉토리에 생성합니다.
tRPC API 구현 개요
섹션 제목: “tRPC API 구현 개요”tRPC API는 라우터가 특정 프로시저로 요청을 위임하는 구조로 구성됩니다. 각 프로시저는 Zod 스키마로 정의된 입력과 출력을 가집니다.
스키마
섹션 제목: “스키마”src/schema
디렉토리에는 클라이언트와 서버 코드 간에 공유되는 타입들이 포함됩니다. 이 패키지에서는 TypeScript 우선 스키마 선언 및 검증 라이브러리인 Zod를 사용하여 이러한 타입을 정의합니다.
예시 스키마는 다음과 같을 수 있습니다:
import { z } from 'zod';
// 스키마 정의export const UserSchema = z.object({ name: z.string(), height: z.number(), dateOfBirth: z.string().datetime(),});
// 해당 TypeScript 타입export type User = z.TypeOf<typeof UserSchema>;
위 스키마에서 User
타입은 다음 TypeScript 인터페이스와 동일합니다:
interface User { name: string; height: number; dateOfBirth: string;}
스키마는 API에서 사용되는 구조를 변경할 때 단일 위치에서 업데이트할 수 있도록 클라이언트와 서버 코드 모두에서 공유됩니다.
스키마는 런타임에 tRPC API에 의해 자동으로 검증되며, 백엔드에서 수동으로 검증 로직을 작성할 필요가 없습니다.
Zod는 .merge
, .pick
, .omit
등 스키마를 결합하거나 파생시키는 강력한 유틸리티를 제공합니다. 자세한 내용은 Zod 문서 사이트에서 확인할 수 있습니다.
라우터와 프로시저
섹션 제목: “라우터와 프로시저”API의 진입점은 src/router.ts
에서 찾을 수 있습니다. 이 파일은 호출되는 작업에 따라 요청을 “프로시저”로 라우팅하는 Lambda 핸들러를 포함합니다. 각 프로시저는 예상 입력, 출력 및 구현을 정의합니다.
샘플 라우터에는 echo
라는 단일 작업이 생성됩니다:
import { echo } from './procedures/echo.js';
export const appRouter = router({ echo,});
예시 echo
프로시저는 src/procedures/echo.ts
에 생성됩니다:
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ result: opts.input.message }));
위 코드를 분석하면:
publicProcedure
는 로깅, 추적, 메트릭을 위한 AWS Lambda Powertools 통합을 포함한 미들웨어 설정이 적용된 공개 API 메서드를 정의합니다.input
은 작업에 필요한 입력 스키마를 받습니다. 이 작업에 대한 요청은 자동으로 이 스키마에 대해 검증됩니다.output
은 작업의 예상 출력 스키마를 정의합니다. 스키마를 준수하지 않는 출력을 반환하면 타입 오류가 발생합니다.query
는 API 구현을 정의하는 함수를 받습니다. 이 구현은opts.input
으로 전달된 입력과opts.ctx
에서 미들웨어에 의해 설정된 컨텍스트에 접근할 수 있습니다.query
에 전달된 함수는output
스키마를 준수하는 값을 반환해야 합니다.
query
사용은 작업이 비변경적임을 나타냅니다. 데이터 검색 메서드를 정의할 때 사용합니다. 변경 작업을 구현하려면 대신 mutation
메서드를 사용하세요.
새 프로시저를 추가할 경우 src/router.ts
의 라우터에 등록해야 합니다.
tRPC API 커스터마이징
섹션 제목: “tRPC API 커스터마이징”오류 처리
섹션 제목: “오류 처리”구현에서 TRPCError
를 발생시켜 클라이언트에 오류 응답을 반환할 수 있습니다. 오류 유형을 나타내는 code
를 전달합니다:
throw new TRPCError({ code: 'NOT_FOUND', message: '요청한 리소스를 찾을 수 없습니다',});
작업 그룹화
섹션 제목: “작업 그룹화”API가 커짐에 따라 관련 작업을 그룹화할 수 있습니다.
중첩 라우터를 사용하여 작업을 그룹화할 수 있습니다:
import { getUser } from './procedures/users/get.js';import { listUsers } from './procedures/users/list.js';
const appRouter = router({ users: router({ get: getUser, list: listUsers, }), ...})
클라이언트는 이 그룹화된 작업을 받으며, 예를 들어 listUsers
작업 호출은 다음과 같이 표시됩니다:
client.users.list.query();
AWS Lambda Powertools 로거는 src/middleware/logger.ts
에 구성되며, opts.ctx.logger
를 통해 API 구현에서 접근할 수 있습니다. 이를 사용해 CloudWatch Logs에 로깅하거나 모든 구조화된 로그 메시지에 추가 값을 포함시킬 수 있습니다:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.logger.info('입력으로 작업 호출됨', opts.input);
return ...; });
로거에 대한 자세한 내용은 AWS Lambda Powertools 로거 문서를 참조하세요.
메트릭 기록
섹션 제목: “메트릭 기록”AWS Lambda Powertools 메트릭은 src/middleware/metrics.ts
에 구성되며, opts.ctx.metrics
를 통해 접근할 수 있습니다. AWS SDK를 임포트하지 않고도 CloudWatch에 메트릭을 기록할 수 있습니다:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.metrics.addMetric('Invocations', 'Count', 1);
return ...; });
자세한 내용은 AWS Lambda Powertools 메트릭 문서를 참조하세요.
X-Ray 추적 세부 조정
섹션 제목: “X-Ray 추적 세부 조정”AWS Lambda Powertools 추적기는 src/middleware/tracer.ts
에 구성되며, opts.ctx.tracer
를 통해 접근할 수 있습니다. API 요청의 성능 및 흐름에 대한 상세 정보를 제공하기 위해 AWS X-Ray에 추적을 추가할 수 있습니다:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { const subSegment = opts.ctx.tracer.getSegment()!.addNewSubsegment('MyAlgorithm'); // ... 캡처할 알고리즘 로직 subSegment.close();
return ...; });
자세한 내용은 AWS Lambda Powertools 추적기 문서를 참조하세요.
커스텀 미들웨어 구현
섹션 제목: “커스텀 미들웨어 구현”미들웨어를 구현하여 프로시저에 제공되는 컨텍스트에 추가 값을 포함시킬 수 있습니다.
예를 들어 API 호출 사용자 정보를 추출하는 미들웨어를 src/middleware/identity.ts
에 구현해 보겠습니다.
이 예제는 auth
가 IAM
으로 설정되었다고 가정합니다. Cognito 인증의 경우 이벤트에서 관련 클레임을 추출하는 것이 더 간단합니다.
먼저 컨텍스트에 추가할 내용을 정의합니다:
export interface IIdentityContext { identity?: { sub: string; username: string; };}
이제 미들웨어를 구현합니다. 구조는 다음과 같습니다:
export const createIdentityPlugin = () => { const t = initTRPC.context<...>().create(); return t.procedure.use(async (opts) => { // 프로시저 실행 전 로직
const response = await opts.next(...);
// 프로시저 실행 후 로직
return response; });};
Cognito 사용자 정보를 추출하기 위해 API Gateway 이벤트에서 사용자 식별자(sub)를 추출하고 Cognito에서 사용자 상세 정보를 조회합니다. REST API와 HTTP API에 따라 구현이 다릅니다:
import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import { initTRPC, TRPCError } from '@trpc/server';import { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import { APIGatewayProxyEvent } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC.context<IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEvent>>().create();
const cognito = new CognitoIdentityProvider();
return t.procedure.use(async (opts) => { const cognitoAuthenticationProvider = opts.ctx.event.requestContext?.identity?.cognitoAuthenticationProvider;
let sub: string | undefined = undefined; if (cognitoAuthenticationProvider) { const providerParts = cognitoAuthenticationProvider.split(':'); sub = providerParts[providerParts.length - 1]; }
if (!sub) { throw new TRPCError({ code: 'FORBIDDEN', message: `호출 사용자를 확인할 수 없음`, }); }
const { Users } = await cognito.listUsers({ // Lambda 환경에 사용자 풀 ID가 설정되었다고 가정 UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new TRPCError({ code: 'FORBIDDEN', message: `subjectId ${sub}에 해당하는 사용자가 없음`, }); }
// 컨텍스트에 identity 제공 return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username: Users[0].Username!, }, }, }); });};
import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import { initTRPC, TRPCError } from '@trpc/server';import { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import { APIGatewayProxyEventV2WithIAMAuthorizer } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC.context<IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEventV2WithIAMAuthorizer>>().create();
const cognito = new CognitoIdentityProvider();
return t.procedure.use(async (opts) => { const cognitoIdentity = opts.ctx.event.requestContext?.authorizer?.iam ?.cognitoIdentity as unknown as | { amr: string[]; } | undefined;
const sub = (cognitoIdentity?.amr ?? []) .flatMap((s) => (s.includes(':CognitoSignIn:') ? [s] : [])) .map((s) => { const parts = s.split(':'); return parts[parts.length - 1]; })?.[0];
if (!sub) { throw new TRPCError({ code: 'FORBIDDEN', message: `호출 사용자를 확인할 수 없음`, }); }
const { Users } = await cognito.listUsers({ // Lambda 환경에 사용자 풀 ID가 설정되었다고 가정 UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new TRPCError({ code: 'FORBIDDEN', message: `subjectId ${sub}에 해당하는 사용자가 없음`, }); }
// 컨텍스트에 identity 제공 return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username: Users[0].Username!, }, }, }); });};
tRPC API 배포
섹션 제목: “tRPC API 배포”tRPC API 생성기는 common/constructs
폴더에 API 배포를 위한 CDK 구성을 생성합니다. CDK 애플리케이션에서 이 구성을 사용할 수 있습니다:
import { MyApi } from ':my-scope/common-constructs`;
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // 스택에 API 추가 const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), }); }}
이 구성은 AWS API Gateway REST/HTTP API, 비즈니스 로직을 위한 AWS Lambda 함수, 선택한 auth
메서드 기반 인증을 포함한 API 인프라를 설정합니다.
타입 안전 통합
섹션 제목: “타입 안전 통합”REST/HTTP API CDK 구성체는 각 작업에 대한 통합을 정의하기 위한 타입 안전 인터페이스를 제공하도록 구성됩니다.
기본 통합
섹션 제목: “기본 통합”정적 defaultIntegrations
를 사용하여 각 작업별로 개별 AWS Lambda 함수를 정의하는 기본 패턴을 활용할 수 있습니다:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
통합 접근
섹션 제목: “통합 접근”API 구성체의 integrations
속성을 통해 타입 안전 방식으로 기본 AWS Lambda 함수에 접근할 수 있습니다. 예를 들어 API가 sayHello
작업을 정의하고 이 함수에 일부 권한을 추가해야 하는 경우 다음과 같이 할 수 있습니다:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello는 API에 정의된 작업에 맞게 타입이 지정됩니다api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));
기본 옵션 커스터마이징
섹션 제목: “기본 옵션 커스터마이징”기본 통합 생성 시 사용되는 옵션을 커스터마이징하려면 withDefaultOptions
메서드를 사용할 수 있습니다. 예를 들어 모든 Lambda 함수를 VPC에 배치하려는 경우:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});
통합 재정의
섹션 제목: “통합 재정의”withOverrides
메서드를 사용하여 특정 작업에 대한 통합을 재정의할 수 있습니다. 각 재정의는 HTTP 또는 REST API에 적합한 CDK 통합 구성체를 타입 안전 방식으로 지정해야 합니다. 예를 들어 getDocumentation
API를 외부 웹사이트 호스팅 문서로 재정의하려는 경우:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});
재정된 통합은 api.integrations.getDocumentation
을 통해 접근할 때 더 이상 handler
속성을 갖지 않습니다.
추가 속성을 통합에 포함시켜 타입 안전성을 유지하면서 다른 유형의 통합을 추상화할 수 있습니다. 예를 들어 REST API용 S3 통합을 생성한 후 특정 작업에 대한 버킷을 참조하려는 경우:
const storageBucket = new Bucket(this, 'Bucket', { ... });
const apiGatewayRole = new Role(this, 'ApiGatewayS3Role', { assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),});
storageBucket.grantRead(apiGatewayRole);
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getFile: { bucket: storageBucket, integration: new AwsIntegration({ service: 's3', integrationHttpMethod: 'GET', path: `${storageBucket.bucketName}/{fileName}`, options: { credentialsRole: apiGatewayRole, requestParameters: { 'integration.request.path.fileName': 'method.request.querystring.fileName', }, integrationResponses: [{ statusCode: '200' }], }, }), options: { requestParameters: { 'method.request.querystring.fileName': true, }, methodResponses: [{ statusCode: '200', }], } }, }) .build(),});
// 다른 파일에서 타입 안전 방식으로 정의한 버킷 속성에 접근 가능api.integrations.getFile.bucket.grantRead(...);
인증자 재정의
섹션 제목: “인증자 재정의”통합에서 options
를 제공하여 특정 메서드 옵션(예: Cognito 인증 사용)을 재정의할 수 있습니다:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // REST용 또는 HTTP API용 HttpUserPoolAuthorizer } }, }) .build(),});
명시적 통합
섹션 제목: “명시적 통합”기본 통합 대신 각 작업에 직접 통합을 제공할 수 있습니다. 이는 각 작업이 다른 유형의 통합을 사용해야 하거나 새 작업 추가 시 타입 오류를 받고자 할 때 유용합니다:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});
라우터 패턴
섹션 제목: “라우터 패턴”단일 Lambda 함수를 사용하여 모든 API 요청을 처리하려는 경우 API의 defaultIntegrations
메서드를 수정하여 통합별이 아닌 단일 함수를 생성할 수 있습니다:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { const router = new Function(scope, 'RouterHandler', { ... }); return IntegrationBuilder.rest({ ... defaultIntegrationOptions: {}, buildDefaultIntegration: (op) => { return { // 모든 통합에서 동일한 라우터 Lambda 핸들러 참조 integration: new LambdaIntegration(router), }; }, }); };}
router
함수를 메서드 내부에서 생성하는 대신 defaultIntegrations
의 매개변수로 정의하는 등 다른 방식으로 코드를 수정할 수도 있습니다.
접근 권한 부여 (IAM 전용)
섹션 제목: “접근 권한 부여 (IAM 전용)”IAM
인증을 선택한 경우 grantInvokeAccess
메서드를 사용해 API 접근 권한을 부여할 수 있습니다. 예를 들어 인증된 Cognito 사용자에게 API 접근 권한을 부여할 수 있습니다:
api.grantInvokeAccess(myIdentityPool.authenticatedRole);
로컬 tRPC 서버
섹션 제목: “로컬 tRPC 서버”serve
타겟을 사용해 API 로컬 서버를 실행할 수 있습니다:
pnpm nx run @my-scope/my-api:serve
yarn nx run @my-scope/my-api:serve
npx nx run @my-scope/my-api:serve
bunx nx run @my-scope/my-api:serve
로컬 서버의 진입점은 src/local-server.ts
입니다. API 변경 시 자동으로 재로드됩니다.
tRPC API 호출
섹션 제목: “tRPC API 호출”타입 안전 방식으로 API를 호출하기 위한 tRPC 클라이언트를 생성할 수 있습니다. 다른 백엔드에서 tRPC API를 호출하는 경우 src/client/index.ts
의 클라이언트를 사용할 수 있습니다:
import { createMyApiClient } from ':my-scope/my-api';
const client = createMyApiClient({ url: 'https://my-api-url.example.com/' });
await client.echo.query({ message: 'Hello world!' });
React 웹사이트에서 API를 호출하는 경우 API 연결 생성기를 사용해 클라이언트를 구성할 수 있습니다.
추가 정보
섹션 제목: “추가 정보”tRPC에 대한 자세한 내용은 tRPC 문서를 참조하세요.