AWS PDK에서 마이그레이션
이 가이드는 AWS PDK 프로젝트를 Nx Plugin for AWS로 마이그레이션하는 예시와 일반적인 지침을 제공합니다.
Nx Plugin for AWS로 마이그레이션하면 PDK 대비 다음과 같은 이점이 있습니다:
- 더 빠른 빌드
- 더 쉬운 사용성 (UI 및 CLI)
- Vibe-coding 지원 (MCP 서버를 사용해 보세요!)
- 더 현대적인 기술 스택
- 로컬 API 및 웹사이트 개발
- 더 많은 제어권 (사용 사례에 맞게 파일 수정 가능)
- 기타 다양한 개선 사항!
예시 마이그레이션: 쇼핑 리스트 애플리케이션
섹션 제목: “예시 마이그레이션: 쇼핑 리스트 애플리케이션”이 가이드에서는 PDK 튜토리얼의 쇼핑 리스트 애플리케이션을 마이그레이션 대상으로 사용합니다. 직접 따라 하려면 해당 튜토리얼의 단계를 따라 대상 프로젝트를 생성하세요.
쇼핑 리스트 애플리케이션은 다음 PDK 프로젝트 유형으로 구성됩니다:
MonorepoTsProject
TypeSafeApiProject
CloudscapeReactTsWebsiteProject
InfrastructureTsProject
워크스페이스 생성
섹션 제목: “워크스페이스 생성”시작으로 새 프로젝트를 위한 워크스페이스를 생성합니다. 기존 프로젝트를 직접 수정하는 방식보다 깔끔한 결과를 얻을 수 있습니다. Nx 워크스페이스 생성은 PDK의 MonorepoTsProject
사용과 동일합니다:
npx create-nx-workspace@21.4.1 shopping-list --pm=pnpm --preset=@aws/nx-plugin@0.50.0 --iacProvider=CDK --ci=skip
npx create-nx-workspace@21.4.1 shopping-list --pm=yarn --preset=@aws/nx-plugin@0.50.0 --iacProvider=CDK --ci=skip
npx create-nx-workspace@21.4.1 shopping-list --pm=npm --preset=@aws/nx-plugin@0.50.0 --iacProvider=CDK --ci=skip
npx create-nx-workspace@21.4.1 shopping-list --pm=bun --preset=@aws/nx-plugin@0.50.0 --iacProvider=CDK --ci=skip
생성된 shopping-list
디렉토리를 선호하는 IDE에서 엽니다.
API 마이그레이션
섹션 제목: “API 마이그레이션”쇼핑 목록 애플리케이션에서 사용된 TypeSafeApiProject
는 다음을 활용했습니다:
- 모델링 언어로 Smithy 사용
- 연산 구현을 위한 TypeScript
- React 웹사이트 통합을 위한 TypeScript 훅 생성
따라서 ts#smithy-api
생성기를 사용해 동등한 기능을 제공할 수 있습니다.
TypeScript Smithy API 생성
섹션 제목: “TypeScript Smithy API 생성”ts#smithy-api
생성기를 실행해 packages/api
에 API 프로젝트를 설정합니다:
- 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
- VSCode에서 Nx 콘솔 열기
- 클릭
Generate (UI)
"Common Nx Commands" 섹션에서 - 검색
@aws/nx-plugin - ts#smithy-api
- 필수 매개변수 입력
- name: api
- namespace: com.aws
- auth: IAM
- 클릭
Generate
pnpm nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive
yarn nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive
npx nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive
bunx nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive
어떤 파일이 변경될지 확인하기 위해 드라이 런을 수행할 수도 있습니다
pnpm nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#smithy-api --name=api --namespace=com.aws --auth=IAM --no-interactive --dry-run
이렇게 하면 model
프로젝트와 backend
프로젝트가 생성됩니다. model
은 Smithy 모델을 포함하며, backend
는 서버 구현을 포함합니다.
백엔드는 Smithy Server Generator for TypeScript를 사용합니다. 이에 대해서는 아래에서 자세히 살펴보겠습니다.
Smithy 모델 마이그레이션
섹션 제목: “Smithy 모델 마이그레이션”기본 Smithy API 프로젝트 구조가 준비되었으므로 모델을 마이그레이션합니다:
-
packages/api/model/src
에 생성된 예제 Smithy 파일 삭제 -
PDK 프로젝트의
packages/api/model/src/main/smithy
디렉토리에서 새 프로젝트의packages/api/model/src
로 모델 복사 -
PDK 애플리케이션과 일치하도록
smithy-build.json
의 서비스 이름 및 네임스페이스 업데이트:smithy-build.json "plugins": {"openapi": {"service": "com.aws#MyApi",... -
main.smithy
의 서비스에 Smithy TypeScript Server SDK 사용 시 필수인ValidationException
오류 추가:main.smithy use smithy.framework#ValidationException/// My Shopping List API@restJson1service MyApi {version: "1.0"operations: [GetShoppingListsPutShoppingListDeleteShoppingList]errors: [BadRequestErrorNotAuthorizedErrorInternalFailureErrorValidationException]} -
생성된 클라이언트에 페이지네이션 정보를 제공하는 트레이트 정의를 위해
packages/api/model/src
에extensions.smithy
파일 추가:extensions.smithy $version: "2"namespace com.awsuse smithy.openapi#specificationExtension@trait@specificationExtension(as: "x-cursor")structure cursor {inputToken: Stringenabled: Boolean} -
get-shopping-lists.smithy
의GetShoppingLists
연산에 새@cursor
트레이트 추가:operations/get-shopping-lists.smithy @readonly@http(method: "GET", uri: "/shopping-list")@paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "pageSize", items: "shoppingLists")@cursor(inputToken: "nextToken")@handler(language: "typescript")operation GetShoppingLists {input := with [PaginatedInputMixin] {@httpQuery("shoppingListId")shoppingListId: ShoppingListId}Nx Plugin for AWS(
api-connection
생성기 통해)의 클라이언트 생성기를 사용하는 경우@paginated
연산에도@cursor
를 사용해야 합니다. -
모든 연산에서
@handler
트레이트 제거. 이 트레이트는 Nx Plugin for AWS에서 지원하지 않으며,ts#smithy-api
를 사용하면 이 트레이트가 생성하는 람다 함수 CDK 구성체와 번들링 타겟이 필요 없습니다(모든 람다 함수에 단일 번들을 사용하기 때문).
이제 모델 변경 사항을 확인하고 생성된 서버 코드가 작동하는지 빌드를 실행합니다. 백엔드 프로젝트(@shopping-list/api
)에 일부 오류가 발생하지만 다음 단계에서 해결할 것입니다.
pnpm nx run-many --target build
yarn nx run-many --target build
npx nx run-many --target build
bunx nx run-many --target build
람다 핸들러 마이그레이션
섹션 제목: “람다 핸들러 마이그레이션”api/backend
프로젝트는 Type Safe API의 api/handlers/typescript
프로젝트와 유사하다고 볼 수 있습니다.
Type Safe API와 ts#smithy-api
생성기의 주요 차이점은 핸들러 구현 시 Smithy Server Generator for TypeScript를 사용한다는 점입니다(Type Safe API의 자체 생성 핸들러 래퍼 대신).
쇼핑 목록 애플리케이션의 람다 핸들러는 @aws-sdk/client-dynamodb
패키지를 사용하므로 먼저 설치합니다:
pnpm add -w @aws-sdk/client-dynamodb
yarn add @aws-sdk/client-dynamodb
npm install --legacy-peer-deps @aws-sdk/client-dynamodb
bun install @aws-sdk/client-dynamodb
그런 다음 PDK 프로젝트의 handlers/src/dynamo-client.ts
파일을 backend/src/operations
로 복사해 핸들러에서 사용할 수 있게 합니다.
핸들러 마이그레이션은 다음 일반 단계를 따릅니다:
-
PDK 프로젝트의
packages/api/handlers/typescript/src
디렉토리에서 새 프로젝트의packages/api/backend/src/operations
로 핸들러 복사 -
my-api-typescript-runtime
임포트 제거 후 생성된 TypeScript Server SDK에서 연산 타입 및ServiceContext
임포트:import {deleteShoppingListHandler,DeleteShoppingListChainedHandlerFunction,INTERCEPTORS,Response,LoggingInterceptor,} from 'myapi-typescript-runtime';import { DeleteShoppingList as DeleteShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js'; -
핸들러 래퍼 익스포트 삭제
export const handler = deleteShoppingListHandler(...INTERCEPTORS,deleteShoppingList,); -
SSDK 사용을 위해 연산 핸들러 시그니처 업데이트:
export const deleteShoppingList: DeleteShoppingListChainedHandlerFunction = async (request) => {export const DeleteShoppingList: DeleteShoppingListOperation<ServiceContext> = async (input, ctx) => { -
LoggingInterceptor
사용을ctx.logger
로 대체 (메트릭스 및 트레이싱 인터셉터도 동일):LoggingInterceptor.getLogger(request).info('...');ctx.logger.info('...'); -
입력 파라미터 참조 업데이트. SSDK는 Smithy 모델과 정확히 일치하는 타입을 제공하므로(바디 파라미터와 별도로 경로/쿼리/헤더 파라미터를 그룹화하지 않음) 입력 참조를 수정:
const shoppingListId = request.input.requestParameters.shoppingListId;const shoppingListId = input.shoppingListId; -
Response
사용 제거. SSDK에서는 일반 객체 반환:return Response.success({ shoppingListId });return { shoppingListId };Response
던지기/반환 대신 SSDK 생성 오류 사용:throw Response.badRequest({ message: 'oh no' });return Response.badRequest({ message: 'oh no' });import { BadRequestError } from '../generated/ssdk/index.js';throw new BadRequestError({ message: 'oh no' }); -
상대 임포트에 ESM 문법 사용(.js 확장자 추가)
-
service.ts
에 연산 추가service.ts import { ServiceContext } from './context.js';import { MyApiService } from './generated/ssdk/index.js';import { DeleteShoppingList } from './operations/delete-shopping-list.js';import { GetShoppingLists } from './operations/get-shopping-lists.js';import { PutShoppingList } from './operations/put-shopping-list.js';// 서비스에 연산 등록export const Service: MyApiService<ServiceContext> = {PutShoppingList,GetShoppingLists,DeleteShoppingList,};
쇼핑 목록 핸들러 마이그레이션
Delete Shopping List
import { DeleteItemCommand } from '@aws-sdk/client-dynamodb';import { deleteShoppingListHandler, DeleteShoppingListChainedHandlerFunction, INTERCEPTORS, Response, LoggingInterceptor,} from 'myapi-typescript-runtime';import { ddbClient } from './dynamo-client';
/** * DeleteShoppingList 연산용 타입 세이프 핸들러 */export const deleteShoppingList: DeleteShoppingListChainedHandlerFunction = async (request) => { LoggingInterceptor.getLogger(request).info( 'Start DeleteShoppingList Operation', );
const shoppingListId = request.input.requestParameters.shoppingListId; await ddbClient.send( new DeleteItemCommand({ TableName: 'shopping_list', Key: { shoppingListId: { S: shoppingListId, }, }, }), );
return Response.success({ shoppingListId, });};
/** * DeleteShoppingList 연산용 AWS Lambda 핸들러 진입점 * deleteShoppingListHandler 메서드가 타입 세이프 핸들러를 래핑하고 입출력 마샬링 관리 */export const handler = deleteShoppingListHandler( ...INTERCEPTORS, deleteShoppingList,);
import { DeleteItemCommand } from '@aws-sdk/client-dynamodb';import { ddbClient } from './dynamo-client.js';import { DeleteShoppingList as DeleteShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js';
/** * DeleteShoppingList 연산용 타입 세이프 핸들러 */export const DeleteShoppingList: DeleteShoppingListOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info( 'Start DeleteShoppingList Operation', );
const shoppingListId = input.shoppingListId; await ddbClient.send( new DeleteItemCommand({ TableName: 'shopping_list', Key: { shoppingListId: { S: shoppingListId!, }, }, }), );
return { shoppingListId, };};
Get Shopping Lists
import { DynamoDBClient, QueryCommand, QueryCommandInput, ScanCommand, ScanCommandInput } from '@aws-sdk/client-dynamodb';import { getShoppingListsHandler, GetShoppingListsChainedHandlerFunction, INTERCEPTORS, Response, LoggingInterceptor, ShoppingList,} from 'myapi-typescript-runtime';import { ddbClient } from './dynamo-client';
/** * GetShoppingLists 연산용 타입 세이프 핸들러 */export const getShoppingLists: GetShoppingListsChainedHandlerFunction = async (request) => { LoggingInterceptor.getLogger(request).info('Start GetShoppingLists Operation');
const nextToken = request.input.requestParameters.nextToken; const pageSize = request.input.requestParameters.pageSize; const shoppingListId = request.input.requestParameters.shoppingListId; const commandInput: ScanCommandInput | QueryCommandInput = { TableName: 'shopping_list', ConsistentRead: true, Limit: pageSize, ExclusiveStartKey: nextToken ? fromToken(nextToken) : undefined, ...(shoppingListId ? { KeyConditionExpression: 'shoppingListId = :shoppingListId', ExpressionAttributeValues: { ':shoppingListId': { S: request.input.requestParameters.shoppingListId!, }, }, } : {}), }; const response = await ddbClient.send(shoppingListId ? new QueryCommand(commandInput) : new ScanCommand(commandInput));
return Response.success({ shoppingLists: (response.Items || []) .map<ShoppingList>(item => ({ shoppingListId: item.shoppingListId.S!, name: item.name.S!, shoppingItems: JSON.parse(item.shoppingItems.S || '[]'), })), nextToken: response.LastEvaluatedKey ? toToken(response.LastEvaluatedKey) : undefined, });};
/** * 문자열화된 토큰 디코딩 * @param token 페이지네이션 요청에 전달된 토큰 */const fromToken = <T>(token?: string): T | undefined => token ? (JSON.parse(Buffer.from(decodeURIComponent(token), 'base64').toString()) as T) : undefined;
/** * 페이지네이션 상세를 불투명 문자열 토큰으로 인코딩 * @param paginationToken 페이지네이션 토큰 상세 */const toToken = <T>(paginationToken?: T): string | undefined => paginationToken ? encodeURIComponent(Buffer.from(JSON.stringify(paginationToken)).toString('base64')) : undefined;
/** * GetShoppingLists 연산용 AWS Lambda 핸들러 진입점 * getShoppingListsHandler 메서드가 타입 세이프 핸들러를 래핑하고 입출력 마샬링 관리 */export const handler = getShoppingListsHandler(...INTERCEPTORS, getShoppingLists);
import { QueryCommand, QueryCommandInput, ScanCommand, ScanCommandInput } from '@aws-sdk/client-dynamodb';import { ddbClient } from './dynamo-client.js';import { GetShoppingLists as GetShoppingListsOperation, ShoppingList } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js';
/** * GetShoppingLists 연산용 타입 세이프 핸들러 */export const GetShoppingLists: GetShoppingListsOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info('Start GetShoppingLists Operation');
const nextToken = input.nextToken; const pageSize = input.pageSize; const shoppingListId = input.shoppingListId; const commandInput: ScanCommandInput | QueryCommandInput = { TableName: 'shopping_list', ConsistentRead: true, Limit: pageSize, ExclusiveStartKey: nextToken ? fromToken(nextToken) : undefined, ...(shoppingListId ? { KeyConditionExpression: 'shoppingListId = :shoppingListId', ExpressionAttributeValues: { ':shoppingListId': { S: input.shoppingListId!, }, }, } : {}), }; const response = await ddbClient.send(shoppingListId ? new QueryCommand(commandInput) : new ScanCommand(commandInput));
return { shoppingLists: (response.Items || []) .map<ShoppingList>(item => ({ shoppingListId: item.shoppingListId.S!, name: item.name.S!, shoppingItems: JSON.parse(item.shoppingItems.S || '[]'), })), nextToken: response.LastEvaluatedKey ? toToken(response.LastEvaluatedKey) : undefined, };};
/** * 문자열화된 토큰 디코딩 * @param token 페이지네이션 요청에 전달된 토큰 */const fromToken = <T>(token?: string): T | undefined => token ? (JSON.parse(Buffer.from(decodeURIComponent(token), 'base64').toString()) as T) : undefined;
/** * 페이지네이션 상세를 불투명 문자열 토큰으로 인코딩 * @param paginationToken 페이지네이션 토큰 상세 */const toToken = <T>(paginationToken?: T): string | undefined => paginationToken ? encodeURIComponent(Buffer.from(JSON.stringify(paginationToken)).toString('base64')) : undefined;
Put Shopping List
import { randomUUID } from 'crypto';import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';import { putShoppingListHandler, PutShoppingListChainedHandlerFunction, INTERCEPTORS, Response, LoggingInterceptor,} from 'myapi-typescript-runtime';import { ddbClient } from './dynamo-client';
/** * PutShoppingList 연산용 타입 세이프 핸들러 */export const putShoppingList: PutShoppingListChainedHandlerFunction = async (request) => { LoggingInterceptor.getLogger(request).info('Start PutShoppingList Operation');
const shoppingListId = request.input.body.shoppingListId ?? randomUUID(); await ddbClient.send(new PutItemCommand({ TableName: 'shopping_list', Item: { shoppingListId: { S: shoppingListId, }, name: { S: request.input.body.name, }, shoppingItems: { S: JSON.stringify(request.input.body.shoppingItems || []), }, }, }));
return Response.success({ shoppingListId, });};
/** * PutShoppingList 연산용 AWS Lambda 핸들러 진입점 * putShoppingListHandler 메서드가 타입 세이프 핸들러를 래핑하고 입출력 마샬링 관리 */export const handler = putShoppingListHandler(...INTERCEPTORS, putShoppingList);
import { randomUUID } from 'crypto';import { PutItemCommand } from '@aws-sdk/client-dynamodb';import { ddbClient } from './dynamo-client.js';import { PutShoppingList as PutShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js';
/** * PutShoppingList 연산용 타입 세이프 핸들러 */export const PutShoppingList: PutShoppingListOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info('Start PutShoppingList Operation');
const shoppingListId = input.shoppingListId ?? randomUUID(); await ddbClient.send(new PutItemCommand({ TableName: 'shopping_list', Item: { shoppingListId: { S: shoppingListId, }, name: { S: input.name!, }, shoppingItems: { S: JSON.stringify(input.shoppingItems || []), }, }, }));
return { shoppingListId, };};
Smithy API 프로젝트를 packages/api
에 일관성 있게 배치하기 위해 초기 이름을 api
로 생성했습니다. 이제 Smithy API가 service Api
대신 service MyApi
를 정의하므로 getApiServiceHandler
를 getMyApiServiceHandler
로 업데이트해야 합니다.
handler.ts
수정:
import { getApiServiceHandler } from './generated/ssdk/index.js'; import { getMyApiServiceHandler } from './generated/ssdk/index.js';
process.env.POWERTOOLS_METRICS_NAMESPACE = 'Api';process.env.POWERTOOLS_SERVICE_NAME = 'Api';
const tracer = new Tracer();const logger = new Logger();const metrics = new Metrics();
const serviceHandler = getApiServiceHandler(Service); const serviceHandler = getMyApiServiceHandler(Service);
local-server.ts
수정:
import { getApiServiceHandler } from './generated/ssdk/index.js';import { getMyApiServiceHandler } from './generated/ssdk/index.js';
const PORT = 3001;
const tracer = new Tracer();const logger = new Logger();const metrics = new Metrics();
const serviceHandler = getApiServiceHandler(Service);const serviceHandler = getMyApiServiceHandler(Service);
추가로 packages/api/backend/project.json
의 metadata.apiName
을 my-api
로 업데이트:
"metadata": { "generator": "ts#smithy-api", "apiName": "api", "apiName": "my-api", "auth": "IAM", "modelProject": "@shopping-list/api-model", "ports": [3001] },
빌드로 검증
섹션 제목: “빌드로 검증”현재까지의 마이그레이션 작업을 검증하기 위해 빌드 실행:
pnpm nx run-many --target build
yarn nx run-many --target build
npx nx run-many --target build
bunx nx run-many --target build
웹사이트 마이그레이션
섹션 제목: “웹사이트 마이그레이션”쇼핑 리스트 애플리케이션에서 사용된 CloudscapeReactTsWebsiteProject
는 CloudScape와 Cognito 인증이 내장된 React 웹사이트를 구성했습니다.
이 프로젝트 유형은 현재 더 이상 사용되지 않는 create-react-app
을 활용했습니다. 이 가이드에서 웹사이트를 마이그레이션하기 위해 ts#react-website
생성기를 사용할 것이며, 이는 Vite와 같은 더 현대적이고 지원되는 기술을 사용합니다.
마이그레이션 과정에서 PDK에서 구성한 React Router에서 TanStack Router로 이동할 것입니다. 이는 웹사이트 라우팅에 추가적인 타입 안전성을 제공합니다.
React 웹사이트 생성
섹션 제목: “React 웹사이트 생성”packages/website
에 웹사이트 프로젝트를 설정하려면 ts#react-website
생성기를 실행하세요:
- 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
- VSCode에서 Nx 콘솔 열기
- 클릭
Generate (UI)
"Common Nx Commands" 섹션에서 - 검색
@aws/nx-plugin - ts#react-website
- 필수 매개변수 입력
- name: website
- 클릭
Generate
pnpm nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive
yarn nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive
npx nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive
bunx nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive
어떤 파일이 변경될지 확인하기 위해 드라이 런을 수행할 수도 있습니다
pnpm nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#react-website --name=website --no-interactive --dry-run
Cognito 인증 추가
섹션 제목: “Cognito 인증 추가”위 React 웹사이트 생성기는 CloudscapeReactTsWebsiteProject
처럼 기본적으로 Cognito 인증을 번들로 제공하지 않습니다. 대신 ts#react-website#auth
생성기를 통해 명시적으로 추가합니다.
- 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
- VSCode에서 Nx 콘솔 열기
- 클릭
Generate (UI)
"Common Nx Commands" 섹션에서 - 검색
@aws/nx-plugin - ts#react-website#auth
- 필수 매개변수 입력
- project: website
- cognitoDomain: shopping-list
- 클릭
Generate
pnpm nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive
yarn nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive
npx nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive
bunx nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive
어떤 파일이 변경될지 확인하기 위해 드라이 런을 수행할 수도 있습니다
pnpm nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#react-website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-run
이렇게 하면 Cognito 호스팅 UI를 사용하여 사용자가 로그인하도록 적절한 리디렉션을 관리하는 React 컴포넌트가 추가됩니다. 또한 packages/common/constructs
에 UserIdentity
라는 Cognito 리소스를 배포하는 CDK 구문이 추가됩니다.
웹사이트를 API에 연결
섹션 제목: “웹사이트를 API에 연결”PDK에서는 Projen 프로젝트를 서로 전달하여 통합 코드를 생성할 수 있었습니다. 이 기능은 쇼핑 리스트 애플리케이션에서 웹사이트가 API와 통합되도록 구성하는 데 사용되었습니다.
Nx Plugin for AWS에서는 api-connection
생성기를 통해 API 통합이 지원됩니다. 다음으로 이 생성기를 사용하여 웹사이트가 Smithy API를 호출할 수 있도록 설정합니다:
- 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
- VSCode에서 Nx 콘솔 열기
- 클릭
Generate (UI)
"Common Nx Commands" 섹션에서 - 검색
@aws/nx-plugin - api-connection
- 필수 매개변수 입력
- sourceProject: website
- targetProject: api
- 클릭
Generate
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive
yarn nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive
npx nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive
bunx nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive
어떤 파일이 변경될지 확인하기 위해 드라이 런을 수행할 수도 있습니다
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive --dry-run
yarn nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive --dry-run
npx nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive --dry-run
bunx nx g @aws/nx-plugin:api-connection --sourceProject=website --targetProject=api --no-interactive --dry-run
이렇게 하면 생성된 TypeScript 클라이언트를 통해 웹사이트가 API를 호출할 수 있는 필요한 클라이언트 제공자와 빌드 대상이 생성됩니다.
AWS Northstar 종속성 추가
섹션 제목: “AWS Northstar 종속성 추가”CloudscapeReactTsWebsiteProject
는 쇼핑 리스트 애플리케이션에서 사용된 @aws-northstar/ui
에 대한 종속성을 자동으로 포함했으므로 여기에 추가합니다:
pnpm add -w @aws-northstar/ui
yarn add @aws-northstar/ui
npm install --legacy-peer-deps @aws-northstar/ui
bun install @aws-northstar/ui
컴포넌트와 페이지 이동
섹션 제목: “컴포넌트와 페이지 이동”쇼핑 리스트 애플리케이션에는 CreateItem
컴포넌트와 ShoppingList
, ShoppingLists
두 페이지가 있습니다. TanStack Router와 Nx Plugin for AWS TypeScript 클라이언트 코드 생성기를 사용하므로 일부 조정을 통해 이를 새 웹사이트로 마이그레이션합니다.
-
PDK 프로젝트의
packages/website/src/components/CreateItem/index.tsx
를 새 프로젝트의 동일한 위치로 복사합니다. -
packages/website/src/pages/ShoppingLists/index.tsx
를packages/website/src/routes/index.tsx
로 복사합니다.ShoppingLists
는 홈 페이지이며 TanStack Router의 파일 기반 라우팅을 사용하기 때문입니다. -
packages/website/src/pages/ShoppingList/index.tsx
를packages/website/src/routes/$shoppingListId.tsx
로 복사합니다.ShoppingList
는/:shoppingListId
경로에 표시할 페이지입니다.
이제 IDE에 일부 빌드 오류가 표시될 것입니다. 아래에 설명된 몇 가지 추가 변경 사항을 적용하여 새 프레임워크에 맞춰야 합니다.
React Router에서 TanStack Router로 마이그레이션
섹션 제목: “React Router에서 TanStack Router로 마이그레이션”파일 기반 라우팅을 사용하므로 웹사이트 로컬 개발 서버를 사용하여 라우트 구성을 자동으로 생성할 수 있습니다. 로컬 웹사이트 서버를 시작합니다:
pnpm nx serve-local website
yarn nx serve-local website
npx nx serve-local website
bunx nx serve-local website
일부 오류가 표시되지만 포트 4200
에서 로컬 웹사이트 서버와 포트 3001
에서 로컬 Smithy API 서버가 시작됩니다.
TanStack Router로 마이그레이션하려면 routes/index.tsx
와 routes/$shoppingListId.tsx
에서 다음 단계를 따르세요:
-
각 라우트를 등록하기 위해
createFileRoute
추가:import { createFileRoute } from "@tanstack/react-router";...export default ShoppingLists;export const Route = createFileRoute('/')({component: ShoppingLists,});import { createFileRoute } from "@tanstack/react-router";...export default ShoppingList;export const Route = createFileRoute('/$shoppingListId')({component: ShoppingList,});파일을 저장하면
createFileRoute
호출과 관련된 타입 오류가 사라집니다. -
useNavigate
훅 교체:임포트 업데이트:
import { useNavigate } from 'react-router-dom';import { useNavigate } from '@tanstack/react-router';useNavigate
에서 반환된navigate
메서드 호출을 타입 안전한 경로로 업데이트:navigate(`/${cell.shoppingListId}`);navigate({to: '/$shoppingListId',params: { shoppingListId: cell.shoppingListId },}); -
useParams
훅 교체:임포트 제거:
import { useParams } from 'react-router-dom';useParams
호출을 위에서 생성한Route
의 훅으로 교체. 이제 타입 안전합니다!const { shoppingListId } = useParams();const { shoppingListId } = Route.useParams();
컴포넌트 임포트 수정
섹션 제목: “컴포넌트 임포트 수정”PDK 프로젝트보다 파일 트리에서 라우트 파일의 위치가 덜 중첩되었으므로 routes/index.tsx
와 routes/$shoppingListId.tsx
에서 CreateItem
임포트를 수정합니다:
import CreateItem from "../../components/CreateItem";import CreateItem from "../components/CreateItem";
AppLayoutContext
도 새 프로젝트에서 위치가 약간 다릅니다:
import { AppLayoutContext } from "../../layouts/App";import { AppLayoutContext } from "../components/AppLayout";
새로 생성된 TypeScript 클라이언트 사용
섹션 제목: “새로 생성된 TypeScript 클라이언트 사용”이제 Nx Plugin for AWS에서 제공하는 개선된 TypeScript 클라이언트를 사용하도록 마이그레이션합니다. 다음 단계를 따르세요:
-
이전 Type Safe API 대신 새로 생성된 클라이언트와 타입 임포트:
import {ShoppingList,usePutShoppingList,useDeleteShoppingList,useGetShoppingLists,} from "myapi-typescript-react-query-hooks";import { ShoppingList } from "../generated/my-api/types.gen";import { useMyApi } from "../hooks/useMyApi";import { useInfiniteQuery, useMutation } from "@tanstack/react-query";routes/$shoppingListId.tsx
에서는ShoppingList
타입을_ShoppingList
로 임포트하므로 동일하게types.gen
에서 임포트합니다. -
새 TanStack Query 훅 인스턴스화:
const getShoppingLists = useGetShoppingLists({ pageSize: PAGE_SIZE });const putShoppingList = usePutShoppingList();const deleteShoppingList = useDeleteShoppingList();const api = useMyApi();const getShoppingLists = useInfiniteQuery(api.getShoppingLists.infiniteQueryOptions({ pageSize: PAGE_SIZE },{ getNextPageParam: (p) => p.nextToken },),);const putShoppingList = useMutation(api.putShoppingList.mutationOptions());const deleteShoppingList = useMutation(api.deleteShoppingList.mutationOptions(),); -
요청 본문 매개변수를 위한
<operation>RequestContent
래퍼 제거:await putShoppingList.mutateAsync({putShoppingListRequestContent: {name: item,},});
TanStack Query v4에서 v5로 마이그레이션
섹션 제목: “TanStack Query v4에서 v5로 마이그레이션”api-connection
생성기에서 추가된 v5와 PDK에서 사용한 TanStack Query v4 간의 차이로 인해 남은 오류를 수정합니다:
-
뮤테이션에서
isLoading
을isPending
으로 교체:putShoppingList.isLoadingputShoppingList.isPending -
쇼핑 리스트 애플리케이션에서 TanStack Query v4의 타입을 기대하는
@aws-northstar/ui
의InfiniteQueryTable
을 사용합니다. 이는 v5의 무한 쿼리와 호환되므로 타입 오류를 억제합니다:<InfiniteQueryTablequery={getShoppingLists}query={getShoppingLists as any}
로컬 웹사이트 방문
섹션 제목: “로컬 웹사이트 방문”이제 http://localhost:4200/ 에서 로컬 웹사이트에 접속할 수 있습니다!
모든 것이 마이그레이션되었으므로 웹사이트가 로드되어야 합니다! 쇼핑 리스트 애플리케이션이 API, 웹사이트, Identity 외에 DynamoDB 테이블에만 의존하므로 shopping_list
DynamoDB 테이블이 해당 리전에 있고 로컬 AWS 자격 증명이 접근 권한이 있다면 웹사이트가 완전히 작동할 것입니다.
그렇지 않더라도 다음에 인프라를 마이그레이션할 예정이므로 괜찮습니다.
Shopping List Page Migration
Shopping Lists 페이지
/* eslint-disable @typescript-eslint/no-floating-promises */import { InfiniteQueryTable } from "@aws-northstar/ui/components";import { Button, Header, Link, SpaceBetween, TableProps,} from "@cloudscape-design/components";import { ShoppingList, usePutShoppingList, useDeleteShoppingList, useGetShoppingLists,} from "myapi-typescript-react-query-hooks";import { useContext, useEffect, useMemo, useState } from "react";import { useNavigate } from "react-router-dom";import CreateItem from "../../components/CreateItem";import { AppLayoutContext } from "../../layouts/App";
const PAGE_SIZE = 50;
/** * Component to render the ShoppingLists "/" route. */const ShoppingLists: React.FC = () => { const [visibleModal, setVisibleModal] = useState(false); const [selectedShoppingList, setSelectedShoppingList] = useState< ShoppingList[] >([]); const getShoppingLists = useGetShoppingLists({ pageSize: PAGE_SIZE }); const putShoppingList = usePutShoppingList(); const deleteShoppingList = useDeleteShoppingList(); const navigate = useNavigate(); const { setAppLayoutProps } = useContext(AppLayoutContext);
useEffect(() => { setAppLayoutProps({ contentType: "table", }); }, [setAppLayoutProps]);
const columnDefinitions = useMemo< TableProps.ColumnDefinition<ShoppingList>[] >( () => [ { id: "shoppingListId", isRowHeader: true, header: "Shopping List Id", cell: (cell) => ( <Link href={`/${cell.shoppingListId}`} onFollow={(e) => { e.preventDefault(); navigate(`/${cell.shoppingListId}`); }} > {cell.shoppingListId} </Link> ), }, { id: "name", header: "Name", cell: (cell) => cell.name, }, { id: "shoppingItems", header: "Shopping Items", cell: (cell) => `${cell.shoppingItems?.length || 0} Items.`, }, ], [navigate], );
return ( <> <CreateItem title="Create Shopping List" callback={async (item) => { await putShoppingList.mutateAsync({ putShoppingListRequestContent: { name: item, }, }); getShoppingLists.refetch(); }} isLoading={putShoppingList.isLoading} visibleModal={visibleModal} setVisibleModal={setVisibleModal} /> <InfiniteQueryTable query={getShoppingLists} itemsKey="shoppingLists" pageSize={PAGE_SIZE} selectionType="single" stickyHeader={true} selectedItems={selectedShoppingList} onSelectionChange={(e) => setSelectedShoppingList(e.detail.selectedItems) } header={ <Header variant="awsui-h1-sticky" actions={ <SpaceBetween size="xs" direction="horizontal"> <Button loading={deleteShoppingList.isLoading} data-testid="header-btn-delete" disabled={selectedShoppingList.length === 0} onClick={async () => { await deleteShoppingList.mutateAsync({ shoppingListId: selectedShoppingList![0].shoppingListId, }); setSelectedShoppingList([]); getShoppingLists.refetch(); }} > Delete </Button> <Button data-testid="header-btn-create" variant="primary" onClick={() => setVisibleModal(true)} > Create Shopping List </Button> </SpaceBetween> } > Shopping Lists </Header> } variant="full-page" columnDefinitions={columnDefinitions} /> </> );};
export default ShoppingLists;
/* eslint-disable @typescript-eslint/no-floating-promises */import { InfiniteQueryTable } from "@aws-northstar/ui/components";import { Button, Header, Link, SpaceBetween, TableProps,} from "@cloudscape-design/components";import { useContext, useEffect, useMemo, useState } from "react";import { useNavigate } from "@tanstack/react-router";import CreateItem from "../components/CreateItem";import { AppLayoutContext } from "../components/AppLayout";import { createFileRoute } from "@tanstack/react-router";import { ShoppingList } from "../generated/my-api/types.gen";import { useMyApi } from "../hooks/useMyApi";import { useInfiniteQuery, useMutation } from "@tanstack/react-query";
const PAGE_SIZE = 50;
/** * Component to render the ShoppingLists "/" route. */const ShoppingLists: React.FC = () => { const [visibleModal, setVisibleModal] = useState(false); const [selectedShoppingList, setSelectedShoppingList] = useState< ShoppingList[] >([]); const api = useMyApi(); const getShoppingLists = useInfiniteQuery( api.getShoppingLists.infiniteQueryOptions( { pageSize: PAGE_SIZE }, { getNextPageParam: (res) => res.nextToken }, ), ); const putShoppingList = useMutation(api.putShoppingList.mutationOptions()); const deleteShoppingList = useMutation( api.deleteShoppingList.mutationOptions(), ); const navigate = useNavigate(); const { setAppLayoutProps } = useContext(AppLayoutContext);
useEffect(() => { setAppLayoutProps({ contentType: "table", }); }, [setAppLayoutProps]);
const columnDefinitions = useMemo< TableProps.ColumnDefinition<ShoppingList>[] >( () => [ { id: "shoppingListId", isRowHeader: true, header: "Shopping List Id", cell: (cell) => ( <Link href={`/${cell.shoppingListId}`} onFollow={(e) => { e.preventDefault(); navigate({ to: '/$shoppingListId', params: { shoppingListId: cell.shoppingListId },}); }} > {cell.shoppingListId} </Link> ), }, { id: "name", header: "Name", cell: (cell) => cell.name, }, { id: "shoppingItems", header: "Shopping Items", cell: (cell) => `${cell.shoppingItems?.length || 0} Items.`, }, ], [navigate], );
return ( <> <CreateItem title="Create Shopping List" callback={async (item) => { await putShoppingList.mutateAsync({ name: item, }); getShoppingLists.refetch(); }} isLoading={putShoppingList.isPending} visibleModal={visibleModal} setVisibleModal={setVisibleModal} /> <InfiniteQueryTable query={getShoppingLists as any} itemsKey="shoppingLists" pageSize={PAGE_SIZE} selectionType="single" stickyHeader={true} selectedItems={selectedShoppingList} onSelectionChange={(e) => setSelectedShoppingList(e.detail.selectedItems) } header={ <Header variant="awsui-h1-sticky" actions={ <SpaceBetween size="xs" direction="horizontal"> <Button loading={deleteShoppingList.isPending} data-testid="header-btn-delete" disabled={selectedShoppingList.length === 0} onClick={async () => { await deleteShoppingList.mutateAsync({ shoppingListId: selectedShoppingList![0].shoppingListId, }); setSelectedShoppingList([]); getShoppingLists.refetch(); }} > Delete </Button> <Button data-testid="header-btn-create" variant="primary" onClick={() => setVisibleModal(true)} > Create Shopping List </Button> </SpaceBetween> } > Shopping Lists </Header> } variant="full-page" columnDefinitions={columnDefinitions} /> </> );};
export const Route = createFileRoute('/')({ component: ShoppingLists,});
Shopping List 페이지
/* eslint-disable @typescript-eslint/no-floating-promises */import { Board, BoardItem, BoardProps,} from "@cloudscape-design/board-components";import { Button, Container, ContentLayout, Header, SpaceBetween, Spinner,} from "@cloudscape-design/components";import { ShoppingList as _ShoppingList, usePutShoppingList, useGetShoppingLists,} from "myapi-typescript-react-query-hooks";import { useEffect, useState } from "react";import { useParams } from "react-router-dom";import CreateItem from "../../components/CreateItem";
type ListItem = { name: string };
/** * Component to render a singular Shopping List "/:shoppingListId" route. */const ShoppingList: React.FC = () => { const { shoppingListId } = useParams(); const [visibleModal, setVisibleModal] = useState(false); const getShoppingLists = useGetShoppingLists({ shoppingListId }); const putShoppingList = usePutShoppingList(); const shoppingList: _ShoppingList | undefined = getShoppingLists.data?.pages[0].shoppingLists[0]!; const [shoppingItems, setShoppingItems] = useState<BoardProps.Item<ListItem>[]>();
useEffect(() => { setShoppingItems( shoppingList?.shoppingItems?.map((i) => ({ id: i, definition: { minColumnSpan: 4 }, data: { name: i }, })), ); }, [shoppingList?.shoppingItems]);
return ( <ContentLayout header={ <Header variant="awsui-h1-sticky" actions={ <SpaceBetween size="xs" direction="horizontal"> <Button data-testid="header-btn-create" variant="primary" onClick={() => setVisibleModal(true)} > Add Item </Button> </SpaceBetween> } > Shopping list: {shoppingList?.name} </Header> } > <CreateItem isLoading={false} title="Add Item" callback={async (item) => { const items = [ ...(shoppingItems || []), { id: item, definition: { minColumnSpan: 4 }, data: { name: item }, }, ]; setShoppingItems(items); putShoppingList.mutate({ putShoppingListRequestContent: { name: shoppingList.name, shoppingListId: shoppingList.shoppingListId, shoppingItems: items.map((i) => i.data.name), }, }); }} visibleModal={visibleModal} setVisibleModal={setVisibleModal} /> <Container> {!shoppingList ? ( <Spinner /> ) : ( <Board<ListItem> onItemsChange={(event) => { const items = event.detail.items as BoardProps.Item<ListItem>[]; setShoppingItems(items); putShoppingList.mutate({ putShoppingListRequestContent: { name: shoppingList.name, shoppingListId: shoppingList.shoppingListId, shoppingItems: items.map((i) => i.data.name), }, }); }} items={shoppingItems || []} renderItem={(item, actions) => ( <BoardItem header={item.data.name} settings={ <Button iconName="close" variant="icon" onClick={actions.removeItem} /> } i18nStrings={{ dragHandleAriaLabel: "Drag handle", dragHandleAriaDescription: "Use Space or Enter to activate drag, arrow keys to move, Space or Enter to submit, or Escape to discard.", resizeHandleAriaLabel: "Resize handle", resizeHandleAriaDescription: "Use Space or Enter to activate resize, arrow keys to move, Space or Enter to submit, or Escape to discard.", }} /> )} i18nStrings={{ liveAnnouncementDndCommitted: () => "", liveAnnouncementDndDiscarded: () => "", liveAnnouncementDndItemInserted: () => "", liveAnnouncementDndItemReordered: () => "", liveAnnouncementDndItemResized: () => "", liveAnnouncementDndStarted: () => "", liveAnnouncementItemRemoved: () => "", navigationAriaLabel: "", navigationItemAriaLabel: () => "", }} empty={<></>} /> )} </Container> </ContentLayout> );};
export default ShoppingList;
// routes/$shoppingListId.tsx/* eslint-disable @typescript-eslint/no-floating-promises */import { Board, BoardItem, BoardProps,} from "@cloudscape-design/board-components";import { Button, Container, ContentLayout, Header, SpaceBetween, Spinner,} from "@cloudscape-design/components";import { useEffect, useState } from "react";import CreateItem from "../components/CreateItem";import { createFileRoute } from "@tanstack/react-router";import { useMyApi } from "../hooks/useMyApi";import { useInfiniteQuery, useMutation } from "@tanstack/react-query";import { ShoppingList as _ShoppingList } from "../generated/my-api/types.gen";
type ListItem = { name: string };
/** * Component to render a singular Shopping List "/:shoppingListId" route. */const ShoppingList: React.FC = () => { const { shoppingListId } = Route.useParams(); const [visibleModal, setVisibleModal] = useState(false); const api = useMyApi(); const getShoppingLists = useInfiniteQuery( api.getShoppingLists.infiniteQueryOptions( { shoppingListId }, { getNextPageParam: (p) => p.nextToken }, ), ); const putShoppingList = useMutation(api.putShoppingList.mutationOptions()); const shoppingList: _ShoppingList | undefined = getShoppingLists.data?.pages?.[0]?.shoppingLists?.[0]; const [shoppingItems, setShoppingItems] = useState<BoardProps.Item<ListItem>[]>();
useEffect(() => { setShoppingItems( shoppingList?.shoppingItems?.map((i) => ({ id: i, definition: { minColumnSpan: 4 }, data: { name: i }, })), ); }, [shoppingList?.shoppingItems]);
return ( <ContentLayout header={ <Header variant="awsui-h1-sticky" actions={ <SpaceBetween size="xs" direction="horizontal"> <Button data-testid="header-btn-create" variant="primary" onClick={() => setVisibleModal(true)} > Add Item </Button> </SpaceBetween> } > Shopping list: {shoppingList?.name} </Header> } > <CreateItem isLoading={false} title="Add Item" callback={async (item) => { const items = [ ...(shoppingItems || []), { id: item, definition: { minColumnSpan: 4 }, data: { name: item }, }, ]; setShoppingItems(items); putShoppingList.mutate({ name: shoppingList?.name ?? 'my list', shoppingListId: shoppingList?.shoppingListId, shoppingItems: items.map((i) => i.data.name), }); }} visibleModal={visibleModal} setVisibleModal={setVisibleModal} /> <Container> {!shoppingList ? ( <Spinner /> ) : ( <Board<ListItem> onItemsChange={(event) => { const items = event.detail.items as BoardProps.Item<ListItem>[]; setShoppingItems(items); putShoppingList.mutate({ name: shoppingList.name, shoppingListId: shoppingList.shoppingListId, shoppingItems: items.map((i) => i.data.name), }); }} items={shoppingItems || []} renderItem={(item, actions) => ( <BoardItem header={item.data.name} settings={ <Button iconName="close" variant="icon" onClick={actions.removeItem} /> } i18nStrings={{ dragHandleAriaLabel: "Drag handle", dragHandleAriaDescription: "Use Space or Enter to activate drag, arrow keys to move, Space or Enter to submit, or Escape to discard.", resizeHandleAriaLabel: "Resize handle", resizeHandleAriaDescription: "Use Space or Enter to activate resize, arrow keys to move, Space or Enter to submit, or Escape to discard.", }} /> )} i18nStrings={{ liveAnnouncementDndCommitted: () => "", liveAnnouncementDndDiscarded: () => "", liveAnnouncementDndItemInserted: () => "", liveAnnouncementDndItemReordered: () => "", liveAnnouncementDndItemResized: () => "", liveAnnouncementDndStarted: () => "", liveAnnouncementItemRemoved: () => "", navigationAriaLabel: "", navigationItemAriaLabel: () => "", }} empty={<></>} /> )} </Container> </ContentLayout> );};
export const Route = createFileRoute('/$shoppingListId')({ component: ShoppingList,});
인프라 마이그레이션
섹션 제목: “인프라 마이그레이션”쇼핑 목록 애플리케이션을 마이그레이션하기 위해 필요한 마지막 프로젝트는 InfrastructureTsProject
입니다. 이는 TypeScript CDK 프로젝트로, Nx Plugin for AWS에서 이에 상응하는 것은 ts#infra
생성기입니다.
Projen 프로젝트뿐만 아니라 PDK는 이러한 프로젝트들이 의존하는 CDK 구문들도 제공했습니다. 우리는 PDK의 CDK 구문 대신 Nx Plugin for AWS가 생성한 구문들을 사용하도록 쇼핑 목록 애플리케이션을 마이그레이션할 것입니다.
TypeScript CDK 인프라 프로젝트 생성
섹션 제목: “TypeScript CDK 인프라 프로젝트 생성”packages/infra
에 인프라 프로젝트를 설정하려면 ts#infra
생성기를 실행하세요:
- 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
- VSCode에서 Nx 콘솔 열기
- 클릭
Generate (UI)
"Common Nx Commands" 섹션에서 - 검색
@aws/nx-plugin - ts#infra
- 필수 매개변수 입력
- name: infra
- 클릭
Generate
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
yarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
npx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
bunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
어떤 파일이 변경될지 확인하기 위해 드라이 런을 수행할 수도 있습니다
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
CDK 인프라 마이그레이션
섹션 제목: “CDK 인프라 마이그레이션”PDK 쇼핑 목록 애플리케이션은 CDK 애플리케이션 스택 내에서 다음 구문들을 인스턴스화했습니다:
- 쇼핑 목록을 저장하는 DynamoDB 테이블용
DatabaseConstruct
- PDK에서 직접 가져온 Cognito 리소스용
UserIdentity
- Smithy API 배포용
MyApi
(PDK의TypeSafeRestApi
CDK 구문을 기반으로 타입 안전 통합을 사용하는 생성된 TypeScript CDK 구문) - PDK의
StaticWebsite
CDK 구문을 래핑한 웹사이트 배포용Website
이제 각각을 새 프로젝트로 마이그레이션하겠습니다.
애플리케이션 스택 복사
섹션 제목: “애플리케이션 스택 복사”PDK 쇼핑 목록 애플리케이션의 packages/infra/src/stacks/application-stack.ts
를 새 프로젝트의 동일한 위치에 복사하세요. 아래에서 해결할 TypeScript 오류가 표시될 것입니다.
데이터베이스 구문 복사
섹션 제목: “데이터베이스 구문 복사”PDK 쇼핑 목록 애플리케이션은 packages/src/constructs/database.ts
에 Database
구문을 가지고 있었습니다. 이를 새 프로젝트의 동일한 위치에 복사하세요.
Nx Plugin for AWS는 PDK Nag보다 조금 더 엄격한 Checkov를 보안 테스트에 사용하므로 다음과 같이 일부 규칙 억제를 추가해야 합니다:
import { suppressRules } from ':shopping-list/common-constructs';...suppressRules( this.shoppingListTable, ['CKV_AWS_28', 'CKV_AWS_119'], 'Backup and KMS key not required for this project',);
application-stack.ts
에서 DatabaseConstruct
임포트를 ESM 구문으로 업데이트하세요:
import { DatabaseConstruct } from '../constructs/database';import { DatabaseConstruct } from '../constructs/database.js';
UserIdentity 구문 마이그레이션
섹션 제목: “UserIdentity 구문 마이그레이션”UserIdentity
구문은 일반적으로 임포트만 조정하면 변경 없이 교체할 수 있습니다.
import { UserIdentity } from "@aws/pdk/identity";import { UserIdentity } from ':shopping-list/common-constructs';...const userIdentity = new UserIdentity(this, `${id}UserIdentity`);
새 UserIdentity
구문에서 사용하는 기본 구문들은 PDK가 @aws-cdk/aws-cognito-identitypool-alpha
를 사용한 반면, 직접 aws-cdk-lib
에서 제공됩니다.
API 구문 마이그레이션
섹션 제목: “API 구문 마이그레이션”PDK 쇼핑 목록 애플리케이션은 Smithy 모델에서 생성된 타입 안전 CDK 구문을 인스턴스화하는 constructs/apis/myapi.ts
에 구문을 가지고 있었습니다.
이 구문 외에도 PDK 프로젝트는 @handler
트레이트를 사용해 생성된 람다 함수 CDK 구문들도 생성했습니다.
Type Safe API와 마찬가지로 Nx Plugin for AWS는 Smithy 모델 기반의 타입 안전 통합을 제공하지만 훨씬 간단하고 유연한 방식으로 구현됩니다. 빌드 시 전체 CDK 구문을 생성하는 대신 최소한의 “메타데이터”만 생성되며, packages/common/constructs/src/app/apis/api.ts
에서 일반적인 방식으로 사용합니다. ts#smithy-api
생성기 가이드에서 구문 사용법을 더 알아볼 수 있습니다.
다음 단계를 따르세요:
-
application-stack.ts
에서Api
구문 인스턴스화stacks/application-stack.ts import { MyApi } from "../constructs/apis/myapi";import { Api } from ':shopping-list/common-constructs';...const myapi = new MyApi(this, "MyApi", {databaseConstruct,userIdentity,});const api = new Api(this, 'MyApi', {integrations: Api.defaultIntegrations(this).build(),});여기서
Api.defaultIntegrations(this).build()
를 사용합니다 - 기본 동작은 API의 각 작업에 대해 람다 함수를 생성하는 것으로,myapi.ts
의 동작과 동일합니다. -
람다 함수에 DynamoDB 테이블 접근 권한 부여
PDK 쇼핑 목록 애플리케이션에서는
DatabaseConsruct
가MyApi
에 전달되어 각 생성된 함수 구문에 관련 권한을 추가했습니다.application-stack.ts
파일에서Api
구문의 타입 안전integrations
속성에 직접 접근하여 이 작업을 수행합니다:stacks/application-stack.ts // 람다 함수에 Dynamo 호출 권한 부여databaseConstruct.shoppingListTable.grantReadData(api.integrations.getShoppingLists.handler,);[api.integrations.putShoppingList.handler,api.integrations.deleteShoppingList.handler,].forEach((f) => databaseConstruct.shoppingListTable.grantWriteData(f)); -
인증된 사용자에게 API 호출 권한 부여
PDK 애플리케이션의
myapi.ts
에서는 인증된 사용자도 API 호출을 위한 IAM 권한을 부여받았습니다.application-stack.ts
에서 동등한 작업을 수행합니다:stacks/application-stack.ts api.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
웹사이트 구문 마이그레이션
섹션 제목: “웹사이트 구문 마이그레이션”마지막으로 PDK 쇼핑 목록 애플리케이션의 packages/infra/src/constructs/websites/website.ts
에 상응하는 packages/common/constructs/src/app/static-websites/website.ts
의 Website
구문을 application-stack.ts
에 추가합니다.
import { Website } from "../constructs/websites/website";import { Website } from ':shopping-list/common-constructs';...new Website(this, "Website", { userIdentity, myapi,});new Website(this, 'Website');
여기서 identity나 API를 웹사이트에 전달하지 않습니다 - Nx Plugin for AWS가 제공하는 각 구문에서 런타임 구성을 관리하며, UserIdentity
와 Api
는 필요한 값을 등록하고 Website
는 정적 웹사이트의 /runtime-config.json
에 배포합니다.
이제 코드베이스의 모든 관련 부분을 새 프로젝트로 마이그레이션했으므로 프로젝트를 빌드합니다.
pnpm nx run-many --target build
yarn nx run-many --target build
npx nx run-many --target build
bunx nx run-many --target build
이제 완전히 마이그레이션된 코드베이스를 확보했으므로 배포를 진행할 수 있습니다. 이 시점에서 두 가지 경로를 선택할 수 있습니다.
완전히 새로운 리소스 사용 (간단한 방법)
섹션 제목: “완전히 새로운 리소스 사용 (간단한 방법)”가장 간단한 접근 방식은 이 애플리케이션을 완전히 새로운 것으로 취급하는 것입니다. 즉, 새로운 DynamoDB 테이블과 Cognito 사용자 풀을 “처음부터 다시 시작”하여 모든 사용자와 그들의 쇼핑 목록을 잃게 됩니다. 이 방법을 사용하려면 다음을 수행하세요:
-
shopping_list
라는 이름의 DynamoDB 테이블 삭제 -
새로운 애플리케이션 배포:
Terminal window pnpm nx deploy infra shopping-list-infra-sandbox/*Terminal window yarn nx deploy infra shopping-list-infra-sandbox/*Terminal window npx nx deploy infra shopping-list-infra-sandbox/*Terminal window bunx nx deploy infra shopping-list-infra-sandbox/*
🎉 완료되었습니다! 🎉
기존 상태 유지 리소스 마이그레이션 (다운타임 없음, 복잡한 방법)
섹션 제목: “기존 상태 유지 리소스 마이그레이션 (다운타임 없음, 복잡한 방법)”실제로는 기존 AWS 리소스를 새로운 코드베이스에서 관리하도록 마이그레이션하면서 고객에게 다운타임이 발생하지 않도록 하는 것이 더 현실적일 것입니다.
쇼핑 목록 애플리케이션의 경우 중요한 상태 유지 리소스는 사용자의 쇼핑 목록을 포함하는 DynamoDB 테이블과 등록된 모든 사용자 정보를 포함하는 Cognito 사용자 풀입니다. 상위 수준 계획은 이 두 가지 핵심 리소스를 유지하고 새로운 스택에서 관리되도록 이동시킨 다음, DNS를 업데이트하여 새로운 웹사이트(및 고객에게 노출된 경우 API)로 트래픽을 전환하는 것입니다.
-
유지하려는 기존 리소스를 참조하도록 새 애플리케이션 업데이트
쇼핑 목록 애플리케이션의 경우 DynamoDB 테이블에 대해 다음을 수행합니다.
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Cognito 사용자 풀의 경우:
packages/common/constructs/src/core/user-identity.ts this.userPool = this.createUserPool();this.userPool = UserPool.fromUserPoolId(this,'UserPool','<your-user-pool-id>',); -
새 애플리케이션 빌드 및 배포:
Terminal window pnpm nx run-many --target buildTerminal window yarn nx run-many --target buildTerminal window npx nx run-many --target buildTerminal window bunx nx run-many --target buildTerminal window pnpm nx deploy infra shopping-list-infra-sandbox/*Terminal window yarn nx deploy infra shopping-list-infra-sandbox/*Terminal window npx nx deploy infra shopping-list-infra-sandbox/*Terminal window bunx nx deploy infra shopping-list-infra-sandbox/*이제 기존 리소스를 참조하는 새 애플리케이션이 구축되었지만 아직 트래픽을 수신하지 않습니다.
-
전체 통합 테스트 수행: 새 애플리케이션이 예상대로 작동하는지 확인합니다. 쇼핑 목록 애플리케이션의 경우 웹사이트를 로드하고 로그인, 쇼핑 목록 생성/조회/수정/삭제가 가능한지 확인합니다.
-
새 애플리케이션에서 기존 리소스 참조 변경 사항을 되돌리되, 아직 배포하지 마세요.
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Cognito 사용자 풀의 경우:
packages/common/constructs/src/core/user-identity.ts this.userPool = this.createUserPool();this.userPool = UserPool.fromUserPoolId(this,'UserPool','<your-user-pool-id>',);이후 빌드 실행:
Terminal window pnpm nx run-many --target buildTerminal window yarn nx run-many --target buildTerminal window npx nx run-many --target buildTerminal window bunx nx run-many --target build -
새 애플리케이션의
packages/infra
폴더에서cdk import
를 사용하여 가져올 리소스 확인New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --force엔터를 눌러 프롬프트를 진행합니다. 리소스가 다른 스택에서 관리되므로 가져오기가 실패합니다(예상된 동작). 이 단계는 가져올 리소스를 확인하기 위한 것입니다. 다음과 같은 출력이 표시됩니다:
Terminal window shopping-list-infra-sandbox/Application/ApplicationUserIdentity/UserPool/smsRole/Resource (AWS::IAM::Role): RoleName 입력(건너뛰려면 공백)shopping-list-infra-sandbox/Application/ApplicationUserIdentity/UserPool/Resource (AWS::Cognito::UserPool): UserPoolId 입력(건너뛰려면 공백)shopping-list-infra-sandbox/Application/Database/ShoppingList/Resource (AWS::DynamoDB::Table): TableName=shopping_list로 가져오기(y/n) y이는 실제로 3개의 리소스를 새 스택으로 가져와야 함을 나타냅니다.
-
이전 단계에서 발견된 리소스에 대해
RemovalPolicy
를RETAIN
으로 설정하도록 기존 PDK 프로젝트 업데이트. 현재 작성 시점에는 User Pool과 DynamoDB 테이블에 대해 기본값이지만 위에서 발견한 SMS Role에 대해 업데이트가 필요합니다:application-stack.ts const userIdentity = new UserIdentity(this, `${id}UserIdentity`, {userPool,});const smsRole = userIdentity.userPool.node.findAll().filter(c => CfnResource.isCfnResource(c) &&c.node.path.includes('/smsRole/'))[0] as CfnResource;smsRole.applyRemovalPolicy(RemovalPolicy.RETAIN); -
제거 정책이 적용되도록 PDK 프로젝트 배포
PDK Application cd packages/infranpx projen deploy -
CloudFormation 콘솔에서 위
cdk import
단계에서 요청된 값 기록- User Pool ID (예:
us-west-2_XXXXX
) - SMS Role 이름 (예:
infra-sandbox-UserIdentityUserPoolsmsRoleXXXXXX
)
- User Pool ID (예:
-
PDK 프로젝트가 리소스를 생성하는 대신 기존 리소스를 참조하도록 업데이트
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Cognito 사용자 풀의 경우:
application-stack.ts const userPool = UserPool.fromUserPoolId(this,'UserPool','<your-user-pool-id>',);const userIdentity = new UserIdentity(this, `${id}UserIdentity`, {// PDK 구성 요소는 IUserPool이 아닌 UserPool을 요구하지만 여전히 작동합니다!userPool: userPool as any,}); -
PDK 프로젝트 재배포: 이제 리소스가 PDK 프로젝트의 CloudFormation 스택에서 관리되지 않습니다.
PDK Application cd packages/infranpx projen deploy -
리소스가 관리 해제되었으므로 새 애플리케이션에서
cdk import
실행하여 실제 가져오기 수행:New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --force프롬프트에 값을 입력하면 가져오기가 성공적으로 완료됩니다.
-
기존 리소스에 대한 변경 사항(이제 새 스택에서 관리됨)이 적용되도록 새 애플리케이션 재배포:
Terminal window pnpm nx deploy infra shopping-list-infra-sandbox/*Terminal window yarn nx deploy infra shopping-list-infra-sandbox/*Terminal window npx nx deploy infra shopping-list-infra-sandbox/*Terminal window bunx nx deploy infra shopping-list-infra-sandbox/* -
새 애플리케이션에 대한 전체 테스트 재수행
-
DNS 레코드를 업데이트하여 새 웹사이트(및 필요한 경우 API)를 가리키도록 설정
Route53 가중치 기반 라우팅을 사용하여 점진적으로 트래픽을 전환할 것을 권장합니다. 처음에는 일부 요청만 새 애플리케이션으로 전달하고, 지표를 모니터링하면서 점차 비중을 높여 최종적으로 모든 트래픽이 새 애플리케이션으로 전환되도록 합니다.
DNS가 없고 웹사이트/API에 자동 생성 도메인을 사용한 경우 CloudFront HTTP 오리진이나 API Gateway HTTP 통합을 통해 요청을 프록시할 수 있습니다.
-
PDK 애플리케이션 지표를 모니터링하여 트래픽이 없는지 확인한 후 최종적으로 기존 CloudFormation 스택 삭제:
Terminal window cd packages/infranpx projen destroy
상당히 복잡한 과정이었지만, 사용자를 새 애플리케이션으로 원활하게 마이그레이션하는 데 성공했습니다! 🎉🎉🎉
이제 PDK 대비 Nx Plugin for AWS의 새로운 이점을 누릴 수 있습니다:
- 더 빠른 빌드
- 로컬 API 개발 지원
- 개발자 친화적인 코드베이스 (MCP 서버 사용해 보기!)
- 더 직관적인 타입 세이프 클라이언트/서버 코드
- 기타 다양한 개선 사항!
자주 묻는 질문
섹션 제목: “자주 묻는 질문”이 섹션은 위 예시 마이그레이션에서 다루지 않은 PDK 기능에 대한 지침을 제공합니다.
일반적으로 PDK에서 전환할 때는 PDK Monorepo와 유사한 Nx 워크스페이스로 프로젝트를 시작하는 것을 권장합니다. 또한 새로운 유형을 구축할 때는 우리의 제너레이터를 기본 요소로 사용하는 것을 추천합니다.
npx create-nx-workspace@21.4.1 my-project --pm=pnpm --preset=@aws/nx-plugin --ci=skip
npx create-nx-workspace@21.4.1 my-project --pm=yarn --preset=@aws/nx-plugin --ci=skip
npx create-nx-workspace@21.4.1 my-project --pm=npm --preset=@aws/nx-plugin --ci=skip
npx create-nx-workspace@21.4.1 my-project --pm=bun --preset=@aws/nx-plugin --ci=skip
CDK 그래프
섹션 제목: “CDK 그래프”CDK Graph는 연결된 CDK 리소스의 그래프를 구축하며 두 가지 플러그인을 제공합니다:
다이어그램 플러그인
섹션 제목: “다이어그램 플러그인”CDK Graph Diagram Plugin은 CDK 인프라에서 AWS 아키텍처 다이어그램을 생성합니다.
유사한 결정론적 접근 방식으로 CDK-Dia를 대안으로 고려해 볼 수 있습니다.
생성형 AI의 발전으로 많은 기초 모델들이 CDK 인프라에서 고품질 다이어그램 생성이 가능해졌습니다. AWS Diagram MCP Server 사용을 권장하며, 이 블로그 게시물에서 단계별 안내를 확인할 수 있습니다.
Threat Composer 플러그인
섹션 제목: “Threat Composer 플러그인”CDK Graph Threat Composer Plugin은 CDK 코드에서 Threat Composer 위협 모델의 초기 버전을 생성합니다.
이 플러그인은 예시 위협들을 포함한 기본 위협 모델을 필터링하여 스택에서 사용하는 리소스 기반으로 필터링하는 방식으로 동작합니다.
특정 예시 위협들을 활용하려면 기본 위협 모델을 복사/필터링하거나, 기초 모델이 유사한 모델을 생성할 때 컨텍스트로 사용할 수 있습니다.
AWS Arch
섹션 제목: “AWS Arch”AWS Arch는 CDK Graph용 CloudFormation 리소스와 관련 아키텍처 아이콘 간 매핑을 제공합니다.
아이콘 관련 리소스는 AWS Architecture Icons 페이지를 참조하세요. Diagrams도 코드로 다이어그램을 구축하는 방법을 제공합니다.
직접 사용하시는 경우 프로젝트를 포크(fork)하여 소유권을 가져가는 것을 고려해 보세요!
파이프라인
섹션 제목: “파이프라인”PDK는 PDKPipelineProject
를 제공하여 CDK 인프라 프로젝트를 설정하고 CDK Pipelines 리소스를 래핑한 CDK 구문을 활용했습니다.
이것에서 마이그레이션하려면 CDK Pipelines 구문을 직접 사용할 수 있습니다. 그러나 실제로는 CDK Stages를 정의하고 적절한 스테이지에 대한 deploy 명령을 직접 실행하는 GitHub actions나 GitLab CI/CD와 같은 것을 사용하는 것이 더 간편할 수 있습니다.
PDK Nag
섹션 제목: “PDK Nag”PDK Nag는 CDK Nag을 래핑하며 프로토타입 구축에 특화된 규칙 집합을 제공합니다.
PDK Nag에서 마이그레이션하려면 CDK Nag을 직접 사용하세요. 동일한 규칙 집합이 필요한 경우 여기 문서를 참고하여 자체 “팩”을 생성할 수 있습니다.
Type Safe API
섹션 제목: “Type Safe API”위의 예시 마이그레이션에서는 Type Safe API에서 가장 일반적으로 사용되는 컴포넌트들을 다루었지만, 아래에 다른 기능들의 마이그레이션 세부 사항을 안내합니다.
OpenAPI로 모델링된 API
섹션 제목: “OpenAPI로 모델링된 API”Nx Plugin for AWS는 Smithy로 모델링된 API는 지원하지만 OpenAPI로 직접 모델링된 API는 지원하지 않습니다. ts#smithy-api
생성기는 수정 가능한 좋은 시작점입니다. Smithy 대신 OpenAPI 스펙을 model
프로젝트의 src
폴더에 정의하고, 클라이언트/서버용 원하는 코드 생성 도구가 NPM에 없는 경우 build.Dockerfile
을 수정하여 사용할 수 있습니다. 원하는 도구가 NPM에 있다면 Nx 워크스페이스에 개발 의존성으로 설치하고 Nx 빌드 대상에서 직접 호출할 수 있습니다.
백엔드
섹션 제목: “백엔드”OpenAPI로 모델링된 타입 세이프 백엔드의 경우 OpenAPI Generator 서버 생성기 중 하나를 고려할 수 있습니다. 이들은 AWS Lambda에 직접 생성되지는 않지만 AWS Lambda Web Adapter를 사용하여 많은 경우에 간극을 메울 수 있습니다.
클라이언트
섹션 제목: “클라이언트”TypeScript 클라이언트의 경우 ts#react-website
생성기와 api-connection
생성기를 ts#smithy-api
예시와 함께 사용하여 클라이언트 생성 및 웹사이트 통합 방식을 확인할 수 있습니다. 이는 open-api#ts-client
또는 open-api#ts-hooks
생성기를 호출하여 클라이언트를 생성하는 빌드 대상을 구성합니다. OpenAPI 스펙을 지정하여 이러한 생성기를 직접 사용할 수 있습니다.
다른 언어의 경우 OpenAPI Generator의 생성기 중 필요한 것이 있는지 확인할 수 있습니다.
ts#nx-generator
생성기를 사용하여 맞춤형 생성기를 구축할 수도 있습니다. OpenAPI에서 코드를 생성하는 방법에 대한 자세한 내용은 해당 생성기의 문서를 참조하세요. Nx Plugin for AWS의 템플릿을 시작점으로 사용할 수 있으며, PDK 코드베이스의 템플릿에서도 영감을 얻을 수 있습니다(Nx Plugin for AWS와 데이터 구조가 약간 다름에 유의).
TypeSpec으로 모델링된 API
섹션 제목: “TypeSpec으로 모델링된 API”TypeSpec의 경우 위의 OpenAPI 섹션이 동일하게 적용됩니다. ts#smithy-api
를 생성한 후 Nx 워크스페이스에 TypeSpec 컴파일러와 OpenAPI 패키지를 설치하고, 모델 프로젝트의 compile
대상을 tsp compile
을 실행하도록 업데이트하여 dist
디렉토리에 OpenAPI 스펙을 출력하도록 할 수 있습니다.
백엔드
섹션 제목: “백엔드”TypeSpec 모델에서 직접 작동하는 TypeSpec HTTP Server generator for JavaScript를 사용하는 것이 권장됩니다.
생성된 서버를 AWS Lambda에서 실행하기 위해 AWS Lambda Web Adapter를 사용할 수 있습니다.
또는 위의 OpenAPI 옵션 중 하나를 사용할 수도 있습니다.
클라이언트
섹션 제목: “클라이언트”TypeSpec은 Type Safe API가 지원하는 세 언어 모두에 대한 자체 코드 생성기를 보유하고 있습니다:
TypeSpec이 OpenAPI로 컴파일될 수 있으므로 위의 OpenAPI 섹션도 적용됩니다.
Smithy로 모델링된 API
섹션 제목: “Smithy로 모델링된 API”위 예시 마이그레이션에서는 ts#smithy-api
생성기 사용으로의 마이그레이션을 설명했습니다. 이 섹션에서는 Python 및 Java 백엔드와 클라이언트에 대한 옵션을 다룹니다.
백엔드
섹션 제목: “백엔드”Smithy Java 코드 생성기는 Java 서버 생성기와 생성된 Java 서버를 AWS Lambda에서 실행하기 위한 어댑터를 포함합니다.
Smithy는 Python용 서버 생성기가 없으므로 OpenAPI를 경유해야 합니다. 잠재적 옵션은 위의 OpenAPI로 모델링된 API 섹션을 참조하세요.
클라이언트
섹션 제목: “클라이언트”Smithy Java 코드 생성기는 Java 클라이언트 생성기를 포함합니다.
Python 클라이언트는 Smithy Python을 확인할 수 있습니다.
TypeScript의 경우 Smithy TypeScript를 사용하거나, OpenAPI 경유 방식을 선택할 수 있습니다(ts#smithy-api
에서는 TanStack Query 훅을 통해 tRPC, FastAPI 및 Smithy API 간 일관성을 위해 이 방식을 선택함).
Smithy Shape 라이브러리
섹션 제목: “Smithy Shape 라이브러리”Type Safe API는 여러 Smithy 기반 API에서 재사용 가능한 Smithy 모델을 포함하는 프로젝트를 구성하는 SmithyShapeLibraryProject
Projen 프로젝트 유형을 제공했습니다.
이를 구현하는 가장 직관적인 방법은 다음과 같습니다:
Shape 라이브러리 생성
섹션 제목: “Shape 라이브러리 생성”-
smithy#project
생성기로 shape 라이브러리 생성:- 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
- VSCode에서 Nx 콘솔 열기
- 클릭
Generate (UI)
"Common Nx Commands" 섹션에서 - 검색
@aws/nx-plugin - smithy#project
- 필수 매개변수 입력
- 클릭
Generate
Terminal window pnpm nx g @aws/nx-plugin:smithy#projectTerminal window yarn nx g @aws/nx-plugin:smithy#projectTerminal window npx nx g @aws/nx-plugin:smithy#projectTerminal window bunx nx g @aws/nx-plugin:smithy#project어떤 파일이 변경될지 확인하기 위해 드라이 런을 수행할 수도 있습니다
Terminal window pnpm nx g @aws/nx-plugin:smithy#project --dry-runTerminal window yarn nx g @aws/nx-plugin:smithy#project --dry-runTerminal window npx nx g @aws/nx-plugin:smithy#project --dry-runTerminal window bunx nx g @aws/nx-plugin:smithy#project --dry-runserviceName
옵션에 임의의 이름 지정(나중에service
shape 제거 예정). -
src
의 기본 모델을 원하는 shapes로 교체 -
smithy-build.json
에서plugins
및 사용하지 않는 Maven 의존성 제거 -
build.Dockerfile
을 최소 빌드 단계로 교체:build.Dockerfile FROM public.ecr.aws/docker/library/node:24 AS builder# 출력 디렉토리RUN mkdir /out# Smithy CLI 설치# https://smithy.io/2.0/guides/smithy-cli/cli_installation.htmlWORKDIR /smithyARG TARGETPLATFORMRUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then ARCH="aarch64"; else ARCH="x86_64"; fi && \mkdir -p smithy-install/smithy && \curl -L https://github.com/smithy-lang/smithy/releases/download/1.61.0/smithy-cli-linux-$ARCH.zip -o smithy-install/smithy-cli-linux-$ARCH.zip && \unzip -qo smithy-install/smithy-cli-linux-$ARCH.zip -d smithy-install && \mv smithy-install/smithy-cli-linux-$ARCH/* smithy-install/smithyRUN smithy-install/smithy/install# 프로젝트 파일 복사COPY smithy-build.json .COPY src src# Maven 캐시 마운트로 Smithy 빌드RUN --mount=type=cache,target=/root/.m2/repository,id=maven-cache \smithy buildRUN cp -r build/* /out/# /out 디렉토리 익스포트FROM scratch AS exportCOPY --from=builder /out /
Shape 라이브러리 사용
섹션 제목: “Shape 라이브러리 사용”서비스 모델 프로젝트에서 다음 변경을 수행하여 shape 라이브러리 사용:
-
project.json
의compile
대상에 워크스페이스를 빌드 컨텍스트로 추가하고 shape 라이브러리의build
대상에 의존성 추가project.json {"cache": true,"outputs": ["{workspaceRoot}/dist/{projectRoot}/build"],"executor": "nx:run-commands","options": {"commands": ["rimraf dist/packages/api/model/build","make-dir dist/packages/api/model/build","docker build --build-context workspace=. -f packages/api/model/build.Dockerfile --target export --output type=local,dest=dist/packages/api/model/build packages/api/model"],"parallel": false,"cwd": "{workspaceRoot}"},"dependsOn": ["@my-project/shapes:build"]} -
build.Dockerfile
을 수정하여 shape 라이브러리의src
디렉토리 복사(shape 라이브러리가packages/shapes
에 위치한다고 가정):build.Dockerfile # 프로젝트 파일 복사COPY smithy-build.json .COPY src srcCOPY --from=workspace packages/shapes/src shapes -
smithy-build.json
에 shapes 디렉토리를sources
에 추가:smithy-build.json {"version": "1.0","sources": ["src/", "shapes/"],"plugins": {...}
인터셉터
섹션 제목: “인터셉터”Type Safe API는 다음 기본 인터셉터를 제공했습니다:
- Powertools for AWS Lambda를 사용한 로깅, 추적, 메트릭 인터셉터
- 처리되지 않은 예외를 다루는 try-catch 인터셉터
- CORS 헤더 반환을 위한 CORS 인터셉터
ts#smithy-api
생성기는 Middy를 사용하여 Powertools for AWS Lambda로 로깅, 추적, 메트릭을 계측합니다. try-catch 인터셉터 동작은 Smithy TypeScript SSDK에 내장되어 있으며, CORS 헤더는 handler.ts
에 추가됩니다.
모든 언어에서 로깅, 추적, 메트릭 인터셉터를 위해 Powertools for AWS Lambda를 직접 사용하세요.
맞춤형 인터셉터 마이그레이션을 위해 다음 라이브러리 사용을 권장합니다:
- TypeScript - Middy
- Python - Powertools for AWS Lambda Middleware Factory
- Java - aws-lambda-java-libs로 간단한 접근 또는 AspectJ를 고려
문서 생성
섹션 제목: “문서 생성”Type Safe API는 Redocly CLI를 사용한 문서 생성을 제공했습니다. 위에서 마이그레이션한 프로젝트에 쉽게 추가할 수 있습니다.
-
Redocly CLI 설치
Terminal window pnpm add -Dw @redocly/cliTerminal window yarn add -D @redocly/cliTerminal window npm install --legacy-peer-deps -D @redocly/cliTerminal window bun install -D @redocly/cli -
redocly build-docs
를 사용하여model
프로젝트에 문서 생성 대상 추가:model/project.json {..."documentation": {"cache": true,"outputs": ["{workspaceRoot}/dist/{projectRoot}/documentation"],"executor": "nx:run-commands","options": {"command": "redocly build-docs dist/packages/api/model/build/openapi/openapi.json --output=dist/packages/api/model/documentation/index.html","cwd": "{workspaceRoot}"},"dependsOn": ["compile"]}}
OpenAPI Generator 문서 생성기도 고려할 수 있습니다.
모의 통합(Mock Integrations)
섹션 제목: “모의 통합(Mock Integrations)”Type Safe API는 생성된 인프라 패키지 내에 모의 구현을 생성했습니다.
JSON 스키마 기반 모의 데이터 생성에 JSON Schema Faker로 전환할 수 있습니다. 이는 OpenAPI 스펙에서 직접 작동하며 CLI를 model
프로젝트 빌드의 일부로 실행할 수 있습니다.
생성된 metadata.gen.ts
를 기반으로 JSON Schema Faker의 출력 JSON 파일을 읽고 적절한 API Gateway MockIntegration
을 반환하도록 CDK 인프라를 업데이트할 수 있습니다(ts#smithy-api
생성기 사용 가정).
혼합 언어 백엔드
섹션 제목: “혼합 언어 백엔드”Type Safe API는 백엔드에 다양한 언어를 혼합하여 구현하는 것을 지원했습니다. CDK에서 API 구성을 인스턴스화할 때 통합에 “오버라이드”를 제공하여 동일하게 구현할 수 있습니다:
const pythonLambdaHandler = new Function(this, 'PythonImplementation', { runtime: Runtime.PYTHON_3_12, ...});
new MyApi(this, 'MyApi', { integrations: Api.defaultIntegrations(this) .withOverrides({ echo: { integration: new LambdaIntegration(pythonLambdaHandler), handler: pythonLambdaHandler, }, }) .build(),});
ts#smithy-api
및 TypeScript 서버 SDK를 사용하는 경우 서비스/라우터에 “스텁”을 생성해야 합니다:
export const Service: ApiService<ServiceContext> = { ... Echo: () => { throw new Error(`Not Implemented`); },};
입력 검증
섹션 제목: “입력 검증”Type Safe API는 SpecRestApi
를 사용하여 OpenAPI 스펙 기반의 기본 API Gateway 요청 본문 검증을 추가했습니다.
ts#smithy-api
생성기를 사용하면 검증은 서버 SDK 자체에서 수행됩니다. 대부분의 서버 생성기에서 동일합니다.
네이티브 API Gateway 검증을 구현하려면 packages/common/constructs/src/core/api/rest-api.ts
를 수정하여 OpenAPI 스펙에서 각 작업의 요청 본문에 대한 JSON 스키마를 읽도록 할 수 있습니다.
WebSocket API
섹션 제목: “WebSocket API”Type Safe API의 모델 기반 API 개발을 사용한 API Gateway 및 Lambda 기반 WebSocket API에 대한 직관적인 마이그레이션 경로는 아쉽게도 없습니다. 하지만 이 섹션에서는 몇 가지 아이디어를 제공합니다.
비동기 API 처리에 특화된 AsyncAPI를 OpenAPI나 TypeSpec 대신 사용해 볼 수 있습니다. AsyncAPI NodeJS 템플릿은 ECS에서 호스팅할 수 있는 Node WebSocket 백엔드를 생성할 수 있습니다.
인프라에 AppSync 이벤트와 Powertools를 고려해 볼 수 있습니다. 이 블로그 포스트가 도움이 될 수 있습니다!
AppSync의 WebSocket을 통한 GraphQL API 사용도 옵션입니다. 이에 대한 GitHub 이슈에 관심을 표시할 수 있으며, AppSync 개발자 가이드에서 샘플 프로젝트 링크를 확인하세요.
Type Safe API와 동일한 벤더 확장을 해석하는 맞춤형 코드 생성기를 구축할 수도 있습니다. OpenAPI 기반 코드 생성기 구축에 대한 자세한 내용은 OpenAPI로 모델링된 API 섹션을 참조하세요. API Gateway WebSocket API Lambda 핸들러용 템플릿은 여기에서, 클라이언트는 여기에서 확인할 수 있습니다.
ts#trpc-api
생성기로 마이그레이션하여 tRPC를 사용할 수도 있습니다. 현재 구독/스트리밍 지원은 없지만 필요 시 GitHub 이슈에 관심을 표시해 주세요.
Smithy는 프로토콜 독립적이지만 아직 WebSocket 프로토콜을 지원하지 않습니다. 관련 GitHub 이슈를 참조하세요.
Python 또는 Java 인프라
섹션 제목: “Python 또는 Java 인프라”현재 PDK에서 지원하는 Python 및 Java로 작성된 CDK 인프라는 Nx Plugin for AWS에서 아직 지원하지 않습니다.
권장하는 접근 방식은 CDK 인프라를 TypeScript로 마이그레이션하거나, 저희 제너레이터를 사용하여 공통 구문 패키지를 원하는 언어로 마이그레이션하는 것입니다. 예를 들어 Amazon Q CLI와 같은 Generative AI를 사용하여 이러한 마이그레이션을 가속화할 수 있습니다. AI 에이전트를 통해 CloudFormation 템플릿이 동일하게 생성될 때까지 마이그레이션을 반복 수행할 수 있습니다.
이 내용은 Python 또는 Java로 생성된 Type Safe API의 인프라에도 동일하게 적용됩니다. 공통 구문 패키지의 일반적인 rest-api.ts
구문을 변환하고 대상 언어에 맞는 간단한 메타데이터 생성기를 직접 구현할 수 있습니다(OpenAPI로 모델링된 API 섹션 참조).
CDK 코드를 추가할 기본 Python 프로젝트에는 py#project
제너레이터를 사용할 수 있습니다(cdk.json
파일을 이동하고 관련 타겟을 추가). Java 프로젝트의 경우 Nx의 @nx/gradle
플러그인을, Maven 프로젝트에는 @jnxplus/nx-maven
을 사용할 수 있습니다.
Projen 사용
섹션 제목: “Projen 사용”PDK는 Projen을 기반으로 구축되었습니다. Projen과 Nx Generators는 근본적인 차이점을 가지고 있어 기술적으로 결합이 가능하지만 이는 일반적으로 안티패턴으로 간주됩니다. Projen은 프로젝트 파일을 코드로 관리하여 직접 수정할 수 없도록 하는 반면, Nx generators는 프로젝트 파일을 한 번 제공한 후 자유롭게 코드를 수정할 수 있도록 합니다.
Projen을 계속 사용하려는 경우 원하는 Projen 프로젝트 유형을 직접 구현할 수 있습니다. Nx Plugin for AWS의 패턴을 따르려면 저희 제네레이터를 실행하거나 GitHub에서 소스 코드를 확인하여 원하는 프로젝트 유형이 어떻게 구성되는지 파악한 후, Projen의 기본 요소를 사용하여 관련 부분을 구현하면 됩니다.