2016년 6월 27일 월요일

Meteor DDP 요점 정리


  1. 준비
    ddp 라이브러리 추가
    $ meteor add ddp
  2. 외부 DDP 연결
    extDDP = DDP.connect("http://externalhost.com:4100");
  3. Collection 연결
    Posts = new Mongo.Collection('posts', extDDP);
    2,3 과정은 client 에서 가장 먼저 실행되도록 client/lib 디렉토리 안에 넣는 것을 추천
  4. 기존 연결을 외부 DDP로 대치
    Meteor.connection = extDDP;
    ex) 활용예
    Meteor.connection = extDDP;
    Meteor.loginWithPassword(login.valiu, password.value);
    Meteor.startup 같은 곳에서 사용하면 좋음.
    만일 login을 그냥 사용하려면
    expDDP.call('login', .... ); 형태로 사용하여야함.
  5. Subscribe 사용
    onCreated 시점에 this.subscribe 대신 DDP 객체의 subscribe를 사용
    Template.postView.onCreated(function() {
      extDDP.subscribe("getPosts", { searchWord: 'blahblah' });
    });
  6. Helper 사용
    3번처럼 했다면 그냥 똑같이 사용
    Tempalte.postView.helpers({
      "posts": function() {
        return Posts.find({});
      }
    });
  7. Method 사용
    Template.postInput.events({
      "submit": function(e) {
        expDDP.call('addPost', inputText.value', function(error, result) {
         /* do something */
        });
        e.preventDefault();
      }
    });

2016년 6월 20일 월요일

microservice 기반 meteor application 설계 /w DDP

특정 업무가 장애가 일어나도 전체가 다운되지 않는 서비스를 Meteor 로 만들 수 있지 않을까 라는 생각을 하고 있었는데 실제로 한번 해보았다.

로긴 서버와 N개의 업무서버가 하나의 DB를 바라보고 클라이언트에서 로긴 서버에 계정생성/접속 후 토큰을 꺼내와서 나머지 서버에 접속하는 그림을 그려보았다.

업무 서버의 경우 웹앱에서 접근 문제가 있기 때문에 CORS를 열어준다.

Meteor.startup ->
  WebApp.rawConnectHandlers.use (req, res, next)->
    res.setHeader "Access-Control-Allow-Origin", "*"
    next()

iOS/Android 앱의 경우는 사실 상관이 없지만 webApp인 경우엔 꼭 필요.

반대로 webApp 인 경우는 client 폴더에만 작업하면 되는데
가장 먼저 실행하는 파일(ex. client/lib) 에 DDP와 Collection 연결하는 부분을 만들어 준다.

# global namespace
@ddps =
  loginDDP : DDP.connect "http://localhost:4100"
  chatDDP : DDP.connect "http://localhost:4200"

# models
@Chats = new Mongo.Collection 'chats', ddps.chatDDP

그리고 Meteor App 이 시작하는 시점에 아래와 같이 구성한다.

Meteor.startup ->
  Meteor.connetion = ddps.loginDDP
  Tracker.autorun ->
    if Meteor.user()?
      unless Meteor.loggingIn()
        console.log Meteor.user()?.username
        ddps.chatDDP.call "login",
         resume: Accounts._storedLoginToken()
        , (e, r)->
          unless e
            console.log "chatDDP login", r
          else
            throw e
    else
      ddps.chatDDP.call "logout", (e)->
        console.log "chatDDP logout" unless e
      console.log "out"

Meteor.connection을 login의 DDP로 지정해놓으면 마치 자기 서버인것 처럼 쓸 수 있어서 편리하다.
그리고 Meteor.user() 가 Reactive Datasource 이므로 Tracker.autorun 안에서 로그인 서버에서 로그인/아웃시 다른 업무 서버도 로그인/아웃 하도록 DDP.call "login" method를 불러준다.

단,  DDP 로그인 시 Accounts 패키지의 _storedLoginToken()을 통해 token값을 꺼내서 로그인 방식과 상관없이 동일하게 처리하면 편리하다.

헬퍼나 템플릿 안에서 쓸때도 기존 방식과 거의 같다.

Template.chatroom.onCreated ->
  ddps.chatDDP.subscribe 'getChats', {}

onCreated 에선 this.subscribe (혹은 @subscribe) 대신 DDP.subscribe를 사용하고 Method.call도 같은 방식으로 ddps.chatDDP.call "addChat", params 와 같이 사용한다.

이와 같은 방식으로 개발 중인데 장단점을 따져보면
장점.
  1. 작게 나눠서 독립적인 서버를 개발하므로 단인 서비스의 복잡도가 낮다.
  2. 오류와 서버다운에 대해 독립적이고 스케일 아웃도 간편
  3. 싼 클라우드 호스팅을 여러개 나눠서 쓸 수 있다.
  4. I/O 부담을 분산한다.
단점.
  1. 개인 개발 서버 환경이 매우 무겁다.(......)
정도인데 꼭 Meteor 가 아닌 다른 프레임웍(Horizon이라던가 Phoenix라던가 혹은 기타 레거시 웹서비스등등)하고 연동도 자유로와서 꽤 연구 가치가 있다.

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


참고자료;

2016년 6월 10일 금요일

내가 underscore라이브러리를 별로 선호하지 않는 이유.


  1. node.js 콘솔을 연다
  2. (function() { return 10; })() --> 10
  3. _ --> 10 (응????????)


REPL에서 function의 반환값이 _에 저장됨. 그래서 _를 조사하면 10이 나옴.
golang에선 _를 blank identifier로 사용.

sum := 0
for _, value := range array {
    sum += value
}

이런 식으로.
논란의 여지는 있지만 단지 길이가 길어서 _를 함수명으로 쓰는 류들에 대해서 별로 좋아하지 않음.
마찬가지 이유로 $를 쓰는 jQuery도 그다지.
결론은 coffeescript 쓰세요. 두번 쓰세요. for in(of) when은 무적입니다.

#하지만 둘 다 같은 사람이 만들었음;