기본 콘텐츠로 건너뛰기

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의 풍족한 서버환경을 어려움 없이 사용할 수 있을 것이다.


참고자료;

이 블로그의 인기 게시물

meteor로 nw.js 개발하기.

실제로 nw.js 어플리케이션을 개발하다보면 UI구현하기 막막하고 수동으로 리프레쉬 하는 것도 귀찮아서 Meteor 연동을 하려고 했더니 생각보다 간단했다.

디렉토리 구조는 먼저 이렇게 잡았다.
`- app
  `-client
  `-public
`- dist
  `- 배포용 html,css,js
  `- package.json
`- package.json 아이디어는 이렇다. nw.js의 시작페이지를 http://localhost:3000으로 두고 배포시엔 meteor client 배포툴인 meteor-build-client를 사용하여 html,css,js 로 분리하는 계획이다.

가장 중요한 nw.js 용 package.json 파일은 아래와 같이 구성한다.
{
  "main": "http://localhost:3000",
  "node-remote": "http://localhost:3000",
  "name": "<앱이름>"
} 이게 전부. 어떻게 보면 Web과 nw.js를 동시에 개발할 수도 있는 환경이기도 한 것이다.
meteor create app 을 해서 meteor용 앱을 만들고 meteor 를 시작한다.
그리고, 위의 package.json이 있는 경로로 돌아가서 nw . 으로 nwjs를 실행한다.

한번 번쩍하더니 잘 된다.
cordova 등에서 index.html 대신 http://localhost:3000을 하는 것도 비슷한 느낌이다.

즐겁게 개발을 일단 마구 하고 실제로 배포하기 위해서는 개발환경이 아니라 html,css,js로 구성된 배포본을 만들어야한다.
npm install -g 해도 되지만 어짜피 Meteor에서만 쓸거
meteor npm install -g meteor-build-client 해버릴거다.

개발은 문제 없어 보이고 배포판을 한번 만들어보자. meteor app 이 있는 경로(meteor run으로 …

RxJS - ReactiveX 인터뷰

A: 왜 RxJS입니까
B: javascript는 참 쉽고 친숙한 언어죠.
A: 별로 그렇게 생각 안합니다만.
B: 그래서 좀 어렵고 있어보이는게 뭘까 싶어서...
A: 네?
B: 함수형이라는게 유행하기도 하고
f(x) 좋쟎습니까? 미스테리~ 미스테리~ 정수정짱짱 으아아

이런 수학선생님이라면 수포자 따윈 A: ...
B: 그리고 반응형이라는 말 뭔가
A: 뭔가?
B: 대충대충해도 막 알아서 할거 같고...
A: 그럴리가요?
B: 안그렇겠죠?
A: 네
B: 네

(잠시만 기다려주세요)

A: 그래도 뭔가 매력이 있으니 이렇게 시간을 내셔서 이것저것 Rx에 대해 글도 쓰고 이야기도 하고 그러시는거 아닌가요?
B: 매력이라.
으음.
제가 팔꿈치 터널 증후근이 좀 있어요.
오른손 세끼손가락, 약지손가락이 저립니다.
A: 무슨 상관이?
B: 그래서 각종 괄호를 쓰는게 너무 힘듭니다.
소중대괄호 만든 사람 죽었으면.
Hello world (ASCII): https://esolangs.org/wiki/Parenthesis_Hell
A: 이미 옛날에 돌아가셨겠죠.
B: 그렇겠네요.
아무튼 그래서 소중대괄호 의존이 적은 커피스크립트를 쓰는데요.
A: 빨리 본론을 말씀해주시죠.
B: 커피스크립트에서 가로로 80자 이상쓰면 Line exceeds maximum allowed length 라고 경고해요.
A: 그래서요?
B: 근데 Rx를 쓰면 코드를 가늘게 쓸 수가 있더라구요.
A: 호오?
B: 그리고 = 쓰는 것도 너무 힘듭니다.
A: 네?
B: 오른손을 쓰쟎아요.
A: ...
그러니까 정리하면
1. 괄호가 힘들다
2. 커피를 쓴다
3. 커피는 길게 쓰면 경고
4. Rx를 쓰면 코드가 가늘다
5. 대입문을 줄이고 싶다.
B: 네
하지만 5번은 생각보다 별로...
A:
B:

B: 아!
A: ?
B: 코드가 가늘어서 좋은 점이.
A: 네.
B: 핸드폰에서 코드를 보기 좋습니다.
A: ... 왜 팔꿈치 터널 증후근이 안 낫는지 알겠습니다.
B: 도와주세요.
쇠고기 사묵으면 나을 것 같습니…

Troubleshooting - Meteor package가 적용이 되지 않을 때

버전 1.5 기준 package.js에서 Package.onUse 에 새 패키지를 추가했는데 인식하지 못하는 경우가 있다.
Package.onUse((api) => {
  api.use([
    'vulcan:core',
    'vulcan:forms',
    'vulcan:accounts' /* <-- 추가함! */
  ]);
...
} 내부패키지건 원격패키지건 안되는 안된다. 이럴 때 meteor add 후 meteor remove 해도 되지만 더 간단한 방법이 있다. meteor update vulcan:accounts 이렇게 update 해주는 방법이 있다. .meteor/package 파일을 건들지 않아서 좋다. 그래도 역시 좋지 않다. Meteor 스럽지 않다. https://github.com/meteor/meteor/issues/7721 현재 1.5.2에서도 해결이 안되었군요. 해결되어 적용되면 다시 글 올리겠습니다.