[PLAY STEEM x PWA] Flutter에서 JS 라이브러리 사용하기

in SCT.암호화폐.Crypto4 years ago (edited)

PLAY STEEM 개발자 이타인클럽입니다.

Flutter에서 JS (자바스크립트) 라이브러리 사용법을 공유합니다.

일반적으로 알려진 방법으로 했을 때, 사용하고자 하는 dsteem (dhive)는 문제가 발생했습니다.
일반적 방법: https://codeburst.io/how-to-use-javascript-libraries-in-your-dart-applications-e44668b8595d

그래서 어렵게 검색하고 테스트하고 해서 해결법을 찾았습니다.
해결방법: https://medium.com/flutter-community/using-javascript-code-in-flutter-web-903de54a2000

대신 제가 구현한 내용으로 설명드리겠습니다.

순서

  1. js 패키지 설치
  2. 빌드된 JS 라이브러리 준비 (여기서는 dhive.js 파일)
  3. 플러터 프로젝트의 web폴더에 JS 관련 폴더 구조 생성
  4. 플러터 프로젝트의 web폴더 밑의 index.html 수정
  5. JS 라이브러리 연결
  6. 사용

1. js 패키지 설치

pubspec.yaml에 다음과 같이 js 패키지를 추가합니다.

dependencies:
  flutter:
    sdk: flutter
  js: ^0.6.3

현재 최신 버전은 0.6.3입니다.

추가하고 패키지를 설치합니다.

$ flutter pub get

2. JS 라이브러리 파일 준비

기존 방법들은 다음과 같이 CDN (contents delivery network)을 사용해도 된다고 나오는데, 전 안됩니다.

    <script
      defer
      src="https://github.com/EtainClub/dhive/tree/master/dist/dhive.js"
    ></script>

그래서, 빌드된 dhive.js 파일을 준비합니다.
dhive를 제가 조금 수정한 파일은 여기서 받습니다.
https://github.com/EtainClub/dhive/blob/master/dist/dhive.js

추가적으로 require.js 파일이 필요합니다.
여기서 받습니다.
https://requirejs.org/docs/download.html

3. web 폴더 구조

플러터에 web폴더가 있어야 합니다.
그리고 다음 그림과 같이 구조를 만듭니다.

image.png

위 구조에서 main.js 파일에 다음과 넣습니다.

define(function (require) {
  window.dhive = require("./scripts/dhive");
});

4. web폴더의 index.html 수정

index.html을 다음과 같이 수정합니다.

    <script src="main.dart.js" type="application/javascript"></script>

    <script src="js/require.js"></script>
    <script>
      require(["js/main"]);
    </script>

5. 라이브러리 연결

이제 마지막 단계입니다. JS 라이브러리를 Flutter에서 사용가능하도록 연결시켜 줍니다.
이를 위해 dsteem.dart 파일을 만듭니다.
저는 lib/js/dsteem.dart에 만들었습니다.

여기서는 dsteem의 클라이언트 부분만 구현합니다. 필요한 기능은 여기에 다 포함시켜야 합니다.

@JS('dhive')
library dsteem;

import 'package:js/js.dart';
import 'dart:async';

@JS('Client')
class Client {
  external factory Client(String server, ClientOptions options);
  external String get address;
  external String get addressPrefix;
  external String get chainId;
  external Blockchain get blockchain;
  external BroadcastAPI get broadcast;
  external DatabaseAPI get database;
  external num get consoleOnFailover;
  external num get failoverThreshold;
  external call2(String api, String method, dynamic params);
  external void updateOperations(String str);
}

@anonymous
@JS()
abstract class ClientOptions {
  external String get chainId;
  external String get addressPrefix;
  external String get timeout;
  external factory ClientOptions(
      {String chainId, String addressPrefix, num timeout});
}

class AccountPostsQuery {
  final String account;
  final int limit;
  final String observer;
  final String sort;
  String? start_author;
  String? start_permlink;
  AccountPostsQuery(
      {required this.account,
      required this.limit,
      required this.observer,
      required this.sort,
      this.start_author,
      this.start_permlink});
}

제일 먼저 다음과 같이 라이브러리 네임스페이스를 지시합니다.

@JS('dhive')
library dsteem;

여기서 @JS('dhive')에 들어가는 것이 중요합니다.
뒤에 오는 것들은 이 네임스페이스에 속하게 됩니다.
따라서 @JS('Client')라고 뒤에 나오는데, 이것 자바스크립트에서 dhive.Client를 의미하게 됩니다.

ClientOptions는 자바스크립트 라이브러리로 넘겨줄 파라미터를 위한 것입니다.
자바스크립트는 object형태가 있는데, dart에는 없습니다. 그래서 이처럼 object형태의 클래스를 만들어서 넘겨줘야 합니다.

AccountPostsQuery도 포스트 데이터를 가져오기 위해 자바스크립트 라이브러리에 넘겨주기 위한 인자를 위해 만든 것입니다.

그런데 Client에서 한가지 이상한 부분이 있습니다.

  external call2(String api, String method, dynamic params);

dhive의 Client.call이란 함수가 있습니다. 라이브러리를 연결하는 것이 때문에 함수 이름이 동일해야 합니다.
그런데 플러터 (엄밀히 dart)에서 call이 키워드로 지정되어 있습니다. 어떤 test_var에 .call()과 같이 사용할 수 있습니다.

이게 문제가 됩니다!! Client의 call함수를 호출할 수가 없습니다. (에러가 발생합니다! undefined...)

방법이 없는 거 같습니다. dhive 라이브러리를 변경하는 방법 말고는요.

그래서 dhive의 Client의 call함수를 call2로 변경하고 다시 빌드해서 dhive.js파일을 만들었습니다.

6. 사용

이제 사용해 봅니다.

단계 5에서 만든 dsteem.dart 파일을 임포트하고 사용합니다.

import 'package:able/js/dsteem.dart';

class SteemApiProvider {
  // dsteem client
  Client client = Client(
      BASE_URL,
      ClientOptions(
          chainId: CHAIN_ID,
          addressPrefix: CHAIN_PREFIX,
          timeout: CLIENT_TIMEOUT));

 // 
Future<List<Post>> fetchAccountPosts(
      String account, String sort, PostRef startPostRef, String username,
      [int limit = NUM_FETCH_POSTS]) async {
    // build query
    final query = AccountPostsQuery(
      account: 'etainclub',
      observer: 'etainclub',
      sort: 'feed',
      limit: limit,
    );
    // fetch posts
    final promisePosts = client.call2(
      POSTS_FETCH_API,
      POSTS_FETCH_METHOD,
      query,
    );
    var results = await promiseToFuture(promisePosts);
    List<Post> postList = [];
    results.forEach((jsObject) {
      // convert jsobject to map
      final map = jsObjectToMap(jsObject) as Map<String, dynamic>;
      final post = Post.fromMap(map);
      postList.add(post);
    });
    return postList;
  }
}

위 코드에서 fetchAccountPosts라는 함수가 있습니다. Client의 call2함수를 이용해서 포스트를 가져오는 함수입니다. 이 때 결과를 dart의 Map<String, dynamic>로 변환하고 이걸 다시 Post라는 모델로 변환합니다.

Post모델은 다음과 같습니다.

class PostRef extends Equatable {
  final String author;
  final String permlink;

  // constructor
  PostRef({required this.author, required this.permlink});

  // getter for properties
  @override
  List<Object> get props => [author, permlink];
}

class Post {
  // post reference
  PostRef postRef;
  PostRef? parentRef;

  // stats
  final DateTime createdAt;
  DateTime? updatedAt;
  int votesCount = 0;
  int resteemCount = 0;
  int commentsCount = 0;
  int bookmarksCount = 0;
  String payout = '0';
  List<dynamic> voters = [];
  bool? nsfw;
  bool isPromoted = false;

  // post
  final int id;
  final int reputation = 25;
  String title = '';
  String body = '';
  String markdownBody = '';
  String summary = '';
  String image = '';
  final String url;

  String jsonMetadata = '';
  List<String> tags = [];
  int depth = 0;
  int children = 0;

  // user action related
  bool voted = false;
  bool downVoted = false;
  bool? bookmarked = false;
  bool? resteemed = false;
  bool? favorited = false;
  bool? commented = false;
  int? votePercent = 0;

  // comments
  bool isComment = false;

  // community
  String? communityTag = '';
  String? communityTitle = '';

  // constructor
  Post({
    required this.createdAt,
    this.updatedAt,
    required this.postRef,
    required this.isComment,
    required this.id,
    required this.title,
    required this.body,
    required this.url,
  });

  // constructor for json
  factory Post.fromJson(dynamic json) {
    return Post(
        createdAt: DateTime.parse(json['created']),
        updatedAt: DateTime.parse(json['updated']),
        postRef: PostRef(author: json['author'], permlink: json['permlink']),
        id: json['post_id'],
        title: json['title'],
        body: json['body'],
        url: json['url'],
        isComment: false);
  }

  // constructor with map
  Post.fromMap(Map<String, dynamic> object)
      : createdAt = DateTime.parse(object['created']),
        updatedAt = DateTime.parse(object['updated']),
        postRef =
            PostRef(author: object['author'], permlink: object['permlink']),
        id = object['post_id'],
        title = object['title'],
        body = object['body'],
        url = object['url'];
}

jsObjectToMap라는 함수는 dsteem과 유사하게 자바스크립트 라이브러리를 사용합니다. json아니 아니라 자바스크립트 객체(Javscript Object)를 Map으로 바꾸는 함수입니다.

@JS()
library js;

import 'package:js/js.dart';
import 'dart:js_util';

@JS('JSON.stringify')
external String stringify(Object obj);

Map jsObjectToMap(jsObject) {
  return Map.fromIterable(
    _getKeysOfObject(jsObject),
    value: (key) => getProperty(jsObject, key),
  );
}

@JS('Object.keys')
external List<String> _getKeysOfObject(jsObject);

다른 방법?

이렇게 하면 자바스크립트 라이브러리를 플러터에서 사용하는 것이 가능합니다. 그런데 이게 좋은 방법일까요? 굳이 블락체인에 있는 데이터를 가져오기 위해 자바스크립트 라이브러리를 써서 가져와서 dart모델로 바꿔야 할까요?

데이터를 가져오기 오는데 그럴 필요는 없습니다. 트랜잭션 사이닝은 당장 dart로 구현이 어려워서 자바스립트 라이브러리를 사용하는게 좋겠지만, 데이터 가져오기는 바로 dart로 구현하는게 좋습니다.

그래서 위 경우에도 fetchAccountPosts함수를 쓰지 않고, dart 함수를 만듭니다.

class SteemApiProvider {

  // http client instance
  http.Client httpClient = http.Client();

  // constructor
  SteemApiProvider();

  Future<List<Post>> fetchPosts(
      {String? account,
      required String observer,
      required String sort,
      required int limit,
      String? tag,
      PostRef? startPostRef}) async {
    final body =
        '{"jsonrpc":"2.0", "method":"$POSTS_FETCH_API.$POSTS_FETCH_METHOD","params":{"account":"$account","limit":$limit,"sort":"$sort","start_author":"${startPostRef?.author}","start_permlink":"${startPostRef?.permlink}"}, "id":1}';

    print('fetchPosts. body: $body');
    var url = Uri.parse(BASE_URL);
    List<Post> postList = [];

    final response = await httpClient.post(url, body: body);
    if (response.statusCode == 200) {
      // convert it to model
      final parsedJson = json.decode(response.body);
      parsedJson['result'].forEach((json) {
        final post = Post.fromJson(json);
        postList.add(post);
      });
      return postList;
    } else {
      throw Exception('error fetching posts');
    }
  }

정리

뭐가 많이 복잡합니다.
요약하면 블록체인의 데이터를 가져오는 것은 dart로 구현하고, signing과 같은 기능은 dsteem 라이브러리를 사용하기 위해 구현합니다.

cc.
@steemcurator01
@steemcurator06

Sort:  

암호문 입니다 ㅠㅠ 늘 수고 많으십니다 ^^ 건승하세요

저도 정신이 없네요~ ^^;

좋은 정보 공유해주셔서 항상 감사합니다. 저도 플러터로 빨리 시작해보고 싶네요. ㅎㅎ

Coin Marketplace

STEEM 0.22
TRX 0.25
JST 0.039
BTC 95759.82
ETH 3324.32
USDT 1.00
SBD 3.17