노트 생성 API 추가하기
노트 생성 API 추가하기
백엔드 작업을 시작하기 위해 먼저 노트를 생성하는 API를 추가해 보겠습니다. 이 API는 노트 객체를 입력으로 받아 새로운 ID와 함께 데이터베이스에 저장합니다. 노트 객체는 content
필드(노트의 내용)와 attachment
필드(업로드된 파일의 URL)를 포함합니다.
함수 추가하기
첫 번째 함수를 추가해 보겠습니다.
프로젝트 루트에 create.js
파일을 생성하고 다음 내용을 추가합니다.
import * as uuid from "uuid";
import AWS from "aws-sdk";
const dynamoDb = new AWS.DynamoDB.DocumentClient();
export async function main(event, context) {
// 요청 본문은 'event.body'에 JSON 문자열로 전달됨
const data = JSON.parse(event.body);
const params = {
TableName: process.env.tableName,
Item: {
// 생성할 아이템의 속성
userId: "123", // 작성자 ID
noteId: uuid.v1(), // 고유한 uuid
content: data.content, // 요청 본문에서 파싱
attachment: data.attachment, // 요청 본문에서 파싱
createdAt: Date.now(), // 현재 유닉스 타임스탬프
},
};
// CORS(Cross-Origin Resource Sharing)를 활성화하기 위해 응답 헤더 설정
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": true,
};
try {
await dynamoDb.put(params).promise();
return {
statusCode: 200,
headers: headers,
body: JSON.stringify(params.Item),
};
} catch (e) {
return {
statusCode: 500,
headers: headers,
body: JSON.stringify({ error: e.message }),
};
}
}
코드에 몇 가지 유용한 주석이 있지만, 여기서는 몇 가지 간단한 작업을 수행합니다.
- AWS JS SDK는 Lambda 함수의 현재 리전을 기반으로 리전을 가정합니다. 따라서 DynamoDB 테이블이 다른 리전에 있다면, DynamoDB 클라이언트를 초기화하기 전에
AWS.config.update({ region: "my-region" });
를 호출하여 리전을 설정해야 합니다. event.body
에서 입력을 파싱합니다. 이는 HTTP 요청 본문을 나타냅니다.- 노트의 내용을 문자열로 포함하는
content
가 있습니다. attachment
도 포함될 수 있습니다. 이는 S3 버킷에 업로드된 파일의 파일명입니다.process.env.tableName
을 사용해 환경 변수에서 DynamoDB 테이블 이름을 읽어옵니다. 이는 아래serverless.yml
에서 설정할 예정입니다. 이렇게 하면 모든 함수에서 하드코딩하지 않아도 됩니다.userId
는 노트 작성자의 ID입니다. 현재는123
으로 하드코딩되어 있습니다. 나중에 인증된 사용자를 기반으로 이 값을 설정할 예정입니다.- DynamoDB에 새로운 객체를 생성하기 위해
noteId
와 현재 날짜를createdAt
으로 설정하여 호출합니다. - DynamoDB 호출이 실패하면 HTTP 상태 코드
500
과 함께 오류를 반환합니다.
API 엔드포인트 설정
이제 함수를 위한 API 엔드포인트를 정의해 보겠습니다.
serverless.yml
파일을 열고 다음 내용으로 교체하세요.
service: notes-api
# 함수를 위한 최적화된 패키지 생성
package:
individually: true
plugins:
- serverless-bundle # Webpack으로 함수 패키징
- serverless-offline
- serverless-dotenv-plugin # .env를 환경 변수로 로드
provider:
name: aws
runtime: nodejs12.x
stage: prod
region: us-east-1
# 이 환경 변수들은 함수에서 process.env로 접근 가능
environment:
tableName: notes
# 'iamRoleStatements'는 Lambda 함수의 권한 정책을 정의
# 이 경우 Lambda 함수는 DynamoDB에 접근할 수 있는 권한을 부여받음
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Scan
- dynamodb:Query
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:DescribeTable
Resource: "arn:aws:dynamodb:us-east-1:*:*"
functions:
# create.js의 메인 함수를 호출하는 HTTP API 엔드포인트 정의
# - path: URL 경로는 /notes
# - method: POST 요청
create:
handler: create.main
events:
- http:
path: notes
cors: true
method: post
여기서 새로 추가한 create 함수를 설정에 포함시켰습니다. 이 함수는 /notes
엔드포인트에서 post
요청을 처리합니다. 단일 HTTP 이벤트에 응답하기 위해 단일 Lambda 함수를 사용하는 이 패턴은 마이크로서비스 아키텍처와 매우 유사합니다. 이와 같은 패턴에 대해서는 Serverless Framework 프로젝트 구성 챕터에서 더 자세히 다룹니다.
environment:
블록을 통해 Lambda 함수에 사용할 환경 변수를 정의할 수 있습니다. 이 변수들은 Node.js의 process.env
변수를 통해 접근 가능합니다. 여기서는 process.env.tableName
을 사용해 DynamoDB 테이블 이름에 접근합니다.
iamRoleStatements
섹션은 Lambda 함수가 접근할 수 있는 리소스를 AWS에 알려줍니다. 이 경우 Lambda 함수가 DynamoDB에 대해 위에 나열된 작업을 수행할 수 있도록 설정했습니다. DynamoDB는 arn:aws:dynamodb:us-east-1:*:*
로 지정했는데, 이는 us-east-1
리전의 모든 DynamoDB 테이블을 가리킵니다. 나중에 가이드에서 테이블 이름을 구체적으로 지정할 예정입니다. 지금은 DynamoDB 테이블이 생성된 리전을 정확히 사용하는지 확인하세요. 이는 나중에 문제가 될 수 있는 일반적인 원인 중 하나입니다. 여기서는 리전이 us-east-1
입니다.
테스트
이제 새로운 API를 테스트할 준비가 되었습니다. 로컬에서 테스트하기 위해 입력 파라미터를 모킹할 것입니다.
프로젝트 루트에 mocks/
디렉토리를 생성합니다.
$ mkdir mocks
mocks/create-event.json
파일을 만들고 다음 내용을 추가합니다.
{
"body": "{\"content\":\"hello world\",\"attachment\":\"hello.jpg\"}"
}
여기서 body
는 함수에서 참조하는 event.body
에 해당합니다. JSON으로 인코딩된 문자열로 전달합니다. attachment
의 경우, 이미 업로드된 hello.jpg
파일이 있다고 가정합니다.
함수를 실행하려면 루트 디렉토리에서 다음 명령어를 실행합니다.
$ serverless invoke local --function create --path mocks/create-event.json
AWS SDK 자격 증명에 여러 프로필이 있는 경우, 명시적으로 하나를 선택해야 합니다. 대신 다음 명령어를 사용합니다:
$ AWS_PROFILE=myProfile serverless invoke local --function create --path mocks/create-event.json
여기서 myProfile
은 사용하려는 AWS 프로필 이름입니다. Serverless에서 여러 AWS 프로필을 사용하는 방법에 대한 자세한 내용은 Configure multiple AWS profiles 챕터를 참조하세요.
응답은 다음과 비슷하게 나타납니다.
{
"statusCode": 200,
"body": "{\"userId\":\"123\",\"noteId\":\"bf586970-1007-11eb-a17f-a5105a0818d3\",\"content\":\"hello world\",\"attachment\":\"hello.jpg\",\"createdAt\":1602891102599}"
}
응답에서 noteId
를 기록해 둡니다. 이 새로 생성된 노트는 다음 챕터에서 사용할 것입니다.
코드 리팩터링
다음 장으로 넘어가기 전에, 모든 API에 대해 동일한 작업을 반복할 예정이므로 코드를 빠르게 리팩터링해 보겠습니다.
먼저 create.js
를 다음 코드로 교체합니다.
import * as uuid from "uuid";
import handler from "./libs/handler-lib";
import dynamoDb from "./libs/dynamodb-lib";
export const main = handler(async (event, context) => {
const data = JSON.parse(event.body);
const params = {
TableName: process.env.tableName,
Item: {
// 생성할 아이템의 속성
userId: "123", // 작성자 ID
noteId: uuid.v1(), // 고유한 uuid
content: data.content, // 요청 본문에서 파싱
attachment: data.attachment, // 요청 본문에서 파싱
createdAt: Date.now(), // 현재 Unix 타임스탬프
},
};
await dynamoDb.put(params);
return params.Item;
});
이 코드는 아직 작동하지 않지만, 우리가 달성하려는 목표를 보여줍니다:
- Lambda 함수를
async
로 만들고 결과를 간단히 반환하고 싶습니다. - DynamoDB 호출 방식을 단순화하고 싶습니다.
new AWS.DynamoDB.DocumentClient()
를 매번 생성하지 않도록 합니다. - Lambda 함수에서 발생하는 모든 오류를 중앙에서 처리하고 싶습니다.
- 마지막으로, 모든 Lambda 함수가 API 엔드포인트를 처리할 것이므로 HTTP 응답을 한 곳에서 처리하고 싶습니다.
이를 위해 먼저 dynamodb-lib
을 생성해 보겠습니다.
프로젝트 루트에 libs/
디렉토리를 생성합니다.
$ mkdir libs
$ cd libs
libs/dynamodb-lib.js
파일을 다음과 같이 생성합니다.
import AWS from "aws-sdk";
const client = new AWS.DynamoDB.DocumentClient();
export default {
get: (params) => client.get(params).promise(),
put: (params) => client.put(params).promise(),
query: (params) => client.query(params).promise(),
update: (params) => client.update(params).promise(),
delete: (params) => client.delete(params).promise(),
};
여기서는 이 가이드에서 필요로 하는 DynamoDB 클라이언트 메서드를 노출하는 편의 객체를 생성합니다.
또한 libs/handler-lib.js
파일을 다음과 같이 생성합니다.
export default function handler(lambda) {
return async function (event, context) {
let body, statusCode;
try {
// Lambda 실행
body = await lambda(event, context);
statusCode = 200;
} catch (e) {
body = { error: e.message };
statusCode = 500;
}
// HTTP 응답 반환
return {
statusCode,
body: JSON.stringify(body),
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": true,
},
};
};
}
이 코드를 자세히 살펴보겠습니다.
- Lambda 함수를 감싸는
handler
함수를 생성합니다. - 이 함수는 Lambda 함수를 인자로 받습니다.
- 그런 다음
try/catch
블록 안에서 Lambda 함수를 실행합니다. - 성공 시 결과를
JSON.stringify
로 변환하고200
상태 코드와 함께 반환합니다. - 오류가 발생하면 오류 메시지를
500
상태 코드와 함께 반환합니다.
중요한 점은 handler-lib.js
를 다른 것보다 먼저 임포트해야 한다는 것입니다. 이는 나중에 Lambda 함수가 처음 호출될 때 초기화해야 하는 오류 처리 기능을 추가할 예정이기 때문입니다.
템플릿 파일 제거
또한, 프로젝트 루트에서 다음 명령어를 실행해 시작 파일을 제거합니다.
$ rm handler.js
다음으로, 특정 ID를 가진 노트를 가져오는 API를 추가할 예정입니다.
일반적인 문제
-
응답
statusCode: 500
함수를 호출할 때
statusCode: 500
응답이 표시된다면, 다음과 같이 디버깅할 수 있습니다. 이 오류는catch
블록에서 우리의 코드에 의해 생성됩니다.libs/handler-lib.js
에console.log
를 추가하면 문제의 원인을 파악하는 데 도움이 됩니다.} catch (e) { // 전체 오류를 출력 console.log(e); body = { error: e.message }; statusCode = 500; }
For help and discussion
Comments on this chapter