노트 생성 API 추가하기

백엔드 작업을 시작하기 위해 먼저 노트를 생성하는 API를 추가해 보겠습니다. 이 API는 노트 객체를 입력으로 받아 새로운 ID와 함께 데이터베이스에 저장합니다. 노트 객체는 content 필드(노트의 내용)와 attachment 필드(업로드된 파일의 URL)를 포함합니다.

함수 추가하기

첫 번째 함수를 추가해 보겠습니다.

Change indicator 프로젝트 루트에 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 엔드포인트를 정의해 보겠습니다.

Change indicator 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를 테스트할 준비가 되었습니다. 로컬에서 테스트하기 위해 입력 파라미터를 모킹할 것입니다.

Change indicator 프로젝트 루트에 mocks/ 디렉토리를 생성합니다.

$ mkdir mocks

Change indicator 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에 대해 동일한 작업을 반복할 예정이므로 코드를 빠르게 리팩터링해 보겠습니다.

Change indicator 먼저 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을 생성해 보겠습니다.

Change indicator 프로젝트 루트에 libs/ 디렉토리를 생성합니다.

$ mkdir libs
$ cd libs

Change indicator 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 클라이언트 메서드를 노출하는 편의 객체를 생성합니다.

Change indicator 또한 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 함수가 처음 호출될 때 초기화해야 하는 오류 처리 기능을 추가할 예정이기 때문입니다.

템플릿 파일 제거

Change indicator 또한, 프로젝트 루트에서 다음 명령어를 실행해 시작 파일을 제거합니다.

$ rm handler.js

다음으로, 특정 ID를 가진 노트를 가져오는 API를 추가할 예정입니다.


일반적인 문제

  • 응답 statusCode: 500

    함수를 호출할 때 statusCode: 500 응답이 표시된다면, 다음과 같이 디버깅할 수 있습니다. 이 오류는 catch 블록에서 우리의 코드에 의해 생성됩니다. libs/handler-lib.jsconsole.log를 추가하면 문제의 원인을 파악하는 데 도움이 됩니다.

    } catch (e) {
      // 전체 오류를 출력
      console.log(e);
    
      body = { error: e.message };
      statusCode = 500;
    }