Available for hire

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

넷플릭스에 알파고 다큐멘터리가 올라와서 봤다. 영상은 이세돌과 알파고의 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 프로젝트를 만들고 필요한 모듈을 설치한다.

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

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

// 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.

<!-- 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로 서버에 요청하면, 오른쪽 데이터가 그 응답으로 반환된다. 쿼리와 결과의 모양을 보자.

query {
  hero {
    name
  }
}
{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

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

Query

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

{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}
{
  "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을 사용하자.

mutation {
  updateHuman(id: "1000", input: {
    name: "Vichyssoise"
  }) {
    name
    height(unit: FOOT)
  }
}
{
  "data": {
    "updateHuman": {
      "name": "Vichyssoise",
      "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은 필수가 아니다.

schema {
  query: Query
  mutation: Mutation
}

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

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들을 마저 정의하자.

type Player {
  id: ID!
  name: String!
  team: Team
}

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

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

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

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

{
  player(id: 100) {
    name
  }
}
{
  "data": {
    "player": {
      "name": "Vichyssoise"
    }
  }
}
{
  player(id: 100) {
    name
    team {
      id
    }
  }
}
{
  "data": {
    "player": {
      "name": "Vichyssoise",
      "team": {
        "id": 99
      }
    }
  }
}
{
  player(id: 100) {
    name
    team {
      players {
        name
      }
    }
  }
}
{
  "data": {
    "player": {
      "name": "Vichyssoise",
      "team": {
        "players": [
          {
            "name": "Vichyssoise"
          }
        ]
      }
    }
  }
}

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

{
  player(id: 100) {
    team {
      players {
        team {
          players {
            team {
              id
            }
          }
        } 
      }
    }
  }
}
{
  "data": {
    "player": {
      "team": {
        "players": [
          {
            "team": {
              "players": [
                {
                  "team": {
                    "id": 99
                  }
                }
              ]
            }
          }
        ]
      }
    }
  }
}

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