AWS AppSync로 서버리스 GraphQL API 만들기

이 예제에서는 SST를 사용해 AWS에서 AppSync GraphQL API를 만드는 방법을 살펴보겠습니다. 사용자가 노트를 조회, 생성, 수정, 삭제, 목록 조회할 수 있도록 할 것입니다.

SST의 Live Lambda Development를 사용할 예정입니다. 이를 통해 AppSync를 로컬에서 변경하고 테스트할 수 있으며, 재배포할 필요가 없습니다.

실제 동작 영상은 다음과 같습니다.

요구사항

SST 앱 생성하기

Change indicator 먼저 SST 앱을 만들어 보겠습니다.

$ npx create-sst@latest --template=base/example graphql-appsync
$ cd graphql-appsync
$ npm install

기본적으로 앱은 AWS us-east-1 리전에 배포됩니다. 이 설정은 프로젝트 루트의 sst.config.ts 파일에서 변경할 수 있습니다.

import { SSTConfig } from "sst";

export default {
  config(_input) {
    return {
      name: "graphql-appsync",
      region: "us-east-1",
    };
  },
} satisfies SSTConfig;

프로젝트 구조

SST 앱은 두 부분으로 구성됩니다.

  1. stacks/ — 앱 인프라

    서버리스 앱의 인프라를 정의하는 코드는 프로젝트의 stacks/ 디렉토리에 위치합니다. SST는 AWS CDK를 사용하여 인프라를 생성합니다.

  2. packages/functions/ — 앱 코드

    API가 호출될 때 실행되는 코드는 프로젝트의 packages/functions/ 디렉토리에 위치합니다.

인프라 설정하기

먼저 AppSync API를 정의해 보겠습니다.

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

import { StackContext, Table, AppSyncApi } from "sst/constructs";

export function ExampleStack({ stack }: StackContext) {
  // 노트 테이블 생성
  const notesTable = new Table(stack, "Notes", {
    fields: {
      id: "string",
    },
    primaryIndex: { partitionKey: "id" },
  });

  // AppSync GraphQL API 생성
  const api = new AppSyncApi(stack, "AppSyncApi", {
    schema: "packages/functions/src/graphql/schema.graphql",
    defaults: {
      function: {
        // 테이블 이름을 함수에 바인딩
        bind: [notesTable],
      },
    },
    dataSources: {
      notes: "packages/functions/src/main.handler",
    },
    resolvers: {
      "Query    listNotes": "notes",
      "Query    getNoteById": "notes",
      "Mutation createNote": "notes",
      "Mutation updateNote": "notes",
      "Mutation deleteNote": "notes",
    },
  });

  // AppSync API ID와 API 키를 출력에 표시
  stack.addOutputs({
    ApiId: api.apiId,
    APiUrl: api.url,
    ApiKey: api.cdk.graphqlApi.apiKey || "",
  });
}

여기서는 AppSyncApi 구성을 사용해 AppSync GraphQL API를 생성합니다. 또한 Table 구성을 사용해 DynamoDB 테이블을 만듭니다. 이 테이블은 GraphQL API로 생성할 노트를 저장합니다.

마지막으로, 테이블을 API에 바인딩합니다.

GraphQL 스키마 정의하기

Change indicator packages/functions/src/graphql/schema.graphql에 다음 내용을 추가합니다.

type Note {
  id: ID!
  content: String!
}

input NoteInput {
  id: ID!
  content: String!
}

input UpdateNoteInput {
  id: ID!
  content: String
}

type Query {
  listNotes: [Note]
  getNoteById(noteId: String!): Note
}

type Mutation {
  createNote(note: NoteInput!): Note
  deleteNote(noteId: String!): String
  updateNote(note: UpdateNoteInput!): Note
}

노트 객체에 대한 타입도 추가합니다.

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

type Note = {
  id: string;
  content: string;
};

export default Note;

함수 핸들러 추가하기

먼저 AppSync 데이터 소스로 사용할 Lambda 함수를 만들어 보겠습니다.

Change indicator packages/functions/src/main.ts 파일을 다음 코드로 교체하세요.

import Note from "./graphql/Note";
import listNotes from "./graphql/listNotes";
import createNote from "./graphql/createNote";
import updateNote from "./graphql/updateNote";
import deleteNote from "./graphql/deleteNote";
import getNoteById from "./graphql/getNoteById";

type AppSyncEvent = {
  info: {
    fieldName: string;
  };
  arguments: {
    note: Note;
    noteId: string;
  };
};

export async function handler(
  event: AppSyncEvent
): Promise<Record<string, unknown>[] | Note | string | null | undefined> {
  switch (event.info.fieldName) {
    case "listNotes":
      return await listNotes();
    case "createNote":
      return await createNote(event.arguments.note);
    case "updateNote":
      return await updateNote(event.arguments.note);
    case "deleteNote":
      return await deleteNote(event.arguments.noteId);
    case "getNoteById":
      return await getNoteById(event.arguments.noteId);
    default:
      return null;
  }
}

이제 리졸버를 구현해 보겠습니다.

노트 생성하기

노트를 생성하는 기능부터 시작해 보겠습니다.

Change indicator packages/functions/src/graphql/createNote.ts 파일을 추가합니다.

import { DynamoDB } from "aws-sdk";
import { Table } from "sst/node/table";
import Note from "./Note";

const dynamoDb = new DynamoDB.DocumentClient();

export default async function createNote(note: Note): Promise<Note> {
  const params = {
    Item: note as Record<string, unknown>,
    TableName: Table.Notes.tableName,
  };

  await dynamoDb.put(params).promise();

  return note;
}

여기서는 주어진 노트를 DynamoDB 테이블에 저장합니다.

Change indicator packages/functions/ 폴더에서 사용 중인 aws-sdk 패키지를 설치합니다.

$ npm install aws-sdk

노트 목록 읽기

이제 모든 노트를 가져오는 함수를 작성해 보겠습니다.

Change indicator packages/functions/src/graphql/listNotes.ts에 다음 코드를 추가합니다.

import { DynamoDB } from "aws-sdk";
import { Table } from "sst/node/table";

const dynamoDb = new DynamoDB.DocumentClient();

export default async function listNotes(): Promise<
  Record<string, unknown>[] | undefined
> {
  const params = {
    TableName: Table.Notes.tableName,
  };

  const data = await dynamoDb.scan(params).promise();

  return data.Items;
}

여기서는 테이블에서 모든 노트를 가져옵니다.

특정 노트 읽기

단일 노트를 가져오는 함수도 비슷한 방식으로 구현할 것입니다.

Change indicator packages/functions/src/graphql/getNoteById.ts 파일을 생성합니다.

import { DynamoDB } from "aws-sdk";
import { Table } from "sst/node/table";
import Note from "./Note";

const dynamoDb = new DynamoDB.DocumentClient();

export default async function getNoteById(
  noteId: string
): Promise<Note | undefined> {
  const params = {
    Key: { id: noteId },
    TableName: Table.Notes.tableName,
  };

  const { Item } = await dynamoDb.get(params).promise();

  return Item as Note;
}

전달된 id를 가진 노트를 가져옵니다.

노트 업데이트하기

이제 노트를 업데이트해 보겠습니다.

Change indicator packages/functions/src/graphql/updateNote.ts 파일을 추가하고 다음 코드를 작성합니다:

import { DynamoDB } from "aws-sdk";
import { Table } from "sst/node/table";
import Note from "./Note";

const dynamoDb = new DynamoDB.DocumentClient();

export default async function updateNote(note: Note): Promise<Note> {
  const params = {
    Key: { id: note.id },
    ReturnValues: "UPDATED_NEW",
    UpdateExpression: "SET content = :content",
    TableName: Table.Notes.tableName,
    ExpressionAttributeValues: { ":content": note.content },
  };

  await dynamoDb.update(params).promise();

  return note as Note;
}

여기서는 전달된 노트의 id와 content를 사용해 노트를 업데이트합니다.

노트 삭제하기

모든 작업을 완료하기 위해 노트를 삭제해 보겠습니다.

Change indicator packages/functions/src/graphql/deleteNote.ts에 다음 코드를 추가하세요.

import { DynamoDB } from "aws-sdk";
import { Table } from "sst/node/table";

const dynamoDb = new DynamoDB.DocumentClient();

export default async function deleteNote(noteId: string): Promise<string> {
  const params = {
    Key: { id: noteId },
    TableName: Table.Notes.tableName,
  };

  // await dynamoDb.delete(params).promise();

  return noteId;
}

현재는 의도적으로 삭제 쿼리를 비활성화했습니다. 이 부분은 나중에 다시 다룰 예정입니다.

지금까지 만든 기능을 테스트해 보겠습니다!

개발 환경 시작하기

Change indicator SST는 Live Lambda Development 환경을 제공합니다. 이를 통해 여러분은 서버리스 앱을 실시간으로 작업할 수 있습니다.

$ npm run dev

이 명령어를 처음 실행하면 앱과 Live Lambda Development 환경을 지원하는 디버그 스택을 배포하는 데 몇 분이 소요됩니다.

===============
 앱 배포 중
===============

SST 앱 준비 중
소스 코드 변환 중
소스 코드 린팅 중
스택 배포 중
dev-graphql-appsync-ExampleStack: 배포 중...

 ✅  dev-graphql-appsync-ExampleStack


스택 dev-graphql-appsync-ExampleStack
  상태: 배포됨
  출력:
    ApiId: lk2fgfxsizdstfb24c4y4dnad4
    ApiKey: da2-3oknz5th4nbj5oobjz4jwid62q
    ApiUrl: https://2ngraxbyo5cwdpsk47wgn3oafu.appsync-api.us-east-1.amazonaws.com/graphql

ApiId는 방금 생성한 AppSync API의 ID이고, ApiKey는 AppSync API의 API 키이며, ApiUrl은 AppSync API의 URL입니다.

SST Console을 사용해 엔드포인트를 테스트해 보겠습니다. SST Console은 SST 앱을 관리할 수 있는 웹 기반 대시보드입니다. 문서에서 자세히 알아보세요.

GraphQL 탭으로 이동하면 GraphQL Playground가 실행 중인 것을 확인할 수 있습니다.

GraphQL 탐색기를 사용하면 앱에서 GraphQLApi와 AppSyncApi 구성을 통해 생성된 GraphQL 엔드포인트를 쿼리할 수 있습니다.

먼저 노트를 생성해 보겠습니다. 아래 뮤테이션을 플레이그라운드의 왼쪽 부분에 붙여넣으세요.

mutation createNote {
  createNote(note: { id: "001", content: "My note" }) {
    id
    content
  }
}

GraphQL 콘솔 노트 생성

이제 SST Console의 DynamoDB 탭으로 이동해 테이블에 값이 생성되었는지 확인해 보겠습니다.

DynamoDB 탐색기를 사용하면 앱의 Table 구성에서 DynamoDB 테이블을 쿼리할 수 있습니다. 테이블을 스캔하거나 특정 키를 쿼리하고, 아이템을 생성하고 편집할 수 있습니다.

DynamoDB 탐색기 노트 생성

이제 방금 생성한 노트를 가져오기 위해 아래 쿼리를 실행해 보겠습니다.

query getNoteById {
  getNoteById(noteId: "001") {
    id
    content
  }
}

GraphQL 콘솔 노트 가져오기

다음으로 업데이트 뮤테이션을 테스트해 보겠습니다.

mutation updateNote {
  updateNote(note: { id: "001", content: "My updated note" }) {
    id
    content
  }
}

GraphQL 콘솔 노트 업데이트

이제 노트를 삭제해 보겠습니다.

mutation deleteNote {
  deleteNote(noteId: "001")
}

GraphQL 콘솔 노트 삭제

삭제가 제대로 동작했는지 확인하기 위해 모든 노트를 가져오는 쿼리를 실행해 보겠습니다.

query listNotes {
  listNotes {
    id
    content
  }
}

GraphQL 콘솔 노트 목록

몇 가지를 확인할 수 있습니다. 첫째, 생성한 노트가 여전히 존재합니다. 이는 deleteNote 메서드가 실제로 쿼리를 실행하지 않기 때문입니다. 둘째, 노트는 이전 쿼리에서 업데이트된 내용을 반영해야 합니다.

변경 사항 적용하기

Change indicator packages/functions/src/graphql/deleteNote.ts 파일에서 쿼리의 주석을 제거해 수정해 보겠습니다.

await dynamoDb.delete(params).promise();

쿼리 편집기로 돌아가서 삭제 뮤테이션을 다시 실행해 보세요.

mutation deleteNote {
  deleteNote(noteId: "001")
}

변경 후 GraphQL 콘솔에서 노트 삭제

이제 목록 쿼리를 실행하면 노트가 제거된 것을 확인할 수 있습니다!

query listNotes {
  listNotes {
    id
    content
  }
}

변경 후 GraphQL 콘솔에서 노트 목록 확인

앱을 다시 배포하지 않아도 변경 사항이 적용된 것을 확인할 수 있습니다.

API 배포하기

이제 API 테스트가 완료되었으니 프로덕션 환경에 배포해 보겠습니다. 여러분은 sst.config.ts에 지정된 dev 환경을 사용하고 있었던 것을 기억할 겁니다. 하지만 이제는 다른 환경에 배포할 예정입니다. 이렇게 하면 다음에 로컬에서 개발할 때 사용자들이 사용하는 API가 망가지지 않습니다.

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

$ npx sst deploy --stage prod

정리하기

마지막으로, 이 예제에서 생성된 리소스들을 다음 명령어로 제거할 수 있습니다.

$ npx sst remove
$ npx sst remove --stage prod

결론

여기까지입니다! 여러분은 AppSync로 구축한 새로운 서버리스 GraphQL API를 갖게 되었습니다. 테스트와 변경을 위한 로컬 개발 환경도 마련했고, 프로덕션에 배포까지 완료했습니다. 이제 사용자들과 공유할 수 있습니다. 아래 레포지토리에서 이 예제에서 사용한 코드를 확인해 보세요. 궁금한 점이 있다면 댓글을 남겨 주세요!