콘텐츠로 이동

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를 두 가지 방법으로 생성할 수 있습니다:

  1. 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
  2. VSCode에서 Nx 콘솔 열기
  3. 클릭 Generate (UI) "Common Nx Commands" 섹션에서
  4. 검색 @aws/nx-plugin - ts#trpc-api
  5. 필수 매개변수 입력
    • 클릭 Generate
    매개변수 타입 기본값 설명
    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 로깅 설정 미들웨어
        • tracer.ts AWS Powertools 추적 설정 미들웨어
        • metrics.ts AWS Powertools 메트릭 설정 미들웨어
      • local-server.ts 로컬 개발 서버용 tRPC 독립 실행형 어댑터 진입점
      • 디렉터리client
        • index.ts 기계 간 API 호출용 타입 안전 클라이언트
    • tsconfig.json TypeScript 설정
    • project.json 프로젝트 설정 및 빌드 타겟

    생성기는 또한 API 배포에 사용할 수 있는 CDK 구성을 packages/common/constructs 디렉토리에 생성합니다.

    tRPC API는 라우터가 특정 프로시저에 요청을 위임하는 구조로 구성됩니다. 각 프로시저는 Zod 스키마로 정의된 입력과 출력을 가집니다.

    src/schema 디렉토리에는 클라이언트와 서버 코드 간에 공유되는 타입들이 포함됩니다. 이 패키지에서는 TypeScript 우선 스키마 선언 및 검증 라이브러리인 Zod를 사용하여 이러한 타입을 정의합니다.

    예시 스키마:

    import { z } from 'zod/v4';
    // 스키마 정의
    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에서 사용되는 구조를 한 곳에서 업데이트할 수 있도록 클라이언트와 서버 코드 모두에서 공유됩니다. Zod는 런타임에 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: src/middleware에 설정된 미들웨어를 포함한 공개 API 메서드 정의
    • input: 작업에 필요한 입력 스키마 정의 (자동 검증)
    • output: 작업 출력 스키마 정의 (스키마 불일치 시 타입 오류 발생)
    • query: 비변경 작업 구현 정의 (데이터 조회용). 변경 작업시 mutation 사용

    새 프로시저 추가 시 src/router.ts에 등록해야 합니다.

    TRPCError를 발생시켜 클라이언트에 오류 응답을 반환할 수 있습니다:

    throw new TRPCError({
    code: 'NOT_FOUND',
    message: '요청한 리소스를 찾을 수 없습니다',
    });

    관련 작업을 그룹화하려면 중첩 라우터를 사용합니다:

    import { getUser } from './procedures/users/get.js';
    import { listUsers } from './procedures/users/list.js';
    const appRouter = router({
    users: router({
    get: getUser,
    list: listUsers,
    }),
    ...
    })

    클라이언트에서는 client.users.list.query() 방식으로 호출합니다.

    src/middleware/logger.ts에 구성된 AWS Lambda Powertools 로거는 opts.ctx.logger로 접근 가능합니다:

    export const echo = publicProcedure
    .input(...)
    .output(...)
    .query(async (opts) => {
    opts.ctx.logger.info('Operation called with input', opts.input);
    return ...;
    });

    자세한 내용은 AWS Lambda Powertools Logger 문서 참조.

    src/middleware/metrics.ts의 메트릭 유틸은 opts.ctx.metrics로 접근:

    export const echo = publicProcedure
    .input(...)
    .output(...)
    .query(async (opts) => {
    opts.ctx.metrics.addMetric('Invocations', 'Count', 1);
    return ...;
    });

    자세한 내용은 AWS Lambda Powertools Metrics 문서 참조.

    src/middleware/tracer.ts의 추적기는 opts.ctx.tracer로 접근:

    export const echo = publicProcedure
    .input(...)
    .output(...)
    .query(async (opts) => {
    const subSegment = opts.ctx.tracer.getSegment()!.addNewSubsegment('MyAlgorithm');
    // ... 로직 구현
    subSegment.close();
    return ...;
    });

    자세한 내용은 AWS Lambda Powertools Tracer 문서 참조.

    컨텍스트에 추가 값을 제공하는 미들웨어를 구현할 수 있습니다. 예시 (src/middleware/identity.ts):

    이 예제는 authIAM으로 설정되었다고 가정합니다. Cognito 인증시 event에서 클레임을 추출하면 더 간단합니다.

    컨텍스트 타입 정의:

    export interface IIdentityContext {
    identity?: {
    sub: string;
    username: string;
    };
    }

    미들웨어 구현 예시 (REST/HTTP API 별):

    // ... REST API 구현 코드 (변경 없음) ...

    생성된 CDK 구성을 스택에 추가:

    import { MyApi } from ':my-scope/common-constructs`;
    export class ExampleStack extends Stack {
    constructor(scope: Construct, id: string) {
    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });
    }
    }

    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 메서드를 수정하여 통합별이 아닌 단일 함수를 생성할 수 있습니다:

    packages/common/constructs/src/app/apis/my-api.ts
    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의 매개변수로 정의하는 등 다른 방식으로 코드를 수정할 수도 있습니다.

    grantInvokeAccess 메서드로 API 접근 권한 부여:

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

    serve 타겟으로 로컬 서버 실행:

    Terminal window
    pnpm nx run @my-scope/my-api:serve

    변경사항이 자동으로 반영됩니다.

    타입 안전 클라이언트 생성 예시:

    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 Connection 생성기 사용 권장

    tRPC 공식 문서에서 더 많은 정보를 확인할 수 있습니다.