기본 콘텐츠로 건너뛰기

Android 에서 DDP 라이브러리를 사용하자.

Meteor는 자체적으로 Cordova 를 통한 iOS/Android 앱을 지원한다.
하지만, Hybrid 앱은 역시 Native에 비해 예측 못할 변수가 많고 성능도 그다지 좋지 않다.
DDP를 사용하여 Native Android 앱과 Meteor 서버를 연결해보자.

새 프로젝트를 만들고 File/Project Structure (cmd+;) 으로 들어가서

하단 +를 누르면

  1. Library Dependency
  2. File Dependency
  3. Module Dependency
세개 나온다.
Choose Library Dependency 창에서 ddp 하고 엔터쳐보자.
지금 현재는 3개 정도 나오는데

com.keysolutions:android-ddp-client:1.0.2.0
이걸 써서 할거다.
선택하고 OK 해서 추가하자.

activity_main.xml 에 버튼 하나 만들고 
AndroidManifest.xml 에

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
이 두 개의 permission 이 있는지 확인하자. 없으면 추가. <application>이랑 나란히 놓으면 된다.

준비 해야할 class 는 일단 두개인데
하나는 어플리케이션 전체에서 DDP 연결을 책임질 MyApplication (Application을 상속받은 클래스)와 DDP에서 subscription 처리를 해줄 LoginDDPState(DDPStateSingleton을 상속받은 클래스)이다.
물론 이름은 임의로 정했다.

먼저 MyApplication.java 를 보자
package com.appsoulute.meteorddpexam1;
import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
public class MyApplication extends Application {
    private static Context sContext = null;
    @Override
    public void onCreate() {
        super.onCreate();
        MyApplication.sContext = getApplicationContext();
        initSingletons();
    }
    private void initSingletons() {
        LoginDDPState.initInstance(MyApplication.sContext, "192.168.0.11", 4100, false);
    }
    public static Context getAppContext() {
        return MyApplication.sContext;
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
    }
}
대략 이런 구조 되시겠다.
복수의 DDP를 사용할 경우도 마찬가지 initSingletons 부분에 Meteor Server 주소, 포트, SSL 사용여부를 initInstance에 넣어주고 LoginDDPState를 구현해주면 되겠다.

만들고 나서 AndroidManifest.xml에 가서 application에 android:name 속성을 추가한다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.appsoulute.meteorddpexam1">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:name=".MyApplication"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

반드시 자동완성을 사용해서 오타를 피한다.

다음은 LoginDDPState 를 만들어보자.
package com.appsoulute.meteorddpexam1;
import android.content.Context;
import com.keysolutions.ddpclient.DDPClient;
import com.keysolutions.ddpclient.android.DDPStateSingleton;
/** * Created by spectrum on 6/14/16. */
public class LoginDDPState extends DDPStateSingleton {
    private LoginDDPState(Context context, String meteorServer, Integer meteorPort, boolean useSsl) {
        super(context, meteorServer, meteorPort, useSsl);
    }

    public static void initInstance(Context context, String meteorServer, Integer meteorPort, boolean useSsl) {
        if (mInstance == null) {
            mInstance = new LoginDDPState(context, meteorServer, meteorPort, useSsl);        }
    }
    public static LoginDDPState getInstance() {
        // Return the instance
        return (LoginDDPState) mInstance;
    }

    @Override
    public void broadcastSubscriptionChanged(String subscriptionName, String changetype, String docId) {
        if (subscriptionName.equals("getChats")) {
            if (changetype.equals(DDPClient.DdpMessageType.ADDED)) {

            } else if (changetype.equals(DDPClient.DdpMessageType.REMOVED)) {

            } else if (changetype.equals(DDPClient.DdpMessageType.UPDATED)) {

            }
        }
        super.broadcastSubscriptionChanged(subscriptionName, changetype, docId);
    }
}
조금 긴 것 같지만 잘 보면 별거 없다.
MyApplication 에서 호출한 initInstance를 만들어주고
getInstance에 싱글톤인 mInstance를 넘겨주는 것 정도한 뒤

broadcastSubscriptionChanged에서 subscription 처리를 하면 된다.
subscription 명하고 changeType에 따라 Map을 만들거나 해서 처리하면 된다.

이제 문제는 MainActivity인데
그전에 Android 의 LifeCycle을 잠시 언급하면
생성시 : onCreated > onStart > onResume
소멸시 : onPause > onStop > onDestroy
순인 걸 기억하자.

먼저, onResume 에서 할게 좀 많은데 대략 아래와 같다.
protected void onResume() {
    super.onResume();
    LoginBroadcastReceiver();
    DDPBroadcastReceiver();

    LoginDDPState.getInstance().connectIfNeeded();    // start connection process if we're not connected
}

두 개의 처리를 해주는데 DDP상태(LoginDDPState)와 DDPBroadcast(DDPBroadcastReceiver)를 다루는 부분을 처리하면 된다.
마지막으로 LoginDDPState 가 연결이 필요하면 connectIfNeeded()로 연결하도록 하자.

LoginBroadcastReceiver는 
private void LoginBroadcastReceiver() {
    mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Bundle bundle = intent.getExtras();
            if (intent.getAction().equals(LoginDDPState.MESSAGE_ERROR)) {
                String message = bundle.getString(LoginDDPState.MESSAGE_EXTRA_MSG);
            } else if (intent.getAction().equals(LoginDDPState.MESSAGE_CONNECTION)) {
                int state = bundle.getInt(LoginDDPState.MESSAGE_EXTRA_STATE);
                if (state == LoginDDPState.DDPSTATE.LoggedIn.ordinal()) {
                    // login complete, so we can close this login activity and go back
                    Log.d("Login", "LoginServer Completed");
                }
            }
        }
    };
    LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver,
        new IntentFilter(LoginDDPState.MESSAGE_ERROR));
    LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver,
        new IntentFilter(LoginDDPState.MESSAGE_CONNECTION));
    if (LoginDDPState.getInstance().getState() == LoginDDPState.DDPSTATE.Closed) {
        showError("Connection Issue", "Error connecting to server.. try again");
    }
}
이와 같은데 MESSAGE_ERROR와 MESSAGE_CONNECTION을 각각 수신하도록 등록해서 오류처리와 접속 후 상태처리를 해주면 된다.

DDPBroadcastReceiver의 경우는
private void DDPBroadcastReceiver() {
    DDPBroadcastReceiver mDDPReceiver = new DDPBroadcastReceiver(LoginDDPState.getInstance(), this) {
        @Override
        protected void onDDPConnect(DDPStateSingleton ddp) {
            super.onDDPConnect(ddp);
            // add our subscriptions needed for the activity here
            ddp.subscribe("get", new Object[] {});
        }
        @Override
        protected void onSubscriptionUpdate(String changeType,
                                            String subscriptionName, String docId) {
            if (subscriptionName.equals("get")) {
            }
        }
        @Override
        protected void onLogin() {
            // update login/logout action button
            findViewById(R.id.linearLayoutLogin).setVisibility(View.INVISIBLE);
            findViewById(R.id.linearLayoutLogout).setVisibility(View.VISIBLE);
        }
        @Override
        protected void onLogout() {
            // update login/logout action button
            findViewById(R.id.linearLayoutLogin).setVisibility(View.VISIBLE);
            findViewById(R.id.linearLayoutLogout).setVisibility(View.INVISIBLE);
        }
    };
}

이와 같이 DDP연결 시 처리 - 여기선 get 이라는 이름의 subscribe 를 신청함 - 와 subscription의 갱신, 로그인/로그아웃시 처리를 해주면 된다.


반면, onPause의 경우는 간단한데
@Overrideprotected void onPause() {
    super.onPause();
    if (mReceiver != null) {
        LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
        mReceiver = null;
    }
    if (mDDPReceiver != null) {
        // unhook the receiver
        LocalBroadcastManager.getInstance(this)
                .unregisterReceiver(mDDPReceiver);
        mDDPReceiver = null;
    }
}
이와 같이 onResume 시에 등록했던 Receiver들을 unregisterReceiver 해주면 된다.

그러면 실제 구현을 해보자.
onCreate에 login/logout 버튼을 연결하여보면
@Overrideprotected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Button btnLogin = (Button)findViewById(R.id.buttonLogin);
    if (btnLogin != null) {
        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LoginDDPState.getInstance().login(
        ((TextView)findViewById(R.id.editTextUserName)).getText().toString(),
        ((TextView)findViewById(R.id.editTextPassword)).getText().toString()
                );
            }
}); } Button btnLogout = (Button)findViewById(R.id.buttonLogout); if (btnLogout != null) { btnLogout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { LoginDDPState.getInstance().logout(); } }); } }

이와 같이 구현할 수 있다.
별로 어려운 건 없다. js 의 경우 callback에 해당하는 Receiver 들만 꼼꼼하게 잘 구현하면 Meteor의 풍족한 서버환경을 어려움 없이 사용할 수 있을 것이다.


참고자료;

이 블로그의 인기 게시물

ESP32 DevBoard 개봉기

오늘 드디어 손에 넣었다. ESP32 DevBoard!
Adafruit 에서 15개 한정 재입고 트윗을 보고 광속 결제.
그리고 1주일의 기다림. 사랑해요 USPS <3
알리를 이용하다보니 1주일 정도는 광속 배송임.
물론 배송비도 무자비함 -_ㅜ
15개 한정판 adafruit 발 dev board
그놈이 틀림없으렸다.
오오 강려크한 포스
ESP32_Core_board_V2라고 적혀있군요.
ESP32 맞구요. 네네. ESP32-D0WDQ6 라고 써있는데 D → Dual-core 0 → No internal flash W → Wi-Fi D → Dual-mode Bluetooth Q → Quad Flat No-leads (QFN) package 6 → 6 mm × 6 mm package body size 라고 함.
길이는 이정도
모듈크기는 이정도
코어는 6mm밖에 안해! 여기에 전기만 넣으면 BLE+WIFI!
밑에 크고 발 8개 달린 놈은 FM25Q32라고 32Mbit 플래시메모리
ESP8266 DevBoard 동생이랑 비교 크고 아름다운 레귤레이터랑 CP2102 USB Driver가 붙어있음.
ESP8266 DevBoard엔 CH340G 인데 확 작아졌네.
머리를 맞대어 보았음.
모듈크기는 아주 약간 ESP32가 더 큰데 워낙에 핀이 많고 촘촘함. ESP8266인 ESP12는 핀 간격이 2.00mm인데 비해
ESP32는 1.27mm 밖에 안함.
딱봐도 비교가 될 정도.
https://www.sparkfun.com/news/2017 크고 아름다운 Pinouts

ESP8266 보드랑 별로 안달라보인다.
http://www.silabs.com/products/mcu/pages/usbtouartbridgevcpdrivers.aspx#mac
에서 CP2102 드라이버를 설치하고
screen 으로 연결해보자.
내 경우엔 tty.SLAB_USBtoUART 로 잡혔다.
어디서 기본 속도가 115200bps 라고 들은 적이 있어서
screen /dev/tty.SLAB_USBtoUART …

즐거운 Online Prototyping Tool 들

jsbin, codepen, jsfiddle 이런 것들은 일단 생략. 너무 유명한 것들이라.

https://launchpad.graphql.com - node.js 기반 graphQL 연습장. 이것만으로도 충분히 훌륭한 백엔드
https://codesandbox.io/ npm 사용이 가능한 클라이언트 사이드 연습장. webpackbin이 너무 문제가 많아서 찾아본 것.

https://scrimba.com 이건 codesandbox+ asciinema(https://asciinema.org/) 같은 느낌인데 키 녹화와 음성 녹화 기능이 추가되었다. 다 좋은데 화살표 키로 빨리감기 뒤로감기 기능이 안되고 익스포트(youtube등)으로 지원이 없는게 아쉽다.

이 둘이 만나면? https://codesandbox.io/s/jvlrl98xw3?from-embed
뭐야 이거 무서워 하지마 ㄷㄷ;  graphql+react-native-web(부왘ㅋㅋ)

https://repl.it/languages 전통을 자랑하는 REPL 도구. 지원 언어 종류가 -_-;;;;;

https://tio.run/# repl.it? 장난함? 얘는 지원 언어가 무려 386종류. J랑 아희도 있다.

https://play.golang.org/ 즐거운 go playground. 소스 포멧팅 넘 좋아.

http://decaffeinate-project.org/repl/ 최고의 coffeescript REPL. 원래 용도는 coffee를 ecma6코드로 바꾸는 것이지만...

https://scaphold.io
https://www.graph.cool/ graphql backend service. scaphold.io는 설치도 필요없는 클라우드. graphcool은 호스팅+클라우드 다있음. 둘 다 막상막하. 푸쉬서버도 되고 뭐 미친득.

https://glitch.com/ gomix에서 결국 glitch로 안착.  node.js

https://www.shadertoy.com 잘하고 싶다! 쉐이다! 오디오도 된다!

http:/…

graphql 연습 /w launchpad

https://launchpad.graphql.com/mw9wkzv99
단순 전체쿼리+조건쿼리+추가

http://graphql.org/graphql-js/passing-arguments/
참고. random ID는 crypto 1.0.1 사용
  type Query {
    Members: [member]
    getMember(id: ID!): member
  }
  type member {
    id: ID!
    text: String
  }
  input memberInput {
    text: String
  }
  type Mutation {
    addMember(member: memberInput): member
  } SQL 정의. facebook 쪽은 스트링에 지지는 거 진짜 좋아하네. *.gql 파일이 있다고 하니 이해해주자.
resolver는 var buffer = [];
const resolvers = {
  Query: {
    Members: (root, args, context) => {
      return buffer;
    },
    getMember: (id)=> {
      return buffer.find(o=>o.id)
    }
  },
  Mutation: {
    addMember(_, {member}) {
      const mm = { ...member, id:randomBytes(10).toString('hex') };
      buffer.push(mm);
      return mm;
    }
  }
}; 평범 평범.
https://dev-blog.apollodata.com/tutorial-graphql-subscriptions-server-side-e51c32dc2951 다음으로 pub/sub 연습.
https://launchpad.graphql.com/xvn94n3ql   type Subscription {
    memberAdded: member
  } member가 added되는 순간을 감시. imp…