TypeScript Lambda 함수 핸들러 정의하기
TypeScript Lambda 함수 핸들러 정의하기 (Define Lambda function handler in TypeScript)
Lambda 함수 핸들러(handler)는 함수 코드에서 이벤트를 처리하는 메서드예요. 함수가 호출되면 Lambda가 핸들러 메서드를 실행하고, 핸들러가 응답을 반환하거나 종료하거나 타임아웃될 때까지 함수가 실행돼요. 이 페이지는 프로젝트 설정, 명명 규칙, 모범 사례를 포함해 TypeScript에서 Lambda 함수 핸들러를 다루는 방법을 설명하고, 주문 정보를 받아 텍스트 파일 영수증을 만들어 Amazon Simple Storage Service(Amazon S3) 버킷에 넣는 TypeScript Lambda 함수 예제도 함께 보여줘요.
본문
TypeScript 프로젝트 설정
로컬 통합 개발 환경(IDE)이나 텍스트 편집기로 TypeScript 함수 코드를 작성하세요. Lambda 콘솔에서는 TypeScript 코드를 만들 수 없어요.
TypeScript Lambda 프로젝트를 초기화하는 방법은 여러 가지예요. 예를 들어 npm, AWS SAM 애플리케이션, AWS CDK 애플리케이션으로 프로젝트를 만들 수 있어요. npm으로 프로젝트를 만들려면:
npm init
함수 코드는 .ts 파일에 있고, 빌드 시점에 JavaScript 파일로 트랜스파일해요. esbuild나 Microsoft의 TypeScript 컴파일러(tsc)를 사용해 TypeScript 코드를 JavaScript로 트랜스파일할 수 있어요. esbuild를 사용하려면 개발 의존성으로 추가하세요.
npm install -D esbuild
일반적인 TypeScript Lambda 함수 프로젝트 구조는 다음과 같아요.
/project-root
├── index.ts — 메인 핸들러 포함
├── dist/ — 컴파일된 JavaScript 포함
├── package.json — 프로젝트 메타데이터와 의존성
├── package-lock.json — 의존성 잠금 파일
├── tsconfig.json — TypeScript 구성
└── node_modules/ — 설치된 의존성
TypeScript Lambda 함수 코드 예제
다음 예제 Lambda 함수는 주문 정보를 받아 텍스트 파일 영수증을 만들고 그 파일을 Amazon S3 버킷에 넣어요. 이 예제는 커스텀 이벤트 타입(OrderEvent)을 정의해요. AWS 이벤트 소스의 타입 정의 import 방법은 Lambda용 타입 정의를 참고하세요.
참고
이 예제는 ES 모듈 핸들러를 사용해요. Lambda는 ES 모듈과 CommonJS 핸들러를 모두 지원해요.
예제 index.ts Lambda 함수
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
// Initialize the S3 client outside the handler for reuse
const s3Client = new S3Client();
// Define the shape of the input event
type OrderEvent = {
order_id: string;
amount: number;
item: string;
}
/**
* Lambda handler for processing orders and storing receipts in S3.
*/
export const handler = async (event: OrderEvent): Promise<string> => {
try {
// Access environment variables
const bucketName = process.env.RECEIPT_BUCKET;
if (!bucketName) {
throw new Error('RECEIPT_BUCKET environment variable is not set');
}
// Create the receipt content and key destination
const receiptContent = `OrderID: ${event.order_id}\nAmount: $${event.amount.toFixed(2)}\nItem: ${event.item}`;
const key = `receipts/${event.order_id}.txt`;
// Upload the receipt to S3
await uploadReceiptToS3(bucketName, key, receiptContent);
console.log(`Successfully processed order ${event.order_id} and stored receipt in S3 bucket ${bucketName}`);
return 'Success';
} catch (error) {
console.error(`Failed to process order: ${error instanceof Error ? error.message : 'Unknown error'}`);
throw error;
}
};
/**
* Helper function to upload receipt to S3
*/
async function uploadReceiptToS3(bucketName: string, key: string, receiptContent: string): Promise<void> {
try {
const command = new PutObjectCommand({
Bucket: bucketName,
Key: key,
Body: receiptContent
});
await s3Client.send(command);
} catch (error) {
throw new Error(`Failed to upload receipt to S3: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
이 index.ts 파일은 다음 코드 섹션으로 구성돼요.
- import 블록: Lambda 함수가 필요로 하는 라이브러리(AWS SDK 클라이언트 등)를 포함해요.
- const s3Client 선언: 핸들러 함수 밖에서 Amazon S3 클라이언트를 초기화해요. Lambda는 초기화 단계에서 이 코드를 실행하며, 클라이언트는 여러 호출에 걸쳐 재사용하기 위해 보존돼요.
- type OrderEvent: 예상 입력 이벤트의 구조를 정의해요.
- export const handler: Lambda가 호출하는 메인 핸들러 함수예요. 배포할 때 Handler 속성에
index.handler를 지정해요. - uploadReceiptToS3 함수: 메인 핸들러가 참조하는 헬퍼 함수예요.
이 함수가 제대로 작동하려면 실행 역할이 s3:PutObject 액션을 허용해야 하고, RECEIPT_BUCKET 환경 변수를 정의해야 해요.
CommonJS와 ES 모듈
Node.js는 CommonJS와 ECMAScript 모듈(ES 모듈) 두 가지 모듈 시스템을 지원해요. Lambda는 실행 환경 초기화 중에 비동기 작업을 완료할 수 있는 top-level await를 지원하므로 ES 모듈 사용을 권장해요.
Node.js는 .cjs 확장자를 CommonJS 모듈로, .mjs 확장자를 ES 모듈로 취급해요. 기본적으로 .js 확장자는 CommonJS 모듈로 취급해요. 함수의 package.json에서 type을 module로 지정하면 .js 파일을 ES 모듈로 취급하도록 설정할 수 있어요. NODE_OPTIONS 환경 변수에 --experimental-detect-module 플래그를 추가하면 Lambda의 Node.js가 .js 파일을 CommonJS로 취급할지 ES 모듈로 취급할지 자동으로 감지하도록 설정할 수 있어요.
Node.js 초기화
Node.js는 이벤트 루프를 사용해 효율적인 비동기 작업을 지원하는 논블로킹 I/O 모델을 사용해요. Lambda는 실행 환경 초기화 중에 시작된 비동기 작업이 초기화 중에 완료되도록 top-level await 사용을 권장해요. 초기화 중에 완료되지 않은 비동기 작업은 보통 첫 번째 함수 호출 중에 실행되며, 예기치 않은 동작이나 오류를 일으킬 수 있어요.
예를 들어 함수 초기화가 AWS Parameter Store에서 파라미터를 가져오는 네트워크 호출을 한다면, 이 작업이 초기화 중에 완료되지 않으면 호출 중에 값이 null일 수 있어요. 초기화와 호출 사이에 지연이 생겨 시간에 민감한 작업에서 오류가 발생할 수도 있어요. 특히 AWS 서비스 호출은 시간에 민감한 요청 서명에 의존하므로 초기화 단계에서 완료되지 않으면 서비스 호출이 실패할 수 있어요.
초기화 중에 작업을 완료하면 일반적으로 콜드 스타트 성능과 Provisioned Concurrency 사용 시 첫 번째 호출 성능이 개선돼요.
핸들러 명명 규칙
함수를 구성할 때 Handler 설정의 값은 파일 이름과 내보낸 핸들러 메서드 이름을 점으로 구분한 것이에요. 콘솔과 이 가이드의 예시에서 생성한 함수의 기본값은 index.handler예요. 콘솔에서 다른 파일 이름이나 함수 핸들러 이름으로 함수를 만들었다면 기본 핸들러 이름을 수정해야 해요.
함수 핸들러 이름 변경(콘솔)
- Lambda 콘솔의 Functions 페이지를 열고 함수를 선택합니다.
- Code 탭을 선택합니다.
- Runtime settings 창까지 스크롤하고 Edit을 선택합니다.
- Handler에 함수 핸들러의 새 이름을 입력합니다.
- Save를 선택합니다.
입력 이벤트 객체 정의와 접근
JSON은 Lambda 함수에서 가장 흔하고 표준적인 입력 형식이에요. 이 예시에서 함수는 다음과 같은 입력을 기대해요.
{
"order_id": "12345",
"amount": 199.99,
"item": "Wireless Headphones"
}
TypeScript에서 Lambda 함수를 다룰 때 type이나 interface로 입력 이벤트의 구조를 정의할 수 있어요. 이 예시에서는 type으로 이벤트 구조를 정의해요.
type OrderEvent = {
order_id: string;
amount: number;
item: string;
}
type이나 interface를 정의한 후 핸들러 시그니처에서 사용해 타입 안전성을 보장해요.
export const handler = async (event: OrderEvent): Promise<string> => {
컴파일 중 TypeScript는 이벤트 객체가 올바른 타입의 필수 필드를 포함하는지 검증해요. 예를 들어 event.order_id를 숫자로 또는 event.amount를 문자열로 사용하려 하면 TypeScript 컴파일러가 오류를 보고해요.
TypeScript 함수의 유효한 핸들러 패턴
콜백 대신 async/await로 함수 핸들러를 선언하는 것을 권장해요. 이 섹션의 예시는 S3Event 타입을 사용해요. 하지만 @types/aws-lambda 패키지의 다른 AWS 이벤트 타입을 쓰거나 자체 이벤트 타입을 정의할 수 있어요.
@types/aws-lambda패키지를 개발 의존성으로 추가합니다.npm install -D @types/aws-lambda- 필요한 타입(
Context,S3Event,Callback등)을 import 합니다.
async 함수 핸들러(권장)
async 키워드는 함수를 비동기로 표시하고, await 키워드는 Promise가 해결될 때까지 함수 실행을 일시 중지해요. 핸들러는 event(함수로 전달된 입력 데이터)와 context(호출, 함수, 실행 환경에 대한 정보) 인수를 받아요.
export const handler = async (event: S3Event): Promise<void> => { };
export const handler = async (event: S3Event, context: Context): Promise<void> => { };
참고
항목 배열을 비동기로 처리할 때는 모든 작업이 완료되도록
Promise.all과 함께await를 사용하세요.forEach같은 메서드는 async 콜백이 완료될 때까지 기다리지 않아요.
동기 함수 핸들러
비동기 작업이 없는 함수라면 다음 시그니처 중 하나로 동기 함수 핸들러를 사용할 수 있어요.
export const handler = (event: S3Event): void => { };
export const handler = (event: S3Event, context: Context): void => { };
응답 스트리밍 함수 핸들러
Lambda는 Node.js에서 응답 스트리밍을 지원해요. 응답 스트리밍 함수 핸들러는 awslambda.streamifyResponse() 데코레이터를 사용하며 event, responseStream, context 세 개의 파라미터를 받아요.
export const handler = awslambda.streamifyResponse(async (event: APIGatewayProxyEvent, responseStream: NodeJS.WritableStream, context: Context) => { });
콜백 기반 함수 핸들러
참고
콜백 기반 함수 핸들러는 Node.js 22까지만 지원돼요. Node.js 24부터는 비동기 작업을 async 함수 핸들러로 구현해야 해요.
콜백 기반 함수 핸들러는 event, context, callback 인수를 사용할 수 있어요. 콜백 인수는 Error와 응답을 기대하며, 응답은 JSON으로 직렬화 가능해야 해요. 유효한 시그니처는 다음과 같아요.
export const handler = (event: S3Event, context: Context, callback: Callback<void>): void => { };
함수는 이벤트 루프가 비워지거나 타임아웃될 때까지 계속 실행돼요. 모든 이벤트 루프 작업이 끝나야 응답이 호출자에게 전송돼요. context.callbackWaitsForEmptyEventLoop를 false로 설정하면 런타임이 응답을 즉시 보내도록 구성할 수 있어요.
예제 — 콜백이 포함된 TypeScript 함수
다음 예시는 API Gateway 통합에 특화된 콜백 타입인 APIGatewayProxyCallback을 사용해요. 대부분의 AWS 이벤트 소스는 위 시그니처에 보인 일반 Callback 타입을 사용해요.
import { Context, APIGatewayProxyCallback, APIGatewayEvent } from 'aws-lambda';
export const lambdaHandler = (event: APIGatewayEvent, context: Context, callback: APIGatewayProxyCallback): void => {
console.log(`Event: ${JSON.stringify(event, null, 2)}`);
console.log(`Context: ${JSON.stringify(context, null, 2)}`);
callback(null, {
statusCode: 200,
body: JSON.stringify({
message: 'hello world',
}),
});
};
핸들러에서 JavaScript v3용 SDK 사용하기
Lambda 함수로 다른 AWS 리소스와 상호작용하거나 리소스를 업데이트하는 경우가 많죠. 이 리소스와 인터페이스하는 가장 간단한 방법은 AWS SDK for JavaScript를 사용하는 것이에요. 모든 지원되는 Lambda Node.js 런타임에는 SDK for JavaScript 버전 3이 포함돼 있어요. 다만 필요한 AWS SDK 클라이언트를 배포 패키지에 포함할 것을 강력히 권장해요. 이렇게 하면 향후 Lambda 런타임 업데이트에서도 최대한의 하위 호환성을 유지할 수 있어요.
함수에 SDK 의존성을 추가하려면 필요한 특정 SDK 클라이언트에 대해 npm install 명령을 사용해요.
npm install @aws-sdk/client-s3
함수 코드에서는 필요한 클라이언트와 명령을 import 해요.
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
그런 다음 Amazon S3 클라이언트를 초기화해요.
const s3Client = new S3Client();
함수를 호출할 때마다 초기화하지 않도록 Amazon S3 클라이언트를 메인 핸들러 함수 밖에서 초기화했어요. SDK 클라이언트를 초기화하면 그 AWS 서비스에 대한 API 호출에 사용할 수 있어요. 예시 코드는 Amazon S3 PutObject API 액션을 다음과 같이 호출해요.
const command = new PutObjectCommand({
Bucket: bucketName,
Key: key,
Body: receiptContent
});
환경 변수 접근하기
핸들러 코드에서 process.env로 어떤 환경 변수든 참조할 수 있어요.
// Access environment variables
const bucketName = process.env.RECEIPT_BUCKET;
if (!bucketName) {
throw new Error('RECEIPT_BUCKET environment variable is not set');
}
전역 상태 사용하기
Lambda는 함수를 처음 호출하기 전에 초기화 단계에서 정적 코드를 실행해요. 초기화 중에 만들어진 리소스는 호출 사이에 메모리에 남으므로, 함수를 호출할 때마다 만들 필요가 없어요. 예시 코드에서 S3 클라이언트 초기화 코드는 핸들러 밖에 있어요. 런타임은 함수가 첫 이벤트를 처리하기 전에 클라이언트를 초기화하고, 클라이언트는 모든 호출에 걸쳐 재사용할 수 있게 유지돼요.
TypeScript Lambda 함수 코드 모범 사례
- Lambda 핸들러를 핵심 로직과 분리하세요. 이렇게 하면 더 단위 테스트가 쉬운 함수를 만들 수 있어요.
- 함수 배포 패키지의 의존성을 제어하세요. AWS Lambda 실행 환경에는 여러 라이브러리가 포함돼 있어요. Node.js와 Python 런타임에는 AWS SDK가 포함돼요. Lambda는 이 라이브러리를 주기적으로 업데이트하며, 이 업데이트로 함수 동작에 미묘한 변화가 생길 수 있어요. 모든 의존성을 배포 패키지에 패키징하세요.
- 의존성의 복잡성을 최소화하세요. 실행 환경 시작 시 빠르게 로드되는 더 단순한 프레임워크를 선호해요.
- 배포 패키지 크기를 런타임 필수 요소로 최소화하세요. 호출 전에 배포 패키지를 다운로드·압축 해제하는 시간이 줄어들어요.
실행 환경 재사용을 활용해 함수 성능을 개선하세요. SDK 클라이언트와 데이터베이스 연결을 함수 핸들러 밖에서 초기화하고, 정적 자산을 /tmp 디렉토리에 로컬로 캐시하세요. 이후 호출은 이 리소스를 재사용할 수 있어요. 함수 실행 시간을 줄여 비용을 절약해요.
호출 간 잠재적 데이터 누출을 피하기 위해 실행 환경에 사용자 데이터, 이벤트, 보안에 영향이 있는 기타 정보를 저장하지 마세요.
Keep-alive 지시어를 사용해 영구 연결을 유지하세요. Lambda는 시간이 지나며 유휴 연결을 정리해요. 영구 연결을 유지하려면 런타임에 연결된 keep-alive 지시어를 사용하세요.
환경 변수로 운영 파라미터를 함수에 전달하세요. 예를 들어 Amazon S3 버킷에 쓴다면 버킷 이름을 하드코딩하지 말고 환경 변수로 구성하세요.
Lambda 함수에서 재귀 호출을 피하세요. 함수가 자신을 호출하면 의도치 않은 호출량과 비용 증가로 이어질 수 있어요. 의도치 않은 호출량이 보이면 함수의 reserved concurrency를 즉시 0으로 설정해 함수에 대한 모든 호출을 제한하면서 코드를 업데이트하세요.
Lambda 함수 코드에서 문서화되지 않은 비공개 API를 사용하지 마세요. Lambda 관리 런타임의 내부 API 업데이트는 하위 호환되지 않을 수 있어, 비공개 API에 의존하면 호출 실패 같은 의도치 않은 결과가 생길 수 있어요.
멱등(idempotent) 코드를 작성하세요. 중복 이벤트가 같은 방식으로 처리되게 보장할 수 있어요. 코드는 이벤트를 올바르게 검증하고 중복 이벤트를 우아하게 처리해야 해요.