range-shuffle

range-shuffle란 이름으로 npm에 처음으로 내가 만든 모듈을 배포해봤다.

LCG를 이용해서 배열없이 수열의 순서를 섞을 수 있고, 섞인 수열의 각 수가 원래 어떤 위치에 있었는지 알 수 있게 하는 코드를 짰는데, 다른 사람들도 범용적으로 쓸 수 있을 것 같다고 생각했다. 그래도 남들한테 보이는 코드라고 리팩토링도 몇 번 씩 하고, 테스트도 만들고, 오픈소스니까 무료 CI까지 붙혔다.

배포과정은 ‘npm에 node.js 모듈 배포하기’에 이미 정리해두었다.(주어만 없을뿐)

설정파일(dotfiles) 관리하기

매번 개발환경을 세팅할 때마다 gist에 백업해놓은 dotfile들을 복사해서 경로에 수동으로 붙혀넣는 것이 귀찮았다. 겨우 파일 세 개지만, 가끔씩 gist에 백업된 파일과 변경사항을 확인해서 백업해놓는 것도 성가신 작업이었다.

Tmuxp를 쓰면서 관리해야할 파일이 네 개가 되었다… 이젠 gist에 수동으로 저장할 게 아니라 repository를 하나 만들어서 관리하려고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/zsh


# Copy config files

## Oh my zsh
ln -sf .zshrc ~/.zshrc

## Tmux
ln -sf .tmux.conf ~/.tmux.conf

## Neovim
mkdir -p ~/.config/nvim
ln -sf init.vim ~/.config/nvim/init.vim

## Tmuxp
mkdir -p ~/.tmuxp
ln -sf tumblbug-backend.yaml ~/.tmuxp/tumblbug-backend.yaml # tumblbug backend


# Restart
source ~/.zshrc

dotfile을 한 곳에 모아두고 위처럼 스크립트를 만들어서 실행해주면 백업된 설정파일들이 로컬에 적용되도록 했다. 각 파일을 심볼릭링크로 연결했다. 이렇게하면 사용중인 dotfile들의 변화를 이 repository 폴더에서 추적할 수 있다. 하드링크 대신 심볼릭링크로 연결한 이유는 repository 폴더를 지울 수 없게 하기 위해서다. 폴더를 지우면 ‘No file or directory’ 에러가 날테니까 개발환경에선 이 폴더가 항상 요구될 것이고, dotfile들이 정말로 추적되고 있나 확인할 필요가 없어진다.

dotfiles로 검색해봤더니 이미 비슷한 방식을 구현한게 많네 ㅋㅋ

npm에 node.js 모듈 배포하기

먼저 npm에 계정을 만들어야 한다. npm 사이트에서 할 수도 있지만, 터미널에서 npm adduser로도 계정을 만들 수 있다. 이미 계정이 있는 경우 npm login으로 로그인하자.

package.json

npm에 배포하기 위해선 배포할 모듈의 루트 디렉토리에 ‘package.json’가 있어야 한다. 아직 ‘package.json’이 없다면 npm init 혹은 yarn init으로 생성할 수 있다. ‘package.json’은 아래와 같은 형식으로 되어있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"name": "Module",
"version": "1.0.0",
"description": "Some module",
"main": "lib/index.js",
"scripts": {
"dev": "babel-node src/index.js",
"prepublish": "babel src --out-dir lib"
},
"repository": {
"type": "git",
"url": "git+https://github.com/user/repo.git"
},
"keywords": [
"keyword1"
],
"author": "user",
"license": "ISC",
"bugs": {
"url": "https://github.com/user/repo/issues"
},
"homepage": "https://github.com/user/repo#readme",
"devDependencies": {
"babel-cli": "^6.24.1",
"babel-core": "^6.24.1",
"babel-preset-env": "^1.4.0"
},
"dependencies": {
"lodash": "^4.17.4"
},
}
  • “name”
  • “version”: 매 배포마다 이 부분을 수정해서 버전을 높혀야 배포할 수 있다. 이미 배포된 것과 버전이 같다면 코드가 달라도 배포되지 않는다.
  • “description”
  • “main”: 패키지가 import되면 실행될 파일.
  • “scripts”: 특정 스크립트는 npm 배포시 훅으로 동작한다. prepublishnpm publish를 하면 배포되기 전에 실행된다. 이런 스크립트 목록은 여기에서 확인할 수 있다.
  • “repository”: 코드의 저장소 주소
  • “keywords”: npm에서 목록에 있는 단어로 검색하면 패키지를 검색결과에 노출한다.
  • “author”
  • “license”
  • “devDependencies”: 패키지를 설치할 때는 제외되는 개발용 의존 패키지 목록.
  • “dependencies”: 패키지를 설치할 때 함께 설치되는 의존 패키지 목록.

.npmignore

‘package.json’과 함께 배포시 또 필요한 파일은 ‘.npmignore’다. 여기엔 패키지를 배포할 때 npm에 업로드할 필요가 없는 파일들의 목록을 적어놓는데, 테스트코드 파일이 대표적인 예다. 만약 ‘.npmignore’가 없다면 ‘.gitignore’에 적힌 파일을 제외시킨다.

아래 파일들은 굳이 ‘.npmignore’에 명시하지 않아도 자동으로 제외시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
node_modules
.*.swp
._*
.DS_Store
.git
.hg
.npmrc
.lock-wscript
.svn
.wafpickle-*
config.gypi
CVS
npm-debug.log

Babel로 컴파일한 패키지 배포하기

소스코드는 javascript의 최신 스펙을 이용해서 작성했더라도, 범용성을 위해 Babel로 컴파일 후 배포해야 한다. 패키지의 구조는 아래와 같다고 하자.

1
2
3
4
5
6
7
8
.
├── .gitignore
├── .npmignore
├── package.json
├── src // 소스코드
│ └── index.js
└── lib // 컴파일된 파일
└── index.js

먼저 ‘package.json’에서 "main": "lib/index.js"로 패키지의 entry point를 컴파일된 파일로 설정한다. 그 다음 "scripts""prepublish": "babel src --out-dir lib"를 추가해서 배포전에 Babel이 컴파일할 수 있도록 한다.
컴파일된 파일은 버전관리할 필요가 없으므로 ‘.gitignore’에 lib를 추가한다.
반대로 소스코드는 배포할 필요가 없으므로 ‘.npmignore’에 src를 추가한다.

이제 npm publish를 실행하면 컴파일된 파일만 배포한다.

Babel로 es6 모듈을 commonJS와 ES6 module 모두 호환되게 하기

아래처럼 es6로 클래스를 만들고 babel compile 후 다른 곳에서 불러오는데 에러가 났다.

1
2
3
4
5
export default class A {}

var A = require('AAA');
var a = new A();
// Uncaught TypeError: _AAA.A is not a constructor

A를 출력해봤더니 constructor function이 object에 담긴 상태로 모듈이 import되었다.

1
2
console.log(A);
// { default: [ Function: A ] }

원인은 Babel에 있었는데, Babel@6부터 commonJS의 module.exports를 더 이상 기본으로 export하지 않고 ‘default’라는 키로 export한다.
때문에 Babel@6으로 컴파일된 모듈을 불러오려면 아래의 방법 중 하나를 써야한다.

1
2
3
4
5
6
7
8
// AAA.js
export default class A {}
// -------
var A = require('AAA').default;
var a = new A();
// or
import A from 'AAA';
const a = new A();

또는

1
2
3
4
5
6
7
8
// A.js
export class A {}
// -------
var A = require('AAA');
var a = new A();
// or
import { A } from 'AAA';
const a = new A();

뭔가 깔끔하지 않아보인다면 ‘babel-plugin-add-module-exports’을 설치하고 .babelrc에 플러그인으로 추가하자.

1
npm install babel-plugin-add-module-exports --save-dev
1
2
3
4
5
6
7
// .babelrc
{
"presets": ["env"],
"plugins": [
"add-module-exports"
]
}

그러면 아래와 같이 쓸 수 있다.

1
2
3
4
5
6
7
8
// A.js
export default class A {}
// -------
var A = require('AAA');
var a = new A();
// or
import A from 'AAA';
const a = new A();

졸업

졸업을 했다. 졸업장에는 두 개의 학위가 적혀있다.

2012년 여름부터 본격적으로 개발을 공부하기 시작했지만, CS를 복수전공으로 듣기 시작한 건 2015년 첫번째 학기부터다. 이 시간의 격차는 내 이야기를 듣는 사람들이 다들 흥미를 가지는 부분이다.

사실 처음에는 CS를 전공하지 않아도 괜찮을 거라 생각했다. 인터넷과 책만으로도 혼자서 두 개의 아이폰 앱을 출시까지 해봤고, 또 이 바닥은 타 분야에 비해서 학력이나 학위를 덜 중요하게 여긴다는 것을 어디선가 들었기 때문이었다.

그리고 이 생각은 곧 바뀌었다. CS 전공은 적어도 나에겐 반드시 필요한 것들을 충족 시킬 수 있는 방법이었다.

  • 자료구조와 알고리즘처럼 코드를 짤 때 필요한 배경지식을 얻고 싶었다. 방법을 모르니 개발 효율이 떨어지고, 내가 무엇을 모르는지 몰라서 효율적인 방법을 어떻게 찾을 수 있는지도 모르는게 문제였다. 아는 개발자가 없으니 계속 혼자 삽질해야 했다.
  • 소프트웨어의 동작 원리에 대한 궁금증을 풀고 싶었다. 하드웨어는 분명 현실에 존재하는데, 코드가 실제로 하드웨어를 어떻게 제어하는지 궁금했다. 현실과 디지털 세상이 어떻게 연결되고 있는지 알고 싶었다.
  • 이건 현실적인 문제인데, 상당수의 소프트웨어 엔지니어 채용공고에서 학위를 필요로 한다. 특히 외국의 경우는 그 비율이 더 높은 것 같았다. 일부러 기회를 좁힐 이유는 없었다.

복학과 함께 복수전공을 신청하기로 했지만, 그 과정도 모험이었다.

내가 복학하는 학기부터 수강신청 시스템이 바뀌어서 복수전공 신청보다 수강신청을 먼저하게 되었다. 결국 CS 과목으로 수강신청을 먼저 하고, 그 다음주에 복수전공을 신청했다. 우리학교에서 복수전공은 직전학기까지의 평점으로 신청자의 순위를 매겨 T.O로 잘라서 당락을 결정한다. 내 평점은 그리 나쁘진 않았지만 당락을 걱정할 수준이었다. 다행히도 그 학기 CS 복수전공 신청은 미달났고 (아마도…), 내 계획은 성공했다.

복수전공을 늦게 시작해서 졸업 이수까지의 학점이 빠듯했다. 계산해보니 졸업할 때까지 모든 학기를 꽉꽉 채워 들어야 딱 맞게 졸업 학점을 맞출 수 있었다. 학점이 아까워서 쉬운 과목은 듣지 않고, 궁금해 했던 과목이나 재미있어보이는 과목 위주로 수강했다. 선수강을 권장하는 과목을 순서대로 듣는 것은 사치였다. 컴퓨터 구조론을 먼저 듣고 논리 회로는 그 다음 학기에 듣거나 알고리즘과 자료구조를 동시에 듣는 식이었다. 5월의 황금연휴동안 16비트 컴퓨터의 회로도를 전지에 직접 손으로 그리는 과제도 신선했다.

그냥… 뭔가 뿌듯해서 끄적여봤다.

Docker-compose의 몇 가지 팁

docker-compose build 시 image의 이름

docker-compose build하면 생성된 이미지의 이름은 folderName_{serviceName}이다. docker build -t로 이미지를 다른 이름으로 생성했는데, docker-compose up하면 이미지를 또 생성하길래 어리둥절 했는데, 이미지의 네이밍이 원인이였다.

로컬 혹은 외부에서 database container에 접근

아래와 같이 docker-compose.yml에서 database container의 포트를 포워딩하면 된다.

1
2
3
mysql:
ports:
- "12312:3306"

mysql container 셋업 시 database를 원하는대로 초기화하려면

docker-compose.yml이 있는 소스코드 루트에 docker-entrypoint-initdb.d 폴더를 만들고, 여기에 *.sql파일을 만들어 원하는 쿼리를 넣어준다.
그리고 docker-compose.yml에 volumes을

1
2
3
mysql:
volumes:
- ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d

Cursor based pagination

다수의 데이터를 나눠서 보여주기 위해서 난 여태까지 offset 기반의 페이지네이션만 써왔다. 다른 방법이 있는지도 몰랐다.

Offset based pagination

1
2
3
SELECT * FROM Posts ORDER BY id DESC LIMIT 0, 10; -- page 1
SELECT * FROM Posts ORDER BY id DESC LIMIT 10, 10; -- page 2
SELECT * FROM Posts ORDER BY id DESC LIMIT 20, 10; -- page 3

offset 기반 페이지네이션은 현재 내가 보고있는 페이지의 위치를 알 수 있는 장점이 있다. 다수의 페이지를 건너뛸 수도 있다. 게시판이나 블로그 같은 곳에서 많이 쓰인다. 페이지 하단에 현재 페이지 번호와 함께 다른 페이지로 이동할 수 있는 버튼들이 있다면 offset 기반의 페이지네이션을 사용한 것이다.

하지만 offset 기반의 페이지네이션은 페이지 이동 시 데이터가 중복되거나 생략될 수 있다. 게시판에서 내가 page 2를 보고 있는 중이라고 가정해보자. 그동안 누군가가 새 글을 올렸다. 이 때 page 3으로 이동했다면 데이터가 하나씩 밀린다. 그래서 이전 페이지의 마지막 글이 page 3에서도 보인다. 또 다른 예로, page 2를 보고 있는 중에 앞 페이지의 글이 지워졌다. 이 때 page 3으로 이동했다면 데이터가 땡겨진다. 그래서 원래 page 3에서 첫번째로 보였어야 할 글이 page 3에서는 보이지 않고 page 2로 다시 돌아가야 보인다.

이런 이유 때문에 offset 기반의 페이지네이션으로는 무한 스크롤등의 UI를 구현하기 어렵다. 하지만 변화하지 않는 데이터에서는 문제가 없다.

Cursor Based Pagination

Cursor 기반 페이지네이션은 이런 문제를 해결해 준다. 현재 페이지의 첫 데이터와 마지막 데이터를 보고 ‘그 다음 X개’, ‘그 이전 X개’의 데이터를 불러오는 방법이다. Cursor 기반 페이지네이션을 위해선 데이터 정렬과 탐색을 위한 column이 반드시 unique해야 한다. 중복될 수 있는 column으로 정렬할 경우 페이지 이동시 데이터가 생략될 수 있다. created_at과 같은 column을 쿼리에 이용하지 않는 이유다.

1
2
3
4
5
-- current page is showing [8, 7, 6]

SELECT * FROM Posts ORDER BY id DESC LIMIT 3; -- first page
SELECT * FROM (SELECT * FROM Posts WHERE id > 8 ORDER BY id ASC LIMIT 3) t ORDER BY id DESC; -- previous page
SELECT * FROM Posts WHERE id < 6 ORDER BY id DESC LIMIT 3; -- next page

cursor 기반 페이지네이션은 페이지 이동시 데이터의 흐름이 매끄럽다. 무한 스크롤도 쉽게 구현할 수 있다. 다만 현재 페이지의 위치를 알 수 없고, 다수의 페이지를 건너 뛸 수 없다.

Improved cursor based pagination

위의 쿼리는 database의 id가 auto_increment의 특성을 가진 것을 이용해서, id가 높다면 더 최신 데이터라는 가정이 담겨있다. id 대신 연속성이 없는 column을 primary key로 사용하거나, 훗날 데이터 사이사이에 다른 데이터를 삽입할 가능성이 있어서 id로 정렬하면 안되는 경우가 있을 수 있다.

이런 경우 2개의 column으로 정렬과 탐색을 하는 cursor 기반 페이지네이션을 구현하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
current page is showing [
{ id: 8, created_at: '2017-01-01 00:00:00' },
{ id: 7, created_at: '2017-01-01 00:00:00' },
{ id: 6, created_at: '2017-01-01 00:00:00' },
]
*/

SELECT * FROM Posts ORDER BY created_at DESC, id DESC LIMIT 3; -- first page

SELECT * FROM (
SELECT * FROM Posts
WHERE created_at > '2017-01-01 00:00:00' OR (created_at = '2017-01-01 00:00:00' AND id > 8)
ORDER BY created_at ASC, id ASC LIMIT 3
) t ORDER BY created_at DESC, id DESC; -- previous page

SELECT * FROM Posts
WHERE created_at < '2017-01-01 00:00:00' OR (created_at = '2017-01-01 00:00:00' AND id < 6)
ORDER BY created_at DESC, id DESC LIMIT 3; -- next page

id 대신 다른 primary key를 사용하고 있다면 쿼리에서 해당 부분만 바꿔주자.

최근 해커톤들에 대한 생각

내가 해커톤에 처음 참여한 건 이 분야에서 일을 하는 다른 사람들을 만나고 싶어서였다. 개발 공부를 시작하긴 했지만 주변에 아는 개발자가 없었다. 그래서 정보를 얻을 수 있는 멘토나 친구를 찾을 수 있는 개발자가 많은 행사를 찾았는데 그게 해커톤이였다.

지금은 다른 개발자들과 함께하면서 영감을 얻고 내가 아직 얼마나 개발을 못하나 자각하기 위해서 간다. 처음 만나는 사람들과 함께 팀을 이뤄서 짧은 시간 동안 무에서 뭔가를 만들어낸다는건 여전히 재미있다.

해커톤이란 단어가 알려지기 시작하면서 해커톤 행사도 많아졌다. 그런데 이런 행사들중 진짜 해커톤은 몇 개 없다. 최근 온오프믹스 등에 올라오는 해커톤들은 아래 세 경우중 하나다.

  1. 아이디어 경연 대회 - 정부 부처가 주도한 가짜 해커톤의 경우 산출물은 잘 보지도 않는다. 그저 심사위원들이 본인이 생각하기에 기발한 아이디어면 수상한다. 하지만 전혀 기발하지 않다. 산업에 대한 트렌드나 기술에 대한 배경지식이 없는 사람이 무슨 자격으로 그들을 심사하는가?

  2. 팀 단위 참가 - 팀 단위로 참가 신청을 받으면서 해커톤이란 이름을 붙히는 행사도 많다. 개인은 참가할 수 없다. 행사 전에 각 팀들이 얼마나 미리 준비 해 오는지 알 수 없다. 팀으로 참가하되 준비를 전혀 해오지 않으면 괜찮지 않냐고 반문할 수 있지만 여태 그런 팀을 본 적이 없다. 행사 시간 내에 만들어야한다는 안내가 있지만 대부분 미리 구상하고 전부 만들어 온다. 주최측에서도 확인할 방법이 없다.

  3. 개인 단위 참가지만 이미 팀 존재 - 작년 말 스타트업위크엔드에서 국민대 학생들이 대거 참가했다. 아마 사회자의 추천으로 참가한 듯 싶다. 그들끼리만 두 팀을 이루었고, 다른 참가자가 같이 할 수 있는 여지조차 없었다.

해커톤은 행사 시간 내에 작동 가능한 산출물을 내어야 한다. 행사의 목적이 그게 아니라면 제발 해커톤 대신 다른 이름을 붙혀줬으면 좋겠다. 앞으로도 해커톤이 있다면 계속 참여는 하겠지만 반복적으로 이런 식이면 조만간 흥미가 떨어질 것 같다.

파이썬 버전 관리 - pyenv

pyenv는 파이썬의 버전 관리 툴이다. 이를 이용하면 한 컴퓨터에서 여러 버전의 파이썬을 설치할 수 있고, 손쉽게 원하는 버전으로 전환할 수 있다.

pyenv는 루비의 버전 관리 툴인 pyenv를 fork했다. pyenv와 사용법이 거의 동일하다.

설치

github에서 코드를 받아 설치한다.

1
$ git clone https://github.com/pyenv/pyenv.git ~/.pyenv

macOS는 Homebrew로 컴파일없이 설치할 수 있다.

1
2
$ brew update
$ brew install pyenv

설치하고나면 사용하고 있는 쉘 설정 파일에 경로를 추가해주자.

1
2
3
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
$ echo 'eval "$(pyenv init -)"' >> ~/.zshrc

사용

Python 설치

1
2
3
4
5
6
7
8
9
10
11
# 사용할 수 있는 pyenv 명령어 목록
$ pyenv

# pyenv로 설치할 수 있는 ruby 버전 목록
$ pyenv install -l

# 특정버전 python 설치
$ pyenv install 2.3.1

# 설치된 python 버전 목록
$ pyenv versions

버전 전환

1
2
3
4
5
6
7
8
9
10
11
# 현재 폴더의 python 버전 설정
# 현재 폴더에 버전이 명시된 .python-version파일을 생성한다
$ pyenv local 3.6.0

# global python 버전 설정
# ~/.pyenv/version에 버전이 명시된다
$ pyenv global 3.6.0

# 현재 쉘의 python 버전 설정
# 환경변수 $PYENV_VERSION에 버전이 명시된다
$ pyenv shell 3.6.0

세 명령어의 우선순위는 shell > local > global 순이다. 예를들어 global로 1.0, local로 2.0을 설정했어도, shell에서 3.0을 사용하면 3.0의 Python을 사용한다.

설정을 해제하고 싶다면 --unset을 이용한다.

1
2
$ pyenv local --unset
$ pyenv shell --unset

Rehash

실행가능한 python 패키지를 설치한 경우 ~/.pyenv/versions/*/bin/*를 갱신시켜주어야한다.

1
$ pyenv rehash

rbenv - 루비 버전 관리

rbenv는 루비의 버전 관리 툴이다. 이를 이용하면 한 컴퓨터에서 여러 버전의 루비를 설치할 수 있고, 손쉽게 원하는 버전으로 전환할 수 있다.

문서에도 잘 설명되어 있지만 간단하게 $PATH 앞에 경로를 추가해 rbenv를 통해 설치한 루비가 실행되도록 하는 원리다. rbenv는 이 경로를 shim이라고 한다.

설치

github에서 코드를 받아 컴파일한다.

1
2
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
$ cd ~/.rbenv && src/configure && make -C src

macOS는 Homebrew로 컴파일없이 설치할 수 있다.

1
2
$ brew update
$ brew install rbenv

설치하고나면 사용하고 있는 쉘 설정 파일에 경로를 추가해주자.

1
2
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.zshrc
$ echo 'eval "$(rbenv init -)"' >> ~/.zshrc

사용

Ruby 설치

1
2
3
4
5
6
7
8
9
10
11
# 사용할 수 있는 rbenv 명령어 목록
$ rbenv

# rbenv로 설치할 수 있는 ruby 버전 목록
$ rbenv install -l

# 특정버전 ruby 설치
$ rbenv install 2.3.1

# 설치된 ruby 버전 목록
$ rbenv versions

명령어 목록에 install, uninstall이 없다면 ruby-build를 설치하자.

버전 전환

1
2
3
4
5
6
7
8
9
10
11
# 현재 폴더의 ruby 버전 설정
# 현재 폴더에 버전이 명시된 .ruby-version파일을 생성한다
$ rbenv local 2.3.1

# global ruby 버전 설정
# ~/.rbenv/version에 버전이 명시된다
$ rbenv global 2.3.1

# 현재 쉘의 ruby 버전 설정
# 환경변수 $RBENV_VERSION에 버전이 명시된다
$ rbenv shell 2.3.1

세 명령어의 우선순위는 shell > local > global 순이다. 예를들어 global로 1.0, local로 2.0을 설정했어도, shell에서 3.0을 사용하면 3.0의 루비를 사용한다.

설정을 해제하고 싶다면 --unset을 이용한다.

1
2
$ rbenv local --unset
$ rbenv shell --unset

Rehash

bundler와 같이 실행가능한 gem을 설치한 경우 ~/.rbenv/versions/*/bin/*를 갱신시켜주어야한다.

1
$ rbenv rehash