Node.js Lambda 함수 핸들러 정의하기
Node.js Lambda 함수 핸들러 정의하기 (Define Lambda function handler in Node.js)
Lambda 함수 핸들러(handler)는 함수 코드에서 이벤트를 처리하는 메서드예요. 함수가 호출되면 Lambda가 이 핸들러 메서드를 실행하고, 핸들러가 응답을 반환하거나 종료하거나 타임아웃될 때까지 함수가 실행돼요. 이 페이지에서는 프로젝트 설정, 명명 규칙, 모범 사례를 포함해 Node.js에서 Lambda 함수 핸들러를 다루는 방법을 설명하고, 주문 정보를 받아 텍스트 영수증 파일을 만들어 Amazon Simple Storage Service(Amazon S3) 버킷에 넣는 Node.js Lambda 함수 예제도 함께 보여줘요.
본문
Node.js 핸들러 프로젝트 설정하기
Node.js Lambda 프로젝트를 초기화하는 방법은 여러 가지예요. 예를 들어 npm으로 표준 Node.js 프로젝트를 만들거나, AWS SAM 애플리케이션, AWS CDK 애플리케이션을 만들 수 있어요.
npm으로 프로젝트를 만들려면 다음 명령을 실행해요.
npm init
이 명령은 프로젝트를 초기화하고, 프로젝트의 메타데이터와 의존성을 관리하는 package.json 파일을 생성해요.
함수 코드는 .js 또는 .mjs JavaScript 파일에 있어요. 다음 예시에서는 ES 모듈 핸들러를 사용하므로 파일 이름을 index.mjs로 지었어요. Lambda는 ES 모듈과 CommonJS 핸들러를 모두 지원해요. 일반적인 Node.js Lambda 함수 프로젝트 구조는 다음과 같아요.
/project-root
├── index.mjs — 메인 핸들러 포함
├── package.json — 프로젝트 메타데이터와 의존성
├── package-lock.json — 의존성 잠금 파일
└── node_modules/ — 설치된 의존성
Node.js Lambda 함수 코드 예제
다음 예제 Lambda 함수는 주문 정보를 받아 텍스트 파일 영수증을 만들고 그 파일을 Amazon S3 버킷에 넣어요.
예제 index.mjs Lambda 함수
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
// Initialize the S3 client outside the handler for reuse
const s3Client = new S3Client();
/**
* Lambda handler for processing orders and storing receipts in S3.
* @param {Object} event - Input event containing order details
* @param {string} event.order_id - The unique identifier for the order
* @param {number} event.amount - The order amount
* @param {string} event.item - The item purchased
* @returns {Promise<string>} Success message
*/
export const handler = async(event) => {
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.message}`);
throw error;
}
};
/**
* Helper function to upload receipt to S3
* @param {string} bucketName - The S3 bucket name
* @param {string} key - The S3 object key
* @param {string} receiptContent - The content to upload
* @returns {Promise<void>}
*/
async function uploadReceiptToS3(bucketName, key, receiptContent) {
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.message}`);
}
}
이 index.mjs 파일은 다음 코드 섹션으로 구성돼요.
- import 블록: Lambda 함수가 필요로 하는 라이브러리(AWS SDK 클라이언트 등)를 포함해요.
- const s3Client 선언: 핸들러 함수 밖에서 Amazon S3 클라이언트를 초기화해요. Lambda는 초기화 단계에서 이 코드를 실행하며, 클라이언트는 여러 호출에 걸쳐 재사용하기 위해 보존돼요.
- JSDoc 주석 블록: JSDoc 주석으로 핸들러의 입력·출력 타입을 정의해요.
- export const handler: Lambda가 호출하는 메인 핸들러 함수예요. 배포할 때 Handler 속성에
index.handler를 지정해요. Handler 속성의 값은 파일 이름과 내보낸 핸들러 메서드 이름을 점(.)으로 구분한 것이에요. - uploadReceiptToS3 함수: 메인 핸들러가 참조하는 헬퍼 함수예요.
이 함수가 제대로 작동하려면 실행 역할이 s3:PutObject 액션을 허용해야 하고, RECEIPT_BUCKET 환경 변수를 정의해야 해요. 호출이 성공하면 Amazon S3 버킷에 영수증 파일이 들어 있어야 해요.
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 모듈로 취급할지 자동으로 감지하도록 설정할 수 있어요.
다음 예시는 ES 모듈과 CommonJS 모듈로 각각 작성한 함수 핸들러예요. 이 페이지의 나머지 예시는 모두 ES 모듈을 사용해요.
예제 — ES 모듈 핸들러
const url = "https://aws.amazon.com/";
export const handler = async(event) => {
try {
const res = await fetch(url);
console.info("status", res.status);
return res.status;
}
catch (e) {
console.error(e);
return 500;
}
};
예제 — CommonJS 모듈 핸들러
const https = require("https");
let url = "https://aws.amazon.com/";
exports.handler = async function (event) {
let statusCode;
await new Promise(function (resolve, reject) {
https.get(url, (res) => {
statusCode = res.statusCode;
resolve(statusCode);
}).on("error", (e) => {
reject(Error(e));
});
});
console.log(statusCode);
return statusCode;
};
Node.js 초기화
Node.js는 이벤트 루프를 사용해 효율적인 비동기 작업을 지원하는 논블로킹 I/O 모델을 사용해요. 예를 들어 Node.js가 네트워크 호출을 하면 함수는 네트워크 응답을 기다리며 차단되지 않고 다른 작업을 계속 처리해요. 네트워크 응답이 도착하면 콜백 큐에 들어가고, 현재 작업이 끝나면 큐의 작업이 처리돼요.
Lambda는 실행 환경 초기화 중에 시작된 비동기 작업이 초기화 중에 완료되도록 top-level await 사용을 권장해요. 초기화 중에 완료되지 않은 비동기 작업은 보통 첫 번째 함수 호출 중에 실행되는데, 이는 예기치 않은 동작이나 오류를 일으킬 수 있어요.
예를 들어 함수 초기화가 AWS Parameter Store에서 파라미터를 가져오는 네트워크 호출을 한다면, 이 작업이 초기화 중에 완료되지 않으면 호출 중에 값이 null일 수 있어요. 초기화와 호출 사이에 지연이 생겨 시간에 민감한 작업에서 오류가 발생할 수도 있어요. 특히 AWS 서비스 호출은 시간에 민감한 요청 서명에 의존하므로 초기화 단계에서 완료되지 않으면 서비스 호출이 실패할 수 있어요.
초기화 중에 작업을 완료하면 일반적으로 콜드 스타트 성능과, Provisioned Concurrency 사용 시 첫 번째 호출 성능이 개선돼요.
핸들러 명명 규칙
함수를 구성할 때 Handler 설정의 값은 파일 이름과 내보낸 핸들러 메서드 이름을 점으로 구분한 것이에요. 콘솔과 이 가이드의 예시에서 생성한 함수의 기본값은 index.handler로, index.js 또는 index.mjs 파일에서 내보낸 핸들러 메서드를 가리켜요.
콘솔에서 다른 파일 이름이나 함수 핸들러 이름으로 함수를 만들었다면 기본 핸들러 이름을 수정해야 해요.
함수 핸들러 이름 변경(콘솔)
- Lambda 콘솔의 Functions 페이지에서 함수를 선택해요.
- Code 탭을 선택해요.
- Runtime settings 창까지 스크롤하고 Edit을 선택해요.
- Handler에 함수 핸들러의 새 이름을 입력해요.
- Save를 선택해요.
입력 이벤트 객체 정의와 접근
JSON은 Lambda 함수에서 가장 흔하고 표준적인 입력 형식이에요. 다음 예시에서 함수는 다음과 같은 입력을 기대해요.
{
"order_id": "12345",
"amount": 199.99,
"item": "Wireless Headphones"
}
Node.js에서 Lambda 함수를 다룰 때 JSDoc 주석으로 입력 이벤트의 예상 구조를 정의할 수 있어요.
/**
* Lambda handler for processing orders and storing receipts in S3.
* @param {Object} event - Input event containing order details
* @param {string} event.order_id - The unique identifier for the order
* @param {number} event.amount - The order amount
* @param {string} event.item - The item purchased
* @returns {Promise<string>} Success message
*/
JSDoc 주석으로 타입을 정의하고 나면 코드에서 이벤트 객체의 필드에 직접 접근할 수 있어요. 예를 들어 event.order_id는 원래 입력에서 order_id 값을 가져와요.
Node.js 함수의 유효한 핸들러 패턴
콜백 대신 async/await로 함수 핸들러를 선언하는 것을 권장해요. async/await는 중첩 콜백이나 프로미스 체이닝 없이 비동기 코드를 간결하고 읽기 쉽게 작성하는 방법이에요. async/await를 쓰면 동기 코드처럼 읽히면서도 여전히 비동기이고 논블로킹인 코드를 작성할 수 있어요.
async 함수 핸들러(권장)
async 키워드는 함수를 비동기로 표시하고, await 키워드는 Promise가 해결될 때까지 함수 실행을 일시 중지해요. 핸들러는 다음 인수를 받아요.
- event: 함수에 전달된 입력 데이터를 포함해요.
- context: 호출, 함수, 실행 환경에 대한 정보를 포함해요.
async/await 패턴의 유효한 시그니처는 다음과 같아요.
export const handler = async (event) => { };
export const handler = async (event, context) => { };
동기 함수 핸들러
비동기 작업이 없는 함수라면 다음 시그니처 중 하나로 동기 함수 핸들러를 사용할 수 있어요.
export const handler = (event) => { };
export const handler = (event, context) => { };
응답 스트리밍 함수 핸들러
Lambda는 Node.js에서 응답 스트리밍을 지원해요. 응답 스트리밍 함수 핸들러는 awslambda.streamifyResponse() 데코레이터를 사용하며 event, responseStream, context 세 개의 파라미터를 받아요. 함수 시그니처는 다음과 같아요.
export const handler = awslambda.streamifyResponse(async (event, responseStream, context) => { });
콜백 기반 함수 핸들러
참고
콜백 기반 함수 핸들러는 Node.js 22까지만 지원돼요. Node.js 24부터는 비동기 작업을 async 함수 핸들러로 구현해야 해요.
콜백 기반 함수 핸들러는 event, context, callback 인수를 사용해야 해요. 예시:
export const handler = (event, context, callback) => { };
콜백 함수는 Error와 응답을 기대하며, 응답은 JSON으로 직렬화 가능해야 해요. 함수는 이벤트 루프가 비워지거나 타임아웃될 때까지 계속 실행돼요. 모든 이벤트 루프 작업이 끝나야 응답이 호출자에게 전송돼요. 함수가 타임아웃되면 대신 오류가 반환돼요. context.callbackWaitsForEmptyEventLoop를 false로 설정하면 런타임이 응답을 즉시 보내도록 구성할 수 있어요.
예제 — 콜백이 포함된 HTTP 요청
다음 예제 함수는 URL을 확인하고 상태 코드를 호출자에게 반환해요.
import https from "https";
let url = "https://aws.amazon.com/";
export const handler = (event, context, callback) => {
https.get(url, (res) => {
callback(null, res.statusCode);
}).on("error", (e) => {
callback(Error(e));
});
};
핸들러에서 JavaScript v3용 SDK 사용하기
Lambda 함수로 다른 AWS 리소스와 상호작용하거나 리소스를 업데이트하는 경우가 많죠. 이런 리소스와 인터페이스하는 가장 간단한 방법은 AWS SDK for JavaScript를 사용하는 것이에요. 지원되는 모든 Lambda Node.js 런타임에는 SDK for JavaScript 버전 3이 포함돼 있어요.
다만 필요한 AWS SDK 클라이언트를 배포 패키지에 포함할 것을 강력히 권장해요. 이렇게 하면 향후 Lambda 런타임 업데이트에서도 최대한의 하위 호환성을 유지할 수 있어요. 추가 패키지를 포함할 수 없는 경우(예: Lambda 콘솔 코드 편집기나 AWS CloudFormation 템플릿의 인라인 코드를 사용할 때)에만 런타임 제공 SDK에 의존하세요.
함수에 SDK 의존성을 추가하려면 필요한 특정 SDK 클라이언트에 대해 npm install 명령을 사용해요. 예시 코드에서는 Amazon S3 클라이언트를 사용했어요. package.json 파일이 있는 디렉토리에서 다음 명령을 실행해 의존성을 추가해요.
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로 어떤 환경 변수든 참조할 수 있어요. 이 예시에서는 정의한 RECEIPT_BUCKET 환경 변수를 다음 코드 줄로 참조해요.
// Access environment variables
const bucketName = process.env.RECEIPT_BUCKET;
if (!bucketName) {
throw new Error('RECEIPT_BUCKET environment variable is not set');
}
전역 상태 사용하기
Lambda는 함수를 처음 호출하기 전에 초기화 단계에서 정적 코드를 실행해요. 초기화 중에 만들어진 리소스는 호출 사이에 메모리에 남으므로, 함수를 호출할 때마다 만들 필요가 없어요. 예시 코드에서 S3 클라이언트 초기화 코드는 핸들러 밖에 있어요. 런타임은 함수가 첫 이벤트를 처리하기 전에 클라이언트를 초기화하고, 클라이언트는 모든 호출에 걸쳐 재사용할 수 있게 유지돼요.
Node.js Lambda 함수 코드 모범 사례
Lambda 함수를 빌드할 때 다음 지침을 따르세요.
- Lambda 핸들러를 핵심 로직과 분리하세요. 이렇게 하면 더 단위 테스트가 쉬운 함수를 만들 수 있어요.
- 함수 배포 패키지의 의존성을 제어하세요. AWS Lambda 실행 환경에는 여러 라이브러리가 포함돼 있어요. Node.js와 Python 런타임에는 AWS SDK가 포함돼요. 최신 기능과 보안 업데이트를 지원하기 위해 Lambda는 이 라이브러리를 주기적으로 업데이트해요. 이 업데이트로 Lambda 함수 동작에 미묘한 변화가 생길 수 있어요. 함수가 사용하는 의존성을 완전히 제어하려면 모든 의존성을 배포 패키지에 패키징하세요.
- 의존성의 복잡성을 최소화하세요. 실행 환경 시작 시 빠르게 로드되는 더 단순한 프레임워크를 선호해요.
- 배포 패키지 크기를 런타임 필수 요소로 최소화하세요. 이렇게 하면 호출 전에 배포 패키지를 다운로드·압축 해제하는 시간이 줄어들어요.
실행 환경 재사용을 활용해 함수 성능을 개선하세요. SDK 클라이언트와 데이터베이스 연결을 함수 핸들러 밖에서 초기화하고, 정적 자산을 /tmp 디렉토리에 로컬로 캐시하세요. 같은 인스턴스가 처리하는 이후 호출은 이 리소스를 재사용할 수 있어요. 함수 실행 시간을 줄여 비용을 절약해요.
호출 간 잠재적 데이터 누출을 피하기 위해 실행 환경에 사용자 데이터, 이벤트, 보안에 영향이 있는 기타 정보를 저장하지 마세요. 함수가 핸들러 내에서 메모리에 저장할 수 없는 변경 가능한 상태에 의존한다면, 사용자별로 별도의 함수나 함수 버전을 만드는 것을 고려해요.
Keep-alive 지시어를 사용해 영구 연결을 유지하세요. Lambda는 시간이 지나며 유휴 연결을 정리해요. 함수를 호출할 때 유휴 연결을 재사용하려고 하면 연결 오류가 발생해요. 영구 연결을 유지하려면 런타임에 연결된 keep-alive 지시어를 사용하세요.
환경 변수로 운영 파라미터를 함수에 전달하세요. 예를 들어 Amazon S3 버킷에 쓴다면 버킷 이름을 하드코딩하지 말고 환경 변수로 구성하세요.
Lambda 함수에서 재귀 호출을 피하세요. 함수가 자신을 호출하거나 함수를 다시 호출할 수 있는 프로세스를 시작하면 의도치 않은 호출량과 비용 증가로 이어질 수 있어요. 의도치 않은 호출량이 보이면 함수의 reserved concurrency를 즉시 0으로 설정해 함수에 대한 모든 호출을 제한하면서 코드를 업데이트하세요.
Lambda 함수 코드에서 문서화되지 않은 비공개 API를 사용하지 마세요. AWS Lambda 관리 런타임의 경우 Lambda는 내부 API에 보안·기능 업데이트를 주기적으로 적용해요. 이 내부 API 업데이트는 하위 호환되지 않을 수 있으며, 함수가 이런 비공개 API에 의존하면 호출 실패 같은 의도치 않은 결과가 생길 수 있어요.
멱등(idempotent) 코드를 작성하세요. 함수에 멱등 코드를 작성하면 중복 이벤트가 같은 방식으로 처리되게 보장할 수 있어요. 코드는 이벤트를 올바르게 검증하고 중복 이벤트를 우아하게 처리해야 해요.