요즘 타이핑을 하는데 손가락의 움직임이 점점 둔해지는 기분이 들어서 하루 5분이라도 짧게 연습하자는 목표를 만들었다. 터미널에서 간단하게 예제 문장을 보여주고 타이핑 하는 정도만 생각했는데 이미 GNU 프로젝트로 gtypist라는 타자연습 패키지가 존재했다.

gtypist 화면

화면에서 벌써 힙력이 느껴진다!

“엄청 유명한데 나만 모르고 있었음” 패키지 같아서 따로 포스팅 안하려고 했는데 꾸준히 타자 연습하겠다는 목표를 적는 핑계와 함께 @justinchronicle님께 터미널이 힙하다는 얘기를 듣고 힙터지는 터미널 도구 연재를 해볼까 생각도 들어서 급 포스팅을 하고 있다.

설치는 간단하다. 대부분의 패키지 관리자를 통해서 간단하게 설치 가능하다.

$ brew install gtypist # in OS X
$ apt-get install gtypist # in Ubuntu

그리고 gtypist라고 입력하면 콘솔에서 타자연습 화면이 나온다. 한메타자교사 같이 화려한(지금 보면 뭐지 싶겠지만) 화면은 아니지만 그래도 타자 연습 예제를 제공하고 타수나 오타율을 확인할 수 있다.

타이핑이 느려진다거나 코드 읽는 눈이 느려진다거나 하는 기분이 들면 실제로 그런지 안그런지는 사실 잘 모르겠지만 의식적으로 연습을 하지 않는다면 현행 유지 내지는 더 둔해지는 것은 당연한 것 같다. 내가 다른 일을 하고 있다면 몰라도 내 일에 가장 많은 시간을 할애하고 있는 기술인데 그냥 사소하게만 여겨서는 안될 것 같다. 사소한 부분도 의식하고 작은 시간이라도 투자해서 꾸준히 연습해야겠다.

최근에 구입한 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를 조심하자는 이상한 결론을 내려본다.

그간 사용하던 맥북 에어를 보내고 집에 있던 넷북으로 간간히 작업을 하고 있었는데 어느 날 갑자기 넷북 디스플레이가 나가버렸다. SSD까지 교체해서 그나마 빨라졌나 했더니 더이상 사용할 수 없게 되어버렸다. 새 맥북이 조만간 나온다길래 기다리려 했는데 해야 할 일이 많아 더이상 미룰 수 없어 맥북 나오기 전까지 사용할 용도로 저렴한 노트북을 하나 구입하게 되었다.

터미널 구동할 정도만 되면 충분하다는 생각에 11인치 정도 선에서 가장 저렴한 노트북을 알아봤다. 작은 사이즈를 고르려니 선택할 수 있는 폭이 좁았다. 후보로 Lenovo ideapad 100s(한국서는 i-slim어쩌고), HP Stream 11, Dell inspiron 11 3000 을 골랐다. Lenovo 제품은 Officeworks에서 행사중이라서 250불 가량이고 HP와 Dell은 300불 정도였다. 스펙은 셋 다 비슷해서 가서 만져보고 구입하기로 결정했다.

키보드는 Lenovo가 마음에 들었지만 Dell 키보드도 괜찮았고 크기도 적당해서 Dell로 구입했다. 사진으로는 HP Stream가 가장 괜찮아 보였는데 실제로 보니 키보드도 그렇고 생각보다 별로였다. Dell inspiron 11 3000을 JB HiFi에서 워런티 포함 AUD305 주고 구입했다. 친구랑 빨간색 사야한다 얘기하면서 갔는데 흰색만 있어서 흰색을 구입했다.


Dell Inspirion 11 3000

Dell inspiron 11 3000 작년 모델은 2-in-1 인데 2016 모델은 그냥 랩탑으로 나왔다. 모델도 두 종류인데 하나는 셀러론 모델이고 하나는 펜티엄 모델이다. 구입한 셀러론 모델의 스펙은 다음과 같다.

  • Intel® Celeron® Processor N3050 (2M Cache, up to 2.16 GHz)
  • Windows 10 Home with OneDrive 64-bit English
  • 2GB Single Channel DDR3L 1600MHz
  • 32GB eMMC Storage
  • Intel® HD Graphics
  • 11.6-inch HD (1366 x 768) Anti-Glare LED-Backlit Display
  • Wireless 802.11ac + Bluetooth 4.0, Dual Band 2.4&5 GHz, 1×1
  • 32 WHr, 2-Cell Battery (Integrated)
  • Integrated Widescreen HD (720p) Webcam with Digital Microphone
  • 45 Watt AC Adapter
  • Height: 18.45-19.88mm (0.73-0.78″) x Width: 292mm (11.5″) x Depth: 196mm (7.72″)
  • Starting at weight 1.18Kg (2.6lbs)
  • up to 9 hours 34 mins
  • HDMI v1.4a, USB 3.0 x 1, USB 2.0 x 1
  • Noble lock security slot, Micro SD card reader (SD/SDHC/SDXC), Headphone/Mic

이 가격대는 브랜드를 불문하고 같은 스펙이라서 사실 큰 의미는 없었다.

Dell Inspirion 11 3000 keyboard

외관은 구 맥북의 미니어처 버전 정도 되는 인상을 받았다. 뚜껑도 반들반들해서 먼지 잘 붙을 것 같지만 흰색이라 티가 잘 안난다. 키보드는 검정이라 괜찮지만 팜래스트는 흰색이라서 때가 좀 탈 것 같다. 디스플레이도 저가형을 감안하고 화면이 나온다는 데에 의의를 둔다면 그럭저럭 이해 할 만 하지만 시야각이 좀 많이 좁은 편이다. 액정도 살짝 어두워서 밝은 곳에서는 조금 침침하게 느껴진다. 스피커 음량은 적당한 편이다.

하드웨어에서 가장 아쉬운 부분은 전원이 켜졌거나 충전중임을 표시하는 인디케이터가 전혀 없다는 점이다. 그리고 메모리타입이라 그런지 fan도 들어있지 않아 소리가 전혀 나지 않는다. 이게 장점인지 단점인지 미묘한데 가볍게 사용하면 크게 발열은 나지 않는데 발열이 조금 나기 시작하면 잘 식지 않는 것 같다.

Dell Inspirion 11 3000

기본적으로 Windows 10이 설치되어 있는데 Dell 제품이 다 그런지 몰라도 McAfee 한 달 이용권이랑 Dropbox 20GB 1년 이용권이 있었다. 켜자마자 맥아피 제품이 구동되며 컴퓨터가 멈춰서 버벅임을 뚫고 바로 McAfee를 삭제했다. 환경 구성하는데 시간을 딱히 사용하고 싶지 않아서 그냥 windows로 사용하려고 했었는데 OEM이라 그런지 설치되어 있는 기본 프로그램이 너무 많아 상당히 무거워서 lubuntu를 설치했다.


장점은 저렴한 가격과 휴대성이고 단점은 성능 정도 될 것 같다. 구 맥북 디자인을 좋아한다면 디자인도 장점이 될 수 있을 것 같다. 할 일이 많은데 도움이 되었으면 좋겠다.

사이드 프로젝트에서 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: 잘못된 디자인의 프랙탈 링크를 받게 되고 공개 처형이 이뤄진다. 모던 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장 지부지상 중 
  • 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의 참조를 반환하기 때문에 연속해서 호출하는 것이 가능하다.

    Visual Studio Code에서 TypeScript을 사용하는 환경을 꾸리는 방법을 정리했다. vscode에 아직 기능이 많은 편은 아니지만 여러 편의 기능이 있어 환경을 구축하는데 활용했다. 물론 실무에서 사용할 땐 webpack이나 여타 task 관리도구를 통해 더 쉽게 사용할 수 있다.

    여기서 작성하게 될 파일 구조는 이렇다.

    .vscode
      tasks.json
    dist
      app.js
      app.js.map
    src
      HelloWorld.ts
    typings
      tsd.d.ts
      angularjs/
      jquery/
      ... tsd로 설치한 애들
    tsconfig.json -- 컴파일 설정
    tsd.json -- tsd로 설치한 인터페이스 정보
    

    먼저 typescript와 tsd를 설치한다.

    npm install typescript tsd -g
    

    tsd로 초기화하고 필요한 라이브러리의 인터페이스를 받는다. 물론 인터페이스만 받는 것이니 실제 라이브러리는 npm이나 bower로 받아서 따로 번들해야 한다.

    tsd init
    tsd install angularjs/angular --save
    

    tsd init 하면 tsd.json 파일이 생성되고 설치된 패키지의 메타 정보가 저장된다.

    tsconfig.json 파일을 추가하고 다음 내용으로 저장한다.

    {
        "compilerOptions": {
            "target": "ES5",
            "module": "commonjs",
            "sourceMap": true,
            "rootDir": "src",
            "out": "dist/app.js"
        }
    }
    

    추가적인 내용이 필요하다면 문서를 참고하거나 vscode의 인텔리센스를 활용해서 내용을 넣을 수 있다. 그냥 공부용이라면 그냥 다음과 같이 추가하는 것만으로도 기본값을 사용할 수 있어 정신건강에 도움이 된다.

    {}
    

    tsconfig.json에 대해서는 TypeScript에서 없이 쓰기 포스트에서 다룬 적이 있다.

    vscode에서 해당 디렉토리를 불러온 후 Shift + Cmd + P로 팔레트를 연 후, Tasks: Configure Task Runner를 실행한다. 그러면 .vscode/tasks.json이라는 파일이 생성되는데 vscode에서 task를 바로 실행할 수 있도록 도와주는 설정 파일이다. tsc를 사용해서 typescript를 컴파일하는 것 외에도 gulp나 webpack 등을 호출할 수 있도록 정리가 되어 있어 필요에 따라 주석을 제거하고 사용하면 된다.

    tsconfig.json에서 컴파일 할 설정을 이미 다 설정했기 때문에 args를 다음처럼 고쳐준다.

        // args is the HelloWorld program to compile.
        "args": [],
    

    이제 src/HelloWorld.ts를 작성한다.

    class Startup {
        public static main(): number {
            console.log("Hello World");
            return 0;
        }
    }
    

    저장한 후, Shift + Cmd + B 또는 팔레트에서 Run Build Task를 하면 설정한 것에 따라서 dist/app.jsdist/app.js.map이 생성되는 것을 확인할 수 있다.

    vscode의 build task로 gulp를 사용하고 싶다면 적당하게 gulpfile.js를 작성하고 tasks.json에서 taskName을 변경해주면 된다.

    gulp는 이렇게 설치해주고,

    npm install gulp -g
    npm install gulp gulp-typescript --save-dev
    

    gulpfile.js는 이렇게 작성한다. 앞서 작성한 tsconfig.json을 그대로 활용할 수 있다. 만약 다르게 처리하고 싶다면 gulp-typescript 문서를 참조한다.

    var gulp = require('gulp');
    var ts = require('gulp-typescript');
    
    var tsProject = ts.createProject('tsconfig.json');
    
    gulp.task('scripts', function () {
        var tsResult = tsProject.src()
            .pipe(ts(tsProject));
    
        return tsResult.js.pipe(gulp.dest('release'));
    });
    

    이제 .vscode/tasks.json을 열어 tsc를 주석 처리하고 gulp를 찾아 주석을 해제한 후, 위 gulpfiles.js의 task 이름에 맞게 taskName을 scripts로 변경한다. 저장 후 vscode의 Run Build Task를 실행하면 gulp로 빌드하는 것을 확인할 수 있다.

    추가로 gulp에서 watch를 사용하고 싶다면 다음과 같이 task를 작성하면 된다.

    gulp.task('watch', ['scripts'], function() {
        gulp.watch(tsProject.config.compilerOptions.rootDir + '/**/*.ts', ['scripts']);
    })
    

    실제로 TypeScript와 Angular1.x 환경을 구축할 때는 generator-gulp-angular 같은 제네레이터를 활용하면 깔끔하게 환경을 구축할 수 있다.

    호주에서도 중고나라 같은 gumtree.com.au라는 웹사이트가 있다. 출퇴근을 자전거로 해보고 싶어서 저렴한 자전거를 찾고 있는데 괜찮은 딜은 검트리에 올라오는 족족 팔리기 때문에 수시로 모니터링 하지 않는 한 저렴한 물건을 구하기가 쉽지 않다.

    주말 아침에 일어나서 검트리 페이지를 새로고침 하는 내 모습이 처량해서 이 작업을 자동화 하는 코드를 작성하게 되었다.

    • 지역, 키워드로 검색 페이지를 긁어온다
    • 각 판매글을 적절하게 파싱한다
    • 이전에 긁어온 글과 비교해서 새 글을 뽑는다
    • 새 글이 있으면 알림을 보낸다
    • 다음 비교를 위해 저장한다
    • 스케줄로 반복한다

    요즘 파이썬을 계속 보고 있지만 내 서버 인스턴스에 이미 설정이 있는 node로 작성하기로 결정했다. 데이터는 그리 크지 않고 단순히 비교용으로 사용하기 때문에 json 파일로 저장하기로 했다. 알림은 메일로 보낼까 하다가 이전부터 익히 들어온 텔레그램 API를 활용하기로 했다.

    어떤 라이브러리를 사용할지 찾아봤다.

    • cherrio node에서 사용할 수 있는 jQuery 구현
    • lodash 데이터 조작을 위한 유틸리티
    • request-promise 이름대로 request에 promise를 끼얹은 라이브러리

    텔레그램 API도 node-telegram-bot-api 같은 라이브러리가 있는데 작성할 때는 별 생각이 없어서 주소를 문서에서 가져다가 직접 호출했다. 추후에는 이 라이브러리로 변경해야겠다.

    스케쥴은 간단하게 crontab에서 5분 간격으로 호출하게 했다. 매 5분마다 페이지를 받아 JSON으로 파싱하고 비교한다.

    텔레그램은 몇 안되지만 html 태그를 포함해 메시지를 보내는 것을 지원한다. 그래서 이미지와 링크를 포함해서 다음 이미지처럼 메시지가 온다.

    봇 이름은 홈랜드에 나오는 캐리찡

    코드를 다 작성하고 느낀 점은,

    • 텔레그램은 처음 사용해봤는데 개발에 활용하기 정말 좋게 문서도 잘 정리되어 있고 기능도 깔끔했음. 나중에 또 기회를 만들어 더 사용해볼 것.
    • 패키지처럼 만드는데 익숙하지 않아서 코드가 많이 너저분. 깔끔하게 만드는 방법을 찾아보고 정리.
    • 파일 입출력에는 동기적으로 동작하는 함수를 사용. 아직 파일 입출력을 비동기적으로 처리하는 것이 익숙하지 않은데 살펴보고 정리할 것.
    • 판매글 비교하기 위한 lodash 코드를 작성하는데 문서를 꽤 오래 봐야 했었음. 문서를 좀 더 보고 유용한 함수를 정리.

    게으름 부려서 작성한 코드가 날 공부하게 한다. 얼마나 좋은 게으름인가! 부지런히 찾아보고 정리해야겠다. 코드는 github에서 확인할 수 있다.

    블로그를 운영하면서 가장 꾸준하게 인기 있는 글은 단연 호주에서 일하는 이야기다. 이 글 덕분인지 이메일로 질문을 자주 받는 편인데 아무래도 질문에 공통점이 많은 편이다. 답장이 거의 비슷한데도 시간을 너무 많이 쓰게 되는 것 같아 이전에 보냈던 메일을 정리해서 올려본다.


    안녕하세요.

    저는 서울에 거주하는 ***라고 합니다.

    먼저 갑작스레 메일 드려 죄송하고, 읽어보시고 경험에서 우러나오는 조언을 보내주실 수 있다면 매우 감사드리겠습니다.

    간략한 제 소개를 하겠습니다.

    컴퓨터 전공으로 학사, 석사 학위를 취득하였고 2009년부터 지금까지 모 연구소에서 근무하고 있습니다. 영상처리 알고리즘을 개발하는 업무를 주로 진행하다가 최근 솔루션화를 위한 si 프로젝트를 진행하였습니다. 개발언어는 *\*고 **등의 라이브러리를 주로 이용합니다.

    제 꿈은 외국에서 살아보는 것입니다. 궁극적으로는 호주, 유럽, 캐나다, 미국 등 여행과 삶을 접목시켜 각 대륙에서 직장을 구하고 여행을 하는, 허황될 수 있는 큰 꿈을 그리고 있습니다. 그 꿈을 이루고자 웹을 통해 제 목표와 가까이 계신 분들을 모니터링하고 어떻게 현재의 위치까지 도달하셨는지 조언을 구하고자 합니다. 이렇게 조언을 구하는 이메일을 처음 보내는 것이기 때문에, 혹 무례하거나 잘못된 모습이 있더라고 넓게 이해하여 주시고 지적하여 주신다면 감사하겠습니다.

    인터넷에서 잘 구할 수 없는 궁금한 점을 질문드리고자 합니다.

    1. 블로그의 글을 읽어보니 워홀로 입국하셔서 직업을 구하셨던데, 이런 케이스를 웹에서 더 찾아보기 어려운 것으로 미루어 짐작하면 쉽지 않은 길인 것 같습니다. 워홀비자의 제약조건하에서도 개발자로 취업한 케이스가 종종 있는지 궁금합니다.

    1.1 만약 가능하다면 구인공고가 올라오는 홈페이지 등을 소개해주신다면 어느 분야가 수요가 많은지 찾아보도록 하겠습니다.

    1.2 저는 영상처리 관련 기술에 경험이 있지만, 이는 너무 한정된 분야에서 사용되므로 관련직종으로 직업을 구하기는 어려울듯 합니다만, 이런 상황에 조언해 주실 수 있는 내용이 있는지요?

    1. 보통 외국에서 일하시는 개발자분들은 본인의 이력서를 웹에 게시해두던데, 이렇게 항상 일자리를 구할만큼 고용이 불안정한가요?

    2. 한국과 비교해 근무환경, 업무강도, 일처리의 합리성 등의 면에서 (즉, 삶의 질에서) 향후에도 호주에서 계속 거주하실 생각이 있으신지요?

    저는 호주에서 개발자로 살아간다는 것에 대해 무엇을 모르는지부터 알아가는 중입니다.

    짧은 답변 하나로도 충분히 도움이 될 것입니다.

    여기까지 읽어주셔서 감사합니다.

    서울에서, *** 드림


    안녕하세요. 김용균입니다.

    메일 잘 받았고 감사하게 잘 읽었습니다. 한국은 많이 덥다던데, 호주는 이제 완전한 겨울이라 많이 춥네요. ^^

    1. 사실 제 경우가 예로서 적합한 케이스는 솔직히 되질 못해서 좋은 답이 될지는 모르겠습니다. 저는 학력도 전혀 관련 없는 학과(사회교육과 지리교육전공)에 졸업도 하지 않은 휴학생인데다가 겨우 3년 남짓한 경력이 전부였습니다. 포스트에서 보셨겠지만 영어 한마디도 제대로 못할 정도로 준비도 안된 상태였고, 사실 지금 생각하면 많이 무모하게 도전한 경우입니다. 저는 오기 전에 호주 취업 사이트(seek.com.au 등)에서 어떤 사람을 많이 찾는지 많이 검색해봤고, 한국서 커버레터와 이력서를 미리 준비해왔습니다.

    호주에 오기 전에 여러 커뮤니티에 가입해 이리저리 수소문도 해보고, 여기 와서 지낸 기간 동안에 알게 된 사람들 안에서는 워홀로 입국해 IT직종에 취업, 스폰서 비자를 받은 케이스는 주변에서는 저밖에 없었습니다. 일반적으로 워킹홀리데이 비자는 해당 비자의 취지에 맞게 오는 사람들이 많다보니 사례가 좀 적지 않나 싶습니다.

    호주는 이민국가이기 때문에 다른 서구권에 비해 해외 취업에 대해 상당히 개방된 편입니다. 여기서 만난 한국 개발자분들은 대부분 한국에서 컴퓨터 전공에 몇 년 경력을 가지고 독립기술이민을 통해 온 분들이 절대적으로 많습니다. 워홀은 한 회사에서 6개월 이상 일 할 수 없는 조항도 있고 여러 까다로운 부분이 있어 특히 자리를 구하기 어려운 반면 독립기술이민은 영주권 비자이고 또한 여기서의 생활에 대해 복지 지원(학비나 가족 수당, 실업급여)이 있는 등의 장점을 보시고 오는 분들이 많습니다. (참고로, 영주권 비자는 한국 국적과 동시에 가지고 있을 수 있습니다.)

    1. 1 호주의 경우, 구인 공고는 대표적으로 seek.com.au 등이 있습니다. 한국에 비해서 리크루트 업체를 통해 중개되는 경우가 많습니다.

    2. 2 영상 처리는 제가 어떤 분야인지 잘 모르겠네요. 제가 여기서 처음으로 일했던 곳이 ***이란 곳이었는데 혹시 이런 분야이신지. 전혀 없지는 않을겁니다만 좀 드물지도 모르겠습니다. 위 리쿠르트 사이트에서 한번 경력과 맞는 곳이 있는지 살펴보면 도움이 될 것 같네요.

    3. 고용이 불안정하다기보다 고용-피고용의 관계가 상당히 유연합니다. 서구권의 직업관은 한국과 많이 달라서, 문화적인 차이에 의해 그런듯 합니다.

    4. 한국에 있을 때 제주 소재의 웹에이전시에서 웹 어플리케이션 개발을 했었는데 근무환경, 업무강도, 일처리 합리성은 이곳이 훨씬 낫습니다. 월등한 대신 영어가 절대적으로 중요해집니다. 영어를 못하면 한국에서 가지던 포지션보다 낮은 자리에 갈 수 밖에 없습니다. 제 경우에도 개발팀장으로 있었지만 discussion은 커녕 communication이 어렵다보니 이곳에서는 주니어 개발자 말고는 할 수 있는 일이 없었습니다. 주니어라 하더라도 급여수준이나 환경은 훨씬 나은 편입니다.

    향후 호주 거주는… 일단 드문 기회를 얻은 상황이니 부지런히 지내고 있습니다. 아직 못한 공부도 더 하고 싶기도 한데 호주에서의 International student 학비는 1년에 3만~4만불 가량 되는데 영주권을 취득한 경우 1년에 4,000불 내외로 저렴하게 가능하기 때문에 영주권을 먼저 취득하고 대학교를 다니려고 하고 있습니다. 덧붙이면 독립기술이민으로 오시는 분들은 대부분 오셔서 석사, 박사 과정 하시는 경우가 많았습니다.

    1. 서구권임에도 저렴한 조건으로 와서 공부할 수 있고 2) 비자를 안정적으로 받을 수 있기 때문에 많이 오신다고 합니다. 예전엔 환율 사정도 좋았다고 하는데 요즘 환율이 많이 올라 이 메리트는 없어진 것 같네요.

    정리하자면, **님은 저보다 훨씬, 훨씬 좋은 상황이라 무얼 하셔도 저보다 더 잘 될 것 같습니다. 학력도 경력도 있기 때문에 생각하신 것처럼 한국 외 어느 국가에서도 충분히 일할 수 있을겁니다. 중요한건 자신이 원하는 포지션에 걸맞는 영어실력입니다. 메일에서 느껴지는 인상으론 엄청 잘하실 것 같습니다만 영어 잘 준비하셔서 좋은 곳에서 일하시길 기대합니다. 호주에만 국한하지 않고 두루두루 살펴보시다보면 좋은 자리 찾으실 수 있을겁니다.

    아래는 해외 취업과 관련해 두루두루 참고할만한 글과 커뮤니티입니다.

    더 궁금하신 사항이 있으시면 언제든지 메일 주시기 바랍니다.

    김용균 드림.


    안녕하세요. **포럼에 질문을 올린 ***입니다.

    ***님하고는 메일로 간단히 몇가지를 물어봤는데요. 어학을 계획으로 오셨다가, 정착한 케이스라 다소 귀감이 되더라구요.

    제가 궁금한 것은 어떻게 일자리를 구하셨는지 인데요. www.seek.com.au 이곳에 CV를 올려서 구하셨는지, 아니면 다른 경로로 구하셨는지 알고 싶구요.

    그리고 멜버른하고 시드니가 아무래도 일자리가 많은 것 같은데, 멜번 쪽으로 가신이유가 혹시 따로 있는지 알고 싶습니다.

    아, 영어는 필리핀등에서 2개월 정도 회화와 IELTS를 공부하고 갈까 생각중입니다.(현지로 바로가는 것은 아무래도 비용때문에…)

    아무래도 처음 가는 곳이라 불안함이 많다보니, 이런 질문들을 하게되네요~

    시간 되실때 천천히 답변 부탁드리며, 향후 호주로 가게되면 제가 맛있는 식사라도 한번 대접하도록 하겠습니다 🙂

    그럼, 즐거운 한주 되시기 바라며 겨울로 아는데, 건강 유의 하세요.


    안녕하세요. 김용균 입니다.

    저도 seek.com.au를 통해서 컨택 많이 해봤는데 일단 대부분 거기에 올라오는 곳이 리쿠르트에서 하는 부분이라 리쿠르트를 통해 면접을 보고 통과가 되면 회사에 면접보는 식이라 영어로 자기어필만 강하게 할 수 있으면 크게 어려운 부분은 없습니다.

    멜번보다는 시드니가 훨씬 자리 많은 편이라고 하더라구요. 제가 멜번을 선택한 이유는… 크게 이유는 없는데 멜번이 왠지 좋아서 선택했습니다.

    저는 아직 대졸도 아니고 경력도 얼마 안되는지라 독립기술이민을 진행하긴 어려워 여기서 스폰서 비자를 통해 계속 지내보려고 하고 있습니다. 아무래도 여기 일들이 6달, 12달 이런 프로젝트가 많은데 워홀 비자는 한 회사에서 6개월이란 제한 때문에 그런 프로젝트를 수행하기가 사실상 불가능이기도 하고… 여기도 영주권자 이상을 풀타임으로 많이 채용하는 편이라서 말입니다. 스폰서비자의 경우 사실상 해당 회사에 종속되어 급여가 오르거나 하는 것도 쉽지 않아서 흔히 노예비자라고도 합니다. 실직하면 28일 내에 해당 스폰이 가능한 회사를 찾아야 비자가 취소가 안되는데 그게 말처럼 쉽지가 않으니까요.

    일단은 워킹으로 오셔서 한번 보시면 어떤 상황인지 아실 수 있으실 겁니다. 영어만 잘 되시면 아무 문제없이 잘 일 하실 수 있을거에요.

    저는 영어가 많이 안되서 인터뷰만 두달동안 이곳저곳 보러 다녔거든요. 영어 많이 준비해오면 도움이 많이 될 것 같습니다.


    안녕하세요. *** 입니다.

    저는 퇴사후 호주 취업을 준비중인 게임 클라이언트 프로그래머입니다. 어학원도 다니고 이력서와 포트폴리오도 보내고 있지만, 답장도 너무 늦고(답장에 2주 정도 소요가 되네요.) 해서 직접 호주로 가서 구하면 어떨까 싶은 생각을 하고 있습니다.

    하지만 제가 호주쪽 게임 회사 실정도 모르고 워홀 비자로 어느 정도 일을 구할수 있는지, 또 어떻게 구할수 있는지 알고 싶습니다.

    1. 워홀 비자로 직장을 구하는 사람을 잘 뽑는지
    2. 워홀로는 한 직장에 6개월이 한계라는데 그럼 매번 옮기거나 하는지
    3. 회자입장에서도 당장 호주에 거주중인(워홀 비자로라도) 사람에게 더 기회를 주는지
    4. 제가 국내에서 경력은 있지만 호주쪽 클라이언트 트랜드라던가 요구하는 인재상을 짐작하기 어렵습니다. 어떤 기술을 준비하는 것이 좋은지.

    일단은 이렇게 4개이지만 조금 더 여쭤보고 싶은게 생길 것 같습니다.


    안녕하세요, 김용균입니다.

    저는 호주에서 웹개발자로 일하고 있어서 게임과는 다소 다른 영역이라 게임 산업 쪽 고용 시장이 어떻게 구성되어 있는지에 대해서는 잘 모르겠네요. 게임 클라이언트 쪽이면 MO나 MMO 일 것 같은데 호주에서 그런 쪽 개발하는 스튜디오 얘기는 들어본 적이 없어서 호주에 맞는 자리가 있을지는 잘 알아보셔야 할 것 같습니다. 게임코디 눈팅해온 걸로는 게임 개발하시는 분들은 대부분 북미나 유럽에 가시는 편으로 알고 있어서요.

    1. 대부분 영주권자 이상을 선호하는 편인데 호주는 고용 유연성이 상당히 높아서 3개월, 6개월, 1년 계약직 같은 자리도 많습니다. 이런 자리의 경우는 비자랑 크게 상관 없이 뽑는 경우가 많은 편입니다. 직장을 구하시려면 1) 영어가 잘되거나 2) 기술적으로 뛰어나거나 둘 중 하나인데 (둘 다 되면 당연히 좋구요) 일반적으로 커뮤니케이션이 가능한걸 요구하는 편입니다. 적어도 기술적으로 뛰어난 걸 증명하려면 자신이 한 일을 말로 설명할 정도는 되야겠죠.

    저는 영어를 잘 못하는 상태에서 와서 인터뷰는 많이 봤지만 결과가 그리 좋지 않았습니다. 영어를 어느 정도 해야 하는가에 대한 궁금증은 저도 가지고 있었는데요. 한국어로 대화하는 걸 영어로 할 수 있을 정도는 되어야 합니다. 저는 듣기는 대충 들렸는데 말하는데 겁도 나고 문법 틀릴까 우물우물 하는 상태를 벗어나는게 힘들었습니다. 대화가 가능하다는 것이 문법적으로 완벽하게 말하는건 의미하는게 아니었습니다. 잘 못 알아들었으면 다시 말해달라고 얘기하고 이해가 되었다 안되었다 이 부분 의견은 동의한다 이 부분은 이렇게 생각한다 얘기할 수 있는게 중요합니다.

    1. 워킹홀리데이 비자로는 한 회사에서 6개월까지만 가능합니다. 제 경우는 6개월 때 스폰서 비자로 변경했습니다. 이 경우는 직장을 어떻게 구하느냐에 따라 많이 달라지는 부분이겠네요.

    2. 1번에서 답변드린 바와 같이 영주권자 이상만 뽑는 경우도 많습니다. 그래도 포지션에 자신이 잘 맞는다 생각하면 지원해보는게 좋겠죠. 해당 회사서 정말 필요로 하면 비자를 지원해줄 겁니다.

    3. 저도 게임 산업 쪽에 관해서는 들어본 바가 없어 어떤 인재를 요구하는지는 잘 모르겠습니다. 기술 요구사항은 일반적으로 구인 공고에서 확인해볼 수 있는데요. seek.com.au 같은 사이트에서 해당 직종을 검색해보시고 어떤 포지션 디테일을 가지고 뽑는지 살펴보는 것이 도움이 될겁니다.

    만약 관련 대학 졸업하셨고 경력이 있는 상황이라면 워킹홀리데이보다 독립기술이민과 같은 영주권을 바로 받을 수 있는 비자를 신청해서 오시는게 낫습니다. 스폰서 비자는 회사에서 지원해줘야 하는 경우라서 쉽게 내주지 않는 경우도 많고 이걸 가지고 장난하는 회사도 많습니다. IT로 호주에 오시는 분들 대부분이 독립기술이민으로 오는 경우가 많습니다. 영주권자 이상은 학비에 대한 혜택이 좋아서 여기와서 석사 받고 그 학력으로 취업하는 경우가 많습니다.

    맨 처음 워킹홀리데이로 와서 힘든 부분은 현지 경력이 없는 부분입니다. 여기는 레퍼런스 체크라는게 있는데 이 사람이 정말 괜찮은 사람인지 이전에 일한 사람에게 물어보는 문화가 있습니다. 제 경우는 호주 오자마자 한 2주짜리 단기 프로젝트를 한 경력이 있고 거기서 꽤 인정을 받았기 때문에 실제 취업 때 레퍼런스로 큰 도움을 받을 수 있었습니다.

    기왕 나오기로 하셨으면 여러가지 옵션을 고민해보시고 그리고 궁금한 점이 있다면 immi.gov.au 같은 정부 사이트에서 자세한 내용을 꼭 살펴보시기 바랍니다. 인터넷에 있는 자료는 out of date인 경우가 많아서 그런 자료 믿다가 비자 취소되고 돌아가는 경우도 많습니다. 만약 북미쪽 취업이 궁금하시다면 포프님 북미취업가이드를 살펴보시는 것도 도움이 될 것 같네요.

    밖에 나오면 생각보다 힘든데 그래도 좋은게 많습니다. 그리고 나와서 영어에 대한 두려움이 줄어드니 꼭 호주가 아니더라도 어느 영어권 국가라도 가서 지낼 수 있을 것 같은 자신감이 생기네요. 심사숙고해서 어디로 갈 지 잘 결정하셨으면 좋겠습니다.

    더 궁금한 부분이 있다면 답장 남겨주시고요. 🙂

    김용균 드림.


    안녕하세요. 현재 HW/SW관련 개발을 공부하고있는 **대학교 4학년 ***라고합니다.

    호주워킹홀리데이를 다녀왔었고, 개발자로 살아가는데 해외취업을 목표를 두고있습니다. 제 주 전문분야는 web이 아닌 firware, Embedded, Linux 분야이고 현재 S사 소프트웨어멤버십 이라고 해서 S사에서 학생개발자들을 키워내는 프로그램에서 2년째 활동하고 있습니다. 그런데 제 꿈은 해외에서 살아가는것입니다. 워킹홀리데이 생활을 하면서 꼭 해외에서 살고싶다는 생각을 했었습니다.

    서론이 길었습니다. 궁금한점이 있어서 연락드렸습니다.

    1. 해외 IT회사를 지원하게 된 계기
    2. 한국에서 일하는것과의 차이점/장단점
    3. 학사학위로도 해외취업에 가능한지. 영어실력은 영어로 면접 볼 정도의 실력에 조금 못미칩니다. 직접 영어를 쓰는 환경에서 일을 안해봐서 많이 부족합니다.

    답장 드립니다.

    저는 오랫동안 웹에 관심을 갖고 공부해왔었습니다. 호주에 오기 전엔 웹에이전시에서 개발팀장으로 근무해 웹사이트 구축에 필요한 실무적 역량을 많이 키울 수 있었습니다. 저는 워킹 홀리데이로 호주에 오면서 한국서 하던 경력대로 일을 할 계획을 세워 영문 이력서 등을 준비해 호주로 넘어왔습니다.

    오기 전에 워킹 홀리데이 비자로 호주에서 개발 직군에 취업한 후기를 많이 검색해봤지만 인터넷이든 워킹홀리데이 책이든 대부분 농장 가서 주천불 버는 얘기 밖에 없었습니다. 그래도 몇 분 워홀로 와서 일하던 분도 알음알을 알게 되서 조언도 받아서 큰 문제는 없겠다 싶어 호주로 와서 여러 회사에 지원했고 현재 회사에 입사해 지금까지 지내고 있습니다.

    1. 해외 IT회사를 지원하게된 계기
    기회가 되서 지원하게 되었습니다. 개발직군에서 일하면 누구나 생각해보는 일이기도 하고요. 또 영어는 개발 직군에서 일하면서 최신 트랜드를 보려면 필수적이니까요. 이건 지원한 계기라기보다 해외에 나온 계기가 될 것 같네요. 해외에서 사는 것 자체가 목표라면 돈 많이 벌어서 노년에 나와 사는게 편하고 좋지 않을까 합니다.</p> 
    
    1. 한국에서 일하는것과의 차이점. 장단점
    일하는 것은 큰 차이 없습니다만 인건비가 비싸서 야근을 거의 안시켜주는 편입니다. 업무 환경은 회사에 따라 다를테고 그 외 생활에서의 차이는 제 블로그 포스트를 읽어보는게 도움이 될 것 같습니다.
    
    1. 학사학위로도 해외취업에 가능한지. 영어실력은 영어로 면접볼정도의 실력에 조금 못미칩니다. 직접 영어를 쓰는 환경에서 일을 안해봐서 많이 부족합니다.

    저는 대학 휴학중이라 학사 학위로 취업을 물어보는건 어떻게 대답 드려야 할 지 모르겠습니다. 워킹홀리데이 비자를 이미 쓰셨으면 호주 유학 후 영주권 취득이나 한국서 경력과 영어 점수를 만들어 영주권을 받아서 오는 두가지 방법이 있는데 여기에 학사 학위는 도움이 될겁니다. 이 이야기도 제 블로그에서 확인할 수 있습니다.

    영어를 잘해야 하는건 당연합니다. 저는 영어 준비를 잘 하고 오지 않아 정말 고생했고 지금도 한참 부족한 편입니다. 업무는 지금까지 해온 것도 있고 경험이 있고 개발쪽은 대부분의 키워드가 영어니 큰 문제가 없었지만 세세한 디테일을 놓치게 되는 경우가 많습니다. 일을 잘하는 것도 중요하지만 영어도 중요합니다. 한국에서 하는 취업 수준에 한국어 수준으로 영어를 써야 한다고 말씀 드리면 감이 좀 올까요? 직군에 따라 많이 다르겠지만 영어는 잘할수록 좋습니다. 기술적으로 뛰어나도 본인이 얼마나 잘하는지는 영어로 어필해야 하기 때문이고요.

    저는 워홀로 오기도 했고 영어를 어짜피 못하니까 괜찮다는 생각도 좀 있었는데 영어를 잘했으면 더 좋은 포지션에서 일할 수 있지 않았을까 생각합니다. 워홀로 올게 아니라 위 말한 다른 방법으로 오신다면 일반적인 의사소통에 지장 없을 정도로는 준비하셔야 합니다. 한국 벗어나면 외국인이고 대화 안되면 당연히 시장에서 배제됩니다. IELTS 점수도 필요할테니 공부하셔야 할테고요.

    영어로 일하는 업무 환경이 필요하시다면 github에서 컨트리뷰션할 오픈소스를 찾아 참여하시거나 메일링 리스트에 참여하는 방법도 있습니다. 유튜브 등으로 올라오는 컨퍼런스도 찾아보시고요. 펌웨어, 임베드면 요즘 IoT로 한창 핫한 오픈소스도 많을테니 참여할 곳도 많을 것 같네요.

    저도 많이 부족해서 이렇게 조언 드릴 입장이 아니라 이렇게 적는게 좀 부끄럽습니다. 목표와 계획 잘 세우셔서 좋은 결실 맺길 기대합니다.


    이전에 쓴 다른 글은 아래서 볼 수 있다.

    예전부터 장만하고 싶은 것 중 하나가 프린터였다. 급한 것은 사무실에서 출력하면 되긴 하지만 집에서 개인적인 용도로 쓰는 것과는 확실히 기분이 다르니까. 물론 부피가 있어 공간도 필요하고 자칫 먼지 수집기로 전락 할 가능성이 높다는 생각에 고민을 오래 했었다. 특히 무거운 프린터를 구입해서 어떻게 집에 들고 올 것인가가 가장 큰 문제였다. 고민만 계속 하고 있었는데 지난 휴가에 차를 빌린 동안 이참에 장만하자는 생각이 들어 레이저 프린터를 구입했다.

    잉크젯 프린터는 훨씬 저렴한 가격에 구입할 수 있지만(캐논 프린터는 15불부터 시작한다.) 잉크 가격이나 헤더 막힘 등 여러 문제를 겪은 기억이 있어서 구입 목록에서 아예 제외했다. 레이저 프린터 중 69불에 wifi 되는 Brother HL-1210W 프린터가 괜찮아 보여 구입하려 했었는데 실물로 보니 안예뻐서 (프린터는 가전이니까 이뻐야) 한참을 망설였다. 그러던 중 Fuji Xerox가 동급 기능의 브라더 프린터에 비해 저렴하고 특히 토너도 저렴하길래 결국 Fuji Xerox DocuPrint P265DW 로 정하게 됐고 Officeworks 매장에서 98불에 구입했다.

    Fuji Xerox DocuPrint P265DW

    종이를 담을 수 있는 트레이가 있고 wifi 기능도 내장되어 있다. 디스플레이도 한 줄 짜리지만 있어서 간단하게 상태를 확인할 수 있었다. 얼마나 쓸 지 모르지만 양면인쇄도 가능하다. 전원 넣고 wifi를 검색해 비밀번호를 넣으니 바로 AirPrint 네트워크 프린터로 잡혀서 사용할 수 있었다. 프린트를 위해 SCSI 케이블을 찾던 시절은 이미 지나간지 오래고 USB도 필요 없고 wifi로 내 네트워크에 접속해서 자동으로 프린터로 잡히니 내가 구석기인이 된 기분이 들었다.

    코드를 꼽아두면 전기를 많이 먹을 것 같아 사용할 때만 전원을 연결했는데 전원 연결하고 wifi까지 자동으로 연결된 후 wifi 버튼에 불이 켜지기 까지 1분이 채 걸리지 않았다.

    슬립과 딥슬립의 차이는 뭐지

    스마트폰을 비롯해 디지털 기기가 일상화되고 그 위에 좋은 오피스 앱/서비스, 클라우드가 자리 잡은 덕분에 Paperless 환경을 쉽게 구축할 수 있지만 나에겐 아직도 손에 잡히고 펜으로 밑 줄 그을 수 있는 종이가 편하게 느껴진다. 요즘 세대가 저장 버튼에 있는 디스켓 아이콘의 맥락을 이해하기 힘든 것처럼 어릴 때부터 종이가 아닌 디바이스를 먼저 접한 세대라면 디바이스보다 종이가 편하다는 그 감각도 달라지지 않을까 생각한다. “이 아이콘은 무슨 뜻이죠?” 처럼 “왜 종이에 출력한걸 보죠? 그냥 화면으로 보면 되는데.” 라고 반문할 날을 상상하게 된다. 그래도 종이는 오래가지 않을까 얘기하지만 그렇게 생각했던 코닥도, 아그파도 무너진 것을 보면 의외로 그 시대가 가까이 있는 것 같다.

    색상을 바꿔요

    눈에 편한 색상을 골라보세요 :)

    Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.