Search
▪️

Sending Http Reqests

일반적으로 Web Server를 통해서 데이터 베이스에 접근하도록 한다. 이렇게 하는 이유는, Flutter App이 직접적으로 데이터 베이스에 접근하는 것은 기술적으로 복잡할 뿐만 아니라 보안에도 좋지 않다.
1.
사람들의 Connection을 일일이 만들고, 조절하고, 관리하는 것에 어려움이 있다.
2.
어플에서 직접적으로 데이터 베이스를 접근한다는 것은 데이터베이스의 credential을 갖고 있다는 뜻이고 이는 곧 데이터 베이스에 읽고 쓸 권한이 있는 ID / PW를 어플이 직접 갖고 있다는 뜻이다. (일반적으로 store에서 다운로드 받은 어플의 source code를 보는 것은 불가능 하지만 100% 배제할 수는 없다. (누군가는 볼 수도 있다는 얘기이다.) 또한 누군가는 Decompile하여 사용자의 credential들을 얻을 수 있게 되고 이는 내 정보를 이용하여 데이터 베이스를 (삭제, 복사, 수정 등) 읽고 쓸 수 있다는 얘기가 된다.)
따라서 어플에서 데이터 베이스의 credential을 얻을 수 없어야 할 뿐더러 direct하게 데이터 베이스에 접근하지 못하도록 할 필요가 있고 이에 따라 Web Server가 필요한 것이다.
일반적인 구조는 Web Server를 두어 Flutter가 HTTP Request를 통해 서버에 연결할 수 있도록 하고, 서버에서 데이터 베이스에 접근할 수 있도록 한다. 해당 과정을 검증하는 코드들은 서버 컴퓨터에 있게 되고 해당 컴퓨터를 방어하는 방어 체계를 두게 된다. 이전과 다른 점은 사람들이 직접 어플을 다운로드 받으면서 코드를 cracking 한다고 정보를 얻기는 힘들다는 것이다.
Flutter에서 HTTP 통신을 하기 위해서는 HTTP Package가 필요하다.
Http Request
Http 통신 방법 중 하나로 Rest API로 통신할 수 있다.
Http Endpoint (URL) + Http Verb = Action
GET / POST / PATCH or PUT / DELETE are each Fetch data / Store data / Update Data or Replace data / Delete data (PATCH는 항목의 일부를 수정하는 것이라면 PUT은 새로운 data block을 생성하여 replace하는 것이다.)
Flutter에서는 일반적으로 data들을 Provider에서 관리하기 하기 때문에 Http Method들을 Provider에서 처리하는 것이 깔끔하다. 일반적인 Local과 다른 점이라 하면 Http Request들은 처리될 시간을 필요로 한다. 이런 Request들은 시간을 필요로 하지만 우리의 클라이언트는 쉴 틈 없이 계속 돌아가고 있다. 따라서 처리에 유의해야 한다.
Firebase를 Dummy Back-end로 사용하여 테스트 해보는 것도 좋은 방법이다.
Firebase 특성상 주어진 링크에 identifier를 추가해야 하며, 끝에 .json을 붙여야 한다.
요청을 보내는 것은 body에 담겨서 전송 된다.body는 JSON (Javascript Object Notation)으로 구성 된다. 따라서 모든 Object들은 JSON으로 변환되어 전송해야 하므로 import 'dart:convert'를 하여 Method를 이용한다.
Future는 Dart의 Core한 class이며 비동기 코드이다. 비동기 연산이 끝날 때까지 기다렸다가 주어진 연산을 수행하는 역할을 한다.
.then()은 Future Type의 연산이 끝나면 수행되고, .catchError()를 통해서 에러 검출도 가능하다.
data를 보내는 어떤 역할을 수행할 때는 항상 서버의 작업이 끝나면 local에 보일 수 있도록 한다. 이 때 local에 나타날 때까지 기다리는 모션 등이 필요하다.
data를 가져올 때는 initState()에 할 수 있도록 한다. 이 때 provider를 사용한다면 context가 필요한데 initState()에서는 context를 사용하지 못한다. 하지만! context를 이용하여 state를 바꾸는 것이 아니라면 initState()에서 사용이 가능하다! 즉, listen을 false로 하면 initState()에서 사용 가능하다는 것이다.
initState()에서 listen을 false로 하여 data를 가져오는 방법 외에도 두가지 방법이 있다.
1.
Future.delayed(Duration.zero).then(() { })로 강제로 delay를 주어 State를 생성한 후에 provider를 통해서 data를 갖고 오게 한다. (사실 zero duration이라 바로 실행하는 것과 다르진 않지만 강제로 비동기 코드로 만들어서 state를 먼저 initialize하게 되는 것이다.)
** ModalRoute에도 적용이 가능한 방법이다.
2.
didChangeDependencies()를 이용하는 방법이 있다. 단, initState()와 달리 여러 번 호출이 가능하기에 한 번만 이뤄지게 만들어야 한다.
비동기 코드에 대해서는 Future Type return을 해줄 필요가 있다. 이런 비동기 연산이 다 끝난 후에 화면 전환이 일어날 수 있도록 해야 한다. 비동기 연산이 끝나기 전까지 loading indicator를 넣는 것도 좋은 방법이다. loading indicator를 이용할 때는, bool형 변수를 이용하는 것이 일반적이다.
일반적으로 삭제 요청을 넣을 때는 삭제할 item에 대해서 copy를 해두는 것이 좋다. 이유는 다음과 같다.
Dart도 unmanaged language라서 참조되고 있지 않은 주소가 차지하고 있는 메모리는 다 해제 시킨다. 무슨 말이냐 함은 removeAt()과 같은 함수는 List에서 해당 항목의 포인터를 잃게 함으로써 Dart의 Garbage Collector가 자동으로 지우게끔 하는 것이지 메모리에서 직접 없애는 것이 아니다. 즉, removeAt을 써서 List에서 삭제 시키더라도 해당 데이터를 남기고 싶으면 다른 변수를 이용해서 해당 포인터를 취득하면 메모리에서 사라지지 않게 할 수 있다.
patch, put, delete 요청의 중요한 특성은 error 캐치가 잘 안 된다는 점이다. (세 Method들은 기존 HTTP Verbs들이 아니라 추가된 Method들이라는 공통점이 있다.) get, post와 같은 http 요청의 경우는 error status code를 받으면 error throw를 하지만 patch, put, delete요청은 그렇지 않다.
보통 200~201은 ok / 300은 redirected / 400 ~ 500은 대체로 error를 나타낸다. 따라서 status code가 400이상이면 get, post와 같은 http 요청은 error을 catch할 수 있도록 error throw를 하지만 나머지 요청들은 status자체가 400이상으로 넘어오지 않는 경우가 있다. 그러므로 일일이 response.statusCode로 처리해야 한다. 이 때, 직접 setting한 statusCode에 대해서 throw Exception()으로 custom error exception을 생성해야 한다.
custom error exception을 만들기 위해서는 Exception class을 구현해야 하며 toString()함수를 override 해야 한다. (throw가 stringify해서 넘기기 때문이다.)
** Dart의 모든 class들은 toString()함수를 지원하는데, 이는 Dart의 모든 class들이 Object라는 class를 상속하고 있기 때문이다.
extends, with가 아닌 implements는 abstract class를 이용하며 이 abstract class는 특성상 direct 호출이 불가능하다. 따라서 모든 abstract class의 함수들을 정의하여 사용해야 한다.
Future Type을 return 받아야 하는 async 함수 내에서는 context가 정상적으로 작동하기 어렵다. 따라서 미리 context를 만들어 둔 변수를 이용하여 받아야 한다.
build()함수 내에서 data를 가져오면서 setState()를 하는 것은 굉장히 미친 짓이 될 수 있으므로 initState() 혹은 didChangeDependencies()와 같은 함수 내에서 fetch 해야 한다. 하지만 이런 방법 말고도 한 가지 방법이 더 있다. FutureBuilder() Widget을 사용하면 된다.
future : Future 타입의 인자
builder : context와 future로 받은 snapshot을 이용한 빌더
snapshot의 경우는 async이다.
snapshot.connectionState == ConnectionState.waiting을 통해 CircularProgressIndicator두면 되고, ConnectionState.done을 통해 나타내려는 Widget을 두면 된다.
json.encode({mapping})
json.decode(response.body)
Fetch 한 data들은 JSON Object이기에 forEach를 통해 Parsing 한다.
또한 특정 List형태를 JSON Object로 보내기 위해서는 바로 encode하면 안되고 map()함수를 통해서 Parsing해줘야 함
JSON Object로 DateTime을 보낼 때는 단순 toString()보다는 toIso8601String()이 낫다.
DateTime을 decode할 때는 DateTime.parse()을 이용
.catchError((error) {})
Http Request에서 catchError()한 번 날리고 Http Request를 호출하는 Widget에서도 catchError() 해준다.
전자는 그냥 error 검출해서 return하고 후자는 return 받은 error을 Widget으로 구현하도록 한다.
그렇게 Widget 구현 후에는 Loading Indicator 도는 것과 Navigator.of(context).pop()하는 것을 없애야 한다. (Future Type을 return하는 Widget을 써야 두 함수에 대해서 동작 시키면서, 위 두 가지를 없앨 수 있다.)
.then() & .catchError() 대신에 async로 만들고 await을 변수로 받을 수 있다. 이 방법이 훨씬 깔끔하다.
async로 가둔 모든 코드들이 비동기 코드를 처리하는 블록이 된다.
async await을 이용하는 Error Handling은 try{} catch (error) {}를 이용 한다. (finally {} 구문은 결과가 어찌 되었든 무조건 실행한다.)
<List>.removeAt()