노트 생성 API 추가하기

노트 앱을 위한 API를 만들어 보겠습니다.

먼저 노트를 생성하는 API를 추가할 것입니다. 이 API는 노트 객체를 입력으로 받아서 새로운 ID와 함께 데이터베이스에 저장합니다. 노트 객체는 content 필드(노트의 내용)와 attachment 필드(업로드된 파일의 URL)를 포함합니다.

API 생성하기

Change indicator infra/api.ts 파일을 다음 코드로 교체하세요.

import { table } from "./storage";

// API 생성
export const api = new sst.aws.ApiGatewayV2("Api", {
  transform: {
    route: {
      handler: {
        link: [table],
      },
    }
  }
});

api.route("POST /notes", "packages/functions/src/create.main");

여기서 몇 가지 중요한 작업을 수행하고 있습니다.

  • SST의 Api 컴포넌트를 사용해 API를 생성합니다. 이는 Amazon API Gateway HTTP API를 만듭니다.

  • link 속성을 사용해 DynamoDB 테이블을 API에 연결합니다. 이를 통해 API가 테이블에 접근할 수 있게 됩니다.

  • API에 추가하는 첫 번째 라우트는 POST /notes입니다. 이 라우트는 노트를 생성하는 데 사용됩니다.

  • transform 속성을 사용해 API의 모든 라우트에 주어진 속성을 적용하도록 지정합니다.

함수 추가하기

이제 노트를 생성할 함수를 추가해 보겠습니다.

Change indicator packages/functions/src/create.ts 파일을 생성하고 다음 내용을 추가합니다.

import * as uuid from "uuid";
import { Resource } from "sst";
import { APIGatewayProxyEvent } from "aws-lambda";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { PutCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";

const dynamoDb = DynamoDBDocumentClient.from(new DynamoDBClient({}));

export async function main(event: APIGatewayProxyEvent) {
  let data, params;

  // 요청 본문은 'event.body'에 JSON 문자열로 전달됨
  if (event.body) {
    data = JSON.parse(event.body);
    params = {
      TableName: Resource.Notes.name,
      Item: {
        // 생성할 아이템의 속성
        userId: "123", // 작성자 ID
        noteId: uuid.v1(), // 고유한 uuid
        content: data.content, // 요청 본문에서 파싱
        attachment: data.attachment, // 요청 본문에서 파싱
        createdAt: Date.now(), // 현재 유닉스 타임스탬프
      },
    };
  } else {
    return {
      statusCode: 404,
      body: JSON.stringify({ error: true }),
    };
  }

  try {
    await dynamoDb.send(new PutCommand(params));

    return {
      statusCode: 200,
      body: JSON.stringify(params.Item),
    };
  } catch (error) {
    let message;
    if (error instanceof Error) {
      message = error.message;
    } else {
      message = String(error);
    }
    return {
      statusCode: 500,
      body: JSON.stringify({ error: message }),
    };
  }
}

코드에 유용한 주석이 있지만 간단히 살펴보겠습니다.

  • event.body에서 입력을 파싱합니다. 이는 HTTP 요청 본문을 나타냅니다.
  • 노트의 내용을 문자열로 포함하는 content가 있습니다.
  • attachment도 포함될 수 있습니다. 이는 S3 버킷에 업로드될 파일의 이름입니다.
  • SST SDK를 사용해 Resource.Notes.name을 통해 연결된 DynamoDB 테이블에 접근할 수 있습니다. 여기서 NotesSST에서 DynamoDB 테이블 생성 챕터에서 정의한 테이블 컴포넌트의 이름입니다. 이전에 link: [table]을 설정함으로써 API가 테이블에 접근할 수 있도록 했습니다.
  • userId는 노트 작성자의 ID입니다. 현재는 123으로 하드코딩되어 있습니다. 나중에 인증된 사용자에 따라 이 값을 설정할 것입니다.
  • DynamoDB에 새로운 객체를 생성하기 위해 noteId와 현재 날짜를 createdAt으로 설정해 호출합니다.
  • DynamoDB 호출이 실패하면 HTTP 상태 코드 500과 함께 오류를 반환합니다.

이제 여기서 사용할 패키지를 설치해 보겠습니다.

Change indicator 터미널에서 functions 폴더로 이동합니다.

$ cd packages/functions 

Change indicator 그리고 packages/functions/ 폴더에서 다음 명령어를 실행합니다.

$ npm install uuid @aws-sdk/lib-dynamodb @aws-sdk/client-dynamodb
$ npm install -D @types/uuid @types/aws-lambda
  • uuid는 고유 ID를 생성합니다.
  • @types/aws-lambda@types/uuid는 TypeScript 타입을 제공합니다.
  • @aws-sdk/lib-dynamodb@aws-sdk/client-dynamodb는 DynamoDB와 통신할 수 있게 해줍니다.

변경 사항 배포하기

터미널로 이동하면 변경 사항이 배포되고 있는 것을 확인할 수 있습니다.

배포가 완료되면 다음과 같은 메시지를 볼 수 있습니다.

+  Complete
   Api: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com

API 테스트

이제 새 API를 테스트할 준비가 되었습니다.

Change indicator 터미널에서 다음 명령어를 실행하세요.

curl -X POST \
-H 'Content-Type: application/json' \
-d '{"content":"Hello World","attachment":"hello.jpg"}' \
<YOUR_Api>/notes

<YOUR_Api>를 위 출력 결과의 Api로 바꿔주세요. 예를 들어, 명령어는 다음과 같이 보일 것입니다.

curl -X POST \
-H 'Content-Type: application/json' \
-d '{"content":"Hello World","attachment":"hello.jpg"}' \
https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com/notes

여기서는 노트 생성 API에 POST 요청을 보내고 있습니다. contentattachment를 JSON 문자열로 전달합니다. 이 경우 attachment는 가상의 파일 이름입니다. 아직 S3에 아무것도 업로드하지 않았습니다.

응답은 다음과 같이 보일 것입니다.

{"userId":"123","noteId":"a46b7fe0-008d-11ec-a6d5-a1d39a077784","content":"Hello World","attachment":"hello.jpg","createdAt":1629336889054}

noteId를 기록해두세요. 다음 장에서 이 새로 생성된 노트를 사용할 것입니다.

코드 리팩토링

다음 장으로 넘어가기 전에 코드를 리팩토링해 보겠습니다. 모든 API에서 동일한 기본 작업을 수행할 것이므로, 이를 core 패키지로 옮기는 것이 합리적입니다.

Change indicator 먼저 create.ts를 다음 코드로 교체하세요.

import * as uuid from "uuid";
import { Resource } from "sst";
import { Util } from "@notes/core/util";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { PutCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";

const dynamoDb = DynamoDBDocumentClient.from(new DynamoDBClient({}));

export const main = Util.handler(async (event) => {
  let data = {
    content: "",
    attachment: "",
  };

  if (event.body != null) {
    data = JSON.parse(event.body);
  }

  const params = {
    TableName: Resource.Notes.name,
    Item: {
      // 생성할 아이템의 속성
      userId: "123", // 작성자 ID
      noteId: uuid.v1(), // 고유한 UUID
      content: data.content, // 요청 본문에서 파싱
      attachment: data.attachment, // 요청 본문에서 파싱
      createdAt: Date.now(), // 현재 Unix 타임스탬프
    },
  };

  await dynamoDb.send(new PutCommand(params));

  return JSON.stringify(params.Item);
});

이 코드는 아직 작동하지 않지만, 우리가 달성하고자 하는 목표를 보여줍니다:

  • Lambda 함수를 async로 만들고 결과를 간단히 반환하고 싶습니다.
  • Lambda 함수에서 발생하는 모든 오류를 중앙에서 처리하고 싶습니다.
  • 마지막으로, 모든 Lambda 함수가 API 엔드포인트를 처리할 것이므로 HTTP 응답을 한 곳에서 처리하고 싶습니다.

Change indicator packages/core/src/util/index.ts 파일을 생성하고 다음 코드를 추가하세요.

import { Context, APIGatewayProxyEvent } from "aws-lambda";

export module Util {
  export function handler(
    lambda: (evt: APIGatewayProxyEvent, context: Context) => Promise<string>
  ) {
    return async function(event: APIGatewayProxyEvent, context: Context) {
      let body: string, statusCode: number;

      try {
        // Lambda 실행
        body = await lambda(event, context);
        statusCode = 200;
      } catch (error) {
        statusCode = 500;
        body = JSON.stringify({
          error: error instanceof Error ? error.message : String(error),
        });
      }

      // HTTP 응답 반환
      return {
        body,
        statusCode,
      };
    };
  }
}

Change indicator 이제 core에서도 Lambda 타입을 사용합니다. packages/core/ 디렉토리에서 다음 명령어를 실행하세요.

$ npm install -D @types/aws-lambda

이제 이 코드를 자세히 살펴보겠습니다.

  • Lambda 함수를 감싸는 handler 함수를 생성합니다.
  • 이 함수는 Lambda 함수를 인자로 받습니다.
  • 그런 다음 try/catch 블록 안에서 Lambda 함수를 실행합니다.
  • 성공하면 결과를 받아 200 상태 코드와 함께 반환합니다.
  • 오류가 발생하면 오류 메시지를 500 상태 코드와 함께 반환합니다.
  • 모든 것을 Util 모듈 내부에 내보내면 Util.handler로 가져올 수 있습니다. 또한 나중에 이 모듈에 다른 유틸리티 함수를 추가할 수 있습니다.

템플릿 파일 제거

현재 사용 중인 템플릿에는 예제 파일이 포함되어 있습니다. 이제 이 파일들을 제거할 수 있습니다.

Change indicator 프로젝트 루트에서 다음 명령어를 실행하세요.

$ rm -rf packages/core/src/example packages/functions/src/api.ts

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


일반적인 문제

  • path가 undefined 타입을 받음

    npx sst dev를 다시 시작하면 새로운 타입 정보를 인식하고 이 오류를 해결할 수 있습니다.

  • 응답 statusCode: 500

    함수를 호출할 때 statusCode: 500 응답이 표시되면, catch 블록에서 우리 코드가 오류를 보고한 것입니다. 위의 util/index.ts 코드에 console.error가 포함되어 있습니다. 이러한 로그를 추가하면 문제를 파악하고 해결하는 데 도움이 됩니다.

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