아이디어 구현하기

난 만들어보면 재미있을 것 같은 아이템 목록을 적어둔다. 이 아이디어들은 혼자서 심심할 때 혹은 해커톤에 나갈 때 하나씩 구현된다. 바쁘다는 핑계로 목록 개수만 늘이고 있었는데 요새 시간이 많아져서 몇 가지 만들어보기로 했다. 좀 정상적인 아이템들은 해커톤 나가서 다른 사람들과 같이 만들어 볼 수 있기 때문에, 그렇지 않은 걸로만 몇 개 골랐다.

Dynamic Dictation

https://pueue.com/dynamic-dictation/

음성과 음량을 글자로 바꿔준다. SpeechRecognition APIAudio API를 적절히 섞어서 목소리의 음량과 표시되는 글자의 크기가 비례하도록 했다.

Uncertainty

https://github.com/pueue/uncertainty

측정될 때 값이 정해지는 데이터 타입. 현재 양자역학의 주류인 코펜하겐 해석에 의하면 입자의 상태는 관측하는 그 순간에 정해진다. Uncertainty 타입의 데이터도 값이 정해지지 않은 상태로 메모리에 존재하지만 사용하는 순간에 값이 특정된다. Object.defineProperty()로 구현할 수 있었다.

idiots.io

https://idiots.io/

익명 직장 채팅 어플. 블라인드라는 앱의 채팅버전이다. 아이템 자체로 특별한 건 없고 GraphQL Subscription을 써보고 싶어서 만들었다. 한 일주일이면 서버와 앱까지 다 만들 수 있을 줄 알았는데, React Native가 발목을 잡아서 며칠 더 걸렸다. 역시 프론트엔드는 어려워.

jongfill

https://github.com/pueue/jongfill

한글에 종성을 채워준다. 숫자를 문자로 출력할 때 고정된 길이로 0을 붙혀서 출력하는 것을 zerofill 또는 zfill이라고 한다. 또 ‘곣맋웏욗’처럼 종성을 분리해서 문장을 쓰는게 인터넷에서 유행했던 적이 있었다. jongfill이란 이름의 이 라이브러리는 얼핏 듣기에는 마치 사람 이름 같지만 어떤 기능을 하는지 직관적으로 표현해주는 가장 완벽한 이름을 가지고 있다.

프로그래밍에서 창의성이란 무엇인가?

넷플릭스에 알파고 다큐멘터리가 올라와서 봤다. 영상은 이세돌과 알파고의 Google Deepmind Challenge match의 비하인드 스토리를 흥미롭게 풀어냈다. 재미있게 시청중에 이세돌의 멘트에 정신이 번쩍했다.

바둑에 정말 창의성이 필요한가?
바둑에서 창의성이란 무엇인가?

프로그래밍에 정말 창의성이 필요한걸까? 프로그래밍에서 창의성이란 무엇인가?

이런 고민들을 앞서 해본 다른 사람들의 생각을 구글링하여 읽어봤다. 대체적인 의견은 프로그래밍은 예술활동에 비견될 수 있다는 것이다. 이 둘은 결과물을 만들기 위해 여러가지 도구를 다양한 방법으로 사용하여 무엇인가를 만드는데에 그 공통점이 있다.

그렇다면 프로그래밍에서 도구는 무엇이고, 다양한 방법은 어떤 것들이 있으며, 작품은 무엇이란 말이지? 장비와 에디터등 개발 환경이 도구라면, 그 위에서 사용하는 언어와 프레임워크, 라이브러리는 개발 방법인가? 아니면 이런것들 모두 도구라고 볼 수 있으니, OOP나 functional programming 같은 개발 패러다임이 개발 방법인가? 심지어 완성된 코드는 누군가에겐 또 다른 도구가 될 수 있다. 이런 것들을 명확하게 구분지을 순 있는 것인가? 구분지을 필요는 있을까?

Socket.io로 카운터 만들기

웹에서 전통적인 HTTP는 클라이언트가 요청을 하면 서버가 응답하는 방법으로 통신한다. 내 게시글에 새 댓글이 달렸는지 확인하기 위해서 새로고침을 눌러봐야 한다. 즉, 요청-응답이 끝나면 연결을 종료하기 때문에 클라이언트가 다시 요청하기 전까지는 서버에서 업데이트된 데이터를 받아올 수 없다. 내놓으라고 하기 전까진 스스로 토해내지 않는게 마치 통신사 환급금을 보는 것 같다.

정말로?

CS 전공을 한 사람이라면 네트워크 실습시간에 소켓 통신으로 TCP와 UDP 채팅 프로그램을 만들어 본 적이 있을 것이다. TCP는 연결된 상대가 데이터를 잘 받았는지 확인하고, UDP는 받던 말던 그냥 보내고 본다는 차이가 있으며, TCP는 전화, UDP는 편지에 비유했던 것으로 기억한다. TCP와 UDP에서 중요한 것은 서버와 클라이언트가 소켓으로 연결되어 있다는 점이다. 소켓은 상대방의 주소(목적지)를 담고 있기 때문에 내가 누구에게 데이터를 보내야하는지 명확하다. 소켓으로 연결되어 있다면 서로 누구나 먼저 데이터를 보낼 수 있다. HTTP는 TCP 프로토콜을 이용한다. 그러니까 웹에서도 TCP를 이용하여 소켓 통신을 할 수 있다. HTML5의 웹소켓(Web Socket)으로 말이다.

Socket.io는 웹소켓을 이용하여 서버와 클라이언트 간의 실시간 양방향 통신을 가능케하는 라이브러리다. Socket.io가 지원하는 브라우저 목록을 보면 웹소켓을 지원하는 브라우저가 아닌 것들이 있다. 웹 소켓을 사용할 수 없는 브라우저는 다른 방식을 이용해서 마치 웹소켓이 작동하는 것처럼 보이게 한단다. 많은 Socket.io를 주제로 작성된 블로그 포스트들이 이 내용을 언급하고 있지만 난 그 내용이 어디있는지 찾지 못했다.


Socket.io를 이용해서 간단한 웹 애플리케이션을 만들어보자. 채팅 프로그램을 구현한 튜토리얼은 이미 아주 많으니까 나는 더 단순한 앱을 만들어보려고 한다.

버튼에 숫자가 적혀있다. 버튼을 누르면 숫자가 1 증가한다. 이 숫자는 모든 클라이언트에게 동일하게 보인다. 내가 버튼을 눌러서 숫자를 높히면, 이 웹에 접속 중인 다른 사람들도 실시간으로 숫자가 늘어나는 것을 볼 수 있다.

먼저 간단하게 Node.js 프로젝트를 만들고 필요한 모듈을 설치한다.

1
2
3
mkdir socket.io-test
cd socket.io-test && npm init
npm install -S express socket.io

server.js 파일을 만들고 서버를 작성한다.

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
// server.js
const app = require('express')()
const http = require('http').Server(app)
const io = require('socket.io')(http)

let count = 0

app.get('/', (req, res) => {
res.sendFile(__dirname + '/client.html');
})

io.on('connection', socket => { // 1
console.log('user connected: ', socket.id)
socket.emit('get count', count) // 2

socket.on('increase count', () => { // 3
console.log('increase!', count)
count++
io.emit('get count', count) // 4
})

socket.on('disconnect', () => { // 5
console.log('user disconnected: ', socket.id)
})
})

http.listen(3000, () => {
console.log('server on!')
})

클라이언트가 접속하면, 현재 소켓의 id를 콘솔에 출력하고 count 값을 전달한다. 이 클라이언트가 ‘increase count’ 이벤트를 발생시키면, count 값을 1 증가하고 접속중인 모든 클라이언트에게 ‘get count’ 이벤트로 증가한 count 값을 전달한다. 이 클라이언트가 접속을 종료하면, 현재 소켓의 id를 콘솔에 출력한다.

  1. on()은 어떤 이벤트가 발생했을 때 어떤 함수를 실행할지 정의하는 메소드다. 클라이언트가 서버에 접속하면 ‘connection’ 이벤트가 발생한다. 함수의 파라미터로는 해당 연결에 대한 Socket이 넘어온다.
  2. emit()는 이벤트를 발생시키는 메소드다. 현재 소켓에 ‘get count’ 이벤트를 발생시킨다. count를 이벤트 데이터로 보낸다.
  3. 소켓으로부터 ‘increase count’ 이벤트가 발생하면 count를 증가시킨다.
  4. 모든 연결된 모든 소켓에 증가한 count를 담은 ‘get count’ 이벤트를 발생시킨다.
  5. 클라이언트가 접속을 종료하면 해당 소켓으로부터 ‘disconnect’ 이벤트가 발생한다.

다음은 클라이언트를 만들자. 파일명은 client.html.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- client.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Counter</title>
</head>
<body>
<button id="count" value="increase"></button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script>
const socket = io()
$('button').click(e => {
socket.emit('increase count') // 1
e.preventDefault()
})
socket.on('get count', count => { // 2
$('#count').text(count)
})
</script>
</body>
</html>
  1. 버튼을 누르면 ‘increase count’ 이벤트를 발생한다.
  2. 서버로부터 ‘get count’ 이벤트가 발생하면 count를 받아 버튼 라벨에 반영한다.

완성했다. 이제 node server.js로 서버를 실행한다. 브라우저 두 개를 띄우고 http://localhost:3000로 접속해서 버튼을 눌러보자.

GraphQL 기초

추석 전 마지막 스프린트 진행이 생각보다 빨라서 enhancement로 API 서버에 GraphQL을 구성했다.

GraphQL은 API 쿼리 랭귀지다. RESTful한 API는 endpoint마다 출력결과 형태가 고정되어서, API client는 필요한 데이터를 위해 API를 여러번 호출하거나, 필요없는 데이터까지 받아올 수 밖에 없는 경우가 많다. GraphQL은 API client가 원하는 데이터를 원하는 모양으로 출력할 수 있게 한다.

API client 입장에서 GraphQL을 이용하여 어떻게 요청하는지부터 알아보자. 서버가 어떻게 만들어지는지, 데이터를 어떻게 불러오는지는 지금 몰라도 좋다. 왼쪽과 같은 GraphQL 쿼리를 POST body로 서버에 요청하면, 오른쪽 데이터가 그 응답으로 반환된다. 쿼리와 결과의 모양을 보자.






1
2
3
4
5
query {
hero {
name
}
}





1
2
3
4
5
6
7
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}



GraphQL API를 사용하는 입장에서는 원하는 모양 그대로 요청하면 되니 아주 직관적이다. 데이터를 읽는 요청을 query라 하고, 데이터를 변경하는 요청을 mutation이라 한다.

Query

query는 데이터를 불러오는 구문이다. RESTful API에서의 GET 메소드에 해당한다.






1
2
3
4
5
6
{
human(id: "1000") {
name
height(unit: FOOT)
}
}





1
2
3
4
5
6
7
8
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 5.6430448
}
}
}



처음 예제와 다르게 시작 부분에 query가 생략되었다. query문의 경우는 생략이 가능하고, mutation문일 경우는 명시 해주어야 한다. human, name, height 등 필요한 리소스의 field를 특정하여 응답 데이터의 형태를 구성할 수 있다. field에는 마치 함수처럼 argument를 전달할 수도 있다.

field는 리소스 그 자체가 될 수도 있고, 리소스의 property가 될 수도 있다. 어떤 field들이 있는지, 어떤 field에 어떤 argument를 쓸 수 있는지는 서버 사이드에서 정의한다.

Mutation

mutation은 데이터를 변경하는 구문이다. 리소스를 생성하는 구문과 수정하는 요청 모두 mutation를 사용 할 수 있다. RESTful API에서의 POSTPUT 혹은 FETCH에 해당한다. REST에서 http 요청 메소드와 CRUD의 관계는 걍제성 없는 약속과 같듯, GraphQL에서도 query로 데이터를 변경할 수는 있다. 하지만 GraphQL이 POST 메소드만 사용하기 때문에 해당 요청이 데이터 변경임을 명시적으로 하기 위해 mutation을 사용하자.






1
2
3
4
5
6
7
8
mutation {
updateHuman(id: "1000", input: {
name: "Pueue"
}) {
name
height(unit: FOOT)
}
}





1
2
3
4
5
6
7
8
{
"data": {
"updateHuman": {
"name": "Pueue",
"height": 5.6430448
}
}
}



해당 요청이 mutation임을 명시적으로 나타냈다. updateHuman은 2개의 argument를 받는다. 이 mutation은 id로 human을 찾아서 input의 내용대로 변경한다. mutation이 어떤 argument를 받을지와 어떻게 작동할지 역시 서버에서 정의한다. 그 다음 query와 마찬가지로 어떤 형태로 데이터를 반환할지 명시한다.


지금까지는 GraphQL API를 어떻게 이용하는지 봤다. 이번엔 GraphQL API가 어떻게 만들어지는지 보자.

GraphQL 서버는 schema와 resolver로 이루어진다. schema는 query와 mutation을 정의하고, 어떤 field에 어떤 하위 field가 있는지를 정의한다. resolver는 field의 데이터를 불러오는 방법을 정의한다. resolver는 GraphQL 그 자체의 영역은 아니다. GraphQL을 사용할 수 있게 해주는 여러 구현체(라이브러리)들이 있어서, 여기에 맞춰 코드를 작성해야 한다.

Schema

schema 정의는 root object부터 시작한다. root object는 query와 mutation을 가지며 mutation은 필수가 아니다.

1
2
3
4
schema {
query: Query
mutation: Mutation
}

queryQuery 타입이고, mutationMutation 타입이다. 이 두 타입은 직접 정의해야 하며 타입 명이 꼭 QueryMutation일 필요는 없다. Query는 어떻게 정의할 수 있을까?

1
2
3
type Query {
player(id: ID!): Player
}

위의 정의는 다음과 같다. Query 타입은 player field가 있다. playerPlayer타입이고 id를 argument로 받는다. 그리고 idID타입이고 null이 아니다.

모든 field와 argument에는 타입이 있다. GraphQL에서 타입의 종류는 2가지로 Scalar 타입과 Object 타입이 있다. Scalar 타입은 이미 정의된 최소 단위의 데이터 타입을 말한다. Int, Float, String, Boolean, ID가 Scalar 타입이며 ID 타입은 object의 캐싱을 위해 사용된다. Object 타입은 field들을 가진 데이터 타입으로 이 field들 또한 Scalar 타입이거나 Object 타입이다. Object 타입은 직접 정의해주어야 하며, 여기선 QueryPlayer가 Object 타입이다. 필요한 Object들을 마저 정의하자.

1
2
3
4
5
6
7
8
9
10
11
type Player {
id: ID!
name: String!
team: Team
}

type Team {
id: ID!
name: String!
players: [Player]!
}

타입 뒤에 !는 해당 데이터가 null이 아님을 나타낸다. [Player]처럼 Array 타입을 정의할 수도 있다.

위에도 언급했듯 query와 mutation은 데이터 변경을 명시화해준다는 것 외에는 차이가 없으니 Mutation 타입 정의는 생략하겠다. (작동 방식에 차이가 좀 있지만 몰라도 상관없음)

위와 같이 구성된 GraphQL 서버를 다음과 같은 구문으로 사용할 수 있다.
















1
2
3
4
5
{
player(id: 100) {
name
}
}





1
2
3
4
5
6
7
{
"data": {
"player": {
"name": "Pueue"
}
}
}





1
2
3
4
5
6
7
8
{
player(id: 100) {
name
team {
id
}
}
}





1
2
3
4
5
6
7
8
9
10
{
"data": {
"player": {
"name": "Pueue",
"team": {
"id": 99
}
}
}
}





1
2
3
4
5
6
7
8
9
10
{
player(id: 100) {
name
team {
players {
name
}
}
}
}





1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"data": {
"player": {
"name": "Pueue",
"team": {
"players": [
{
"name": "Pueue"
}
]
}
}
}
}



심지어 이런 짓도 가능하다.






1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
player(id: 100) {
team {
players {
team {
players {
team {
id
}
}
}
}
}
}
}





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"data": {
"player": {
"team": {
"players": [
{
"team": {
"players": [
{
"team": {
"id": 99
}
}
]
}
}
]
}
}
}
}



요청 구문에서 제일 하위 field들은 모두 Scalar 타입임에 주목하자.

Withcamp 해커톤

hhkb_pro2

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

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

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

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

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

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

9xd 해커톤

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

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

기대한 만큼 만족도 컸다.

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

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

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

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

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

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

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'],
}, (accessToken, refreshToken, profile, done) => {
const { id, email } = profile._json;
const user = {
provider: 'facebook',
providerID: id,
email: email,
}
return done(null, user);
}));
app.use(passport.initialize());

로그인을 수행할 route를 추가한다. /login/facebook으로 접속하면 페이스북이 호스팅하는 페이지로 이동해서 사용 허가를 요구할 것이다. scope는 앱의 권한 범위이고, 사용자에게 앱을 허가 받을때 요구하는 권한이 보여진다. 권한 목록은 Permissions Reference - Facebook Login에서 볼 수 있다.

/login/facebook/callback은 인증 허가 이후 리다이렉트되는 주소. 이 주소들은 화이트리스트로 관리하며, 위에서 ‘Valid OAuth redirect URIs’에 이 주소를 추가하지 않았다면 ‘URL Blocked’ 에러가 뜰 것이다. 인증에 실패하거나 거부하면 failureRedirect로 이동한다. 성공하면 passport strategy에서 반환하는 데이터를 req.user로 이용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.get('/login/facebook',
passport.authenticate('facebook', {
scope: ['public_profile', 'email'],
})
);

app.get('/login/facebook/callback',
passport.authenticate('facebook', {
session: false,
failureRedirect: '/login/facebook',
}), (req, res) => {
return res.json(req.user);
}
);

마지막으로 서버를 실행한다.

1
2
3
4
const port = 3000;
app.listen(port, () => {
console.log(('App is running at http://localhost:%d'), port);
});

이제 브라우저를 열고 http://localhost:3000/login/facebook 에 접속하자. 페이스북이 호스팅하는 페이지를 경유해야하기 때문에 curl등은 사용할 수 없다. 페이스북에 로그인하고 앱 사용 권한을 허가해주면 http://localhost:3000/login/facebook/callback 로 리다이렉트되면서 사용자 정보를 확인할 수 있다.

1
2
3
4
5
{
"provider": "facebook",
"providerID": "[YOUR_APP_ID]",
"email": "[YOUR_FACEBOOK_EMAIL]"
}

전체 코드 보기

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에서 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 = 'SECRET';

서버 객체를 만들고 body-parser를 사용한다. 새로운 비밀번호가 request의 body로 담겨 전달될 것이기 때문에 body-parser가 필요하다.

1
2
3
4
5
6
const express = require('express');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

로그인을 수행할 route를 추가한다. 로그인에 실패하면 401을 반환하고, 로그인에 성공하면 토큰을 반환한다. 토큰은 위에서 정의한 비밀키로 만들어진다. 토큰의 만료기한을 발행일로부터 하루로 한다. 토큰의 payload에는 id와 username을 담아서 반환한다.

1
2
3
4
5
6
7
8
9
10
const jwt = require('jsonwebtoken');

app.post('/login', (req, res) => {
const { username, password } = req.body;
if (username !== user.username || password !== user.password) {
return res.status(401).end(); // login failed
}
const token = jwt.sign({ id: user.id, username: user.username }, SECRET, { expiresIn: '1d' });
return res.json({ accessToken: token });
});

passport에 jwt strategy를 사용한다. payload에 담긴 id가 데이터베이스에 있는 id와 일치하면 토큰이 인증된다. 토큰을 추출하는 방법에는 여러가지가 있으나, 예제에서는 request header에 ‘Authorization’키에서 토큰을 추출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;

passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeader(),
secretOrKey: SECRET
}, (payload, next) => {
if (payload.id !== user.id) {
next(null, false);
} else {
next(null, user);
}
}));
app.use(passport.initialize());

비밀번호 변경 route를 추가한다. passport.authenticate('jwt', { session: false })로 이 route의 request를 jwt strategy로 검사한다.

1
2
3
4
5
6
7
8
9
10
11
app.put('/users/:userId', passport.authenticate('jwt', { session: false }), (req, res) => {
// Authorization
if (req.params.userId != req.user.id) {
return res.status(403).end();
}

const newPassword = req.body.password;
user.password = newPassword;

return res.status(200).json({ result: 'success' });
});

서버를 실행한다.

1
2
3
4
const port = 3000;
app.listen(port, () => {
console.log(('App is running at http://localhost:%d'), port);
});

이제 서버에 로그인을 요청하면 토큰을 반환한다.

1
2
3
4
5
6
curl -X POST \
-H 'Content-Type: application/json' \
-d '{ "username": "test", "password": "123" }' \
http://localhost:3000/login

# -> {"accessToken":"[ACCESS_TOKEN]"}

이 토큰을 request ‘Authorization’ header에 넣고 비밀번호를 바꿔보자. 토큰 앞에 ‘JWT’를 넣어 토큰의 타입을 전달해야 한다.

1
2
3
4
5
6
7
curl -X PUT \
-H 'Content-Type: application/json' \
-H 'Authorization: JWT [ACCESS_TOKEN]' \
-d '{ "password": "123456" }' \
http://localhost:3000/users/1

# -> {"result":"success"}

전체 코드 보기

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/pueue/nextjs-typescript-boilerplate다.