컨텐츠로 건너뛰기

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를 생성할 수 있습니다:

  1. 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
  2. VSCode에서 Nx 콘솔 열기
  3. 클릭 Generate (UI) "Common Nx Commands" 섹션에서
  4. 검색 @aws/nx-plugin - ts#trpc-api
  5. 필수 매개변수 입력
    • 클릭 Generate

    옵션

    매개변수 타입 기본값 설명
    apiName 필수 string - The name of the API (required). Used to generate class names and file paths.
    directory string packages The directory to store the application in.

    생성기 출력 결과

    생성기는 <directory>/<api-name> 디렉토리에 다음 프로젝트 구조를 생성합니다:

    • 디렉터리schema
      • 디렉터리src
        • index.ts 스키마 진입점
        • 디렉터리procedures
          • echo.ts “echo” 프로시저를 위한 Zod 기반 공유 스키마 정의
      • tsconfig.json TypeScript 설정
      • project.json 프로젝트 설정 및 빌드 타겟
    • 디렉터리backend
      • 디렉터리src
        • init.ts 백엔드 tRPC 초기화
        • router.ts tRPC 라우터 정의 (Lambda 핸들러 API 진입점)
        • 디렉터리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 구현하기

    위에서 볼 수 있듯이 tRPC API는 schemabackend 두 가지 주요 구성 요소로 이루어져 있으며, 워크스페이스 내 개별 패키지로 정의됩니다.

    스키마

    스키마 패키지는 클라이언트와 서버 코드 간에 공유되는 타입을 정의합니다. 여기서는 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 문서 사이트에서 확인할 수 있습니다.

    백엔드

    backend 폴더에는 API 구현이 포함되어 있으며, 여기서 API 작업과 해당 입력, 출력, 구현을 정의합니다.

    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 }));

    위 코드를 분석하면:

    • publicProceduresrc/middleware에 설정된 미들웨어를 포함하여 API의 공개 메서드를 정의합니다. 이 미들웨어에는 로깅, 추적, 메트릭을 위한 AWS Lambda Powertools 통합이 포함됩니다.
    • input은 작업에 대한 예상 입력을 정의하는 Zod 스키마를 받습니다. 이 작업에 전송된 요청은 자동으로 이 스키마에 대해 검증됩니다.
    • output은 작업에 대한 예상 출력을 정의하는 Zod 스키마를 받습니다. 스키마를 준수하지 않는 출력을 반환하면 구현 단계에서 타입 오류가 발생합니다.
    • query는 API 구현을 정의하는 함수를 받습니다. 이 구현은 opts를 수신하며, opts.input에는 작업에 전달된 입력, opts.ctx에는 미들웨어에서 설정된 컨텍스트가 포함됩니다. query에 전달된 함수는 output 스키마를 준수하는 출력을 반환해야 합니다.

    구현 정의에 query를 사용하는 것은 작업이 변경을 유발하지 않음을 나타냅니다. 데이터 검색 메서드를 정의할 때 사용합니다. 변경 작업을 구현하려면 대신 mutation 메서드를 사용하세요.

    새 작업을 추가할 경우 src/router.ts의 라우터에 등록해야 합니다.

    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 추적 미세 조정

    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 트레이서 문서를 참조하세요.

    커스텀 미들웨어 구현

    프로시저에 제공되는 컨텍스트에 추가 값을 포함시키기 위해 미들웨어를 구현할 수 있습니다.

    예를 들어 src/middleware/identity.ts에 API 호출 사용자 정보를 추출하는 미들웨어를 구현해 보겠습니다.

    먼저 컨텍스트에 추가할 내용을 정의합니다:

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

    이 추가 속성은 선택 사항으로 정의됩니다. tRPC는 이 미들웨어가 올바르게 설정된 프로시저에서 이 속성이 정의되도록 관리합니다.

    다음으로 미들웨어를 구현합니다. 기본 구조는 다음과 같습니다:

    export const createIdentityPlugin = () => {
    const t = initTRPC.context<IIdentityContext>().create();
    return t.procedure.use(async (opts) => {
    // 프로시저 실행 전 로직
    const response = await opts.next(...);
    // 프로시저 실행 후 로직
    return response;
    });
    };

    이 예시에서는 Cognito 사용자 정보를 추출하기 위해 API 게이트웨이 이벤트에서 사용자 subject ID(“sub”)를 추출하고 Cognito에서 사용자 정보를 조회합니다:

    import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';
    export const createIdentityPlugin = () => {
    const t = initTRPC.context<IIdentityContext>().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 백엔드 생성기는 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');
    }
    }

    이 설정은 AWS API Gateway HTTP API, 비즈니스 로직용 AWS Lambda 함수, IAM 인증을 포함한 API 인프라를 구성합니다.

    접근 권한 부여

    grantInvokeAccess 메서드를 사용해 API 접근 권한을 부여할 수 있습니다. 예를 들어 인증된 Cognito 사용자에게 API 접근 권한을 부여할 수 있습니다:

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

    로컬 tRPC 서버

    serve 타겟을 사용해 API 로컬 서버를 실행할 수 있습니다:

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

    로컬 서버의 진입점은 src/local-server.ts입니다.

    tRPC API 호출하기

    타입 안전 방식으로 API를 호출하기 위해 tRPC 클라이언트를 생성할 수 있습니다. 다른 백엔드에서 tRPC API를 호출하는 경우 src/client/index.ts의 클라이언트를 사용할 수 있습니다:

    import { createMyApiClient } from ':my-scope/my-api-backend';
    const client = createMyApiClient({ url: 'https://my-api-url.example.com/' });
    await client.echo.query({ message: 'Hello world!' });

    React 웹사이트에서 API를 호출하는 경우 API 연결 생성기를 사용해 클라이언트를 구성할 수 있습니다.

    추가 정보

    tRPC에 대한 자세한 내용은 tRPC 문서를 참조하세요.