tag: 개발 이야기

iterm에서 zsh 사용할 때 `Opt + 방향키` 설정하기

2016년 4월 10일

이전에도 iterm을 설치했었지만 키맵이 영 익숙해지지 않고 기본 터미널과 맞추려니 이것저것 찾아보는게 귀찮아서 계속 터미널을 사용하고 있었다. neovim을 설치하는 차에 iterm3 베타가 나왔다는 얘기가 생각나서 iterm도 설치했다.

Opt + 방향키로 단어 사이 이동을 종종 하는 편인데 iterm 키맵엔 이 설정이 포함되어 있지만 쉘에서 추가적인 설정이 필요하다. 구글링 해보면 ~/.inputrc에 다음과 같이 추가하면 동작한다고 하는데 이 방법은 bash를 사용하는 경우에 해당한다.

"\e\e[D": backward-word
"\e\e[C": forward-word

zsh의 경우는 ~/.zshrc에 다음처럼 추가하면 된다.

bindkey "\e\e[D" backward-word
bindkey "\e\e[C" forward-word

추가. 설정할 때는 몰랐는데 iTerms의 키맵이 어떻게 되어 있는가에 따라 다르다. junho85님의 경우는 아래 키맵으로 설정했다고 한다.

bindkey -e
bindkey "^[[1;9C" forward-word
bindkey "^[[1;9D" backward-word

Gradle로 Java 프로젝트 시작하기 요약

2016년 4월 2일

spring의 gradle로 프로젝트 시작하기를 따라하며 정리한 글이다.

먼저 brew로 java와 의존성 및 빌드 관리/자동화 도구인 gradle을 설치한다.

$ brew tap caskroom/cask
$ brew install brew-cask
$ brew cask install java
$ brew install gradle

문제없이 설치되었다면 버전 정보를 출력한다.

$ gradle -v

gradle로 프로젝트를 초기화한다.

$ gradle init

초기화하면 기본적으로 gradle wrapper를 생성해주는데 이 스크립트는 gradle이 없는 환경에서도 gradle을 사용할 수 있도록 돕는 스크립트다.

예제 클래스를 먼저 작성한다.

$ mkdir -p src/main/java/hello
// src/main/java/hello/HelloWorld.java

package hello;

public class HelloWorld {
  public static void main(String[] args) {
    Greeter greeter = new Greeter();
    System.out.println(greeter.sayHello());
  }
}
// src/main/java/hello/Greeter.java

package hello;

public class Greeter {
  public String sayHello() {
    return "Hello world!";
  }
}

빌드와 관련한 모든 설정은 build.gradle에 담겨 있다. 빌드를 위해 다음 내용을 build.gradle에 추가한다.

apply plugin: 'java'

그리고 빌드를 하면 build 디렉토리를 생성하고 빌드를 진행한다.

$ gradle build

아래 내용을 추가해서 어플리케이션을 직접 구동할 수 있다.

apply plugin: 'application'
mainClassName = 'hello.HelloWorld'

gradle을 설치한 환경에서는 gradle을 사용해도 되겠지만 다음과 같이 앞에서 생성한 wrapper를 사용해서 구동할 수 있다.

$ ./gradlew run

다음은 튜토리얼에서 최종적으로 작성하게 되는 gradle 파일이다.

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'application'

mainClassName = 'hello.HelloWorld'

// tag::repositories[]
// 서드파티 라이브러리의 소스 출처를 추가한다
repositories {
    mavenCentral()
}
// end::repositories[]

// tag::jar[]
// 빌드에서 jar를 생성할 때 메타를 추가한다
jar {
    baseName = 'gs-gradle'
    version =  '0.1.0'
}
// end::jar[]

// tag::dependencies[]
// 버전 의존성을 추가한다
sourceCompatibility = 1.8
targetCompatibility = 1.8

// 의존 라이브러리를 추가한다
dependencies {
    compile "joda-time:joda-time:2.2"
}
// end::dependencies[]

// tag::wrapper[]
// wrapper로 설치할 gradle version을 정한다
task wrapper(type: Wrapper) {
    gradleVersion = '2.3'
}
// end::wrapper[]

tmux 사용에 도움되는 설정과 플러그인 정리

검은 것은 배경이요 흰 것은 글씨니, 터미널 환경을 더 편하게 사용할 수 있는 tmux 설정기

2016년 2월 29일

최근에 구입한 Dell 노트북에 조금이라도 가볍게 사용해보려고 Lubuntu를 설치해서 사용하고 있다. 트랙패드가 예전에 비해 많이 나아지긴 했지만 아무래도 맥북에서 사용하던 것과는 많이 달라서 좀 더 키보드 친화적인 환경을 꾸려야겠다는 생각이 들었다. 그러던 중 tmux와 다시 친해질 기회인 것 같아서 tmux를 설치하게 되었다.

어제 tmux 이야기를 트위터에 올렸더니 ujuc님이 powerline이란 멋진 tmux 플러그인을 소개해주시고, 사용하는 rc 파일을 공유해주셨다.

만약 tmux 플러그인에 관심이 있다면 이 페이지가 도움이 된다.

그 외에도 간단하게 설치할 수 있는 것도 많이 보였다.

구글 검색해보면 이것저것 유용한 도구가 많이 나온다.

tmux 설정 되돌리기

tmux.conf 재미있는 점이 default 파일이 존재하지 않는다는 점이다. 설정 하나를 변경하면 기존 설정을 알지 못하는 이상 다시 기본 설정으로 돌아갈 수가 없다. 그 눈 아픈 기본값 초록색 상태 막대로 한번에 돌아갈 방법이 없다는 뜻이다.

그래서 tmux 기본 설정을 어딘가 추출해서 보관해두면 다시 돌아오는데 편리하다. 현재 tmux에 설정된 값은 다음 명령어로 추출할 수 있다.

$ tmux show -g | sed 's/^/set-option -g /' > ~/.tmux.current.conf

구글링 해보면 멋지게 꾸며진 tmux.conf를 많이 볼 수 있다. 나처럼 설정을 잘 모르고 적용했다가 명령을 시작하기 위해 사용하는 프리픽스인 Ctrl + b를 이상한걸로 변경해서 종료도 못하고 오고가도 못하는 상황을 마주할 수도 있으니 꼭 기본 설정을 추출해두자.

tmux.conf를 적용하는 명령은 source-file이다.

$ tmux source-file ~/.tmux.current.conf

직접 설정 변경하기

내 경우는 터미널 폰트를 비트맵으로 사용하고 있어 앞서 powerline을 적용하니 대다수가 깨져 이쁘게 적용되질 않았다. 게다가 사양 탓인지 좀 느려지는 기분이라서 간단하게 색상 바꾸고 필요한 것만 설치하기로 했다.

tmux에서 가장 필요했던 부분은 배터리 잔량 표시와 일자/시간 표시였다. 일자/시간은 기본적으로 가능한 부분이라 배터리 잔량 표시는 다음 프로그램을 설치했다.

아쉽게도 잔량 표시 그림은 그려지지 않지만 수치가 나오니 그럭저럭 만족하고 있다.

기분 전환 겸 상태 막대 색상도 초록에서 연한 회색(colour235)로 변경했다. 사용할 수 있는 색상은 다음 스크립트로 확인할 수 있다.

for i in {0..255} ; do
    printf "\x1b[38;5;${i}mcolour${i}\n"
done

누가 이 결과를 보기 좋게 github에 올려뒀다.

원하는 색상이 나오지 않을 때

이 색상 설정은 256color 모드로 실행하지 않은 터미널에서는 동작하지 않는다. 색상이 적용되지 않는다면 다음 설정을 참고하자.

# .bashrc or .zshrc 에 추가
export TERM=xterm-256color
alias tmux="tmux -2"

# .tmux.conf 에 추가
set -g default-terminal "screen-256color"

OS X의 터미널은 기본적으로 256color로 설정되어 있다.


tmux의 기본적인 기능은 예전 요약했던 내용이나 nanhapark님의 포스트를 참고하면 되겠다. 물론 이런 요약본도 tmux.conf 한방에 모두 변경될 수 있어서 tmux.conf를 조심하자는 이상한 결론을 내려본다.

JavaScript의 Generator와 Koa.js 소개

2016년 2월 22일

사이드 프로젝트에서 Express를 오랜 기간 사용했었는데 hapi 가 좋다는 얘기를 듣고는 hapi를 많이 사용해왔다. Hapi도 단순하긴 하지만 “설정만 넣으면 되는” 단순함이라서 설정에 들어가는 수고가 꽤 컸다. 최근에는 토이 프로젝트에서 API를 작성하는데 에러 발생 여부에 따라서 {"ok": true} 하나 넣어주는 작업에 오만가지 코드를 작성해야 했다. express와 다르게 미들웨어에서 request, response에 접근할 수 있는 포인트가 워낙에 많아 더 복잡하게 느껴졌다. 그러던 중 예전에 잠시 비교글로 봤던 koa를 살펴봤는데 지금 필요한 상황에 맞는 것 같아 koa로 다시 코드를 작성했고 마음에 드는 구석이 많아서 간단한 소개를 작성한다.

Koa는 ES2015의 문법 중 하나인 제너레이터를 적극적으로 활용하고 있는 웹 프레임워크다. 모든 요청과 처리를 제너레이터를 활용해 파이프라인을 만드는 것이 특징이며 그 덕분에 깔끔한 async 코드를 손쉽게 작성할 수 있다. Express 만큼은 아니더라도 다양한 라이브러리를 제공하고 있고, express의 라이브러리나 미들웨어도 thenify나 co로 변환해서 활용할 수 있을 만큼 확장성이 높다.

이 포스트는 제너레이터를 먼저 살펴보고, 제너레이터를 유용하게 사용할 수 있는 co를 살펴본 후, KoaJS를 간단하게 살펴보는 것으로 마무리한다.


제너레이터 Generator

다른 언어에도 이미 존재하고 있기 때문에 크게 특별한 기능은 아니지만 ES6에서의 구현을 간단히 정리하려고 한다.

일반적인 함수의 경우, 매 실행마다 같은 흐름으로 모든 코드를 실행하지만 Generator 함수는 실행 중간에서 값을 반환할 수 있고, 다른 작업을 처리한 후에 다시 그 위치에서 코드를 시작할 수 있다. 이 제너레이터는 반복 함수 iterator를 next()로 제공하고 결과를 value로, 진행 상황을 done으로 확인할 수 있다.

구구단을 제너레이터로 작성하면 다음과 같다.

function* nTimesTable(n) {
  for(var i = 1; i <= 9; i++) yield n * i;
}

제너레이터는 위와 같이 function* fnName(){} 식으로 *을 넣어 선언한다. 익명 함수의 경우도 function*(){} 식으로 선언한다.

이제 이터레이터(iterator)를 nineTimesTable에 반환 받는다.

var nineTimesTable = nTimesTable(9);

이터레이터는 next()를 통해 실행할 수 있다. 이 함수로 중단한 위치의 결과가 반환된다.

var result = nineTimesTable.next();
console.log(result); // { value: 9, done: false }
result = nineTimesTable.next();
console.log(result); // { value: 18, done: false }
result = nineTimesTable.next();
console.log(result); // { value: 27, done: false }

// keep calling...

result = nineTimesTable.next();
console.log(result); // { value: 72, done: false }
result = nineTimesTable.next();
console.log(result); // { value: 81, done: false }
result = nineTimesTable.next();
console.log(result); // { value: undefined, done: true }

매 반복 실행에서 value를 반환하지만 동시에 done으로 해당 함수가 yield 결과 없이 종료되었는지 확인할 수 있다. 마지막에 별도의 return 값이 없기 때문에 valueundefined가 된다.

이런 이터레이터의 반환 특징을 이용하면 다음과 같이 iterator를 호출하는 함수를 작성할 수 있다.

function caller(iter) {
  var result, value;
  while(result = iter.next()) {
    if(result.done) break;
    value = result.value || value;
  }
  return value;
}

var result = caller(nTimesTable(3));
console.log(result); // 27

donetrue를 반환할 때까지 해당 이터레이터를 실행해 결과값을 가져오는 caller를 작성했다. 만약 매 반복에서 특정 함수를 실행하고 싶다면 다음처럼 작성할 수 있다. 앞서 작성한 nTimesTable 함수가 더 많은 내용을 반환하도록 수정했다.

function * nTimesTable(n) {
  for(var i = 1; i <= 9; i++) yield { n: n, i: i, result: n * i };
}

function caller(iter, func) {
  var result, value;
  while(result = iter.next()) {
    if(result.done) break;
    value = result.value || value;
    if(func) func(value);
  }
  return value;
}

caller(nTimesTable(3), value => {
  console.log('%d x %d = %d', value.n, value.i, value.result);
});

앞서 작성한 caller는 제너레이터 내의 yield에 대해서는 처리를 하지 못한다. 제너레이터에서 이터레이터를 반환하고 진행을 중단했을 때 해당 이터레이터를 처리해서 다시 반환해야 한다. 결과를 넣고 다시 진행할 수 있도록 작성해야 하는 것이다.

function* getAnimalInCage() {
  yield "Wombat";
  yield "Koala";
  return "Kangaroo";
}

function* Cage() {
  var cageAnimals = getAnimalInCage();

  var first = yield cageAnimals;
  var second = yield cageAnimals;
  var third = yield cageAnimals;

  console.log(first, second, third);
}

Cage 제너레이터를 실행하면 yield를 3번 사용했기 때문에 최종 console.log가 출력하는 결과를 보기까지 4번에 걸쳐 실행된다.

var cage = Cage();
var firstStop = cage.next();
// {value: iterator, done: false}

첫 번째 yield 결과가 firstStop에 저장되었다. cageAnimals는 위에서 코드에서와 같이 getAnimalInCage 제너레이터가 생성한 이터레이터다. 이 이터레이터에 next() 메소드로 값을 받은 후, 그 값을 다시 first 변수에 다음과 같이 반환한다.

var firstAnimal = firstStop.value.next();
// firstAnimal: {value: "Wombat", done: false}
var secondStop = cage.next(firstAnimal.value);

next의 인자값으로 첫 결과인 Wombat을 넣었다. 이전에 멈췄던 위치인 첫 번째 yield로 돌아가 함수 내 first에는 Wombat이 저장된다. 나머지도 동일하게 진행된다.

var secondAnimal = secondStop.value.next();
// secondAnimal: { value: 'Koala', done: false }

var thirdStop = cage.next(secondAnimal.value);
var thirdAnimal = thirdStop.value.next();
// thirdAnimal: { value: 'Kangaroo', done: true }

var lastStop = cage.next(thirdAnimal.value);

// Wombat Koala Kangaroo

마지막 Kangaroo는 yield가 아닌 return이기 때문에 done이 true를 반환한다. 앞서 직접 호출해서 확인한 코드는 반환하는 값이나 호출하는 형태가 일정한 것을 볼 수 있다. 즉 재사용 가능한 형태로 만들 수 있다는 의미다.

다음은 catchEscapedAnimal()getTodaysZookeeper() 함수를 이용한 Zoo 제너레이터 예시다.

function catchEscapedAnimal() {
  return function(done) {
    setTimeout(function() {
      done(null, {name: 'Kuma', type: 'Bear'});
    }, 1000);
  };
}

function* getTodaysZookeeper() {
  yield {status: 'loading'};
  return {status: 'loaded', name: 'Edward'};
}

function* Zoo() {
  var animal = yield catchEscapedAnimal();
  var zookeeper = yield getTodaysZookeeper();

  console.log('%s catches by %s', animal.name, zookeeper.name);
}

catchEscapedAnimal()은 ajax를 사용하는 경우를 가정해서 setTimeout을 이용해 콜백을 호출하는 형태로 작성되었다. getTodaysZookeeper()는 일반적인 제너레이터 함수로 첫 호출에는 loading을, 두번째 호출에서 최종 값을 전송한다. Zoo도 앞에서 본 Cage처럼, 중간에 yield를 사용한다. 이 함수를 처리하기 위한 compose 함수는 다음과 같다.

function compose(iter, value, next) {
  var result = iter.next(value);
  if(result.done) return next ? next(value) : value;
  else if(typeof result.value == 'function') {
    return result.value(function(err, data) {
      if(err) throw err;
      compose(iter, data);
    });
  } else if(typeof result.value.next == 'function') {
    var _iter = iter;
    next = function(result){
      compose(_iter, result);
    };
    iter = result.value;
    result = iter.next();
  }
  return compose(iter, result.value, next);
}

compose 함수는 다음과 같은 경우의 수를 다룬다.

  • yield 된 값이 함수일 때, 호출 체인을 연결할 수 있도록 next 함수를 넘겨줌 (기존 callback 방식)
  • yield 된 값이 이터레이터일 때, 이터레이터가 done을 반환할 때까지 호출한 후 최종 값을 반환
  • 그 외의 결과를 반환할 때, 해당 값을 이터레이터에 넣고 다시 compose를 호출
  • 이터레이터가 종료(done == true)되었을 때, next 함수가 있다면 해당 함수로 호출을 진행하고 없으면 최종 값을 반환하고 종료

이 함수를 이용한 결과는 다음과 같다. setTimeout()에 의해 중간 지연이 진행되는 부분도 확인할 수 있다.

compose(Zoo());
// Kuma catches by Edward

제너레이터를 코루틴으로, co

나름 잘 동작하지만 흐름을 보기 위해서 만든 함수라서 허술한 부분이 많다. 이런 부분에서 사용할 수 있는 것이 바로 co다. co는 제너레이터를 코루틴처럼 사용할 수 있도록 돕는 라이브러리로 앞서 작성했던 compose 함수와 같은 역할을 한다.

var co = require('co');
co(Zoo());
// Kuma catches by Edward

이 라이브러리는 내부적으로 Promise 패턴을 사용하고 있어서 callback이든 Promise든 제너레이터든 모두 잘 처리한다. 실제로 제너레이터를 사용하고 싶다면 이 라이브러리를 사용하는 것이 큰 도움이 된다.

Koa

Koa는 앞서 이야기한 co 라이브러리를 기본적으로 적용하고 있는 HTTP 미들웨어 라이브러리로 경량에 간단한 기능을 제공하는 것을 특징으로 한다. 제너레이터를 기본적으로 사용할 수 있어서 앞서 배운 내용을 손쉽게 적용할 수 있다.

코드를 작성하기에 앞서 간단하게 koa를 설치한다.

$ npm install --save koa

Hello World를 작성하면 다음과 같다.

var koa = require('koa');
var app = koa();

app.use(function* () {
  this.body = {"message": "Hello World"};
});

app.listen(3000);

이제 http://localhost:3000에 접속하면 해당 json이 출력되는 것을 확인할 수 있다.

앞서 작성한 코드도 포함해보자.

var koa = require('koa');
var app = koa();

function catchEscapedAnimal() {
  return function(done) {
    setTimeout(function() {
      done(null, {name: 'Kuma', type: 'Bear'});
    }, 50);
  };
}

function* getTodaysZookeeper() {
  yield {status: 'loading'};
  return {status: 'loaded', name: 'Edward'};
}

function* Zoo() {
  var animal = yield catchEscapedAnimal();
  var zookeeper = yield getTodaysZookeeper();

  this.body = { message: animal.name + ' catches by ' + zookeeper.name };
}

app.use(Zoo);
app.listen(3000);

Koa의 모든 추가 기능은 미들웨어 구조로 제너레이터를 통해 작성하게 된다. callback은 물론 Promise 패턴도 더 깔끔하게 사용할 수 있다.

요청과 응답은 모두 this에 주입되서 전달되고 흐름은 첫 인자에 next를 추가해 제어할 수 있다. 요청에 대한 응답 내용이 있으면 ok를 추가해보자.

app.use(function* (next) {
  yield next;
  if(this.body) {
    this.body.ok = true;
  } else {
    this.body = { ok : false };
  }
});

다음과 같은 방식으로 토큰 검증도 가능하다.

app.use(function* (next) {
  var requestToken = this.request.get("Authorization");
  var accessToken = yield AccessTokensModel.findAccessTokenAsync(token);
  if(accessToken) {
    yield next;
  } else {
    this.body = { error: 'invalid_token' };
  }
});

세부적인 내용은 koa 웹페이지에서 다루고 있다. 단순하고 간편한 기능을 원한다면 꼭 살펴보자. 실제 사용하게 될 때는 koa-bodyparser, koa-router와 같은 패키지를 같이 사용하게 된다. 패키지 목록은 koa 위키에서 확인할 수 있다.

제너레이터도 충분히 편한 기능이지만 koa는 현재 await/async 문법을 지원하기 위한 다음 버전 개발이 진행되고 있다. 더 가독성도 높고 다른 언어에서 이미 구현되어 널리 사용되고 있는 문법이라 더 기대된다.


더 읽을 거리

떠나세요, PHP 개발자여. 아니면 잘하든가!

코드 없는 PHP 이야기. PHP 개발자만 보세요.

2016년 2월 15일

PHP 개발자는 그 태생부터 죄에 속한 것과 같이 업을 쌓고 산다. 아무리 좋은 디자인과 아키텍처, 방법론으로 무장하고 있더라도 그 죄성은 쉽게 씻겨지지 않는다. 어디서든 PHP 개발자라는 얘길 하면 PHP: 잘못된 디자인의 프랙탈 링크를 받게 되고 공개 처형이 이뤄진다. 모던 PHP로 개발하면 된다지만 이전 PHP에 비해 그나마 모던한 것이지 다른 언어와 비교했을 때는 이제 시작한 수준에 불과하다. 개발과 아예 관련이 없는 모임이나 PHP 개발자 모임 외에는 PHP는 쉽고 편한 언어다, 같은 발언은 물론 대화에 PHP를 올리는 것 자체가 금기다. 언급 되더라도 마치 인종차별적 농담과 같이 지저분한 곳에만 사용된다.

어디 가서 PHP 얘기 꺼냈을 때

PHP를 새로 배우려고 하는 사람, 또는 2년 이하의 경력을 가진 사람은 이런 정신적 고통에 시달리지 말고 해방되길 바란다. 평생의 짐으로 껴앉고 살 필요 없이 더 멋진 언어를 선택하고 이 고통에서 벗어나자. 아래 내용도 더 읽을 필요가 없다.

하지만 3년 이상의 시간을 PHP와 함께 했다면 아무리 PHP가 최악이더라도 쉽게 벗어날 수 없다. 커리어를 이쪽으로 계속 쌓아온 사람이라면 마치 기차가 탈선하는 것과 같은 공포감을 느낄 수 밖에 없다. 그래도 갈아타는 것이 좋다. 3년은 크게 느껴지지만 100세 수명이라면 겨우 3%만 할애한 것이다. 물론 커리어 전환에서의 공포는 경력이다. 앞서 적은 것처럼 어디서도 PHP가 대접받지 못하기 때문에 그 전환에서 챙겨갈 수 있는 경력이 대체로 적다. (대부분의 경우, 신입 취급이다.) 경력을 인정 받지 못하면 자연스레 연봉이나 제반 사항이 발목을 잡는다. 그래서 떠나는 결정은 쉬운 일이 아니다. 내 경우는 호주에서 빨리 정착하기 위해 기존 경력을 살려야 했기에 여전히 PHP 개발자로 남아 있다. 새로운 언어를 배워 새 출발 하는 일은 쉽지 않지만 분명 가치 있는 일이고 나에게 있어서는 이후 과제 중 하나다.

반대로 다른 언어를 바꾸는 이득이 크지 않아서 계속 PHP를 사용할 것이라는 분들은 계속 이쪽 길을 가는 데 고민이 없다. 이득이 작다고 생각하는 사람이라면 PHP를 3년 이상 사용하면서 큰 문제를 느끼지 못해본 사람일 경우가 크다. 물론 언어에서 문제를 느끼지 못했다면 그냥 계속 사용하면 된다. 대체로 이런 케이스는 평생 쓴다. 가장 큰 문제는 이런 분들 중에 학습에 무딘 경우가 많아 잘못되고 오래된 지식을 경험이라는 이름으로 덮어서 오용하는 분이 꽤 있다. 이런 분들이 주로 코드의 정당성을 부여하기 위해서 페이스북이 PHP를 쓴다, 워드프레스가 점유율이 가장 높다는 등의 이야기를 끌어다가 쓴다.

페이스북이 PHP 쓴다고 말할 때


모르는 걸 아는 것은 좋은 일이지만 자신이 무엇을 아는지 알지 못하는 것은 병이다.1 PHP에서 문제를 한번도 느껴보지 못한 사람이라면 어떤 언어든 다른 프로그래밍 언어를 학습하자. 프로그래밍 언어는 다양한 문제를 위한 다양한 해법과도 같다. 각종 php 포럼에서 시시덕거리며 유물과 같은 코드 스니핏 공유하지 말고, 말도 안되는 코드를 블로그에 공유하지 말자. 사람보다 코드가 오래 간다. 그리고 다른 언어나 프레임워크를 비하하는 일은 제발 하지 말자. 본전도 못 찾을 뿐더러 정신승리만 남을 뿐이다. 그리고 제발 공부하자. 내가 대충 짠 코드가 다른 사람을 죽일 수 있다. PHP 코드가 레거시이기 이전에 개발하는 사람이 레거시면 어떡하나.

만약 앞에서 이야기한 모든 고통과 괴로움을 덮고서 PHP 개발을 계속 하려고 한다면 그나마 할 수 있는 조언이 몇 가지 있다. PSR 기반의 코딩 가이드, 네임스페이스 사용 등 모던 PHP라고 불리는 것들을 빠르게 도입하는 것이 그중 하나다. 기초는 PHP The Right Way 한국어판부터 시작하자. 패키지를 작성하는 방법이나 패키지 작성 체크리스트를 보고 모르는 부분이 있다면 심화 학습하자. 앞서 간략하게 설명한 글인 당신이 PHP 개발자라면 2016년 놓치지 말고 해야 할 것들을 봐도 된다. 실무에 빠르게 적용하고 싶다면 Laravel 튜토리얼을 살펴보자. PHP Storm과 같은 IDE를 사용하거나 에디터에서 제공되는 PHP를 위한 플러그인을 찾아 설치하는 것도 잊지 말자. 커뮤니티도 중요하다. 모던 PHP 사용자 모임에 가입해서 살펴보자.

PHP 글 더 읽기

  • 노자 도덕경 71장 지부지상 중 
  • Node.js의 Events `EventEmitter` 번역

    2016년 2월 9일

    EventEmitter는 Node.JS에 내장되어 있는 일종의 옵저버 패턴 구현이다. node 뿐만 아니라 대부분의 프레임워크나 라이브러리에서 이 구현을 쓰거나 유사한 구현을 활용하고 있는 경우가 많다. DOM Event Listener를 사용해본 경험이 있다면 사실 특별하게 새로운 기능은 아니지만, 요즘 이 패턴으로 작성된 라이브러리가 많고 특히 node 코어 라이브러리도 이 구현을 사용한 경우가 많아 살펴볼 만한 내용이다.

    물론 Node 뿐만 아니라 front-end 환경에서도 Olical/EventEmitter와 같은 구현을 통해 손쉽게 활용할 수 있다.

    이 글은 Node.js v5.5.0의 Events 문서를 기준으로 번역했고 버전에 따라 내용이 변경될 수 있다.


    이벤트

    Node.js의 코어 API 대부분은 관용적으로 비동기 이벤트 기반 아키텍처를 사용해서 만들어졌다. (“에미터 emitter”로 불리는) 어떤 종류의 객체를 이벤트 이름으로 정의된 특정 이벤트에 정기적으로 전달해 “리스너 listener”로 불리는 함수 객체를 실행한다.

    예를 들어 net.Server 객체는 매번 사용자가 접속할 때마다 이벤트를 호출하고 fs.ReadStream은 파일을 열 때마다 이벤트를 호출한다. stream은 어떤 데이터든 데이터를 읽을 수 있는 상황이 되면 이벤트를 호출한다.

    이벤트를 내보내는 모든 객체는 EventEmitter 클래스의 인스턴스다. 이 객체는 하나 이상의 함수를 이벤트로 사용할 수 있도록 이름을 넣어 추가하는 eventEmiter.on() 함수를 사용할 수 있다. 이벤트 이름은 일반적으로 카멜 케이스로 작성된 문자열이지만 JavaScript의 프로퍼티 키로 사용할 수 있는 모든 문자열을 사용할 수 있다.

    EventEmitter 객체로 이벤트를 호출할 때, 해당 이벤트에 붙어 있는 모든 함수는 _동기적_으로 호출된다. 호출을 받은 리스너가 반환하는 결과는 어떤 값이든 _무시_되고 폐기된다.

    다음은 EventEmitter 인스턴스를 단일 리스너와 함께 작성한 예다. eventEmitter.on() 메소드는 리스너를 등록하는데 사용한다. 그리고 eventEmitter.emit() 메소드를 통해 등록한 이벤트를 호출할 수 있다.

    const EventEmitter = require('events');
    const util = require('util');
    
    function MyEmitter() {
      EventEmitter.call(this);
    }
    util.inherits(MyEmitter, EventEmitter);
    
    const myEmitter = new MyEmitter();
    myEmitter.on('event', () => {
      console.log('an event occurred!');
    });
    myEmitter.emit('event');

    어떤 객체든 상속을 통해 EventEmitter가 될 수 있다. 위에서 작성한 예는 util.inherits() 메소드를 사용했으며 이는 프로토타입으로 상속하는 방법으로 전통적인 Node.js 스타일이다. ES6 클래스 문법으로도 다음과 같이 사용할 수 있다:

    const EventEmitter = require('events');
    
    class MyEmitter extends EventEmitter {}
    
    const myEmitter = new MyEmitter();
    myEmitter.on('event', () => {
      console.log('an event occurred!');
    });
    myEmitter.emit('event');

    인자와 this를 리스너에 전달하기

    eventEmitter.emit() 메소드는 인자로 받은 값을 리스너 함수로 전달한다. 이 과정에서 기억해야 할 부분이 있는데 일반적으로 EventEmitter를 통해 호출되는 리스너 함수 내에서는 this가 이 리스너 함수를 부착한 EventEmitter를 참조하도록 의도적으로 구현되어 있다.

    const myEmitter = new MyEmitter();
    myEmitter.on('event', function(a, b) {
      console.log(a, b, this);
        // Prints:
        //   a b MyEmitter {
        //     domain: null,
        //     _events: { event: [Function] },
        //     _eventsCount: 1,
        //     _maxListeners: undefined }
    });
    myEmitter.emit('event', 'a', 'b');

    ES6의 Arrow 함수를 리스너로 사용하는 것은 가능하지만 이 기능의 명세대로 이 함수 내에서의 this는 더이상 EventEmitter 인스턴스를 참조하지 않는다:

    const myEmitter = new MyEmitter();
    myEmitter.on('event', (a, b) => {
      console.log(a, b, this);
        // Prints: a b {}
    });
    myEmitter.emit('event', 'a', 'b');

    비동기 vs. 동기

    EventListener는 모든 리스너를 등록한 순서대로 동기적으로 처리한다. 즉 이벤트를 적절한 순서로 처리하는 것을 보장해 경쟁 조건(race condition)이나 로직 오류를 피하는 것이 중요하다. 이 모든 것이 적절하게 구현되었을 때, setImmediate()이나 process.nextTick()메소드를 사용해 리스너 함수를 비동기도 동작하도록 전환할 수 있다.

    const myEmitter = new MyEmitter();
    myEmitter.on('event', (a, b) => {
      setImmediate(() => {
        console.log('this happens asynchronously');
      });
    });
    myEmitter.emit('event', 'a', 'b');

    단 한 번만 동작하는 이벤트

    eventEmitter.on() 메소드로 등록된 리스너는 이벤트 이름이 호출되는 매 횟수만큼 실행된다.

    const myEmitter = new MyEmitter();
    var m = 0;
    myEmitter.on('event', () => {
      console.log(++m);
    });
    myEmitter.emit('event');
      // Prints: 1
    myEmitter.emit('event');
      // Prints: 2

    eventEmitter.once()메소드로 등록한 리스너는 호출한 직후 제거되어 다시 호출해도 실행되지 않는다.

    const myEmitter = new MyEmitter();
    var m = 0;
    myEmitter.once('event', () => {
      console.log(++m);
    });
    myEmitter.emit('event');
      // Prints: 1
    myEmitter.emit('event');
      // Ignored

    오류 이벤트

    EventEmitter 인스턴스에서 오류가 발생했을 때의 전형적인 동작은 'error' 이벤트를 호출하는 것이다. 이 경우는 Node.js에서 특별한 경우로 다뤄진다.

    오류가 발생한 EventEmitter'error' 이벤트로 등록된 리스너가 하나도 없는 경우에는 오류가 던져지고(thrown) 스택 추적이 출력되며 Node.js의 프로세스가 종료된다.

    const myEmitter = new MyEmitter();
    myEmitter.emit('error', new Error('whoops!'));
      // Throws and crashes Node.js

    Node.js 프로세스가 멈추는 것을 막기 위해서는 process.on('uncaughtException') 이벤트에 리스너를

    등록하거나 domain 모듈을 사용할 수 있다. (하지만 domain 모듈은 더이상 사용하지 않는다(deprecated))

    const myEmitter = new MyEmitter();
    
    process.on('uncaughtException', (err) => {
      console.log('whoops! there was an error');
    });
    
    myEmitter.emit('error', new Error('whoops!'));
      // Prints: whoops! there was an error

    개발자가 항상 'error' 이벤트에 리스너를 등록하는 것이 가장 좋은 방법이다:

    const myEmitter = new MyEmitter();
    myEmitter.on('error', (err) => {
      console.log('whoops! there was an error');
    });
    myEmitter.emit('error', new Error('whoops!'));
      // Prints: whoops! there was an error

    클래스: EventEmitter

    EventEmitter 클래스는 events 모듈에 의해서 정의되고 제공된다.

    const EventEmitter = require('events');

    모든 EventEmitters는 새로운 이벤트를 등록할 때마다 'newListner' 이벤트를 호출하고 리스너를 제거할 때마다 'removeListner'를 호출한다.

    이벤트: ‘newListener’

    • event {String|Symbol} 이벤트명
    • listener {Function} 이벤트 처리 함수

    EventEmitter 인스턴스는 인스턴스 자신의 'newListener' 이벤트를 리스너를 내부 리스너 배열에 추가하기 전에 호출한다.

    'newListener' 이벤트에 리스너가 전달되기 위해 이벤트 명칭과 추가될 리스너의 참조가 전달된다.

    실제로 리스너가 추가되기 전에 이 이벤트가 호출된다는 점으로 인해 다음과 같은 중대한 부작용이 나타날 수 있다. 어떤 추가적인 리스너든 동일한 명칭의 리스너를 'newListener' 콜백에서 먼저 등록한다면 추가하려는 해당 리스너가 실제로 등록되기 전에 이 함수가 먼저 추가될 것이다.

    const myEmitter = new MyEmitter();
    // Only do this once so we don't loop forever
    myEmitter.once('newListener', (event, listener) => {
      if (event === 'event') {
        // Insert a new listener in front
        myEmitter.on('event', () => {
          console.log('B');
        });
      }
    });
    myEmitter.on('event', () => {
      console.log('A');
    });
    myEmitter.emit('event');
      // Prints:
      //   B
      //   A

    이벤트: ‘removeListener’

    • event {String|Symbol} 이벤트명
    • listener {Function} 이벤트 처리 함수

    'removeListener' 이벤트는 리스너가 _제거된 후_에 실행된다.

    EventEmitter.listenerCount(emitter, event)

    안정성: 0 – 추천 안함: emitter.listenerCount()을 대신 사용할 것.

    인자로 넘긴 emitter에 해당 event가 리스너를 몇 개나 갖고 있는지 확인하는 클래스 메소드다.

    const myEmitter = new MyEmitter();
    myEmitter.on('event', () => {});
    myEmitter.on('event', () => {});
    console.log(EventEmitter.listenerCount(myEmitter, 'event'));
      // Prints: 2

    EventEmitter.defaultMaxListeners

    기본값으로 한 이벤트에 최대 10개 리스너를 등록할 수 있다. 이 제한은 각각의 EventEmitter의 인스턴스에서 emitter.setMaxListeners(n) 메소드로 지정할 수 있다. 모든 EventEmitter 인스턴스의 기본값을 변경하려면 EventEmitter.defaultMaxListeners 프로퍼티를 사용할 수 있다.

    EventEmitter.defaultMaxListeners 설정을 변경할 때는 모든 EventEmitter 인스턴스에게 영향을 주기 때문에 이 변경 이전에 만든 부분에 대해서도 주의해야 한다. 하지만 emitter.setMaxListeners(n)를 호출해서 설정한 값이 있다면 EventEmitter.defaultMaxListeners의 값보다 우선으로 적용된다.

    참고로 이 값은 강제적인 제한이 아니다. EventEmitter 인스턴스는 제한된 값보다 더 많은 리스너를 추가할 수 있지만 EventEmitter 메모리 누수의 가능성이 있는 것으로 보고 stderr를 통해 경고를 보내 개발자가 문제를 인지할 수 있게 한다. 어떤 EventEmitteremitter.getMaxListeners()emitter.setmaxListeners() 메소드를 사용해 이 경고를 임시로 피할 수 있다:

    emitter.setMaxListeners(emitter.getMaxListeners() + 1);
    emitter.once('event', () => {
      // do stuff
      emitter.setMaxListeners(Math.max(emitter.getMaxListeners() - 1, 0));
    });

    emitter.addListener(event, listener)

    emitter.on(event, listener)의 별칭이다.

    emitter.emit(event[, arg1][, arg2][, …])

    event에 등록된 리스너를 등록된 순서에 따라 동기적으로 호출한다. 제공되는 인자를 각각 리스너로 전달한다.

    이벤트가 존재한다면 true, 그 외에는 false를 반환한다.

    emitter.getMaxListeners()

    현재 EventEmitter에 지정된 최대 리스너 수를 반환한다. 기본값은 EventEmitter.defaultMaxListeners이며 emitter.setMaxListeners(n)로 변경했을 경우에는 그 값을 반환한다.

    emitter.listenerCount(event)

    • event {Value} The type of event
    • event {Value} 이벤트 이름

    해당 event 이름에 등록되어 있는 리스너의 수를 반환한다.

    emitter.listeners(event)

    해당 event에 등록된 리스너 배열의 사본을 반환한다.

    server.on('connection', (stream) => {
      console.log('someone connected!');
    });
    console.log(util.inspect(server.listeners('connection')));
      // Prints: [ [Function] ]

    emitter.on(event, listener)

    listener 함수를 지정한 event의 리스너 배열 가장 끝에 추가한다. listener가 이미 추가되어 있는 함수인지 확인하는 과정이 없다. 같은 조합의 eventlistener를 여러 차례 추가했다면 추가한 만큼 여러번 호출된다.

    server.on('connection', (stream) => {
      console.log('someone connected!');
    });

    EventEmitter의 참조를 반환하기 때문에 연속해서 호출하는 것이(chaining) 가능하다.

    emitter.once(event, listener)

    일회성 listener 함수를 event에 등록한다. 이 이벤트는 다음 차례 event가 호출될 때 한 번만 실행한 후 제거된다.

    server.once('connection', (stream) => {
      console.log('Ah, we have our first user!');
    });

    EventEmitter의 참조를 반환하기 때문에 연속해서 호출하는 것이 가능하다.

    emitter.removeAllListeners([event])

    모든 리스너, 또는 지정한 event의 리스너를 제거한다.

    코드 다른 곳에 추가되어 있는 리스너를 제거하는 것은 좋지 않은 방법이다. 특히 EventEmitter 인스턴스가 다른 컴포넌트나 모듈에서 생성되었을 때는 더 유의해야 한다. (예를 들어, 소켓이나 파일 스트림 등)

    EventEmitter의 참조를 반환하기 때문에 연속해서 호출하는 것이 가능하다.

    emitter.removeListener(event, listener)

    특정 event에 등록되어 있는 특정 listener를 제거한다.

    var callback = (stream) => {
      console.log('someone connected!');
    };
    server.on('connection', callback);
    // ...
    server.removeListener('connection', callback);

    removeListener는 한 인스턴스 내에 존재하는 리스너 배열에서 해당 리스너를 하나 제거한다. 만약 한 리스너를 여러 차례 특정 event에 등록해서 배열 내 여러 개 존재한다면, 그 모든 리스너를 지우기 위해서는 removeListener를 여러 번 호출해야 한다.

    이 메소드로 리스너가 하나 지워진 후에 등록되어 있는 리스너의 위치 인덱스가 변경되는데 리스너가 내부 배열로 관리되기 때문이다. 이 동작은 리스너가 호출되는 순서에는 영향을 주지 않지만 emitter.listeners() 메소드로 생성한 리스너 배열의 사본이 있다면 다시 생성해야 할 필요가 있다.

    EventEmitter의 참조를 반환하기 때문에 연속해서 호출하는 것이 가능하다.

    emitter.setMaxListeners(n)

    EventEmitters의 기본값인 10보다 더 많은 리스너가 등록되어 있다면 경고가 출력된다. 이 함수는 어디에서 메모리 누수가 발생하는지 찾는데 유용하다. 명백하게도 모든 이벤트가 10개의 리스너 제한이 필요한 것은 아니다. emitter.setMaxListeners() 메소드를 사용하면 특정 EventEmitter 인스턴스에 대해 그 제한을 변경할 수 있다. Infinity (또는 ``)을 지정하면 리스너를 숫자 제한 없이 등록할 수 있다.

    EventEmitter의 참조를 반환하기 때문에 연속해서 호출하는 것이 가능하다.