2015년 5월 3일 일요일

[Unity3D] 유니티에서 최적화 단계 밟기


여기서는 다른곳에서 이미 거론된 Unity3D메모리 최적화에 관련된 이야기를 더 하지 않는다.
texture는 npot를 쓰지말라 라는 등의 이미 나돌고 있는 이야기들은 기본적으로 숙지해야할 이야기들이니, 다른곳을 참조하시라. 개인적으로는 Unity3d를 첫 게임 개발 엔진으로 접한 개발자들에게 도움이 되지 않을까 하는 생각을 한다.

1. Garbage 트레이싱

Profiler를 이용, deep profiler를 사용하여 garbage가 발생하는 구간을 특정해 찾아낸다.

주로 봐야할 부분은 대체적으로 주기적으로 garbage가 축적되어 발생하는 부분들
예를들자면 애니메이션 재생을 위한 스크립트 등에서 주기적으로 garbage가 발생하는 구간.

마냥 garbage를 줄이기 위해 보이는대로 줄여나가기보다는 주기적인 garbage발생처를 우선순위로 생각하자.

대체적으로 작은곳에 함정들이 존재하는데, 매우 작은 garbage가 주기적으로 생성되는 부분이나
key-value boxing이 매우 많은 양이 발생해 묵직한 덩어리가 되는때가 있다.
이런 부분에서는 boxing에 주의가 필요하고, 단순 임시 배열들 (mesh에 대한 vertex입력을 위한 일시 배열 등)은 단순하게라도 pooling을 실시하자.

2. Scene Memory 총량 관리

대체적으로 1개 scene단위로 모든 표시될 가능성이 있는 객체들은 하나도 빠짐없이 scene로딩중에 마치는것이 옳다. 그렇지 않아도 도중에 instantiate할 수 있겠으나. instantiate자체에 현 시점에서는 async로 동작하는 내용이 있지 않기에 덩치가 큰 내용물을 생성할경우 필연적으로 렉이 발생한다.

처음에 숨겨둔 객체들을 다 켜서 메모리의 걱정이 될 정도라면 아에 처음부터 그 scene에 들어가지 않게끔 물량 제어를 하라. 어차피 유저가 모두 한번씩 이용을 한다고 생각하면 최종적으로는 모두 사용될 메모리다.
gc이기에, 잠깐 메모리에 적재했다가 안쓴다고 바로 release한다는 플로우는 생각할 수 없다. gc.collect를 강제로 부른다면 렉의 주범이 될것이고. 반복되는 alloc-dealloc은 최적화에서는 모두 제거되어야할 대상이다.

대체적으로 렉의 주범이 되는 것들은 instantiate의 물량 그 자체와
awake에 맞물려있는 초기화 코드들때문.
디자인 하기 나름이긴 하겠으나, 가급적 Start에서 coroutine을 사용하지 말라.
이는 키자마자 끊어버리면 처음 스택 이후에는 실행 안될 가능성이 높아진다.
나중에 사용할 오브젝트들을 disable상태로 어딘가에 pooling하는 것을 전제로 생각하고
awake에서 비 활성화 상태의 초기화 동작을 모두 마친 후, pooling으로 비 활성화중이던 객체를 생성하라.

Scene단위의 메모리 체크는 특정 scene에 진입한 이후 Profiler - memory - (Simple->Detailed)에서 객체별로 확인하자.

Texture의 용량도 그렇지만 SerializedField에 해당하는 Mono의 개체수도 생각하자. 상상 이상으로 커다란 덩치를 자랑한다. ScriptableObject의 Serialization과 동일한 기능을 이용하고 있을것이라 생각되는데, int 1개에 대해 3KB정도의 용량을 소모하고 있었던 것으로 기억한다. (소스를 보진 않은터라 장담할 순 없으나, 만개정도 생성한후, 비어있는 Mono와의 차이를 쟀을때 이정도였던것으로 기억한다)
운용상에 생긴 garbage등도 포함되어 있으리라.

3. pooling

대체적으로 UI를 보면 주요 이슈가 되는건 이른바 무한 스크롤리스트, 말하자면 pooling scroll list라 할 수 있겠다. 리스트의 종류도 여럿 있는 경우가 대부분이니 (친구, 선물, 퀘스트 등등) 리스트의 entity view구현에 pooling을 제어할 수 있는 인터페이스를 포함해 구현하라. 개인적으로는 오브젝트가 늘어난다고 풀의 구현체를 여럿 만들지는 않는다. 하나로 끝내두고 이를 generic화 한다. gameobject라면 관리용 view에 대해 이를 실시한다.

대체적으로 OnActivate, OnDeactivate, OnRefresh의 이벤트 핸들링이 필요하더라.

4. UI의 객체는 View<->Model로 분할된 코드 디자인으로

사실 이 부분은 호 불호가 갈리는 부분이다. 개인적으로 pooling이 가능한 UI용 객체를 디자인할때, MVVM의 View-Model data binding이 가장 스마트한 방법이라 생각한다. 개인적으론 이 부분의 솔루션으로 Reactive Extension을 이용하고 있으며, 유니티에는 UniRx라는 본가 Rx의 handport 라이브러리가 존재한다. 한번은 타 프레임워크에서 접했었고, 이후에 두어번즈음 독자적인 MVVM을 작성해봤는데 UniRx자체는 신뢰해도 될만한 퀄리티의 라이브러리라 생각된다.

View와 Model을 분할함으로서 처음 디자인할때 다소 복잡하게 느껴지지만 앱 디자인 전체로 봤을때는 나중에 어떤 데이터 처리를 해서 UI상에 반영한다는 상황이 왔을때 일 자체가 심플하게 끝나고 코드 디자인에도 큰 영향을 미치지 않는다.

더불어 pooling을 해야하는 상황이 왔을때 더욱 빛을 발휘한다. view에 대해 자신이 표시할 데이터의 참조만 바꿔주면 끝나기 때문. 대체적으로 view에 대해서는 어떤 데이터를 보러가라는 식으로(table, id)디자인을 하고, 이 레퍼런스를 옮겨주기만 하면 view에 대한 반영이 끝난다. 같은 객체를 재사용해야하는 pooling에 있어서는 보다 일을 심플하게 마칠 수 있다.

5. LINQ사용시의 주의점들

LINQ는 어느 사용자들간에 있어서는 기피 대상으로 자리잡고 있다.
사용은 편하지만 느리다는 말들이 많다. 확실히 극단적인 비교의 예로 Dictionary와 비교를 하자면 느릴수 밖에 없다. 모든 검색이 순차검색인 Linear search에 기반하고 있고, 생각해보면 한 항목마다의 조건부 제어이기 때문에 그럴수밖에 없다.

모든 탐색코드를 LINQ로 실시하란 말은 하지 않는다. 오히려 한번 필터링을 거친 데이터는 필요에 따라 ToDictionary()등의 메소드를 이용해 적절한 자료구조에 배치해야한다.
심지어 foreach는 끝단의 필터링을 마친 데이터에 대해서는 한번은 사용하는게 맞다.
기존 기능의 완전 대체가 아닌 적재 적소에 사용해야할 기능이라 생각하자.

LINQ를 기반으로 한 Functional Programming을 하게되면 코드가 짧아지는 경향이 있을 뿐만 아니라, code에 딱히 주석이 없더라도 어떤 일을 하고 있는 코드인지를 한눈에 알 수 있게된다.

Reactive Extension에서도 IObservable에 대해 LINQ를 사용하고 있는데, 기본적으로 IObservable은 데이터 스트림이긴 하지만 한순간에 큰 데이터가 날아오는 경우가 없기때문에 문제가 되지 않는다.

다만, 현 시점에서 Unity3D의 System.Linq;는 사용하지 않는게 좋다. 그게 iOS빌드라면 더더욱 그렇다. AOT build에서 컴파일 되지 않는 코드가 구버전 Mono Implementation에 존재하고 있고, 현재 Unity3D는 이 해결된 코드가 올라와있지 않다.

대안으로는 https://github.com/RyotaMurohoshi/UniLinq 이 git-repos의 AOT safe한 라이브러리를 대안으로 이용하자, 기존 LINQ코드들을 똑같이 이용할 수 있다. Beta표기라 걱정인가? 문제가 생기면 오픈소스이기에 내부를 들여다 볼 수도 있을것이고. 막상 소스를 살펴보면 일일히 소스에서 for로 찾아가던때가 상기될 것이다. 또한 상기 라이브러리는 개인 개발자의 hand-port-ish가 아니다. 위 기능은 문제가 해결된 mono의 코드에서 발췌한 것이라 들었다.

LINQ의 사용 의의는 코드의 퀄리티의 증진이다. 쉽게 말하자면 읽기 쉬운 코드를 쓰자는 말이다.
비즈니스 로직을 구성하면 끝단의 컨텐츠 구성은 하드코딩일 수밖에 없는데,
그 와중에도 읽기 편한 코드를 구성하려면 이런 기능들을 숙지해 나가는 노력이 필요하다 생각한다.

2015년 3월 25일 수요일

[VirtualBox] Host to guest network access

개요
Virtual Box(OSX)에 인스톨된 CentOS에 대해 Network over access하는 방법을 정리

참고자료
http://askubuntu.com/questions/198452/no-host-only-adapter-selected
http://superuser.com/questions/424083/virtualbox-host-ssh-to-guest
https://www.youtube.com/watch?v=Qz_uj_Po2lA

수순

호스트와 게스트를 연결

1. VirtualBox Preference > Network > Host-only Network 에서 호스트 전용 네트워크 어댑터를 1개 추가.
2. VirtualBox VM #n Settings > Network > Adapter 2(or 3 or 4) 에서
Host-only Adapter에서 #1에서 추가된 어댑터를 추가

인터넷 문제를 해결(CentOS7)
* yum이 안되거나 한다면 일단 이를 확인하자

1. 네트워크 어댑터 목록을 확인
...$ nmcli d
DEVICE TYPE STATUS ...
lo loopback unmanaged
enp0s3 ethernet disconnected // <------

2. ipcfg설정
...$ sudo vi /etc/sysconfig/network-scripts/ifcfg-enp0s3
마지막줄의 ONBOOT=yes로 변경설정

3. service network restart
or reboot

4. 설정을 살핌
ip a

5. 인터넷이 되는지 확인
ping 8.8.8.8

호스트에서 게스트로 연결이 되는지 확인

1. ip a
enp0s8정도 되는 기기의 ip를 특정하자. (Host-only adapter로 추정되는 물건)
이번에는 192.168.56.101이라 나왔음

2. 호스트측에서 접근을 확인
ping 192.168.56.101

3. sshd 설정
yum -y install openssh-server openssh-clients
chkconfig sshd on

4. 외부에서 ssh 접근 확인
ssh [username]@192.168.56.101
이후 비번등을 입력해서 접근.

2015년 3월 22일 일요일

[Chef] Solo 입문 메모

개요
Chef Solo입문에 관한 메모

참고자료
http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/cloud/automation

본문

- Chef는 Code를 통한 Infrastructure Provisioning 툴이다

- 코드로 정의된 인프라 구조를 Recipe라 한다

- Chef는 Server, Solo라는 두가지형태가 크게 있는데,
   Server는 복수대의 서버를 유지관리할 수 있는 기능까지 갖추고 있고,
   Solo는 1개 서버의 설정만 해당된다. Solo는 Server의 일부라 할 수 있다.

- Ruby에 기반한다

- 유사 툴로서 Puppet이 대표적이다.

- Facebook의 인프라 유지에 이용되고 있다.

- Idempotent. 멱등성이 보장된 환경. 같은 코드를 몇번 돌리든 코드가 의도하는 바가 유지된다.

Solo 도입순

chef-dk 인스톨

# path는 https://downloads.chef.io/chef-dk/ 에서
wget https://opscode-omnibus-packages.s3.amazonaws.com/el/6/x86_64/chefdk-0.4.0-1.x86_64.rpm
rpm -ivh chefdk-0.4.0-1.x86_64.rpm 

chef-repo init
# {REPONAME}에 리포지토리의 이름을 지정한다.
chef generate repo {REPONAME}

knife 설정
knife configure

Cookbook 작성
cd {REPONAME}
knife cookbook create {COOKBOOK_NAME} -o cookbooks

recipe json 작성
{PATH-TO-REPO}/localhost.json
{
 "run_list" : [
 "recipe[hello]"
 ]

}

recipe ruby 작성
# solo.rb
file_cache_path "/tmp/chef-solo"

cookbook_path ["/root/{REPONAME}/cookbooks"]

chef 실행
{PATH-TO-REPO}$ sudo chef-solo -c solo.rb -j localhost.json 

Starting Chef Client, version 12.0.3
Compiling Cookbooks...
Converging 1 resources
Recipe: hello::default
  * log[hi there.] action write
  

Running handlers:
Running handlers complete
Chef Client finished, 1/1 resources updated in 2.792128558 seconds

패키지의 설정 사례

default.rb에 대해 다음과 같은 예제를 추가해보도록 하자

log "hi there"

execute 'yum update -y' do
  ignore_failure true
end

package "ImageMagick" do
 action :install
end

yum update실시후, 임의의 패키지 매니저로부터 ImageMagick을 인스톨 하는 설정이다.
패키지의 인스톨 자체에 관해서는 특정 패키지 매니저를 지정하지 않는다.

yum이 아닌 apt-get(ubuntu)이나 homebrew(OSX)와 같은 환경이라면 해당 update는 제각각 추가를 해주고, 다만 실패하는 경우를 대비해서 ignore_failure true옵션을 걸어줌으로서 환경에 독립적인 스크립트를 작성할 수 있다.

chef 실행
{PATH-TO-REPO}$ sudo chef-solo -c solo.rb -j localhost.json

Starting Chef Client, version 12.0.3
Compiling Cookbooks...
Converging 3 resources
Recipe: hello::default
  * log[hi there] action write
  
  * execute[yum update -y] action run
    - execute yum update -y
  * yum_package[ImageMagick] action install
    - install version 6.7.8.9-10.15.amzn1 of package ImageMagick

Running handlers:
Running handlers complete

Chef Client finished, 3/3 resources updated in 14.833352817 seconds

2015년 3월 19일 목요일

[Node.js] Bluebird Promise Coroutine

개요
Bluebird Promise에서의 Coroutine사용방법

참조
https://github.com/petkaantonov/bluebird/blob/master/API.md#generators

필수
Node.js >= 0.11.2(현 시점에서 0.12.0이 stable)

수순

실행 커맨드는 이하와 같다. app을 기동할때 다음과 같은 옵션이 필요하다

node --harmony-generators app.js
혹은
node --harmony app.js

mocha 테스트에 있어서도 --harmony옵션을 붙여서 실행할 필요가 있다.

참조 공식 문서로부터 수정한 coroutine 사용예제 (for sails.js)

 /**  
  * CoroutineController  
  *  
  * @description :: Server-side logic for managing coroutines  
  * @help    :: See http://links.sailsjs.org/docs/controllers  
  */  
 var Promise = require("bluebird");  
 function PingPong() {  
 }  
 PingPong.prototype.ping = Promise.coroutine(function* (val,lim,cb) {  
   console.log("Ping?", val);  
   yield Promise.delay(1); // ms  
   if(val < lim) {  
     this.pong(val+1,lim,cb);  
   }  
   else  
   {  
     cb();  
   }  
 });  
 PingPong.prototype.pong = Promise.coroutine(function* (val,lim,cb) {  
   console.log("Pong!", val);  
   yield Promise.delay(1); // ms  
   if(val < lim) {  
     this.ping(val + 1,lim,cb)  
   }  
   else  
   {  
     cb();  
   }  
 });  
 module.exports = {  
   test:function(req,res,next) {  
     var pingpong = new PingPong();  
     pingpong.ping(0,5000,function() {  
       console.log("done");  
       res.json({"message":"done"});  
     });  
   }  
 };  

결과

Ping? 0
Pong! 1
...
Ping? 4996
Pong! 4997
Ping? 4998
Pong! 4999
Ping? 5000
Pong! 5001
done

2015년 3월 12일 목요일

[uFrame 1.5] MVCVM인 이유

개요
uFrame에서 Controller가 ViewModel과 분리되어 있는 이유

본문

ViewModel은 기본적으로 복수개가 생성되는게 전제이므로, Controller (Business Logics)의 경우 구조적으로 ViewModel에 포함될 경우 ViewModel의 갯수만큼 메소드 역시 복제되는 결과를 낳는다. 결과적으로 메모리상에 같은 로직이 대거 복제되어 버리는 결과로 이어지기 때문에, Controller와 ViewModel은 분리되어 각 모델별로 Controller는 1개씩만 작성된다. 그렇기 때문에 Controller의 각 메소드의 제 1 인수는 ViewModel이다 (1:n의 관계이기에)

2015년 3월 10일 화요일

[NGUI] UISprite에 대한 Custom Shader 적용

개요

NGUI에서 UISprite에 대한 Custom Shader의 적용 방법을 정리하고자 한다.
여기서 쉐이더는 일단 desaturate시키는 쉐이더를 예로 들어보도록 하자.

참조

http://www.tasharen.com/forum/index.php?topic=1256.0

설명
우선, NGUI의 Sprite는 1개의 Atlas단위로 쉐이더가 지정되어있다.
참고 링크에서도 이야기가 되고 있지만. 사실 기본적인 의도대로라면 1번의 drawcall에서 shader의 변경이란게 불가능하기 때문에
(정말로? 음... 사실 Sprite에서 임의의 쉐이더를 정의해놓고 drawcall을 쪼개는 한이 있더라도 선택 운용하게 해주면 좋겟다.)
수순대로라면 drawcall을 분리해서 사용해야하는 수순이 된다. 그래서 NGUI에서는 Atlas단위에 있어서 근본적인 설계딴에서 별도의 쉐이더를 분류하고 있지 않다.

자, 우선 여기서 생각해야할 제약조건은

1. Atlas단위로 shader는 고정되어있다.
2. shader를 분리해서 별개의 atlas를 작성해서 이용한다면 일단 렌더링시에 별도의 drawcall로 운용이 된다. 유색 - 무색 - 유색으로 끼게되면 하나의 텍스쳐라도 3번의 드로우콜이 필요하게 될것이다.

그리고 우리가 여기서 생각해야할 부분은

1. 그렇다면 Atlas는 우선적으로 제각각 작성해야할 것이다.
2. 그런데 완전 같은 이미지 소스에 대한 desaturation임에도 불구하고 별도의 texture를 작성할 필요가 있는가?
 -> 흑백 이미지와 컬러 이미지를 따로 작성한다면 유지보수에 있어서도 귀찮아지는 문제점이 발생할것이고, 행여나 실수를 하게되면 리소스가 결여되는 문제가 생길 수 있다.
 -> 심지어 메모리가 2배로 사용된다. 어찌되었든 바람직하진 않다.

여기서 간단히 생각해볼 수 있는것은. NGUI를 확장한다거나 하는 방법이 아닌
1개의 texture를 각각 참조하는 UIAtlas를 2개 작성하는 방법이 그 해결책의 하나가 될 수 있다고 생각한다.

우선 Desaturate를 실시하기 위한 shader에 관해선
아래 링크에 괜찮은 샘플들이 있으니 이 부분은 가져다 쓰도록 하자.

http://forum.unity3d.com/threads/desaturation-grayscale-shader-for-ios.82105/

그리고 문제의 UIAtlas인데 수순은 다음과 같다.

예를들어 UIItem이라는 atlas가 있다고 하자.

그럼 이 atlas의 구성은 다음과 같을 것이다.

UIItem.prefab
UIItem.mat
UIItem.png

우리는 이중에서 UIITem.prefab, UIItem.mat의 2개 파일을 복사할 필요가 있다.
Unity상에서 이 두개의 파일을 선택하여 Ctrl+D / OSX: Command+D 를 눌러서 복제를 하자

아마 UIItem 1과 같은 느낌으로 파일명이 바뀌었을건데, 여기서 양쪽파일명을 Desaturate 아니면 Gray와 같이 1의 부분을 대치하여 파일명을 정리해주자.

이후에 UIItem.prefab의 UIAtlas Component의 Material을 복제된 mat파일을 보도록 다시 링크를 이어주자 (복제했더니 원본을 보고있더라)
그리고 mat쪽에서 상기 링크에 있는 흑백화 쉐이더를 붙여준다.

이후에 UIItem을 이용하고 있던 Sprite에 대해서 이번에 복제 작성한 UIItem???를 연결시켜주면
에디터에서는 여전히 컬러가 들어간 이미지가 표시되지만 게임화면에서는 흑백으로 표시되는것을 알 수 있다.

UIAtlas의 instance자체에서 텍스쳐에 대한 매핑정보를 보존하고 있기 때문에, 한번 갱신을 했다면 이 복제 atlas도 다시 만들어줄 필요가 있다.
이 방법도 다소 귀찮긴 하지만. 적어도 Sprite의 렌더링에 있어서 가장 큰 크기를 차지하는 텍스처가 중복되지 않아도 된다는 장점이 있다.

NGUI에서 공식적으로 서포트 되어주었으면 하지만, 적어도 그때까지는 이런 방법을 강구할 필요가 있을것 같다.
필요에 따라서는 복제 갱신하는 툴이나 배치를 작성해도 되지 않을까

2015년 2월 21일 토요일

[SocketIO] BestHTTP vs UniWeb

개요
UniWeb, BestHTTP를 SocketIO에 관련해 비교해 보고자 한다.
구입을 고려하는 개발자분들에게 참고가 되었으면 한다
Socket.IO에 관해서는 1.0과 0.9에 있어서 프로트콜상의 차이가 존재한다.
개인적으로 구현자체에 있어서는 BestHTTP쪽에 손을 들어주고 싶다는 생각이 들지만,
만약 서버쪽의 구현이 Socket.IO 0.9버전이며 이를 대체할 수 없다면 UniWeb이나 다른 구현체를 고려해야 하지 않나 하는 생각이 든다.

· BestHTTP UniWeb
가격
Price
55 USD (Pro) / 35 USD (Basic) 45 USD
Socket IO Transport Handshake with Long Polling & Websocket
(서버의 수평확장시 서버에서 Sticky Session이 지원되어야함)
Websocket
SocketIO Version (1.0을 기점으로 앱에 영향이 있다) ^1.0 ~0.9
기본 운용 방식
Basic Operations
From code From Code + Requires Component Attachment
문서화정도
Documentize
Document exists Sample only
Heartbeat Impl O X (+bugs)
Handshake parameters customization ready X (개발자에게 클레임 완료) O (from options)
Async Operation Event-handled Based Coroutine Based
SocketIO Binary Support O X

2015년 2월 20일 금요일

[nginx] nginx load balancer proxing

개요
nodejs는 싱글스레드 기반에서 운용되기 때문에, 멀티코어 환경에서 구동시 CPU를 100% 활용하지 않는다.

따라서, 1개 서버에서 코어수 만큼의 nodejs server를 기동한 환경을 전제로 했을때 nginx측에서 해당 서버의 각 nodejs server instance에 대해 load balancer proxy를 실시해주면 최대한의 자원을 이용할 수 있다.

참고자료
http://sailsjs.org/#!/documentation/concepts/Deployment/Scaling.html
# needs sticky session.

http://nginx.org/en/docs/http/load_balancing.html
#basics
#session presistance (sticky session)

수순

nodejs instance는 forever를 이용해 제각각 구동한다.
로컬의 파일시스템에서의 임시 파일들이 존재하므로, 소스들은 1개 instance단위로 분리해 배치토록 하자.

1. init script for forever nodejs app

우선 forever를 통한 sails.js app의 구동


forever start /<앱1의 경로>/app.js --port 5001 --prod
forever start /<앱2의 경로>/app.js --port 5002 --prod
forever start /<앱3의 경로>/app.js --port 5003 --prod
...

상기와 같은 코드를 /etc/init.d/{service_name}에 init script로서 배치한다.

#!/bin/bash
# chkconfig: 345 20 80
# description: portfolio sailsjs daemon
# processname: portfolio
### 상기 3줄을 통해 chkconfig에서 설정 가능케한다.
# ===============================
# init script
#
# @author nanhaprak
# @link http://nodeqa.com/nodejs_ref/25 # 참고한 설정
# ===============================
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin #forever나 node의 path문제 해결. 일반적인 path설정
APPHOME=/home/portfolio/ #예를들면 이번엔 이렇다.
case "$1" in
  start)
    forever start ${APPHOME}/app1/app.js --port 5001
    forever start ${APPHOME}/app2/app.js --port 5002
    ;;
  stop)
    forever stop ${APPHOME}/app1/app.js --port 5001
    forever stop ${APPHOME}/app2/app.js --port 5002
    ;;
  restart)
    forever restart ${APPHOME}/app1/app.js --port 5001
    forever restart ${APPHOME}/app2/app.js --port 5002
    ;;
  list)
    forever list
    ;;
  *)
    echo ###partial###C6F705E4-0637-40D0-B8A9-DF8D14EF4030quot;Usage: $0 {start|stop|list}"
    exit 1
esac

exit 0

2. nginx websocket load balancer configuration

1에서 nodejs자체의 구동을 완료했으므로, 이어서 nginx의 로컬서버내 로드밸런서 프록시를 작성토록 하자.

현재 작성중인 앱의 경우, socket.io, transport with websocket을 사용하고 있으므로, 프록시가 websocket에 대응될 필요가 있다.

우선 load balancer

/etc/nginx/nginx.conf

일부분만. 아래 upstream이 include되도록 하고, upstream목록은 따로 빼도록 하자.

...
http {
...
    include       /etc/nginx/upstreams;

이하, upstream 목록

/etc/nginx/upstreams

upstream sails_upstreams {
ip_hash; # sticky session support. (중요)
server 127.0.0.1:5001;
server 127.0.0.1:5002;
}

이하, virtual host 설정

/etc/nginx/conf.d/{appname}.conf

server {

    listen 80; # http인경우. https라면 443이 될것이다.
    server_name 호스트명;
    location / {

        # websocket proxy configuration
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_http_version 1.1;

        # general proxy configuration
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Queue-Start "t=${msec}000";

        # pass
        proxy_pass http://sails_upstreams;
    }
}

[redis] install redis on aws ec2 instance

개요
aws에서 각 ec2에 직접 redis를 설치하려 한 경우, 직접 빌드를 하거나 다른 yum repository에서 모듈을 끌어올 필요가 있다.

elasticache는 비용이 들어가기 때문에 포트폴리오에서는 패스... 실제 운용에서는 상관없을법한 내용일듯 하다.

수순

cd /usr/local/src
wget http://download.redis.io/releases/redis-2.8.19.tar.gz
tar xvfz redis-2.8.19.tar.gz 
cd redis-2.8.19
./configure
make -j4 && make install -j4
cd utils/
./install_server.sh 

설치는 상기로 종료.

redis-cli

상기 cli tool을 이용해 진입후 테스트

127.0.0.1:6379> ping
PONG
127.0.0.1:6379> exit

2015년 2월 11일 수요일

[UniRx] Reactive Extension 4 : Array -> Observable

개요
배열을 Observable로 변환하는 방법

수순
IObservable<int> ob = new int[]{1,2,3,4}.ToObservable();

[UniRx] Reactive Extension 3 : DistinctUntilChanged, Throttle

개요

게임의 옵션메뉴에 있어서, 설정한 값들을 바로 저장, 적용하며 과도한 저장 작업이 일어나지 않도록, 메모리상의 데이터만 변경을 실시간으로 실시하며, 일정시간 변경이 없을때에 한정하여 저장작업을 실시하는 로직을 UniRx를 이용하여 표현 해 보고자 한다.

소스
   Observable  
                .EveryUpdate ()  
                .Select (_ => _hashConfigs (optionControl))  
                .DistinctUntilChanged ()  
                .Do(_=> {  
                     // update volume data on distinct.   
                     this._updateVolume(optionControl);  
                })  
                .Throttle (new TimeSpan(0,0,1))  
                .Do (_ => {  
                     // throttled value will be stored in 1 sec  
                     this._storeData(optionControl);  
                })  

updateVolume() 에서는, 내부적으로 구현되어있는 AudioManager에 대하여 볼륨을 적용하는 로직이 포함되어 있다.

optionControl은 ViewModel이다.

_hashConfigs는 ViewModel이 가지는 당시 시점에서의 데이터값의 hash값이다.
예를들어 MasterVolume,BGMVoulme,SEVolume이란 세가지 property가 있다고 가정한다면 이하와 같이 구현된다.

 int _hashConfigs(OptionControlViewModel optionControl) {  
           return      optionControl.MainVolume.GetHashCode () ^  
                     optionControl.SEVolume.GetHashCode () ^  
                     optionControl.BGMVolume.GetHashCode ()     ;  
      }  

상기 로직이 의미하는 바는

EveryUpdate() : 매 프레임마다 체크
.Select(_=> _hashConfigs(optionControl)) : 해싱값을 검사
.DistinctUntilChanged() : 변동이 있는 경우만을 추려서
.Do : 실시
.Throttle() : 일정 기간(1초) 동안 입력이 있었던 자료중의 마지막 하나.
.Do : 실시.

위와 같은 느낌으로 설명될 수 있다.

첫번째 Do의 경우, Distinct를 통해 중복값 만을 추려낸, 연속 입력에 개의치 않는 값이 주어지므로, storage에 기록하는 내용을 제외한 실질적인 볼륨 적용의 로직이 적용되는 것이 적절하다.

Throttle에서 연속입력이 추려지고 나면, 마지막의 한번에 대해서만 storage에 저장하는 로직을 적용한다면 무의미하게 많은 저장을 실시해 높은 비용을 지불하지 않고도 적절한 타이밍에 저장을 할 수 있다.

2015년 2월 6일 금요일

[uFrame 1.5] Customize Loading Scene

개요
현 시점에서 Loading Scene에 관한 문서화가 이루어져 있지 않은 관계로 사용방법을 정리하고자 한다.

참고
uFrame에 기본적으로 첨부되어있는 Loading scene을 참고

기본적으로 LevelLoaderView가 uFrameComplete에 구현되어 포함되어 있다.
OnGUI기반으로 작성된 UnityGUILevelLoaderView는 이 LevelLoaderView를 inherit하고있으며, 동일한 구조로 작성하면 된다.

Progress는 각 SceneManager의 *Scene.cs의 Load에서 progress에 대해 작업중인 메세지와 progress factor를 0.0-1.0 사이의 값으로 feedback해주면 된다.

수순

1. NGUI

이번에는 NGUI를 활용하여 Loading Scene을 구성해보고자 한다.

NGUI 3.x의 버전(이번에는 3.7.7)에서 하나의 UIRoot이하 UIWidget은, 각각 하나의 Layer를 가질 필요가 있다. Loading Scene은 로딩이 완료될때까지는 해당 Object가 파기되지 않으므로, 두개의 UIRoot가 공존하게 된다. 따라서 Loading용 UI는 Layer를 다르게 설정해 기존 UI와 구분되게할 필요가 있다.

이를 수행하지 않은 경우, 기존에 UIRoot에 대해 Anchor설정을 실시한 Object들은 제각각 알맞은 위치를 참조하지 않는 버그가 존재한다.

http://www.tasharen.com/forum/index.php?topic=11126.0

따라서, Loading용 UI레이어는 LoadingUI라던가 하는식으로 기존의 레이어와는 분리토록 하자.

그 이외에는 NGUI Slider나, 기타 NGUI용 위젯들을 이용하여 화면을 구성하고, 관련된 조작을 LevelLoaderView Inheritance에서 설정하면 되겠다.

2. LoadingSceneObject : MonoBehaviour

Loading scene용 Object는 DontDestroyOnLoad옵션이 걸려 이전 화면과 다음 화면의 transition이 종료될 때 까지화면에 존재하다, 특정 타이밍에 모두 파기되게 된다.

이를 위해서 Loading용 Scene의 GameObject에는 파기를 위한 마커와 같은것이 존재하는데, uFrame에서는 LoadingSceneObject : MonoBehaviour 와 같은 형태로 준비되어있다.
기본적으로 샘플로 제공된 Loading Scene의 LoadingSceneView라는 GameObject와 같은 구성에서 그 하위에 오브젝트들을 구성하면 LoadingSceneObject가 이미 첨부되어있기에 상관없으나, 그 밖에 화면을 구성하고자 한다면 LoadingSceneObject를 각 GameObject에 붙여두도록 하자. 이를 실시하지 않은 경우 Loading이 끝나더라도 오브젝트가 파기되지 않는다.

3. LevelLoaderView inheritances

Loading Scene의 LoadingSceneView(GameObject)에 LevelLoaderView의 inheritnace를 붙여줄 필요가 있다. UnityGUILevelLoaderView.cs를 참고하면 알겠지만, 아래에 간단하게 NGUI를 활용한 경우의 구성을 첨부한다.

 using UnityEngine;  
 public class CustomLevelLoaderView : LevelLoaderView  
 {  
      public UISlider m_slider;  
      public UILabel m_label;  
      public void Update() {  

           if(this.m_slider != null)
           {
                this.m_slider.value = Model.Progress;  
           }
           
           if (m_label != null) 
           {  
                this.m_label.text = Model.Status;  
           }  

      }  
 }  

4. Progress Feedback

예를들어 ItemScene.cs라는 SceneManager가 있다고 가정해보자, 그렇다면 해당 SceneManager의 경로는

{UnityProject}/Assets/{uFrameProject}/SceneManagers/ItemScene.cs

와 같은 경로가 될것이고, 정상적으로 작성된 SceneManager라면 아래와 같은 맴버 함수가 있을거라 생각된다.

public override System.Collections.IEnumerator Load(UpdateProgressDelegate progress)

여기서 progress에 대해 피드백을 실시하면 앞서 작성한 loading scene에서 LoadingSceneViewModel에서 보고된 데이터값을 subscribe하여 화면에 출력해주게 된다.

무의미하지만 적당하게 coroutine으로 작성된 예시를 보이자면

for (int i=0i<5i++) {
   progress("dummy "+i,i/5f);
   yield return new UnityEngine.WaitForSeconds(0.25f);        
}


와 같이 사용해볼 수 있겠다.

2015년 1월 20일 화요일

[UniRx] Reactive Extension - 2 IObservable의 형 변환

개요
IObserable<T>의 운용에 있어서 가장 기초적인 이해를 위해 T가 의미하는 바와 그 운용방법을 정리하고자 한다.

본론
IObservable<T>는, Rx에 의해 정의된 데이터 스트림의 인터페이스이다.
앞의 포스팅의 기본 정의를 보면 이벤트가 총 3개가 있는것을 알 수가 있다.

OnNext(data);
OnError(exception);
OnCompleted();

OnCompleted에 대해서는 이렇다할 데이터가 넘어오지 않는다.
이는 어디까지나 데이터의 흐름이 끊겼음을 알려주는 신호일 뿐이다.

단순하게 Coroutine의 대용으로 생각해 버리기에는 Rx의 탄생배경과 그 근간이 조금 다르다.
Coroutine은 비동기의 흐름을 하나의 thread에서 수행하기 위해 본래 1개의 callstack을 임의로 분할해(yield에 의해) 운용하는 것으로, 비동기 처리에 초점이 맞추어진것이라 한다면.

Rx의 경우 어떤 데이터가 지속적으로 변하는것을 전제로 생각되어진, 그 변화에 반응하는 반응형 프로그래밍 기법이라 할 수 있을것 같다.

이러한 반응성 프로그래밍을 하기 위해서 사용되고 있는 대표적인 기법이 Observable pattern이고, IObservable<T>는 그 인터페이스라 할 수 있다.

IObservable<T>는 T가 지속적으로 변화할 데이터이며, 이를 감시하겠다는 것을 뜻한다.

따라서 이 Observer를 subscribe한다는 말은 어떤 감시 룰을 실제로 운용하겠다는 것을 의미한다. 이 Observe라는 행위를 통해, IObservable<T>는 운용이 된다.

기본적으로 generic인 이 인터페이스를 운용하려 할때, 원하는 데이터가 변화할때가 있다. 예를들면 JSON Parsing라던가가 그 예다. 이미 generic에 의해 타입이 지정되어 있는 이 type의 observable에 대해, 다른 타입으로 어떻게 변환하는가에 대한 의문을 가질 수 있을듯 하다.

예를 들어서 ObservableWWW.Get()에 의해 웹서버에 있는 json파일을 가져와 이를 parse한다 생각해보자.

uFrame에서 공식적으로 채택되어 있는 JSON parser는 simpleJSON이므로, simpleJSON를 기준으로 예를 들어볼까 한다. 다른 implementation을 사용한다고 하더라도, 이해하는데 큰 차이는 없을거라 생각된다.

var owww = ObservableWWW.Get(url).Select(src=> JSON.Parse(src));

위와 같은 Chaining으로 변환이 가능하다.

이후에 owww를 subscribe하면 간단하게 parsing이 완료된 데이터를 얻을 수 있다.

ObservableWWW.Get(url)은, IObservable<string>이다.
여기서 Select라는 LINQ구문의 확장을 사용하면 src에 대해 가공을 실시할 수 있고, 그 결과 IObservable<JSONNode>가 반환된다.

[UniRx] Reactive Extension - 1 : Observable 직접 작성하기

개요

UniRx를 사용함에 앞서 가장 기본이 되는 Observable작성방법을 생각보다 찾기 힘들었던 관계로 글로 남겨두고자 한다.

본론

Rx는 Promise++ specs를 따르고 있다고 한다.
https://gist.github.com/staltz/868e7e9bc2a7b8c1f754

Promise/A+사양인 bluebird의 경우, 아래와 같은 형태의 Promise를 정의하는 것이 가능하다.

// Definition

var Promise = require("bluebird");
var example = new Promise(function(resolve,reject) {
    try {
    // something do
    var some_result_value = ...;
    // then
    resolve(some_result_value);
    } catch(ex) {
    reject(ex);
    }
});

// Usage

example
.then(function(some_result_value) {
// some_result_value is coming.
}).catch(function(ex) {
// ex is coming.
});

Rx의 경우 Promise의 일종이기때문에 비슷하게 정의를 할 수가있는데
위와 같은 정의를 하자면

// Definition

var example = Observable.Create<T>(observer=> {

    try {
       // something do
       T some_result_value = ...;
       observer.OnNext(some_result_value);
    } catch(ex) {
        observer.OnError(ex);
    }
    observer.OnCompleted();

    return Disposable.Create(()=> {
         // something dispose here if required
    });

});

// Usage

example.Subscribe(some_result_value=> {
      // some_result_value is coming
},ex=> {
      // ex is coming
},()=> {
      // OnCompleted calling
});

위와 같이 사용할 수 있다.

ex를 알기 쉽게 하기 위해 try catch를 넣었으나, 비동기 코드를 작성하려 할 경우 일반적인 비동기 운용에 기반한 코드를 작성하고, 필요한 시점에 위에 적힌것과 같은 이벤트 호출을 실시하면 된다.

혹은 간단하게 Coroutine용 IEnumerator function을 작성한 후, Observable.FromCoroutine()을 사용하는 방법도 있다.

2015년 1월 18일 일요일

[uFrame 1.5] Diagram에서 일일히 connect하지 않고 직접 transition하기

개요

사실 다이어그램을 사용해서 플로우를 정리하는 것을 전제로하는 프레임워크에서 이런 설정은 사용에 따라 독이 될수도 있지만, 어떤 구조에서는 이 일일히 연결하는 작업이 오히려 구성에 있어서 중첩구현, 구조의 복잡화 등을 야기할 수 있다고 생각이 된다.

따라서 diagram에서 scene transition 설정을 일일히 설정하지 않고 암묵적으로 고정값으로 transition을 수행하고 싶은 경우 아래와 같은 실행코드를 command의 실행내용에 첨부하자.

수순

전체에서 공유할 1개의 subsystem과 그 기본 element를 각각 작성후, element에 대해서 공유할 커맨드를 추가하고, 각 subsystem에서 이 element름 참조하여 inheritance base로서 상속하여 Execute{COMMAND}를 각 view에서 호출 가능하게 한것을 전제로 한다.
그냥 다른 subsystem에서 view를 구성하여, 연결하지 않은 상태의 scene에서 공통적인 view를 호출하면 그만인 문제였다. 단, 각 Command는 사용하기 위해서 Registered Instance에 등록 될 필요가 있다. 상속하게 되는경우 base만큼의 메모리가 중첩 사용되어서 메모리 사용에 있어서 좋지 않다.

다이어그램에서 이 COMMAND는 따로 transition설정치 않고, COMMAND의 실행 구문에 아래의 코드를 추가하여 별다른 설정없이 특정 scene으로 transition하게끔 설정한다.

{SceneManager} = 작성한 scene manager의 이름
{LevelName} = scene file name
            GameManager.TransitionLevel<{SceneManager}> (  
                (container) =>   
                     {   
                          container._{SceneManager}Settings = new {SceneManager}Settings ();   
                          // something do on finished.  
                          Debug.Log("transition done");  
                     },  
                     new string[]{"{LevelName}"}  
           );  
상기의 기본 코드를 기반으로 작성한 generic function
 
 using System;

 /// <summary>  
 /// Transition uFrame Scene.  
 ///   
 /// Recommend place this function on singleton class or statically
 ///   
 /// Synopsys  
 /// TransitionScene<TitleScene> ("TitleScene");  
 ///   
 /// Compitable with uFrame 1.5.1 rc2  
 /// @author Donghyun You  
 /// @since 2015. 1. 18.  
 /// </summary>  
 /// <param name="sceneName">Scene name that registered on builtin levels</param>  
 /// <param name="onFinished">On finished.</param>  
 /// <typeparam name="T">The 1st type parameter.</typeparam>  
 public void TransitionScene<T>(string sceneName,Action onFinished=null) where T : SceneManager {  
      if (string.IsNullOrEmpty (sceneName))   
      {  
           throw new ArgumentNullException("sceneName");  
      }  
      Type type      = typeof(T);  
      onFinished      = onFinished ?? delegate() {};  
      GameManager.TransitionLevel<T> (  
           (container) =>   
           {   
                var settingParam = container.GetType ().GetField("_"+type.Name+"Settings");  
                var settingInstance = Activator.CreateInstance(Type.GetType (type.Name+"Settings"));  
                settingParam.SetValue(container,settingInstance);  
                // something do on finished.  
                onFinished();  
           },  
           new string[]{sceneName}  
      );  
 }  

2015년 1월 16일 금요일

[uFrame 1.5] 개발규약

 개요
uFrame을 통한 개발에 있어서 일부 알아두어야할 사전 규약, 구조중 별도 문서화 하기 애매한 기능들을 이하에 정리.

상시 수정함.

본문

1. RenderSettings의 설정
    uFrame에서는 Scene단위로 RenderSetting이 가능하도록 RenderSettings와 같은 내용이
   GameManager에 준비되어 있다. 해당 Scene으로 이동했을때 마다 GameManager>RenderSettings에 설정된 내용대로 다시 설정된다.

   일단 기존의 RenderSettings에서 설정후, GameManager의 LoadFromScene버튼으로 바로 불러오는것 또한 가능하다.

[uFrame 1.5] Scene Transition

개요
uFrame에서 scene transition을 가능하게 하기까지의 수순을 정리
생각보다 복잡하다.

수순

1. SceneManager를 작성
2. SceneManager에 대해 1개의 subsystem을 작성. 이를 connect한다.

subsystem은 scene을 transition할 command를 가지는 1개의 element를 필요로 하며, view또한 필요로 한다.

3. 1개의 SceneManager용으로 작성된 Subsystem에 대해 root element1개를 작성
4. root element에 대해 view를 1개 작성하고 이를 root element에 연결한다.
5. root element에 scene transition용 command를 작성한다.
6. scene간의 이동이므로 적어도 상기 2개의 scene을 구성한다.
7. 각 SceneManager에 대해 각각 transition을 작성, 이동할 scene과 연결한다.
8. Save and compile
9. SceneManager에 대해 실제 Scene을 작성한다(SceneManager의 context menu)
10. 작성된 scene에 4에서 작성한 view를 각각 화면상의 특정 오브젝트에 attach하거나 create함.
11. 각 scene의 SceneManager의 instance에서,<COMMAND> Transition -> Scene의 목록을 늘려 이동할 scene파일의 이름을 지정한다.
12. 작성된 scene파일을 build target에 추가한다
13. Loading이라는 특수 scene또한 build target에 추가한다.
14. 게임을 실행
15. 게임을 실행하면 view component의 inspector는 그 내용이 달라져있다. 제일 아래의 command에서  transition용 command를 실행.

위에서는 정적인 scene에 대해 이동하게 설정하나, string으로 scene파일을 지정하게끔 설정하는것 또한 가능하다. command에 대해 string을 받게끔 설정하고, SceneManager 소스코드에서public override IEnumerable<string> GetToTitleSceneScenes (string arg)를 override하여 return []{arg};와 같이 설정하여 SceneManager insepector에서 설정한것이 아닌 Command의 호출시의 인수에 의거하여 Scene을 이동하게끔 설정한다 (공식 video tutorial에 나와있는 내용)

http://invertgamestudios.com/uFrameAPI/Default/webframe.html#Scene%20Transitions.html

2015년 1월 15일 목요일

[uFrame 1.5] uFrame에서 특정 타입을 diagram에서 이용 가능하게함

개요
uFrame의 plugin을 작성하여, uFrame에서 특정 타입이 이용 가능하게 함.

수순
uFrame의 기본적인 플러그인 소스코드가 아래 패스에 배치되어 있음.
이를 참고로, 동일한 플러그인을 작성하여 Editor폴더 이하에 배치하면 로드됨.

Editor는 굳이 uFrameComplete폴더 이하일 필요는 없음

[Unity Project/Assets]/uFrameComplete/uFrame/uFramePlugins/CorePlugin/Editor/UFrameEditorPlugin.cs

namespace dependancies

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using Invert.StateMachine;
using Invert.uFrame;
using Invert.uFrame.Code.Bindings;
using Invert.uFrame.Editor;
using Invert.uFrame.Editor.ElementDesigner;
using Invert.uFrame.Editor.ElementDesigner.Commands;
using UniRx;
using UnityEngine;


Inheritance

public class UFrameModelTypesPlugin : DiagramPlugin


Required Overrides
 public override decimal LoadPriority  
   {  
     get { return 1; }  
   }  
   public override bool Enabled  
   {  
     get { return true; }  
     set {}  
   }  
   public override void Initialize (uFrameContainer container)  
   {  
     var typeContainer = uFrameEditor.TypesContainer;  

     // Color는 기본 설정에 포함되어있지 않음.
     typeContainer.RegisterInstance (new ElementItemType () {  
       Type = typeof(Color),  
       Group = "",  
       Label = "Color",  
       IsPrimitive = true,  
       IsUnityEngine = true,  
     },"Color");  

   }  

현재 1.5rc2버전을 기준으로
Namespace이하에 배치된 타입에 관해서는 자동 생성에서 하위 namespace route가 없어져버리는 버그가 발생하고 있다. 다른 해결방법을 찾지 못한다면 자동생성후 매번 using namespace ...를 해줘야하는 불편함이 있다.

2015년 1월 4일 일요일

[Sails.js] Sailsjs에서 crontab

개요
sails.js에서 내부 service, model등이 이용 가능한 상태로 구동중인 sails.js app에서 crontab실행을 실시

참고자료
https://github.com/balderdashy/sails/issues/2092#issuecomment-56043637

수순
상기 url에 나와있는대로 sailsjs용 hook을 작성해, config에서 job의 내용을 설정. 실행함.
sails.js의 hook의 스펙이 알기 어려운 경우 sails-generate-hook을 도입해서 sails generate hook [hookname]함

2015년 1월 2일 금요일

[Sails.js] Sailsjs에서 Unit Testing

개요
nodejs, 정확히는 javascript로 상용프로젝트상에서 작업을 진행하기에 있어서, TDD를 적용한 프로젝트진행이 필요하다는 이야기가 들려서 한번 Unit Testing기능을 확인해보고자 한다.

Javascript는 여타 언어와는 다르게 실행되기 전까지는 코드의 문제 조차 확인되지 않는 인터프리터의 일종이므로, 각 기능에 대한 단위별 테스트 기능, 이른바 유닛 테스트의 자동화를 실시할 필요가 있다.

통상적으로 중장기 프로젝트가 아니면 TDD의 도입 자체는 다시한번 생각해봐야 할 부분이긴 하나, 상기 이유때문에 테스트 자동화가 필요하다면 일단 한번은 짚고 넘어가야되지 않는가 하는 생각이 든다.

TDD의 도입을 꺼려하는 이유중의 하나로, 하나의 기능을 만듦으로 인해서 추가적으로 작성되어야할 테스트 코드와, 기능의 사양변화가 빈번한 이 업계에 있어, 이런 변화에 대한 TDD를 위한 유닛 테스트 코드의 추가적인 작성, 변동이 들어지는데 sails.js에서도 이러한 커스텀 코드는 작성 하지 않으면 안되지만, 그래도 어느정도 구조가 잡혀있기 때문에, 비교적 간편하게 테스트 코드를 작성하는 것이 가능하다.

sails.js에서는 mocha라고 하는 테스트 모듈을 공식적인 테스트 모듈로 채택하고 있고, 다른 복잡한 방법을 사용하지 않는다면, 거의 테스트를 위한 코드이 작성 만으로 작업을 마칠 수 있다.

수순
http://sailsjs.org/#/documentation/concepts/Testing
기본적으로 상기 문서의 수순만으로 거의 모든 테스트 환경을 구축할 수 있다.
중간중간 필요로 해지는 모듈은 npm을 통해서 도입하도록 하자.

단, mocha.opts의 설명이 "그냥 이 페이지에 나와있는 설명을 참고로 작성하시오"
라 되어있는데, 이 부분에서 test를 위한 파일이 로드 되게끔 설정할 필요가 있다.
다른 옵션들이 추가된다고 하더라도, mocha.opts의 후미에 하기옵션을 붙여놓도록 하자.

test/**/*.test.js IRC채널에서 힌트를 던져준 da_wunder씨에게 감사의 말씀을 전한다.
하기는 당시 참고로 던져준 샘플이다.
https://github.com/tarlepp/angular-sailsjs-boilerplate/blob/master/backend/test/mocha.opts