26. 플러터 : 서버/클라이언트 연동 (1)

플러터를 사용하려고 했던 최초의 목적은 서버와의 연동이었다.
딥러닝 서버를 구축하고
스마트폰 앱을 사용해서 결과를 보여주기 위해서.

첫 번째 시간으로 JSON 서비스를 제공하는 서버로부터 데이터를 가져오는 코드를 구성해 봤다.
좋은 코드가 있어 참고했음을 밝힌다.
이곳에서 참고한 코드를 확인할 수 있다.

최종적으로는 여러 개의 데이터를 리스트 형태로 보여준다.
원본 코드에서는 포스트(post)를 가져다 사용했는데
http에서 get과 post라는 단어가 핵심 용어로 사용되기 때문에 일부러 사용자(user)를 선택했다.

총 10개의 데이터 중에서 첫 번째 사용자의 정보는 아래와 같다.
참조했던 코드보다 훨씬 복잡하다. 사전(map) 안에 사전이 존재하는 형태다.

// 첫 번째 사용자 데이터
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
},


플러터와 관련된 json 내용은 이곳에서 확인할 수 있다.
fromJson과 toJson 함수를 만드는 방법을 비롯해서 유용한 코드가 많으니 꼭 살펴보기 바란다.
이번 코드에서는 fromJson 생성자만 만들어서 코드를 보기좋게 꾸민다.



서버와의 연동을 위한 처음 코드는 http 모듈을 프로젝트에서 사용할 수 있도록 연동하는 부분이다.
다트에서는 pubspec.yaml 파일에서 연동 작업을 한다.
pubspec.yaml 파일을 열고 http 모듈을 아래 코드처럼 추가한다.
콜론 오른쪽에 아무 숫자도 쓰지 않으면 최신 버전을 가져오라는 뜻이고
지금처럼 명시하면 해당 버전을 갖고 오라는 뜻이다.
^ 기호는 명시한 버전보다 높아야 함을 뜻한다.
추가된 코드는 http가 포함된 한 줄뿐이다.

dependencies:
flutter:
sdk: flutter

# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
http: ^0.12.0


주석을 많이 붙이고
동일하게 동작하는 코드도 여럿 붙였더니.. 많이 길어졌다.

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() => runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('서버/클라이언트')),
body: MyApp(),
))
);

class User {
final int userId;
final String name;
final String email;
final String phone;

// 사전 안에 포함된 사전. 디코딩을 했기 때문에 문자열이 아니라 사전(map)이 되어야 한다.
// 이때 사전의 값으로는 여러 가지가 올 수 있기 때문에 dynamic 키워드가 온다.
// 엣갈리면 앞에 나온 사용자 데이터에서 company 항목을 찾아서 확인해 볼 것.
final Map<String, dynamic> company;

User({this.userId, this.name, this.email, this.phone, this.company});

// fromJson 생성자. 이 함수를 호출하면 User 객체를 만들 수 있기 때문에 생성자라고 부른다.
// factory는 클래스 함수로 생성자를 만들 때 사용하는 키워드.
// 전역 함수처럼 동작하기 때문에 this 키워드를 사용할 수 없다.
factory User.fromJson(Map<String, dynamic> userMap) {
return User(
userId: userMap['id'],
name: userMap['name'],
email: userMap['email'],
phone: userMap['phone'],
company: userMap['company'],
);
}

// 위와 동일한 방법으로 factory 키워드를 생략할 수 있다.
// User.fromJson(Map<String, dynamic> userMap)
// : userId = userMap['id']
// , name = userMap['name']
// , email = userMap['email']
// , phone = userMap['phone']
// , company = userMap['company']
}

// json 서버로부터 사용자 데이터 중에서 첫 번째 데이터 1개만 가져옴
Future<User> fetchUser() async {
// 첫 번째를 가져오기 때문에 주소 마지막에 '1'이 붙어있다.
// http 프로토콜의 get 방식으로 데이터를 가져온다.
// get은 가져온다는 뜻이 아나리 어떤 방식으로 데이터를 가져올지를 알려주는 방식(method)을 의미한다.
final response = await http.get('https://jsonplaceholder.typicode.com/users/1');

// 웹 서버로부터 정상(200) 데이터 수신
if (response.statusCode == 200) {
// json 데이터를 수신해서 User 객체로 변환
final userMap = json.decode(response.body);
return User.fromJson(userMap);

// fromJson 생성자를 만들지 않고 직접 User 객체를 생성할 수도 있다.
// return User(
// userId: userMap['id'],
// name: userMap['name'],
// email: userMap['email'],
// phone: userMap['phone'],
// company: userMap['company']
//);
}

// ok가 아니라면 예외 발생.
// 실제 상황에서는 데이터 수신에 실패했을 때의 처리를 제공해야 한다.
// 다시 읽어야 한다던가 빈 데이터 또는 에러를 표시한다던가.
throw Exception('데이터 수신 실패!');
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final style = TextStyle(fontSize: 21, height: 2.0);
return Column(
children: <Widget>[
FutureBuilder(
future: fetchUser(),
builder: (context, snapshot) {
if (snapshot.hasData) {
// 변수에 저장할 필요없이 Text 위젯에 바로 전달해도 된다.
// userId는 int 자료형을 갖기 때문에 문자열 변환이 필요하다.
final userId = snapshot.data.userId.toString();
final name = snapshot.data.name;
final email = snapshot.data.email;
final phone = snapshot.data.phone;
final company = snapshot.data.company;

return Column(
children: <Widget>[
Center(child: Text(userId, style: style)),
Center(child: Text(name, style: style)),
Center(child: Text(email, style: style)),
Center(child: Text(phone, style: style)),
Center(child: Text(company['name'], style: style)),
Center(child: Text(company['catchPhrase'], style: style)),
Center(child: Text(company['bs'], style: style)),
],
);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}

return CircularProgressIndicator();
},
),
],
);
}
}


CircularProgressIndicator 객체의 모양은 이곳에서 확인할 수 있다.
잘 동작하고 있다는 것을 보여줄 때 사용하는 인디케이터의 한 가지이다.

참.. json 서버의 역할을 너무 잘 수행해 주는 JSONPlaceHolder 사이트는 꼭 가봐야 한다.
홈 화면 아래에 내려가면 common으로 제공하는 api 목록을 확인할 수 있다.
앱에서 서버에 접근하기 전에
크롬 등의 브라우저를 사용해서 해당 주소가 잘 동작하는지 먼저 확인해야 한다. 꼭!