서버리스 앱 개발하기 3 - 서버리스 프레임워크 써서 플러터 앱과 연결하기
목차
이 글은 서버리스 프레임워크를 쓰는 법,
아마존 람다를 통해 다이나모 디비로부터
데이터 불러오는 법을 다루고 있습니다.
(데이터 저장은 이 글을 참고해주세요 2편 - 아마존 DynamoDB 시작하기. CRUD부터 Scan까지 )
이 글을 끝까지 읽으면 아주 기초적인 서버리스 모바일(플러터) 앱을 만들어 볼 수 있습니다.
1.Serverless framwork
1.1. Serverless framwork 시작하기
람다와 다이나모DB를 연결하기 전에 서버리스 프레임워크를 먼저 알아보도록 할께요.
웹 콘솔에서 aws 람다, api 게이트웨이 설정을 하는 건 불편하죠.
이를 코드로 편하게 배포할 수 있게 하는게 Serverless framwork (링크) 입니다.
1.1.1 Serverless 설치
npm install serverless -g
설치가 잘 되었는지 확인하려면 serverless -v 를 쳐보세요. 버전 확인이 되면 성공입니다.
서버리스 프레임워크로 aws를 관리하려면 접근 권한을 줘야해요.
1.1.2 AWS IAM에서 자격증명(Credential) 만들기
- 우선 AWS에 접속해서 IAM 페이지에 들어가 주세요.
유저를 하나 추가합니다.
- 유저를 추가할 때 꼭 Programmatic access를 체크해줘야 됩니다. (아래 그림 참고)
유저 추가를 쭉 하다보면 접근 키(Access key, Secret key)를 받을 수 있는 페이지가 나옵니다.
키를 다운받은 다음에 잘 보관해주세요. 이 키들을 서버리스 프레임워크에서 쓸 겁니다.
키를 받고 나면 iam 페이지에서 나오도록 합시다.
1.1.3. Serverless 접근 권한 주기
아까 받은 키들을 서버리스에서 설정해주세요.
xxxxxxxxxx
serverlsss login
serverless config --provider aws --key 액세스키 --secret 시크릿액세스키
1.1.4. serverless 기본 템플릿 만들어보기
서버리스 프레임워크는 템플릿을 써서 기본적인 애플리케이션을 만들 수 있습니다.
xxxxxxxxxx
serverless create -t aws-nodejs -p serverless-start
cd serverless-start
템플릿을 만들면 파일 2개가 생깁니다. handler.js와 serverless.yml 입니다.
handler.js
- 람다 실행 코드
x'use strict';
module.exports.hello = async (event, context) => {
return {
statusCode: 200,
body: JSON.stringify({
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event,
}),
};
// Use this code if you don't use the http event with the LAMBDA-PROXY integration
// return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};
serverless.yml
- 서버리스 프레임워크 설정파일
xxxxxxxxxx
service sls-start
provider
name aws
runtime nodejs8.10
functions
hello
handler handler.hello // handler.js에 있는 hello 함수를 실행
serverless.yml을 조금 수정하고 배포를 해보겠습니다.
1.1.5. serverless.yml 수정 및 배포
serverless.yml
- 서버리스 프레임워크 설정파일
xxxxxxxxxx
service sls-start
provider
name aws
runtime nodejs8.10
region ap-northeast-2
functions
hello
handler handler.hello
events // rest api를 만들거니 events는 http로 하겠습니다.
http
path hello
method get
이제 serverless deploy 를 터미널에 입력해봅시다.
성공적으로 배포되면 아래처럼 뜰 거에요.
xxxxxxxxxx
endpoints:
GET - https://kzf2871d.execute-api.ap-northeast-2.amazonaws.com/dev/hello
functions:
hello: sls-start-dev-hello
GET 옆의 url을 브라우저에 붙여넣기해 봅시다.
응답이 나오면 성공이에요!
이제 다이나모 DB와 람다를 연결해보겠습니다.
이제부터 쓸 데이터는 저번글 에서 다이나모 DB에 넣었던 Food 테이블의 데이터입니다.
Food 테이블에 데이터가 들어가 있다고 생각하고 설명할께요.
2 DynamoDB 와 람다 연결
2.1. 설정하기
다이나모DB를 쓰려면 사용 권한(iam role 지정)이 있어야해요.
serverless.yml
을 수정해주도록 합시다.
xxxxxxxxxx
service sls-start
provider
name aws
runtime nodejs8.10
stage dev
region ap-northeast-2
iamRoleStatements
Effect"Allow"
Action
dynamodb:*
Resource"*"
배포를 해볼께요.
xxxxxxxxxx
sls deploy
배포가 성공적으로 되었나요?
이제 다이나모 디비에 접근할 수 있습니다!
이를 위한 람다함수를 만들어보도록 할께요.
다 완성되면 아래 그림 같은 구조가 됩니다.
2.2 람다함수 추가하기
프레임워크 설정에다 어떤 람다 파일을 쓸지 추가해줄께요.
serverless.yml
xxxxxxxxxx
provider
name aws
runtime nodejs8.10
stage dev
region ap-northeast-2
iamRoleStatements
Effect"Allow"
Action
dynamodb:*
Resource"*"
functions
hello
handler handler.hello
events
http
path hello
method get
get
handler api/recipes.get
events
http
path recipes
method get
list
handler api/recipes.list
events
http
path recipes/ type
method get
functions 부분에 get, list 함수를 추가했습니다.
get함수는 쿼리 스트링(queryString)에 따라 값을 불러오는 함수이구요.
list함수는 음식의 종류(type)에 따라 데이터를 불러옵니다.
api 폴더에 recipes.js를 추가하고 배포해 볼께요.
api/recipes.js
xxxxxxxxxx
'use strict';
const AWS = require('aws-sdk');
AWS.config.update({
region: 'ap-northeast-2',
endPoint: 'http://dynamodb.ap-northeast-2.amazonaws.com'
});
const docClient = new AWS.DynamoDB.DocumentClient();
const TableName = 'Food';
module.exports.get = async (event, callback) => {
let queryParam = event.queryStringParameters;
let type = queryParam['type'];
const name = queryParam['name'];
const params = {
TableName : TableName,
Key : {
"type": type,
"name": name,
}
}
const result = await docClient.get(params).promise();
const response = {
statusCode: 200,
body: JSON.stringify(result)
}
return response;
}
module.exports.query = async (event, callback) => {
const { type } = event.pathParameters;
const params = {
TableName: TableName,
KeyConditionExpression: "#type = :type",
ExpressionAttributeNames: {
"#type": "type"
},
ExpressionAttributeValues: {
":type": type
}
}
const result = await docClient.query(params).promise();
const response = {
statusCode: 200,
body: JSON.stringify(result['Items'])
}
console.log(result);
return response;
}
배포를 해볼께요.
xxxxxxxxxx
sls deploy
배포가 끝났나요?
이제 함수가 제대로 동작하는지 테스트 해볼께요.
2.3.1 동작 테스트 - GET 함수
우선 GET 함수는 아래의 주소를 브라우저에 붙여 넣어주세요.
xxxxxxxxxx
https://kzf2871d.execute-api.ap-northeast-2.amazonaws.com/dev/recipes?type=Western&name=Carbonara
DB에 데이터가 있다면 아래처럼 나올 거에요.
xxxxxxxxxx
{
"Item": {
"ingredients": {
"garlic": "1",
"bacon": "50g",
"spaghetti": "90g",
"egg": "1"
},
"price": 13000,
"name": "Carbonara",
"type": "Western",
"orderCount": 50
}
}
2.3.2 동작 테스트 - list 함수
list 함수를 테스트 해볼께요.
xxxxxxxxxx
https://kzf2871d.execute-api.ap-northeast-2.amazonaws.com/dev/recipes/Western
type이 Western인 아이템을 모두 조회하는 걸 알 수 있습니다.
xxxxxxxxxx
[
{
"ingredients": {
"garlic": "1",
"bacon": "50g",
"spaghetti": "90g",
"egg": "1"
},
"price": 13000,
"name": "Carbonara",
"type": "Western",
"orderCount": 50
},
{
"ingredients": {
"honey maid graham cracker crumbs": "1 3/4 cups",
"eggs": "3",
"butter, melted": "1/3 cup",
"sugar, divided": "1 1/4 cups",
"philadelphia cream cheese": "3 packages(8 ounce)",
"vanilla": "2 teaspoons vanilla"
},
"price": 13000,
"name": "Cheese cake",
"type": "Western",
"orderCount": 77
}
]
3.플러터 앱에서 보여주기
이번엔 API Gateway와 플러터 앱을 연결해 보도록 하겠습니다.
앱은 최대한 간단히 만들어보도록 할께요.
앱은 3개의 클래스로 구성되어 있습니다.
- UI를 그리는 위젯 클래스(main.dart)
- 데이터 저장 하는 모델 클래스( recipe.dart)
- http로 데이터 요청하는 api 클래스 (api.dart)
플러터 구조
일단 http package를 설정 파일에 추가해주세요.
dart 2.0 부터는 http 패키지를 항상 추가해줘야합니다.
pubspec.yaml
xxxxxxxxxx
version 1.0.0+1
environment
sdk">=2.0.0-dev.68.0 <3.0.0"
dependencies
flutter
sdk flutter
cupertino_icons ^0.1.2
http ^0.12.0
dev_dependencies
flutter_test
sdk flutter
AWS로부터 받은 데이터를 저장하는 클래스를 만들어볼께요.
recipe.dart
xxxxxxxxxx
class Recipe {
String name;
String type;
int orderCount;
int price;
Map<String, dynamic> ingredients;
Recipe({this.price, this.name, this.type, this.orderCount, this.ingredients});
Recipe.fromJson(Map<String, dynamic> json) : // json데이터를 클래스 형식으로 변환
name = json['name'],
type = json['type'],
price = json['price'],
orderCount = json['orderCount'],
ingredients = json['ingredients'];
}
api.dart
xxxxxxxxxx
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'model/recipe.dart';
class API {
String baseUrl = "https://kzf2871d.execute-api.ap-northeast-2.amazonaws.com/dev";
final client = http.Client();
Future<Recipe> fetchRecipe(String type, String name) async {
var path = "/recipes?type=${type}&name=${name}";
var url = baseUrl + path;
final response = await client.get(url); // http get 메소드로 리퀘스트를 보냅니다.
var data = json.decode(utf8.decode(response.bodyBytes)); // utf8로 디코딩해야 한글이 깨지지 않습니다.
return Recipe.fromJson(data['Item']);
}
}
response data를 utf8로 디코딩해야 한글이 깨지지 않습니다.
main.dart
xxxxxxxxxx
import 'package:flutter/material.dart';
import 'package:flutter_api_gateway/model/recipe.dart';
import 'api.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
API api = API();
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool loadRecipe = false;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
loadRecipe
? FutureBuilder<Recipe>(
future: widget.api.fetchRecipe("Western", "Carbonara"),
builder: (context, snapshot) {
if (snapshot.hasData) {
Recipe recipe = snapshot.data;
print(recipe.price);
return recipeView(recipe);
} else {
return Text("no data!");
}
},
)
: Text("data is not loaded!"),
],
),
),
floatingActionButton: FloatingActionButton( // 버튼을 누르면 데이터를 불러옵니다.
onPressed: () {
setState(() {
loadRecipe = true;
});
},
tooltip: 'Data Load',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
Widget recipeView(Recipe recipe) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 18.0, horizontal: 9.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("음식종류 : ${recipe.type}"),
Text("음식이름 : ${recipe.name}"),
Text("가격 : ${recipe.price}")
],
),
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.2),
borderRadius: BorderRadius.all(
Radius.circular(10.0),
),
),
);
}
}
버튼을 누르면 데이터가 화면에 표시됩니다.
생각보다 마지막편 업데이트가 늦었네요!
AWS 서버리스 시리즈는 3편으로 마무리하고
추후 새로운 내용이 있으면 연재하도록 하겠습니다.
다음에는 플러터 글로 찾아뵙도록 하겠습니다!