AWS 시작하기
Android 애플리케이션 구축
AWS Amplify를 사용하여 간단한 Android 애플리케이션 만들기
모듈 4: GraphQL API 및 데이터베이스 추가
이 모듈에서는 Amplify CLI와 라이브러리를 사용하여 GraphQL API를 구성하고 앱에 추가합니다.
소개
앱을 생성하고 사용자 인증으로 앱을 구성했으니 이제 API와 생성, 읽기, 업데이트, 삭제(CRUD) 작업을 데이터베이스에 추가하겠습니다.
이 모듈에서는 Amplify CLI와 라이브러리를 사용하여 앱에 API를 추가해봅니다. 여기서 생성할 API는 GraphQL API로, Amazon DynamoDB(NoSQL 데이터베이스) 기반의 AWS AppSync(관리형 GraphQL 서비스)를 활용합니다. GraphQL에 대한 자세한 소개는 이 페이지를 참조하십시오.
여기서 구축할 앱은 사용자가 메모를 작성하고 삭제하고 나열할 수 있는 메모 앱입니다. 이 예제를 통해 일반적인 유형의 CRUD+L(생성, 읽기, 업데이트, 삭제 및 나열) 애플리케이션을 구축하는 방법을 살펴볼 수 있습니다.
배우게 될 내용
- GraphQL API 생성 및 배포
- API와 상호 작용하는 프런트 엔드 코드 작성
주요 개념
API – 여러 소프트웨어 중개자 간의 통신과 상호 작용을 지원하는 프로그래밍 인터페이스를 제공합니다.
GraphQL – 애플리케이션의 유형이 지정된 표현을 기반으로 쿼리 언어 및 서버 측 API 구현 방식입니다. 이 API 표현 방식은 GraphQL 유형 시스템을 기반으로 한 스키마를 사용하여 선언됩니다. GraphQL에 대해 자세히 알아보려면 이 페이지를 참조하십시오.
완료 시간
20분
사용되는 서비스
구현
-
GraphQL API 서비스 및 데이터베이스 생성
GraphQL API와 지원 데이터베이스를 생성하려면 터미널을 열고 프로젝트 디렉터리에서 다음 명령을 실행합니다.
amplify add api ? Please select from one of the below mentioned services: select GraphQL and press enter ? Provide API name: select the default, press enter ? Choose the default authorization type for the API: use the arrow key to select Amazon Cognito User Pool and press enter ? Do you want to configure advanced settings for the GraphQL API: select the default No, I am done and press enter ? Do you have an annotated GraphQL schema? keep the default N and press enter ? What best describes your project: choose any model, we are going to replace it with our own anyway. Press enter ? Do you want to edit the schema now? type Y and press enter
프로젝트를 초기화(amplify init)할 때 선택한 기본 텍스트 편집기가 열리고 미리 구축된 데이터 스키마가 표시됩니다.
스키마를 삭제하고 앱 GraphQL 스키마로 바꿉니다.
type NoteData @model @auth (rules: [ { allow: owner } ]) { id: ID! name: String! description: String image: String }
이 데이터 모델은 NoteData 클래스 1개와 4개의 속성으로 구성됩니다. ID와 이름은 필수입니다. 설명과 이미지는 선택적 문자열입니다.
@model 변환기는 이러한 데이터를 저장할 데이터베이스를 생성합니다.
@auth 변환기는 이 데이터에 대한 액세스를 허용하는 인증 규칙을 추가합니다. 이 프로젝트에서는 NoteData의 소유자만 데이터에 액세스할 수 있습니다.
작업이 완료되면 꼭 저장한 후 터미널로 돌아가 Amplify CLI에서 작업을 완료했음을 확인합니다.
? Press enter to continue, press enter.
몇 초 후에 다음과 같은 성공 메시지가 표시됩니다.
GraphQL schema compiled successfully.
-
클라이언트 측 코드 생성
Amplify는 방금 생성된 GraphQL 데이터 모델 정의에 따라 앱에서 데이터를 표시할 클라이언트 측 코드(Swift 코드)를 생성합니다.
코드를 생성하려면 터미널에서 다음 명령을 실행합니다.
amplify codegen models
이렇게 하면 java/com/amplifyframework.datastore.generated.model 디렉터리에 Java 파일이 생성됩니다.
➜ Android Getting Started git:(master) ✗ ls -al app/src/main/java/com/amplifyframework/datastore/generated/model total 24 drwxr-xr-x 4 stormacq admin 128 Oct 7 15:27 . drwxr-xr-x 3 stormacq admin 96 Oct 7 15:27 .. -rw-r--r-- 1 stormacq admin 1412 Oct 7 15:27 AmplifyModelProvider.java -rw-r--r-- 1 stormacq admin 7153 Oct 7 15:27 NoteData.java
이 파일은 자동으로 프로젝트로 가져와집니다.
-
API 서비스 및 데이터베이스 배포
방금 생성한 백엔드 API와 데이터베이스를 배포하려면 터미널로 이동하여 다음 명령을 실행합니다.
amplify push # press Y when asked to continue ? Are you sure you want to continue? accept the default Y and press enter ? Do you want to generate code for your newly created GraphQL API type N and press enter
몇 분 후에 다음과 같은 성공 메시지가 표시됩니다.
✔ All resources are updated in the cloud GraphQL endpoint: https://yourid.appsync-api.eu-central-1.amazonaws.com/graphql
-
Android Studio 프로젝트에 API 클라이언트 라이브러리 추가
코드 작업을 시작하기 전에 Android Studio로 돌아가 이전에 추가한 다른 ‘amplifyframework’ 구현과 함께 모듈의 ‘build.gradle’에 다음 종속 구성 요소를 추가하고 메시지가 표시되면 [지금 동기화(Sync Now)]를 클릭합니다.
dependencies { implementation 'com.amplifyframework:aws-api:1.4.0' implementation 'com.amplifyframework:aws-auth-cognito:1.4.0' }
-
런타임 시 Amplify 라이브러리 초기화
Backend.kt를 열고 Amplify 초기화 시퀀스의 initialize() 메서드에 줄을 추가합니다. 완성된 try/catch 블록은 다음과 같습니다.
try { Amplify.addPlugin(AWSCognitoAuthPlugin()) Amplify.addPlugin(AWSApiPlugin()) Amplify.configure(applicationContext) Log.i(TAG, "Initialized Amplify") } catch (e: AmplifyException) { Log.e(TAG, "Could not initialize Amplify", e) }
-
GraphQL 데이터 모델과 앱 모델 사이에 브리징 추가
프로젝트에는 노트를 나타내는 데이터 모델이 이미 있습니다. 이 자습서에서는 해당 모델을 계속해서 사용하고 NoteData를 Note로 간편하게 변환하는 것이 좋습니다. UserData.kt를 열고 두 가지 구성 요소를 추가합니다. 그중 하나는 UserData.Note에서 NoteData 객체를 반환하는 동적 속성이고, 다른 하나는 반대로 API NoteData를 받아 Userdata.Note를 반환하는 정적 메서드입니다.
데이터 클래스 Note에 다음을 추가합니다.
// return an API NoteData from this Note object val data : NoteData get() = NoteData.builder() .name(this.name) .description(this.description) .image(this.imageName) .id(this.id) .build() // static function to create a Note from a NoteData API object companion object { fun from(noteData : NoteData) : Note { val result = Note(noteData.id, noteData.name, noteData.description, noteData.image) // some additional code will come here later return result } }
생성된 코드에서 NoteData 클래스를 가져와야 합니다.
-
Backend 클래스에 API CRUD 메서드 추가
API를 호출하는 3개의 메서드(Note 쿼리 메서드, 새 Note 생성 메서드 및 Note 삭제 메서드)를 추가합니다. 이러한 메서드는 앱 데이터 모델(Note)에서 작동하므로 사용자 인터페이스에서 쉽게 상호 작용할 수 있습니다. 이러한 메서드는 Note를 자동으로 GraphQL의 NoteData 객체로 변환합니다.
Backend.kt 파일을 열고 Backend 클래스 끝에 다음 조각을 추가합니다.
fun queryNotes() { Log.i(TAG, "Querying notes") Amplify.API.query( ModelQuery.list(NoteData::class.java), { response -> Log.i(TAG, "Queried") for (noteData in response.data) { Log.i(TAG, noteData.name) // TODO should add all the notes at once instead of one by one (each add triggers a UI refresh) UserData.addNote(UserData.Note.from(noteData)) } }, { error -> Log.e(TAG, "Query failure", error) } ) } fun createNote(note : UserData.Note) { Log.i(TAG, "Creating notes") Amplify.API.mutate( ModelMutation.create(note.data), { response -> Log.i(TAG, "Created") if (response.hasErrors()) { Log.e(TAG, response.errors.first().message) } else { Log.i(TAG, "Created Note with id: " + response.data.id) } }, { error -> Log.e(TAG, "Create failed", error) } ) } fun deleteNote(note : UserData.Note?) { if (note == null) return Log.i(TAG, "Deleting note $note") Amplify.API.mutate( ModelMutation.delete(note.data), { response -> Log.i(TAG, "Deleted") if (response.hasErrors()) { Log.e(TAG, response.errors.first().message) } else { Log.i(TAG, "Deleted Note $response") } }, { error -> Log.e(TAG, "Delete failed", error) } ) }
생성된 코드에서 ModelQuery, ModelMutation 및 NoteData 클래스를 가져와야 합니다.
마지막으로, 애플리케이션이 시작될 때 API를 호출하여 현재 로그인한 사용자의 노트 목록을 쿼리해야 합니다.
Backend.kt 파일에서 updateUserData(withSignInStatus: Boolean) 메서드를 다음과 같이 업데이트합니다.
// change our internal state and query list of notes private fun updateUserData(withSignedInStatus : Boolean) { UserData.setSignedIn(withSignedInStatus) val notes = UserData.notes().value val isEmpty = notes?.isEmpty() ?: false // query notes when signed in and we do not have Notes yet if (withSignedInStatus && isEmpty ) { this.queryNotes() } else { UserData.resetNotes() } }
이제, 새 Note를 생성하고 목록에서 Note를 삭제할 수 있는 사용자 인터페이스를 생성하기만 하면 됩니다.
-
Add Note에 Edit 버튼 추가
백엔드 및 데이터 모델 조각이 준비되었으니 이 섹션의 마지막 단계로 사용자가 새 Note를 생성하고 삭제할 수 있는 기능을 제공하면 됩니다.
a. Android Studio의 res/layout에서 새 레이아웃을 생성합니다. [레이아웃(layout)]을 마우스 오른쪽 버튼으로 클릭하고 [새로 만들기(New)]를 선택한 다음 [레이아웃 리소스 파일(Layout Resource File)]을 선택합니다. 이름을 activity_add_note로 지정하고 다른 모든 기본값을 사용합니다. [확인(OK)]을 클릭합니다.
방금 만든 activity_add_note 파일을 열고 아래의 코드를 붙여 넣어 생성된 코드를 대체합니다.
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:fillViewport="true"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="8dp"> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Create a New Note" android:textSize="10pt" /> <EditText android:id="@+id/name" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:hint="name" android:inputType="text" android:lines="5" /> <EditText android:id="@+id/description" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:hint="description" android:inputType="textMultiLine" android:lines="3" /> <Space android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <Button android:id="@+id/addNote" style="?android:attr/buttonStyleSmall" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:backgroundTint="#009688" android:text="Add Note" /> <Button android:id="@+id/cancel" style="?android:attr/buttonStyleSmall" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:backgroundTint="#FFC107" android:text="Cancel" /> </LinearLayout> </ScrollView>
이는 노트 제목과 설명을 입력할 수 있는 매우 간단한 레이아웃입니다.
b. AddNoteActivity 클래스를 추가합니다.
java/com.example.androidgettingstarted에서 새 kotlin 파일 AddActivityNote.kt를 열고 다음 코드를 추가합니다.
package com.example.androidgettingstarted import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import kotlinx.android.synthetic.main.activity_add_note.* import java.util.* class AddNoteActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_add_note) cancel.setOnClickListener { this.finish() } addNote.setOnClickListener { // create a note object val note = UserData.Note( UUID.randomUUID().toString(), name?.text.toString(), description?.text.toString() ) // store it in the backend Backend.createNote(note) // add it to UserData, this will trigger a UI refresh UserData.addNote(note) // close activity this.finish() } } companion object { private const val TAG = "AddNoteActivity" } }
마지막으로, 매니페스트에서 AndroidManifest.xml을 열고 이 작업 요소를 애플리케이션 노드 내의 아무 곳에나 추가합니다.
<activity android:name=".AddNoteActivity" android:label="Add Note" android:theme="@style/Theme.GettingStartedAndroid.NoActionBar"> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value="com.example.androidgettingstarted.MainActivity" /> </activity>
c. Main Activity에 "Add Note" FloatingActionButton을 추가합니다. res/layout에서 activity_main.xml을 열고 기존 Floating Action 버튼 위에 다음을 추가합니다.
<com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fabAdd" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_gravity="bottom|end" android:layout_margin="@dimen/fab_margin" android:visibility="invisible" android:src="@drawable/ic_baseline_post_add" app:fabCustomSize="60dp" app:fabSize="auto"/>
res/drawable에 "Add Note" 아이콘을 추가합니다. drawable을 마우스 오른쪽 버튼으로 클릭하고 [새로 만들기(New)], [벡터 자산(Vector Asset)]을 선택합니다. 이름으로 ic_baseline_add를 입력하고 클립아트에서 add 아이콘을 선택합니다. [다음(Next)]과 [마침(Finish)]을 차례로 클릭합니다.
d. "Add Note" 버튼을 처리할 코드를 추가합니다.
"Add Button"을 사용하기 위해 마지막으로 해야 할 두 가지 일은 버튼이 isSignedIn 값에 따라 표시되거나 사라지도록 하는 것과 버튼의 탭을 처리할 코드를 추가하는 것입니다.
mainActivity.kt를 열고 onCreate() 메서드의 끝에 다음을 추가합니다.
// register a click listener fabAdd.setOnClickListener { startActivity(Intent(this, AddNoteActivity::class.java)) }
그런 다음 여전히 onCreate() 메서드에 있는 상태에서 UserData.isSignedIn.observe를 다음으로 바꿉니다.
UserData.isSignedIn.observe(this, Observer<Boolean> { isSignedUp -> // update UI Log.i(TAG, "isSignedIn changed : $isSignedUp") //animation inspired by https://www.11zon.com/zon/android/multiple-floating-action-button-android.php if (isSignedUp) { fabAuth.setImageResource(R.drawable.ic_baseline_lock_open) Log.d(TAG, "Showing fabADD") fabAdd.show() fabAdd.animate().translationY(0.0F - 1.1F * fabAuth.customSize) } else { fabAuth.setImageResource(R.drawable.ic_baseline_lock) Log.d(TAG, "Hiding fabADD") fabAdd.hide() fabAdd.animate().translationY(0.0F) } })
정상적으로 작동하는지 확인하기 위해 프로젝트를 빌드합니다. [빌드(Build)] 메뉴를 클릭하고 [프로젝트 만들기(Make Project)]를 선택하거나 Mac에서 ⌘F9을 입력합니다. 오류가 발생하지 않아야 합니다.
애플리케이션을 실행하면 사용자가 로그인할 때 "Add Note" 버튼이 나타나고 사용자가 로그아웃할 때 사라집니다. 이제 노트를 추가할 수 있습니다.
-
Delete 동작에 Swipe 추가
노트의 목록에 터치 핸들러를 추가하여 swipe-to-delete 동작을 추가할 수 있습니다. 터치 핸들러는 빨간색 배경과 삭제 아이콘을 그리고 터치가 해제될 때 Backend.delete() 메서드를 호출하는 역할을 담당합니다.
a. SimpleTouchCallback이라는 새 클래스를 생성합니다. java/com에서 esample.androidgettingstarted를 마우스 오른쪽 버튼으로 클릭하고 [새로 만들기(New)], Kotlin 파일을 차례로 선택한 후 SwipeCallback을 이름으로 입력합니다.
아래 코드를 새 파일에 붙여 넣습니다.
package com.example.androidgettingstarted import android.graphics.Canvas import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.util.Log import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView // https://stackoverflow.com/questions/33985719/android-swipe-to-delete-recyclerview class SwipeCallback(private val activity: AppCompatActivity): ItemTouchHelper.SimpleCallback( 0, ItemTouchHelper.LEFT ) { private val TAG: String = "SimpleItemTouchCallback" private val icon: Drawable? = ContextCompat.getDrawable( activity, R.drawable.ic_baseline_delete_sweep ) private val background: ColorDrawable = ColorDrawable(Color.RED) override fun onChildDraw( c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean ) { super.onChildDraw( c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive ) val itemView = viewHolder.itemView val backgroundCornerOffset = 20 val iconMargin = (itemView.height - icon!!.intrinsicHeight) / 2 val iconTop = itemView.top + (itemView.height - icon.intrinsicHeight) / 2 val iconBottom = iconTop + icon.intrinsicHeight val iconRight: Int = itemView.right - iconMargin if (dX < 0) { val iconLeft: Int = itemView.right - iconMargin - icon.intrinsicWidth icon.setBounds(iconLeft, iconTop, iconRight, iconBottom) background.setBounds( itemView.right + dX.toInt() - backgroundCornerOffset, itemView.top, itemView.right, itemView.bottom ) background.draw(c) icon.draw(c) } else { background.setBounds(0, 0, 0, 0) background.draw(c) } } override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean { Toast.makeText(activity, "Moved", Toast.LENGTH_SHORT).show() return false } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) { Toast.makeText(activity, "deleted", Toast.LENGTH_SHORT).show() //Remove swiped item from list and notify the RecyclerView Log.d(TAG, "Going to remove ${viewHolder.adapterPosition}") // get the position of the swiped item in the list val position = viewHolder.adapterPosition // remove to note from the userdata will refresh the UI val note = UserData.deleteNote(position) // async remove from backend Backend.deleteNote(note) } }
이 코드의 중요한 줄은 onSwiped() 메서드에 있습니다. 이 메서드는 스와이프 제스처가 완료되면 호출됩니다. 목록에서 스와이프된 항목의 위치를 수집하고, UI를 업데이트하는 UserData 구조와 클라우드 백엔드에서 해당 노트를 제거합니다.
b. 이제 클래스가 시작되었으므로 res/drawable에 "Delete" 아이콘을 추가하겠습니다. drawable을 마우스 오른쪽 버튼으로 클릭하고 [새로 만들기(New)], [벡터 자산(Vector Asset)]을 선택합니다. ic_baseline_delete_sweep를 이름으로 입력하고 클립아트에서 "delete sweep" 아이콘을 선택합니다. [다음(Next)]과 [마침(Finish)]을 차례로 클릭합니다.
c. 아래 코드를 새 파일에 붙여 넣습니다. 스와이프하여 삭제 제스처 핸들러를 RecyclerView에 추가합니다.
Java/com/ample.androidgettingstarted에서 MainActivity.kt를 열고 setupRecyclerView에 다음 두 줄의 코드를 추가합니다.
// add a touch gesture handler to manager the swipe to delete gesture val itemTouchHelper = ItemTouchHelper(SwipeCallback(this)) itemTouchHelper.attachToRecyclerView(recyclerView)
-
구축 및 테스트
정상적으로 작동하는지 확인하기 위해 프로젝트를 빌드하고 실행합니다. 도구 모음에서 [실행(Run)] 아이콘 ▶을 클릭하거나 ^ R을 입력합니다. 오류가 발생하지 않아야 합니다.
아직 로그인되어 있다면 앱이 빈 목록에서 시작됩니다. Note를 추가할 수 있는 Add Note 버튼이 포함되어 있습니다. Add Note 기호를 누르고 제목을 입력한 후 설명을 입력하고 Add Note 버튼을 누르면 노트가 목록에 나타납니다.
행을 왼쪽으로 밀어 메모를 삭제할 수 있습니다.
전체 흐름은 다음과 같습니다.