9xd 해커톤

9xd에서 첫번째 해커톤을 한다길래 얼른 신청했다. 9xd 모임 신청이 수강신청만큼이나 경쟁률이 높아서 이번 해커톤도 아마 신청자 수가 만만치 않았으리라.

예전에 몇몇 해커톤에 불평하는 글을 쓰고 페이스북에 링크한 적이 있는데 9xd를 만든 진유림님이 그걸 공유하신 덕에 내 블로그 일일 방문자가 400을 찍었었다. 나와 비슷한 생각을 하고 계신 것 같아서 직접 주최하는 해커톤에 대한 기대가 컸다.

기대한 만큼 만족도 컸다.

팀 구성을 미리 하지 않았다. 참가자를 받으면서 팀을 미리 구성했지만 행사 당일에 팀을 발표했다. 참가 신청을 받을 때 참가자의 기술스택도 파악해서 아웃풋이 없는 팀이 나오지 않도록 적절하게 팀을 구성한 것도 박수.

주제가 어느정도 좁혀져 있었다. 주최측에서 정하는게 아니라 참가자끼리 주제를 여러개 만들고 랜덤으로 뽑아서 선정했다. 다수의 해커톤 경험을 미루어 볼 때, ‘무엇을 만들까’로 이야기가 길어지는 팀은 대부분 실패한다. 이 때문에 모든 팀이 빠르게 구현에 집중할 수 있지 않았나 싶다.

축제 분위기를 위한 많은 장치가 있었다. BM은 신경쓰지 않고 정말 만들고 싶었던 재미있는 장난감을 만들 수 있게 유도하고, 모든 팀이 상을 받고(우리 팀이 받은 한 뼘 크기 쿠키런 피규어가 젤 맘에 든다), 맥락없는 춤 타임도 있었다.

맥락없는 춤 타임은 왜 있는지 모르겠지만 ㅋㅋ

우리팀은 ‘월급 루팡도 측정기’를 만들었다. Rescue Time과 비슷한데, 업무와 연관된 사이트 접속 비율을 측정해서 화면에 을 뿌려주는 크롬 확장 프로그램이다. 크롬 확장 프로그램은 처음 만들어보았는데, 웹 프론트엔드를 조금만 할 수 있다면 누구나 쉽게 만들 수 있다. API 문서도 상당히 깔끔하게 되어 있다. 발표할 때는 네이버를 비업무사이트의 예로 두고 시연했는데, 참가자 중에 네이버 개발자 분이 계셨다. ㅋㅋㅋㅋㅋㅋ

프로그램도 알찼고, 축제 같은 느낌, 무엇보다 내가 좋아하는 ‘어설픈 행사 진행과 사회’가 있어서 만족스러웠다.(공감 못하면 그냥 넘어가자…ㅋㅋ

Withcamp 해커톤

첫 야외 해커톤에, 첫 하드웨어 해커톤이다.

우리팀은 얼굴 각도를 인식해서 자동으로 뿅망치 맛을 보여주는 참참참 기계를 만들기로 했다. 톱질, 망치질, 사포질을 위해 몸을 쓰는 해커톤이라니! 신선하지 않을 수가 없다.

우리팀 팀원은 기계공학과 대학생과 대학원생들이었다. 설계하는데 다들 알파벳으로 이야기하니까 뭔가 있어보였다. 대충 설계를 끝내고 주최측에서 제공하지 않지만 필요한 물품들을 다이소에서 구매했다. 행사 당일 길거리에서 누가 버린 작은 책장과 목재를 가져다가 프레임을 만들고, 다이소에서 샀던 악력기를 뜯고 부셔서 뿅망치의 스프링으로 사용했다.

이 친구들은 코딩도 어느정도 할 줄 알아서 난 기기간 시리얼 통신만 열어주고, 코드가 작동하지 않으면 검수해서 고쳐주는 정도만 했다. 사실 코딩보다 하드웨어 만지는게 더 재미있어 보여서 대부분의 코드는 C#할 줄 아는 다른 팀원이 짰다. 뜨거운 물에 넣으면 녹아서 모양을 만들 수 있는 물라스틱, 사람 얼굴을 인식해서 데이터를 뽑아주는 인텔 리얼센스, 20만원짜리 모터(이름을 까먹었다)와 시리얼 통신으로 작동하는 하드웨어 모듈까지 조금도 쉴 새 없이 계속 새로웠다.

설계의 착오로 뿅망치 장전을 위해 낚시줄을 사용해 감아서 발표 때 1회 시연하고 망가져 버렸지만, 제대로 작동하는 걸 확인했다. 사회자님 눈은 괜찮으실라나.

모듈과 부품들 잔뜩 사놓고 취미삼아 만져봐도 재밌을 것 같다. 시간만 있다면…

ID vs UUID

제목이 논리적으로 말이 안되어서 어그로 글인줄 알 수도 있겠지만, 직관적으로 무슨 내용의 글인지 바로 알 수 있는 짧은 제목을 찾다보니 저래됐다… 진짜 제목은 ‘primary key로써 순차증가하는 숫자키 vs UUID’.

새로운 서비스를 만들면서 데이터베이스의 primary key의 데이터 타입을 uuid로 할까 그냥 기본값으로 순차증가하는 정수로 할까 고민하면서 구글링 해봤다.

UUID의 장점

  1. 범용적으로 유니크함. 사실 정말 유니크하진 않다. 하지만 경우의 수가 어마어마해서 ‘유일함을 보장’한다고 함. 순차증가키를 사용하면 id=1인 데이터는 전세계에 무수히 많지만, uuid키를 사용하면 해당 id를 가진 데이터는 유일무이하다.
  2. 외부에서 다른 리소스에 임의로 접근을 시도할 확률이 낮아짐. 순차증가 키는 이전 리소스와 다음 리소스의 id를 추측할 수 있지만, uuid는 불가능하다.
  3. Client side에서 id를 생성 가능. 순차증가 키는 데이터베이스를 거치지 않으면 다음에 생성될 데이터의 id를 알 수 없지만, uuid는 그냥 생성해서 삽입하면 된다.

UUID의 단점

  1. 속도. DBMS에 따라 다르지만 순차증가 키에 비해 인덱싱 속도가 느리다.
  2. 용량. 8byte인 big integer보다 2배 크다. 외부 키까지 고려하면 이 차이는 무시못할 수준.

내부에선 순차증가 키를 사용하고 외부와는 uuid를 사용하는 방법도 있었는데, uuid의 단점을 커버하기 위해 장점을 다 포기하는 꼴이라 좋은 전략이라고 볼 수 없다.

결국 UUID를 사용하기로 결정했다. 통신없이 id 생성이 가능한 점이 마이크로 서비스에서 유리하다고 판단했고, 테이블의 병합과 분리가 간단해서 확장성이 높다고 봤다. 또 테이블을 조인하는 쿼리를 실행할 때 사람이 실수할 확률을 낮춰줄 수 있지 않을까 하는 기대도 있다.

데이터가 적은 서비스 초기에 퍼포먼스 이슈는 무시해도 될 것 같고, 데이터가 많아지면 유연한 확장성으로 퍼포먼스 이슈를 해결할 수 있으리라 생각한다.

Express.js에서 oauth로 서드파티 사용자 인증하기

Express.jsPassport.js를 이용해서 페이스북 계정을 인증해보자. 먼저 아래 과정을 통해 우리가 만든 웹 서버가 페이스북 oauth를 이용할 수 있게 한다.

  1. Facebook for Developer에서 새로운 App을 만든다.
  2. 그 다음 Add product > Facebook login을 setup 한다.
  3. 우측 메뉴에 있는 Facebook login의 setting 페이지에서 ‘Valid OAuth redirect URIs’에 ‘http://localhost:3000/login/facebook/callback'을 추가한다. Save changes를 누른다.

필요한 모듈은 다음과 같다.

1
npm install express passport passport-facebook

서버 객체를 만든다.

1
2
const express = require('express');
const app = express();

passport에 facebook strategy를 사용한다. 아까 만든 App의 대시보드에서 App ID와 App Secret을 찾아서 clientIdclientSecret에 입력한다.

인증되면 페이스북으로부터 accessToken, refreshToken과 사용자 정보를 가져온다. 이걸 적절하게 가공한 후에 리턴하면 /login/facebook/callback 라우트에서 req.user로 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const passport = require('passport');
const FacebookStrategy = require('passport-facebook');

passport.use(new FacebookStrategy({
clientID: [YOUR_APP_ID],
clientSecret: [YOUR_APP_SECRET],
callbackURL: '/login/facebook/callback',
profileFields: ['id', 'email', 'name'],

Express.js에서 JWT로 사용자 인증하기

JWT(JSON Web Token)은 정보를 안전하게 전달하는 방법이다. 이때 안전하다는 말은 정보가 타인에게 노출되지 않고 전달된다는 의미가 아니다. 수신자가 받은 정보가 원하는 발신자로 부터 온 것이 맞는지, 전달 도중 누군가에 의해 내용이 바뀌지는 않았는지를 검증할 수 있다는 의미다. JWT는 토큰의 노출을 가정하고 만들어졌다. 전달되는 정보를 암호화하지도 않는다. 그래서 중요한 정보는 JWT로 전달하면 안된다.

JWT는 Header, Payload, Signiture로 구성된다. Header는 토큰의 메타정보를 가진다. Payload는 전달할 정보를 가진다. Signiture는 Header와 Payload를 암호화한 내용을 가진다.

서버에서 JWT를 받았을 때 header와 payload를 signiture와 바교하여 이 토큰을 믿을 수 있는지 알 수 있다. 서버에서 발행한 토큰을 서버에서 검증하기 때문에 signiture를 만들 때 사용하는 암호키를 외부에 노출할 필요가 없다.

JWT를 이용한 사용자 인증 절차는 아래와 같다.

  1. Client에서 username과 비밀번호를 server에 전달. (Login)
  2. 로그인이 성공하면 서버에서 JWT​를 반환.
  3. Client는 매 request마다 ​이 JWT를 함께 server에 전달.
  4. Server는 JWT를 검증하고 request를 수행.

Express.jsPassport.js를 이용해서 사용자 인증을 해보자. 특정 사용자의 비밀번호 변경 request가 왔을 때 이를 수행하는 예제다.

먼저 필요한 모듈을 설치한다.

1
npm install express body-parser passport passport-jwt jsonwebtoken

서버에서 사용할 Database는 단 하나의 사용자 객체다.

1
2
3
4
5
const user = {
id: 1,
username: 'test',
password: '123'
};

JWT의 signiture를 위한 암호키를 정의한다.

1
const SECRET

Github 첫 star

Github에서 처음으로 스타를 받았다. 작은 오픈소스에 내 PR이 merge된 적도 있고, 몇 개의 저장소에 issue를 올려본 적은 있었지만, 내 저장소에서 다른 개발자와의 상호작용이 일어난 건 처음이다.

새로운 프로젝트를 Next.js와 ​Typescript로 만들고 있는데, Next.js에 예제로 있던 ‘with-typescript’는 컴파일된 파일과 소스파일이 같은 곳에 위치하는 등 너무 대충 만들어져 있었다. 컴파일된 파일 위치를 분리시키고 에셋들을 불러올 수 있도록 세팅한 후 저장소를 만들었다.

Next.js 저장소에서 Typescript loader를 사용할 수 있게 하는 PR에다가 이 PR이 merge되기 전까지는 이걸 써보라는 식으로 홍보 댓글도 썼다. 댓글이 효과가 있었는지 스타도 2개나 받고, HMR이 안된다며 해결책과 함께 issue를 등록해주신 분도 계셨다.

아마 Next.js가 ts-loader를 지원하게 되면 필요없어질 짧은 수명의 저장소겠지만, 뭔가 나도 오픈소스 생태계에 한 발짝 들어선 느낌이다.

아! 이 글의 주인공은 https://github.com/scon-io/nextjs-typescript-boilerplate다.

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로 검색해봤더니 이미 비슷한 방식을 구현한게 많네 ㅋㅋ

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

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”: 매 배포마다 이 부분을 수정해서 버전을 높