MongoDB를 개인 프로젝트에서 자주 사용하긴 하는데 항상 쓰던 방식대로만 사용하고 있어서 스키마를 제대로 구성하고 있는지 검색하다가 이 글을 찾게 되었다. MongoDB 블로그에 올라온 포스트인 6 Rules of Thumb for MongoDB Schema Design을 읽고 나서 SQL과 어떻게 다른 전략을 갖고 스키마를 구성해야 하는지 생각하는데 도움이 많이 되었다. 원글은 세 부분으로 나눠 게시되어 있어서 주제를 더 상세하게 다루고 있으므로 이 요약이 불충분하다면 해당 포스트를 확인하자.


SQL에 경험이 있지만 MongoDB가 처음이라면, MongoDB에서 일대다(One-to-N, 왜 N인지는 보면 안다.) 관계를 어떻게 작성할지 자연스레 궁금증을 갖게 된다. 이 글의 주제는 객체 간의 관계를 다루는 방법에 대한 이야기다.

기초

다음 세 가지 방법으로 관계를 작성할 수 있다.

  • One to Few 하나 당 적은 수
  • One to Many 하나 당 여럿
  • One to Squillions 하나 당 무지 많은 수

각각 방법은 장단점을 갖고 있어서 상황에 맞는 방법을 활용해야 하는데 One-to-N에서 N이 어느 정도 규모/농도 되는지 잘 판단해야 한다.

One-to-Few

// person
{
  name: "Edward Kim",
  hometown: "Jeju",
  addresses: [
    { street: 'Samdoil-Dong', city: 'Jeju', cc: 'KOR' },
    { street: 'Albert Rd', city: 'South Melbourne', cc: 'AUS' }
  ]
}

하나 당 적은 수의 관계가 필요하다면 위 같은 방법을 쓸 수 있다. 쿼리 한 번에 모든 정보를 갖을 수 있다는 장점이 있지만, 내포된 엔티티만 독자적으로 불러올 수 없다는 단점도 있다.

One-to-Many

// 편의상 ObjectID는 2-byte로 작성, 실제는 12-byte
// parts
{
  _id: ObjectID('AAAA'),
  partno: '123-aff-456',
  name: 'Awesometel 100Ghz CPU',
  qty: 102,
  cost: 1.21,
  price: 3.99
}

// products
{
  name: 'Weird Computer WC-3020',
  manufacturer: 'Haruair Eng.',
  catalog_number: 1234,
  parts: [
    ObjectID('AAAA'),
    ObjectID('DEFO'),
    ObjectID('EJFW')
  ]
}

부모가 되는 문서에 배열로 자식 문서의 ObjectID를 저장하는 방식으로 구현한다. 이 경우에는 DB 레벨이 아닌 애플리케이션 레벨 join으로 두 문서를 연결해 사용해야 한다.

// category_number를 기준으로 product를 찾음
> product = db.products.findOne({catalog_number: 1234});
// product의 parts 배열에 담긴 모든 parts를 찾음
> product_parts = db.parts.find({_id: { $in : product.parts } } ).toArray() ;

각각의 문서를 독자적으로 다룰 수 있어 쉽게 추가, 갱신 및 삭제가 가능한 장점이 있지만 여러번 호출해야 하는 단점이 있다. join이 애플리케이션 레벨에서 처리되기 때문에 N-to-N도 쉽게 구현할 수 있다.

One-to-Squillions

이벤트 로그와 같이 엄청나게 많은 데이터가 필요한 경우, 단일 문서의 크기는 16MB를 넘지 못하는 제한이 있어서 앞서와 같은 방식으로 접근할 수 없다. 그래서 부모 참조(parent-referencing) 방식을 활용해야 한다.

// host
{
  _id : ObjectID('AAAB'),
  name : 'goofy.example.com',
  ipaddr : '127.66.66.66'
}
// logmsg
{
  time : ISODate("2015-09-02T09:10:09.032Z"),
  message : 'cpu is on fire!',
  host: ObjectID('AAAB')       // Host 문서를 참조
}

다음과 같이 Join한다.

// 부모 host 문서를 검색
> host = db.hosts.findOne({ipaddr : '127.66.66.66'});  // 유일한 index로 가정
// 최근 5000개의 로그를 부모 host의 ObjectID를 이용해 검색
> last_5k_msg = db.logmsg.find({host: host._id}).sort({time : -1}).limit(5000).toArray()

숙련

앞서 살펴본 기초 방법과 함께, 양방향 참조와 비정규화를 활용해 더 세련된 스키마 디자인을 만들 수 있다.

양방향 참조 Two-Way Referencing

// person
{
  _id: ObjectID("AAF1"),
  name: "Koala",
  tasks [ // task 문서 참조
    ObjectID("ADF9"), 
    ObjectID("AE02"),
    ObjectID("AE73") 
  ]
}

// tasks
{
  _id: ObjectID("ADF9"), 
  description: "Practice Jiu-jitsu",
  due_date:  ISODate("2015-10-01"),
  owner: ObjectID("AAF1") // person 문서 참조
}

One to Many 관계에서 반대 문서를 찾을 수 있게 양쪽에 참조를 넣었다. Person에서도 task에서도 쉽게 다른 문서를 찾을 수 있는 장점이 있지만 문서를 삭제하는데 있어서는 쿼리를 두 번 보내야 하는 단점이 있다. 이 스키마 디자인에서는 단일로 atomic한 업데이트를 할 수 없다는 뜻이다. atomic 업데이트를 보장해야 한다면 이 패턴은 적합하지 않다.

Many-to-One 관계 비정규화

앞서 Many-to-One에서 필수적으로 2번 이상 쿼리를 해야 하는 형태를 벗어나기 위해, 다음과 같이 비정규화를 할 수 있다.

// products - before
{
  name: 'Weird Computer WC-3020',
  manufacturer: 'Haruair Eng.',
  catalog_number: 1234,
  parts: [
    ObjectID('AAAA'),
    ObjectID('DEFO'),
    ObjectID('EJFW')
  ]
}

// products - after
{
  name: 'Weird Computer WC-3020',
  manufacturer: 'Haruair Eng.',
  catalog_number: 1234,
  parts: [
    { id: ObjectID('AAAA'), name: 'Awesometel 100Ghz CPU' }, // 부품 이름 비정규화
    { id: ObjectID('DEFO'), name: 'AwesomeSize 100TB SSD' },
    { id: ObjectID('EJFW'), name: 'Magical Mouse' }
  ]
}

애플리케이션 레벨에서 다음과 같이 사용할 수 있다.

// product 문서 찾기
> product = db.products.findOne({catalog_number: 1234});  
// ObjectID() 배열에서 map() 함수를 활용해 part id 배열을 만듬
> part_ids = product.parts.map( function(doc) { return doc.id } );
// 이 product에 연결된 모든 part를 불러옴
> product_parts = db.parts.find({_id: { $in : part_ids } } ).toArray();

비정규화로 매번 데이터를 불러오는 비용을 줄이는 장점이 있다. 하지만 part의 name을 갱신할 때는 모든 product의 문서에 포함된 이름도 변경해야 하는 단점이 있다. 그래서 비정규화는 업데이트가 적고, 읽는 비율이 높을 때 유리하다. 업데이트가 잦은 데이터에는 부적합하다.

One-to-Many 관계 비정규화

// parts - before
{
  _id: ObjectID('AAAA'),
  partno: '123-aff-456',
  name: 'Awesometel 100Ghz CPU',
  qty: 102,
  cost: 1.21,
  price: 3.99
}

// parts - after
{
  _id: ObjectID('AAAA'),
  partno: '123-aff-456',
  name: 'Awesometel 100Ghz CPU',
  product_name: 'Weird Computer WC-3020', // 상품 문서 비정규화
  product_catalog_number: 1234,           // 얘도 비정규화
  qty: 102,
  cost: 1.21,
  price: 3.99
}

앞과 반대로 비정규화를 하는 방법인데 이름 변경 시 Many-to-One에 비해 수정해야 하는 범위가 더 넓은 단점이 있다. 앞에서 처리한 비정규식과 같이 업데이트/읽기 비율을 고려해서 이 방식이 적절한 패턴일 때 도입해야 한다.

One-to-Squillions 관계 비정규화

Squillions를 비정규화한 결과는 다음과 같다.

// logmsg - before
{
  time : ISODate("2015-09-02T09:10:09.032Z"),
  message : 'cpu is on fire!',
  host: ObjectID('AAAB')
}

// logmsg - after
{
  time : ISODate("2015-09-02T09:10:09.032Z"),
  message : 'cpu is on fire!',
  host: ObjectID('AAAB'),
  ipaddr : '127.66.66.66'
}

> last_5k_msg = db.logmsg.find({ipaddr : '127.66.66.66'}).sort({time : -1}).limit(5000).toArray()

사실, 이 경우에는 둘을 합쳐도 된다.

{
    time : ISODate("2015-09-02T09:10:09.032Z"),
    message : 'cpu is on fire!',
    ipaddr : '127.66.66.66',
    hostname : 'goofy.example.com'
}

코드에서는 이렇게 된다.

// 모니터링 시스템에서 로그 메시지를 받음.
logmsg = get_log_msg();
log_message_here = logmsg.msg;
log_ip = logmsg.ipaddr;

// 현재 타임 스탬프를 얻음
now = new Date();
// 업데이트를 위한 host의 _id를 찾음
host_doc = db.hosts.findOne({ ipaddr: log_ip },{ _id: 1 });  // 전체 문서를 반환하지 말 것
host_id = host_doc._id;

// 로그 메시지, 부모 참조, many의 비정규화된 데이터를 넣음
db.logmsg.save({time : now, message : log_message_here, ipaddr : log_ip, host : host_id ) });
// `one`에서 비정규화된 데이터를 push함
db.hosts.update( {_id: host_id }, {
    $push : {
      logmsgs : {
        $each:  [ { time : now, message : log_message_here } ],
        $sort:  { time : 1 },  // 시간 순 정렬
        $slice: -1000          // 마지막 1000개만 뽑기
      }
    }
  });

정리

6가지 원칙

장미빛 MongoDB를 위한 6가지 원칙은 다음과 같다.

  1. 피할 수 없는 이유가 없다면 문서에 포함할 것.
  2. 객체에 직접 접근할 필요가 있다면 문서에 포함하지 않아야 함.
  3. 배열이 지나치게 커져서는 안됨. 데이터가 크다면 one-to-many로, 더 크다면 one-to-squillions로. 배열의 밀도가 높아진다면 문서에 포함하지 않아야 함.
  4. 애플리케이션 레벨 join을 두려워 말 것. index를 잘 지정했다면 관계 데이터베이스의 join과 비교해도 큰 차이가 없음.
  5. 비정규화는 읽기/쓰기 비율을 고려할 것. 읽기를 위해 join을 하는 비용이 각각의 분산된 데이터를 찾아 갱신하는 비용보다 비싸다면 비정규화를 고려해야 함.
  6. MongoDB에서 어떻게 데이터를 모델링 할 것인가는 각각의 애플리케이션 데이터 접근 패턴에 달려있음. 어떻게 읽어서 보여주고, 어떻게 데이터를 갱신한 것인가.

생산성과 유연성

이 모든 내용의 요점은 MongoDB가 데이터베이스 스키마를 작성할 때 애플리케이션에서 필요로 하는 모든 요구를 만족할 수 있도록 기능을 제공하고 있다는 점이다. 애플리케이션에 필요로 하는 데이터를 필요한 구조에 맞게 불러올 수 있어 쉽게 활용할 수 있다.

더 읽을 거리

Elm 강의를 보고 있는데 멱집합을 생성하는 함수가 과제로 나왔다. 한참을 고민하다가 결국 검색을 해보게 되었다. 반성하는 차원에서 How is this powerset generating function working?를 짧게 번역했다. 수학공부 부지런히 해야겠다.


이 멱집합 생성 함수는 어떻게 동작하는거죠?

리스트를 넣어 멱집합을 생성하는 함수를 만들려고 하는데 다음과 같은 코드를 인터넷에서 찾게 되었다. 설명은 없었지만 테스트 해보니 정상적으로 동작했다. 이 함수가 어떻게 동작하는지 이해할 수가 없었다. 설명을 해주면 감사하겠다.

generateSubset [] = [[]]
generateSubset (x:xs) = let p = generateSubset xs in p ++ map (x:) p

Daniel Wagner의 답변

멱집합의 특성으로 쉽게 증명할 수 있다. P(A ∪ B) = {a ∪ b | a ∈ P(A) 또는 b ∈ P(B)}. 특히 집합 S와 그 집합에 포함된 원소인 s로 분해하고 S’의 모든 원소는 s가 아닐 때,

P(S) = P({s} ∪ S')
     = {a ∪ b | a ∈ P({s}), b ∈ P(S')}

여기서 P({s})는 직접 계산하기 좋을 정도로 충분히 작다. P({s}) = {{}, {s}}. 이 사실로 다음과 같이 유도한다.

P(S) = {a ∪ b | a ∈ {{}, {s}}, b ∈ P(S')}
     = {b | b ∈ P(S')} ∪ {{s} ∪ b | b ∈ P(S')}
     = P(S') ∪ {{s} ∪ b | b ∈ P(S')}
     = let p = P(S') in p ∪ {{s} ∪ b | b ∈ p}

이 방법은 멱집합이 빈 원소를 갖는 집합이기 때문에 사용 가능한 계산 방법이다. 먼저 원소를 고른 후, 나머지로 멱집합을 연산한다. 그 후 각각의 부분집합에 추가하거나 더하지 않는다. 질문한 함수는 이 부분을 코드로 전환한 것으로 리스트를 집합으로 사용했다.

-- P         ({s} ∪ S') = let p = P(S')             in p  ∪ {{s} ∪ b | b ∈ p}
generateSubset (x:xs)   = let p = generateSubset xs in p ++     map (x:) p

재귀를 위해 기본값을 넣어주는 일만 남았다. 멱집합의 정의에 따라 다음과 같이 추가한다.

-- P          ({}) = {{}}
generateSubset []  = [[]]

얼마 전 제이펍 출판사 베타리더스 3기에 선정되었다. 선정 되자마자 <함수 프로그래밍 실천 기술>이란 제목의 책을 베타리딩하게 되었는데 함수형 프로그래밍에 대해 전반적인 내용과 세세한 개념을 Haskell로 설명하는 책이었다. 함수형 프로그래밍에 대한 책을 처음 읽어봐서 생소한 개념도 많았지만 다른 언어로의 비교 코드를 많이 제시하고 있어 전체적인 이해에 도움이 많이 되었다. 조만간 출간된다고 하니 관심이 있다면 제목을 적어두는 것도 좋겠다 🙂

함수형 언어를 얘기하면 모나드가 꼭 빠지지 않는다. 이 포스트는 Monads in JavaScript의 번역글이다. 이 글이 모나드에 대해 세세하게 모든 이야기를 다룬 것은 아니지만 모나드의 아이디어를 JavaScript로 구현해서 이 코드에 익숙하다면 좀 더 쉽게 접근할 수 있는 글이라 번역으로 옮겼다. 쉽게 이해하기 어렵지만 이해하면 정말 강력하다는 모나드를 이 글을 통해 조금이나마 쉽게 이해하는데 도움이 되었으면 좋겠다.


모나드는 순서가 있는 연산을 처리하는데 사용하는 디자인 패턴이다. 모나드는 순수 함수형 프로그래밍 언어에서 부작용을 관리하기 위해 광범위하게 사용되며 복합 체계 언어에서도 복잡도를 제어하기 위해 사용된다.

모나드는 타입으로 감싸 빈 값을 자동으로 전파하거나(Maybe 모나드) 또는 비동기 코드를 단순화(Continuation 모나드) 하는 등의 행동을 추가하는 역할을 한다.

모나드를 고려하고 있다면 코드의 구조가 다음 세가지 조건을 만족해야 한다.

  1. 타입 생성자 – 기초 타입을 위한 모나드화된 타입을 생성하는 기능. 예를 들면 기초 타입인 number를 위해 Maybe<number> 타입을 정의하는 것.
  2. unit 함수 – 기초 타입의 값을 감싸 모나드에 넣음. Maybe 모나드가 number 타입인 값 2를 감싸면 타입 Maybe<number>의 값 Maybe(2)가 됨.
  3. bind 함수 – 모나드 값으로 동작을 연결하는 함수.

다음의 TypeScript 코드가 이 함수의 일반적인 표현이다. M은 모나드가 될 타입으로 가정한다.

interface M<T> {

}

function unit<T>(value: T): M<T> {
    // ...
}

function bind<T, U>(instance: M<T>, transform: (value: T) => M<U>): M<U> {
    // ...
}

Note: 여기에서의 bind 함수는 Function.prototype.bind 함수와 다르다. 후자의 bind는 ES5부터 제공하는 네이티브 함수로 부분 적용한 함수를 만들거나 함수에서 this 값을 바꿔 실행할 때 사용하는 함수다.

JavaScript와 같은 객체지향 언어에서는 unit 함수는 생성자와 같이 표현될 수 있고 bind 함수는 인스턴스의 메소드와 같이 표현될 수 있다.

interface MStatic<T> {
    new(value: T): M<T>;
}

interface M<T> {
    bind<U>(transform: (value: T) => M<U>):M<U>;
}

또한 여기에서 다음 3가지 모나드 법칙을 준수해야 한다.

  1. bind(unit(x), f) ≡ f(x)
  2. bind(m, unit) ≡ m
  3. bind(bind(m, f), g) ≡ bind(m, x => bind(f(x), g))

먼저 앞 두가지 법칙은 unit이 중립적인 요소라는 뜻이다. 세번째 법칙은 bind는 결합이 가능해야 한다는 의미로 결합의 순서가 문제가 되서는 안된다는 의미다. 이 법칙은 덧셈에서 확인할 수 있는 법칙과 같다. 즉, (8 + 4) + 2의 결과는 8 + (4 + 2)와 같은 결과를 갖는다.

아래의 예제에서는 화살표 함수 문법을 사용하고 있다. Firefox (version 31)는 네이티브로 지원하고 있지만 Chrome (version 36)은 아직 지원하지 않는다.

Identity 모나드

identity 모나드는 가장 단순한 모나드로 값을 감싼다. Identity 생성자는 앞서 살펴본 unit과 같은 함수를 제공한다.

function Identity(value) {
  this.value = value;
}

Identity.prototype.bind = function(transform) {
  return transform(this.value);
};

Identity.prototype.toString = function() {
  return 'Identity(' + this.value + ')';
};

다음 코드는 덧셈을 Identity 모나드를 활용해 연산하는 예시다.

var result = new Identity(5).bind(value =>
                 new Identity(6).bind(value2 =>
                     new Identity(value + value2)));

Maybe 모나드

Maybe 모나드는 Identity 모나드와 유사하게 값을 저장할 수 있지만 어떤 값도 있지 않은 상태를 표현할 수 있다.

Just 생성자가 값을 감쌀 때 사용된다.

function Just(value) {
  this.value = value;
}

Just.prototype.bind = function(transform) {
  return transform(this.value);
};

Just.prototype.toString = function() {
  return 'Just(' + this.value + ')';
};

Nothing은 빈 값을 표현한다.

var Nothing = {
  bind: function() {
    return this;
  },
  toString: function() {
    return 'Nothing';
  }
};

기본적인 사용법은 identity 모나드와 유사하다.

var result = new Just(5).bind(value =>
                 new Just(6).bind(value2 =>
                      new Just(value + value2)));

Identity 모나드와 주된 차이점은 빈 값의 전파에 있다. 중간 단계에서 Nothing이 반환되면 연관된 모든 연산을 통과하고 Nothing을 결과로 반환하게 된다.

다음 코드에서는 alert가 실행되지 않게 된다. 그 전 단계에서 빈 값을 반환하기 때문이다.

var result = new Just(5).bind(value =>
                 Nothing.bind(value2 =>
                      new Just(value + alert(value2))));

이 동작은 수치 표현에서 나타나는 특별한 값인 NaN(not-a-number)과도 유사하다. 결과 중간에 NaN 값이 있다면 NaN은 전체 연산에 전파된다.

var result = 5 + 6 * NaN;

Maybe 모나드는 null 값에 의한 에러가 발생하는 것을 막아준다. 다음 코드는 로그인 사용자의 아바타를 가져오는 예시다.

function getUser() {
  return {
    getAvatar: function() {
      return null; // 아바타 없음
    }
  }
}

빈 값을 확인하지 않는 상태로 메소드를 연결해 호출하면 객체가 null을 반환할 때 TypeErrors가 발생할 수 있다.

try {
  var url = getUser().getAvatar().url;
  print(url); // 여기는 절대 실행되지 않음
} catch (e) {
  print('Error: ' + e);
}

대안적으로 null인지 확인할 수 있지만 이 방법은 코드를 장황하게 만든다. 코드는 틀리지 않지만 한 줄의 코드가 여러 줄로 나눠지게 된다.

var url;
var user = getUser();
if (user !== null) {
  var avatar = user.getAvatar();
  if (avatar !== null) {
    url = vatar.url;
  }
}

다른 방식으로 작성할 수 있을 것이다. 비어 있는 값을 만날 때 연산이 정지하도록 작성해보자.

function getUser() {
  return new Just({
    getAvatar: function() {
      return Nothing; // 아바타 없음
    }
  });
}

var url = getUser()
            .bind(user => user.getAvatar())
            .bind(avatar => avatar.url);

if(url instanceof Just) {
  print('URL has value: ' + url.value);
} else {
  print('URL is empty');
}

List 모나드

List 모나드는 값의 목록에서 지연된 연산이 가능함을 나타낸다.

이 모나드의 unit 함수는 하나의 값을 받고 그 값을 yield하는 generator를 반환한다. bind 함수는 transform 함수를 목록의 모든 요소에 적용하고 그 모든 요소를 yield 한다.

function* unit(value) {
  yield value;
}

function* bind(list, transform) {
  for (var item of list) {
    yield* transform(item);
  }
}

배열과 generator는 이터레이션이 가능하며 그 반복에서 bind 함수가 동작하게 된다. 다음 예제는 지연을 통해 각각 요소의 합을 만드는 목록을 어떻게 작성하는지 보여준다.

var result = bind([0, 1, 2], function (element) {
  return bind([0, 1, 2], function* (element2) {
    yield element + element2;
  });
});

for (var item of result) {
  print(item);
}

다음 글은 다른 어플리케이션에서 JavaScript의 generator를 어떻게 활용하는지 보여준다.

Continuation 모나드

Continuation 모나드는 비동기 일감에서 사용한다. ES6에서는 다행히 직접 구현할 필요가 없다. Promise 객체가 이 모나드의 구현이기 때문이다.

  1. Promise.resolve(value) 값을 감싸고 pormise를 반환. (unit 함수의 역할)
  2. Promise.prototype.then(onFullfill: value => Promise) 함수를 인자로 받아 값을 다른 promise로 전달하고 promise를 반환. (bind 함수의 역할)

다음 코드에서는 Unit 함수로 Promise.resolve(value)를 활용했고, Bind 함수로 Promise.prototype.then을 활용했다.

var result = Promise.resolve(5).then(function(value) {
  return Promise.resolve(6).then(function(value2) {
      return value + value2;
  });
});

result.then(function(value) {
    print(value);
});

Promise는 기본적인 continuation 모나드에 여러가지 확장을 제공한다. 만약 then이 promise 객체가 아닌 간단한 값을 반환하면 이 값을 Promise 처리가 완료된 값과 같이 감싸 모나드 내에서 사용할 수 있다.

두번째 차이점은 에러 전파에 대해 거짓말을 한다는 점이다. Continuation 모나드는 연산 사이에서 하나의 값만 전달할 수 있다. 반면 Promise는 구별되는 두 값을 전달하는데 하나는 성공 값이고 다른 하나는 에러를 위해 사용한다. (Either 모나드와 유사하다.) 에러는 then 메소드의 두번째 콜백으로 포착할 수 있으며 또는 이를 위해 제공되는 특별한 메소드 .catch를 사용할 수 있다.

Promise 사용과 관련된 기사는 다음과 같다:

Do 표기법

Haskell은 모나드화 된 코드를 작업하는데 도움을 주기 위해 편리 문법(syntax sugar)인 do 표기법을 제공하고 있다. do 키워드와 함께 시작된 구획은 bind 함수를 호출하는 것으로 번역이 된다.

ES6 generator는 do 표기법을 JavaScript에서 간단하고 동기적으로 보이는 코드로 작성할 수 있게 만든다.

전 예제에서는 maybe 모나드가 다음과 같이 직접 bind를 호출했었다.

var result = new Just(5).bind(value =>
                 new Just(6).bind(value2 =>
                     new Just(value + value2)));

다음은 같은 코드지만 generator를 활용했다. 각각의 호출은 yield로 모나드에서 값을 받는다.

var result = doM(function*() {
  var value = yield new Just(5);
  var value2 = yield new Just(6);
  return new Just(value + value2);
});

이 작은 순서를 generator로 감싸고 그 뒤에 bind를 값과 함께 호출해 yield로 넘겨준다.

function doM(gen) {
  function step(value) {
    var result = gen.next(value);
    if (result.done) {
      return result.value;
    }
    return result.value.bind(step);
  }
  return step();
}

이 방식은 다른 Continuation 모나드와 같은 다른 모나드에서도 사용할 수 있다.

Promise.prototype.bind = Promise.prototype.then;

var result = doM(function*() {
  var value = yield Promise.resolve(5);
  var value2 = yield Promise.resolve(11);
  return value + value2;
}());

result.then(print);

다른 모나드와 같은 방식으로 동작하도록 thenbind로 별칭을 붙였다.

promise에서 generator를 사용하는 예는 Easy asynchrony with ES6를 참고하자.

연결된 호출 Chained calls

다른 방식으로 모나드화 된 코드를 쉽게 만드는 방법은 Proxy를 활용하는 것이다.

다음 함수는 모나드 인스턴스를 감싸 proxy 객체를 반환한다. 이 객체는 값이 있는지 없는지 확인되지 않은 프로퍼티라도 안전하게 접근할 수 있게 만들고 모나드 내에 있는 값을 함수에서 활용할 수 있게 돕는다.

function wrap(target, unit) {
  target = unit(target);
  function fix(object, property) {
    var value = object[property];
    if (typeof value === 'function') {
      return value.bind(object);
    }
    return value;
  }
  function continueWith(transform) {
    return wrap(target.bind(transform), unit);
  }
  return new Proxy(function() {}, {
    get: function(_, property) {
      if(property in target) {
        return fix(target, property);
      }
      return continueWith(value => fix(value, property));
    },
    apply: function(_, thisArg, args) {
      return continueWith(value => value.apply(thisArg, args));
    }
  });
}

이 래퍼는 빈 객체를 참조할 가능성이 있는 경우에 안전하게 접근하는 방법을 제공한다. 이 방식은 실존적 연산자(?.) 구현 방식과 동일하다.

function getUser() {
  return new Just({
    getAvatar: function() {
      return Nothing; // 아바타 없음
    }
  });
}

var unit = value => {
  // 값이 있다면 Maybe 모나드를 반환
  if (value === Nothing || value instanceof Just) {
    return value;
  }
  // 없다면 Just를 감싸서 반환
  return new Just(value);
}

var user wrap(getUser(), unit);

print(user.getAvatar().url);

아바타는 존재하지 않지만 url을 호출하는 것은 여전히 가능하며 빈 값을 반환 받을 수 있다.

동일한 래퍼를 continuation 모나드에서 일반적인 함수를 실행할 때에도 활용할 수 있다. 다음 코드는 특정 아바타를 가지고 있는 친구가 몇명이나 있는지 반환한다. 예제는 보이기엔 모든 데이터를 메모리에 올려두고 사용하는 것 같지만 실제로는 비동기적을 데이터를 가져온다.

Promise.prototype.bind = Promise.prototype.then;

function User(avatarUrl) {
  this.avatarUrl = avatarUrl;
  this.getFriends = function() {
    return Promise.resolve([
      new User('url1'),
      new User('url2'),
      new User('url11'),
    ]);
  }
}

var user = wrap(new User('url'), Prommise.resolve);

var avatarUrls = user.getFriends().map(u => u.avatarUrl);

var length = avatarUrls.filter(url => url.contains('1')).length;

length.then(print);

여기서 모든 프로퍼티의 접근과 함수의 호출은 간단하게 값을 반환하는 것이 아니라 모나드 안으로 진입해 Promise를 실행해 결과를 얻게 된다.

ES6의 Proxies에 대한 자세한 내용은 Array slices를 참고하자.


원본 포스트 https://curiosity-driven.org/monads-in-javascript (CC BY 3.0)

요즘 함수형 프로그래밍에 대한 관심이 많아져 여러가지 글을 찾아 읽고 있다. JavaScript에서도 충분히 활용 가능한데다 JS의 내부를 더 깊게 생각해볼 수 있고 다른 각도로 문제를 사고해보는데 도움이 되는 것 같아 한동안은 이와 관련된 포스트를 번역하려고 한다.

커링(currying)은 함수형 프로그래밍 기법 중 하나로 함수를 재사용하는데 유용하게 쓰일 수 있는 기법이다. 커링이 어떤 기법인지, 어떤 방식으로 JavaScript에서 구현되고 사용할 수 있는지에 대한 글이 있어 번역했다. 특히 이 포스트는 함수를 작성하고 실행하는 과정을 하나씩 살펴볼 수 있어 좋았다.

원본은 Kevin Ennis의 Currying in JavaScript에서 확인할 수 있다.


나는 최근 함수형 프로그래밍에 대해 생각을 많이 하게 되었다. 그러던 중 curry 함수를 작성하는 과정을 공유하면 재미있을 것 같다는 생각이 들었다.

처음 듣는 사람을 위해 간단히 설명하면, 커링은 함수 하나가 n개의 인자를 받는 과정을 n개의 함수로 각각의 인자를 받도록 하는 것이다. 부분적으로 적용된 함수를 체인으로 계속 생성해 결과적으로 값을 처리하도록 하는 것이 그 본질이다.

어떤 의미인지 다음 코드를 보자:

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );

curried( 1 )( 2 )( 3 ); // 6

면책 조항

이 포스트는 기본적으로 클로저와 Function#apply()와 같은 고차함수에 익숙한 것을 가정하고 작성했다. 이런 개념에 익숙하지 않다면 더 읽기 전에 다시 복습하자.

curry 함수 작성하기

앞서 코드에서 볼 수 있듯 curry는 인자로 함수를 기대하기 때문에 다음과 같이 작성한다.

function curry( fn ) {

}

다음으로 얼마나 많은 인자가 함수에서 필요로 할지 알아야 한다. (인자의 갯수 arity 라고 부른다.) 인자의 갯수를 알기 전까지 몇 번이나 새로운 함수를 반환하고, 어느 순간에 함수 대신 값을 반환하게 될지 알 수가 없다.

함수에서 몇개의 인자를 기대하는지 length 프로퍼티를 통해 확인할 수 있다.

function curry( fn ) {
  var arity = fn.length;
}

이제 여기서부터 약간 복잡해진다.

기본적으로는, 매번 curry된 함수를 호출할 때마다 새로운 인자를 배열에 넣어 클로저 내에 저장해야 한다. 그 배열에 있는 인자의 수는 원래 함수에서 기대했던 인자의 수와 동일해야 하며, 그 이후 호출 가능해야 한다. 다를 때엔 새로운 함수로 반환한다.

이런 작업을 하기 위해 (1) 인자 목록을 가질 수 있는 클로저가 필요하고 (2) 전체 인자의 수를 확인할 수 있는 함수와 함께, 부분적으로 적용된 함수를 반환 또는 모든 인자가 적용된 원래의 함수에서 반환되는 값을 반환해야 한다.

여기서는 resolver라 불리는 함수를 즉시 실행한다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {

  }());
}

이제 resolver 함수와 함께 해야 할 첫번째 일은 지금까지 입력 받은 모든 인자를 복사하는 것이다. Array#slice 메소드를 이용, arguments의 사본을 memory라는 변수에 저장한다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
  }());
}

다음으로 resolver가 함수를 반환하게 만들어야 한다. 함수 외부에서 curry된 함수를 호출하게 될 때 접근할 수 있게 되는 부분이다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
    return function() {

    };
  }());
}

이 내부 함수가 실제로 호출이 될 때마다 인자를 받아야 한다. 또한 이 추가되는 인자를 memory에 저장해야 한다. 그러므로 먼저 slice()를 호출해 memory의 복사본을 만들자.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
    return function() {
      var local = memory.slice();
    };
  }());
}

이제 새로운 인자를 Array#push로 추가한다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
    return function() {
      var local = memory.slice();
      Array.prototype.push.apply( local, arguments );
    };
  }());
}

좋다. 이제까지 받은 모든 인자를 새로운 배열에 포함하고 있으며 부분적으로 적용된 함수를 연결(chain)하고 있다.

마지막으로 할 일은 지금까지 받은 인자의 갯수를 실제로 curry된 함수의 인자 수와 맞는지 비교해야 한다. 길이가 맞다면 원래의 함수를 호출하고 그렇지 않다면 resolver가 또 다른 함수를 반환해 인자 수에 맞게 더 입력 받아 memory에 저장할 수 있어야 한다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
    return function() {
      var local = memory.slice();
      Array.prototype.push.apply( local, arguments );
      next = local.length >= arity ? fn : resolver;
      return next.apply( null, local );
    };
  }());
}

지금까지 작성한 내용을 앞서 보여줬던 예제와 함께 순서대로 살펴보자.

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );

curriedvolume 함수를 앞서 작성한 curry 함수에 넣은 결과가 된다.

여기서 무슨 일이 일어났는지 다시 살펴보면:

  1. volume의 인자 수 즉, 3을 arity에 저장했다.
  2. resolver를 인자 없이 바로 실행했고 그 결과 memory 배열은 비어 있다.
  3. resolver는 익명 함수를 반환했다.

여기까지 이해가 된다면 curry된 함수를 호출하고 길이를 넣어보자.

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );
var length = curried( 2 );

여기서 진행된 내용을 살펴보면 다음과 같다:

  1. 여기서 실제로 호출한 것은 resolver에 의해 반환된 익명 함수다.
  2. memory(아직은 비어 있음)를 local에 복사한다.
  3. 인자 (2)를 local 배열에 추가한다.
  4. local의 길이가 volume의 인자 갯수보다 적으므로, 지금까지의 인자 목록과 함께 resolver를 다시 호출한다. 새로운 memory 배열과 함께 새로 생성된 클로저는 첫번째 인자로 2를 포함한다.
  5. 마지막으로, resolver는 클로저 바깥에서 새로운 memory 배열에 접근할 수 있도록 새로운 함수를 반환한다.

이 과정으로 내부에 있던 익명 함수를 다시 반환한다. 하지만 이번에는 memory 배열은 비어 있지 않다. 앞서 입력한, 첫번째 인자인 (인자 2)가 내부에 있다.

앞서 만든 length 함수를 다시 호출한다.

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );
var length = curried( 2 );
var lengthAndWidth = length( 3 );
  1. 여기서 호출한 것은 resolver에 의해 반환된 익명 함수다.
  2. resolver는 앞에서 입력한 인자를 포함하고 있다. 즉 배열 2 를 복사해 local에 저장한다.
  3. 새로운 인자인 3local 배열에 저장한다.
  4. 아직도 local의 길이가 volume의 인자 갯수보다 적으므로, 지금까지의 인자 목록과 함께 resolver를 다시 호출한다. 그리고 이전과 동일하게 새로운 함수를 반환한다.

이제 lengthAndWidth 함수를 호출해 값을 얻을 차례다.

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );
var length = curried( 2 );
var lengthAndWidth = length( 3 );

console.log( lengthAndWidth( 4 ) ); // 24

여기서의 순서는 이전과 약간 다르다.

  1. 다시 여기서 호출한 함수는 resolver에서 반환된 익명 함수다.
  2. resolver는 앞에서 입력한 인자를 포함한다. 배열 [ 2, 3 ]를 복사해 local에 저장한다.
  3. 새로운 인자인 4local 배열에 저장한다.
  4. 이제 local의 길이가 volume의 인자 갯수와 동일하게 3을 반환한다. 그래서 새로운 함수를 반환하는 대신 지금까지 입력 받아서 저장해둔 모든 인자와 함께 volume 함수를 호출해 결과를 반환 받는다. 그 결과로 24 라는 값을 받게 된다.

정리

아직 이 커링 기법을 필수적으로 적용해야만 하는 경우를 명확하게 찾지는 못했다. 하지만 이런 방식으로 함수를 작성하는 것은 함수형 프로그래밍에 대한 이해를 향상할 수 있는 좋은 방법이고 클로저와 1급 클래스 함수와 같은 개념을 강화하는데 도움을 준다.

현재 Project Decibel에서 구인중이다. 보스턴 지역에서 이런 JavaScript 일을 하고 싶다면 이메일을 부탁한다.

그리고 이 포스트가 좋다면 내 트위터를 구독하라. 다음 한 달 중 하루는 글을 쓰기 위해 노력할 예정이다.


새로운 개념을 배워가는 과정에서 JavaScript의 새 면모를 배우게 되는 것 같아 요즘 재미있게 읽게 되는 글이 많아지고 있다. 지금 회사에서 JS를 front-end에서 제한적으로 사용하고 있는 수준이라서 아쉽다는 생각이 들 때도 많지만 이런 포스트를 통해 매일 퍼즐을 풀어가는 기분이라 아직도 배워야 할 부분이 많구나 생각하게 된다.

벌써 2015년도 반절이 지났다. 여전히 어느 것 하나 깊게 알고 있는 것이 없는 기분이라 아쉬운데 남은 한 해는 겉 알고 있는 부분을 깊이있게 접근할 수 있는 끈기를 챙길 수 있었으면 좋겠다.

이상한모임 슬랙 #dev-frontend 채널에서 Lodash에 대해 이야기하다 지연 평가(Lazy Evaluation)를 지원한다는 이야기를 듣고 검색하게 되었다. 검색 결과로 찾은, Filip Zawada의 How to Speed Up Lo-Dash ×100? Introducing Lazy Evaluation 포스트를 번역한 글이다.

지연평가는 필요할 때만 수행하는 평가 방식으로 함수형 프로그래밍에서는 널리 사용되는 방법이다. 이 글은 lodash 뿐만 아니라 지연평가가 어떤 방식으로 접근하고 동작하는가에 대해 쉽게 설명하고 있다.


Lo-Dash와 같은 라이브러리는 더이상 빨라질 수 없을 정도로 충분히 빠르다고 항상 생각했다. Lo-Dash는 자바스크립트를 짜내다시피 해 다양한 기술을 완벽하게 잘 섞었다. 이 라이브러리는 JavaScript의 가장 빠른 문장, 적응형 알고리즘을 위해 사용하며 때로는 부수적으로 발생하는 예기치 못한 재귀를 피하기 위해 성능을 측정할 때에도 사용한다.

지연 평가 Lazy Evaluation

하지만 내가 잘못 생각했다. Lodash는 획기적으로 빨라지는 것이 가능했다. 이 일에 필요한 것은 미세한 최적화에 대한 생각을 멈추고 올바른 알고리즘을 사용하고 있는지 살피는 것으로 시작해야 한다. 예를 들면, 일반적인 반복문에서 반복에 걸리는 단위 시간을 최적화하려 한다:

var len = getLength();
for(var i = 0; i < len; i++) {
    operation(); // <- 10ms - 어떻게 9ms로 만들 수 있을까?!
}

이런 경우는 대부분 어렵고 매우 제한적이다. 때로는 getLength()를 최적화 하는 것이 더 의미있다. 이 함수가 반환하는 값이 작을수록, 10ms 주기는 짧아진다.

다음 코드는 간략하게 작성한 Lodash에서 지연평가를 하는 방식이다. 이 방법은 주기의 횟수를 줄이는 것이지 주기에 걸리는 시간을 줄이는 것이 아니다. 다음 예를 고려해보자:

function priceLt(x) {
   return function(item) { return item.price < x; };
}
var gems = [
   { name: 'Sunstone', price: 4 }, { name: 'Amethyst', price: 15 },
   { name: 'Prehnite', price: 20}, { name: 'Sugilite', price: 7  },
   { name: 'Diopside', price: 3 }, { name: 'Feldspar', price: 13 },
   { name: 'Dioptase', price: 2 }, { name: 'Sapphire', price: 20 }
];

var chosen = _(gems).filter(priceLt(10)).take(3).value();

$10보다 작은 가격의 보석 3개를 고르려고 한다. 일반적인 Lodash 접근인 엄격한 평가에서는 8개의 보석을 모두 걸러낸 후 앞 3개를 골라낸다:

Lodash naïve approach

충분히 쿨하지 않다. 이 방식은 8개의 모든 요소를 처리하지만 사실 필요로 하는 것은 그 중 5개 뿐이다. 지연 평가 알고리즘에서는 이 방식과 대조적으로, 배열에서 적은 숫자의 요소를 가져와 올바른 결과를 얻는다. 다음을 살펴보자:

Lo-Dash regular approach

이 방식으로 쉽게 37.5% 성능 향상을 만들었다. 이는 단순한 예시이며 사실 1000+배 성능 향상이 있는 예도 들 수 있다. 다음을 보자:

var phoneNumbers = [5554445555, 1424445656, 5554443333, … ×99,999];

// "55"가 포함된 전화번호 100개를 획득
function contains55(str) {
    return str.contains("55");
};

var r = _(phoneNumbers).map(String).filter(contains55).take(100);

이 예제에서 99,999개의 요소를 검사하게 되는데, 이 모두를 다 실행하지 않고 예를 들어 1,000개의 요소만 검사해도 결과를 얻을 수 있게 된다. 벤치마크에서 이 엄청난 성능 향상을 확인할 수 있다:

benchmark

파이프라이닝

지연 평가에 또 다른 잇점이 있는데 “파이프라이닝” 이라고 부른다. 이 아이디어는 체인으로 실행되는 동안 값이 전달되기 위해 배열이 생성되는 경우를 회피한다는 점이다. 모든 동작은 하나의 요소에 한번에 실행되야 한다. 다음 코드를 보면:

var result = _(source).map(func1).map(func2).map(func3).value();

간단하게 Lo-Dash가 어떻게 해석하는지 작성하면 다음과 같다. (엄격한 평가)

var result = [], temp1 = [], temp2 = [], temp3 = [];

for(var i = 0; i < source.length; i++) {
   temp1[i] = func1(source[i]);
}

for(i = 0; i < source.length; i++) {
   temp2[i] = func2(temp1[i]);
}

for(i = 0; i < source.length; i++) {
   temp3[i] = func3(temp2[i]);
}
result = temp3;

반면 지연 평가에서는 다음과 같이 실행된다:

var result = [];
for(var i = 0; i < source.length; i++) {
   result[i] = func3(func2(func1(source[i])));
}

임시 배열이 존재하지 않는다는 점은 극적인 성능 향상을 가져온다. 특히 배열이 크고 메모리 접근이 비싼 경우에서는 말이다.

유예 실행 Deferred execution

지연 평가가 가져온 또 다른 잇점은 유예 실행이다. 체인을 만들게 되면 언제나 .value()를 명시적으로나 암시적으로 호출하기 전까지는 연산되지 않는다. 이 접근은 쿼리를 먼저 준비하게 하고 나중에 실행하게 해 가장 최신의 데이터를 얻게 된다.

var wallet = _(assets).filter(ownedBy('me'))
                      .pluck('value')
                      .reduce(sum);

$json.get("/new/assets").success(function(data) {
    assets.push.apply(assets, data); // update assets
    wallet.value(); // returns most up-to-date value
});

이와 같은 방식은 몇가지 경우에서 또한 속도 향상을 가져온다. 실행 속도가 중요한 경우에 복잡한 쿼리를 일찍 만든 후 나중에 실행할 수 있게 된다.

정리

지연 평가는 새로운 아이디어가 아니다. 이미 LINQ, Lazy.js 등 여러 뛰어난 라이브러리에서 사용하고 있다. 내가 믿기에 Lo-Dash가 만든 큰 차이는 Underscore API를 그대로 유지하면서도 새롭고 강력한 내부의 엔진을 얻게 되었다는 사실이다. 새로운 라이브러리를 배울 필요도, 작성한 코드를 크게 변경할 필요도 없이 라이브러리를 업그레이드하면 된다.

Lo-Dash를 사용하지 않더라도 이 글이 영감을 줬기를 바란다. 자신의 어플리케이션에서 병목을 찾아 jsperf.com의 try/fail 스타일의 최적화는 그만 할 때도 되었다. 대신 나가서 커피를 마시며 알고리즘에 대해 생각해야 할 때다. 창의성이 중요하지만 알고리즘 개론와 같은 좋은 책으로 배경지식을 다지는 것도 좋다. 행운을 빈다!


번역에 피드백 주신 Heejoon Lee님 감사드립니다.

이상한모임 슬랙 #dev-frontend 채널에서 함수가 1급 시민이라는 얘기가 나온 적이 있었다. Wikipedia를 읽다가 Partial Application에 대한 이야기가 있어 검색하던 중 John Resig이 작성한 Partial Application in JavaScript를 읽게 되었다. 2008년 글이라 요즘 코드와는 조금 다른 부분이 있지만 개념을 잡기에는 충분히 도움이 되는 것 같아 번역했다.


면밀하게 보면, 부분만 사용한 함수는 함수가 실행되기 전에 미리 인자를 지정할 수 있는, 흥미로운 기법이다. 이와 같은 효과로 부분만 반영된 함수는 호출할 수 있는 새로운 함수를 반환한다. 다음 예제를 통해 이해할 수 있다:

String.prototype.csv = String.prototype.split.partial(/,\s*/);
var results = "John, Resig, Boston".csv();
alert( (results[1] == "Resig") + " The text values were split properly." );

위에서는 일반적으로 사용하는 String의 .split() 메소드에 인자로 미리 정규표현식을 저장하는 경우다. 그 결과로 만들어진 새로운 함수 .csv()를 쉼표로 분리된 값을 배열로 변환하는데 사용할 수 있다. 함수 인자를 앞에서부터 필요한 만큼 채우고 새로운 함수를 리턴하는 방식을, 일반적으로 커링(currying)이라 부른다. 간단하게 커링은 어떻게 구현되는지 다음 프로토타입 라이브러리에서 확인할 수 있다:

Function.prototype.curry = function() {
  var fn = this, args = Array.prototype.slice.call(arguments);
  return function() {
    return fn.apply(this, args.concat(
      Array.prototype.slice.call(arguments)));
  };
};

상태를 기억하기 위해 클로저(closure)를 사용한 좋은 케이스다. 이 경우에 미리 입력한 인수(args)를 저장하기 위해서 새로 만들어지는 함수에 전달되었다. 새로운 함수는 인수가 미리 입력되게 되고 새로운 인수도 하나로 합쳐져(concat) 전달된다. 그 결과, 이 메소드는 인수를 미리 입력할 수 있게 되고 활용 가능한 새 함수를 반환하게 된다.

이제 이 스타일의 부분 어플리케이션은 완전 유용하지만 더 좋게 만들 수 있다. 만약 주어진 함수에서 단순히 앞에서부터 인수를 입력할 것이 아니라 비어있는 모든 인수를 채우기 원한다면 어떻게 해야할까. 다음과 같은 형태의 부분 어플리케이션 구현은 다른 언어에도 존재하지만 JS에서는 Oliver Steele가 Function.js 라이브러리에서 시연했다. 다음 구현을 살펴보자:

Function.prototype.partial = function (){
  var fn = this, args = Array.prototype.slice.call(arguments);
  return function(){
    var arg = 0;
    for ( var i = 0; i < args.length && arg < arguments.length; i++)
      if ( args[i] === undefined )
        args[i] = arguments[arg++];
    return fn.apply(this, args);
  }
}

이 구현은 근본적으로 curry() 메소드와 비슷하지만 중요한 차이점이 존재한다. 특히 이 함수가 호출될 때, 미리 입력하고 싶지 않은 인수에 대해 undefined를 입력하는 것으로 나중에 입력하도록 만들 수 있다. 이 방식의 구현은 인수를 병합하는데 더 편리하게 활용할 수 있게 돕는다. 인수를 배정하는 과정에서 비어있는 곳에 적절한 간격으로 처리해 나중에 실행할 때 조각을 맞출 수 있게 만든다.

위에서 문자열 분리 함수를 생성하는데 사용한 예에도 있지만 다른 방식에서 어떻게 새 함수 기능을 활용할 수 있는지 확인하자. 함수를 간단하게 지연해서 실행하도록 하는 함수를 생성할 수 있다.

var delay = setTimeout.partial(undefined, 10);
delay(function(){
  alert( "A call to this function will be temporarily delayed." );
});

delay라는 이름의 새로운 함수를 만들었다. 언제든 함수를 인자로 넣으면 10ms 후에 비동기적으로 실행하게 된다.

이벤트를 연결(binding) 하기 위한, 간단한 함수를 만들 수 있다:

var bindClick = document.body.addEventListener
                  .partial('click', undefined, false);

bindClick(function() {
  alert( "Click event bound via curried function." );
});

이 기법은 라이브러리에서 이벤트를 연결하기 위해 사용하는, 간단한 헬퍼 메소드로 사용할 수 있다. 이 결과로 단순한 API를 제공해 최종 사용자가 불필요한 인수로 인해 번거롭게 되는 경우를 줄이고 단일 함수를 호출하는 횟수를 줄일 수 있다.

클로저를 사용하면 결과적으로 코드에서의 복잡도를 쉽고 간단하게 줄일 수 있어서 JavaScript 함수형 프로그래밍의 강력함을 확인하게 된다.

PHP Package Checklist의 번역 글이다. 패키지 개발을 하지 않고 있더라도 PHP 개발을 하고 있다면 충분히 염두해볼 만한 내용이 포함되어 있고 참고할 이야기가 많다.

패키지명을 현명하게 선택하기

  • 다른 프로젝트에서 사용되고 있지 않은 이름을 선택한다.
  • 패키지명과 PHP 네임스페이스가 일치하도록 관리한다.
  • 성이나 개인 닉네임을 PHP 네임스페이스로 사용하지 않는다.

소스를 공개적으로 호스팅하기

  • 공개 프로젝트는 GitHub를 무료로 사용할 수 있다.
  • GitHub은 이슈를 관리하고 기능 요청이나 풀 리퀘스트에 도움이 된다.
  • 대안으로 Bitbucket도 사용할 수 있다.

Autoloader 친화적으로 개발하기

  • PSR-4와 호환이 되는 네임스페이스를 사용한다.
  • 코드는 src 폴더 내에 넣는다.

Composer를 통해 배포하기

  • PHP를 위한 의존성 관리 도구인 Composer에서 라이브러리를 사용 가능하게 한다.
  • Composer의 주 리포지터리인 Packagist에 등록한다.

프레임워크에 대해 독립적으로 개발하기

  • 프로젝트를 하나의 프레임워크에 제한을 두지 않는다.
  • 서비스 프로바이더를 제공해 특정 프레임워크에서 사용할 수 있도록 지원한다.

코딩 스타일을 따르기

  • PSR-2 코딩 스타일 가이드를 철저히 지킬 것을 강력하게 권장한다.
  • 빠르게 자동으로 코드를 수정해주는 PHP Coding Standards Fixer를 사용한다.
  • 코딩 표준에 대해 자동으로 확인해주는 PHP Code Sniffer를 사용한다.

유닛 테스트를 작성하기

  • 주요 코드를 커버하는 것에 초점을 둔다.
  • PHPUnit은 사실상 표준인 PHP 유닛 테스트 프레임워크다.
  • 대안으로 phpspec, Behat, atoum, Codeception이 있다.

DocBlock을 사용하기

  • 인라인 문서화를 위해 DocBlock을 제공한다.
  • DocBlock은 PhpStorm과 같은 IDE의 코드 완성을 향상하는데 도움이 된다.
  • phpDocumentor를 활용해 API 문서로 자동 변환이 가능하다.

유의적 버전을 사용하기

  • 버전 번호를 관리하는데 유의적 버전을 사용한다.
  • 주버전.부버전.수버전(MAJOR.MINOR.PATCH) 시스템을 사용한다.
  • 개발 버전의 업그레이드는 변경으로 인해 깨지는 것을 걱정하지 않도록 안전하게 제공해야 한다.
  • 릴리즈마다 tag로 버전을 적는 것을 잊지 말자.

변경 로그를 유지하기

  • 매 릴리즈를 할 때마다 변경 로그를 깔끔하게 공개한다.
  • Keep a CHANGELOG 양식을 사용하는 것을 고려한다.

지속적인 통합(continuous integration)을 사용하기

  • 자동으로 코딩 표준과 테스트를 구동하는 서비스를 사용한다.
  • 다양한 버전의 PHP에서 테스트를 구동하는 좋은 방법이다.
  • 풀 리퀘스트가 제출될 때 자동으로 구동할 수 있다.
  • Travis-CI, scrutinizer, circleci를 사용할 수 있다.

상세한 문서를 작성하기

  • 좋은 문서화는 성공적인 패키지에 필수적인 요소다.
  • 적어도 설명을 포함한 README를 작성해 리포지터리에 포함한다.
  • 문서를 GitHub Pages로 제공하는 것을 고려한다.
  • 대안으로 Read the Docs를 활용할 수 있다.

라이센스를 포함하기

  • 라이센스를 포함하는 것은 현재까지 한 일을 보호할 수 있는 작은 방법이다.
  • choosealicense.com 사이트를 참고한다. 대부분의 PHP 프로젝트는 MIT 라이센스를 사용한다.
  • 적어도 LICENSE 파일은 라이브러리에 포함해야 한다.
  • Dockblock에도 라이센스를 포함할 것을 고려해본다.

기여를 환영하기

  • 당신의 프로젝트를 누군가 돕길 원한다면 당연히 그걸 물어봐야 한다.
  • CONTRIBUTING 파일을 사용해 프로젝트 기여를 환영하자.
  • 이 파일에 테스트와 같은 프로젝트 요구 사항을 설명하는 내용을 작성한다.

위 리스트의 모든 내용을 한번에 적용할 수 없다면 필요한 부분부터 점차적으로 적용해 나가도록 하자. Autoloader는 익숙해지면 include 지옥에서 벗어날 수 있는 강력한 기능이다. 위에서 소개된, 코딩 스타일을 자동으로 교정해주는 도구들은 문법 고민을 덜어주고 비지니스 로직에 집중할 수 있도록 돕는다. 현대적인 PHP 개발을 생각하고 있다면 위 모든 항목 하나하나 살펴보는 것이 도움이 된다.

더 읽을 거리

좋은 커밋 메시지 작성하기에서 레퍼런스였던 On commit messages를 번역한 글이다. 이전의 글은 커밋 메시지에 대한 글이긴 했지만 간략한 편이었다. 이 글에서는 어떤 방식으로 커밋을 구성해야 하고 어떻게 커밋을 보내면 안되는지 등 실제적인 수준에서 참고할 만한 이야기가 많았다.


지난 몇 주 동안 놀랄 만큼 많은, 커밋 메시지에 대한 토론을 읽게 되었다. 그 중에는 개발자와 함께 막 새 프로젝트를 시작하려는 사람들도 많았다. 그래서 그들을 돕기 위해 커밋을 할 때 해야 할 일과 그 일을 왜 해야 하는지에 대한 목록을 작성해봤다. (힌트: 리눅스 커널 메일링 리스트는 이 일을 아주 바람직하게 하고 있다. 그곳에서 배우자.)

모든 소프트웨어 프로젝트는 협동 프로젝트다. 적어도 프로젝트엔 두 명의 개발자가 일하게 되어 있다. 나 혼자 최초 개발자로 스스로 개발을 한다고 해도 몇 주, 몇 달이 지난 후에 작성한 코드가 무엇인지 생각해내야 하는 미래의 내가 존재한다. 미래의 나는 새로운 버그가 발생하거나 새로운 기능을 추가해야 할 때마다 매번 특정 부분의 코드에 대한 맥락을 다시 파악해야만 한다.1

코드 조각의 맥락을 다시 파악하는 일은 정말 낭비일 수밖에 없다. 물론 이 일을 완전히 회피할 수는 없겠지만, 이 시간을 최소화할 수 있도록 노력해야 한다. 바로 커밋 메시지가 그 역할을 담당한다. 그렇기 때문에 커밋 메시지를 보면 그 개발자가 좋은 협력자인지 아닌지를 알 수 있게 된다.

좋은 커밋 메시지는 패치에 관한 다음 세 가지 질문에 답을 할 수 있어야 한다:

  • 왜 이 코드가 필요한가? 코드는 버그 수정, 기능 추가, 성능 향상, 신뢰성, 안정성을 위한 변경일 수 있다. 물론 단순한 오·탈자 교정일 수도 있다.
  • 어떻게 이슈를 해결했는가? 짧아서 명백한 패치의 경우 이 부분을 생략해도 된다. 다만 깊은 수준의 묘사로 어떻게 문제에 접근했는지 나타내야 한다.
  • 패치가 어떤 영향을 만드는가? 명백한 부분을 포함해 벤치마크 결과, 부작용 등을 포함할 수 있다.

이 세 가지 질문으로 실제 코드 변경에 대한 맥락을 알 수 있게 된다. 또한 리뷰어와 다른 개발자가 그 맥락을 통해 차이점을 보고 적절한 방법을 선택해 문제에 접근했는지 확인하게 된다. 또한 좋은 커밋 메시지는 메인테이너에게 안정 브랜치에 포함해도 괜찮은지, 또는 배포에 포함해도 괜찮은지 결정하는데 도움된다.

이 세가지 질문에 대한 답이 없는 패치는 대부분 쓸모 없는 패치다. 이런 경우에는 이 패치가 어떤 일을 하고 어떻게 이슈를 해결했는지 직접 찾아야 하기 떄문에 리뷰어에게 부담만 될 뿐이다. 복잡할대로 복잡한 패치에 많은 수의 리뷰어가 필요한 상황. 그 의미는 결국 원 개발자가 좋은 커밋 메시지를 작성하지 않았다는 이유만으로 많은 인력 투입 시간(man-hours)이 낭비된다는 뜻이다. 거기에 만약 메인테이너가 프로젝트의 소스 컨트롤 관리 통제를 철저히 하고 있다면, 개발자가 제출한 패치는 거절 당할 것이고 개발자는 다시 시간을 소비해 패치를 다시 작성해야 하며, 리뷰어는 리뷰에 또 시간을 소비하게 되는, 최악의 경우도 발생할 수 있다. 이처럼 시간 낭비는 빠르게 늘어난다. 단지 몇 분 시간을 투자해서 커밋 메시지를 작성하는 것이 이런 비경제적인 시간 소비를 없앨 수도, 최악의 상황을 만들 수도 있는 것이다.

오픈소스가 아닌 일반 소프트웨어 회사도 이 내용을 충분히 고려해야 한다. 적당한 소스 컨트롤 관리 규칙이 없으면 결국 비용이 발생한다.

어떻게 해야 더 잘할 수 있을까

물론 이상적인 커밋 메시지는 이래야 한다는 엄격한 정의는 없다. 하지만 몇 가지 일반적인 규칙은 있다. 커밋은 정확히 하나의 로직 변경을 포함해야 한다. 로직 변경은 새로운 기능을 추가하거나 특정 버그를 수정하는 것 등을 의미한다. 몇개의 단어로 고수준의 변화를 묘사할 수 없다면 단일 커밋으로는 너무 복잡한 상태인 것이다. 변경은 가능한 한 그 스스로 이해할 수 있도록 간결해야 한다. 많은 패치에서 발생한 에러가 작은 패치에서 발생한 에러보다 낫다. 가장 우선이 되는 규칙으로, 커밋 메시지만 읽어도 다른 개발자가 납득할 만큼 비슷한 시간을 들여 같은 패치를 구현할 수 있어야 한다.

git을 사용한다면 git add -p (또는 -i)를 활용해 각각의 변경 사항에 따라 로직을 이해할 수 있는 수준의 단일 커밋 단위로 쪼개야 한다.

Git 커밋 양식

만약 패치를 git으로 제출한다면, 그 양식은 거의 표준화 되어 있다. 첫 행은 변경에 대한 요약이다. (행의 최대 길이는 프로젝트마다 다르지만 일반적으로 행 당 50자에서 78자 사이다.) 이 첫 줄을 가장 많이 보게 되고 그만큼 중요하다. 많은 git 도구가 이 방식으로 동작하거나 이 양식에 최적화되어 있다. 첫 행 요약 다음으로 빈 행을 입력하고 그 뒤로 필요에 따라 패치에 대한 상세 내역을 여러 문단으로 작성한다. 코드를 설명하지 말고 의도와 접근 방식을 설명한다. 로그는 현재형으로 작성한다.

로그를 사랑하는 방법을 배울 것

나는 과거에 CVS를 사용했는데 (SVN도 조금) 이 도구는 정말 사용하기가 쉽지 않았다. 거의 쓸모가 없었는데 도구도 그랬고 사용 가능한 정보도 그랬다. 현재는 코드를 들여다 보는 것 보다 git의 로그를 더 자주 본다. git 로그 도구는 일하고 있는 프로젝트에서 CVS의 로그나 커밋 규칙에 비해 대단히 뛰어나기 때문에 훨씬 편리하다. 코드보다 git 로그를 더 많이 붙잡고 왜 이 코드가 이런 방법으로 작성되었는지 git을 통해 살펴보는데 대부분의 시간을 쓴다. 이 방법은 확실히 많은 시간과 노력을 아끼게 해준다. X 서버 버그에서 가장 짜증나는 점은 코드가 XFree86에서 넘어오는 과정에 git 히스토리가 남아있지 않은 곳에서 나타난다는 점이다. 만약 아직 소스 컨트롤 관리의 로그 도구를 사용하고 있지 않다면 이 도구와 더 친해지길 추천한다.

하면 안되는 것

커밋을 할 때 평균적으로 나타나는 몇 가지 일반적인 죄악이 있다. (그렇다. 읔.)

  • 소스 컨트롤 관리는 백업 시스템이 아니다! 개인적으로 정말 싫어하는 유형이다. 개발자 중에는 이 도구를 퇴근용 커밋 즉, 퇴근하기 직전에 변경한 모든 코드를 커밋하는 사람이 있다. 그 결과는 아무짝에 쓸모가 없다. 코드 이곳 저곳에서 확인되는 변경점은 몇개월이 지나면 그 누구도 이해할 수가 없다. 실제 코드 작성자를 포함해서 말이다. (덧붙여: 대학교는 절대 이런 쓰레기 방식으로 가르치지 말 것.)
  • 파일 당 커밋. 파일 하나 이상에서 로직 변경이 실제로 발생하지 않았는데도 커밋하는 경우가 있는데 지나치게 분리해서 커밋하면 안된다.
  • 게으른 커밋 메시지, “여러가지 고치고 정리했음” 또는 이와 비슷하게 작성한 모든 커밋을 의미한다. 이런 경우를 비FOSS 프로젝트에서 종종 본 적이 있었는데 이런 커밋은 결국 당신에게 되돌아와 상처를 입힌다. 알려진 버그인지 알아내는 것은 불가능에 가깝고 문제를 분리하기도 어려울 뿐더러 그 누구도 프로젝트에서 무슨 일이 일어나고 있는지 따라가기 어렵게 만든다.
  • 하나의 패치 안에 두 가지 변경. 예를 들면 “버그 2345를 수정했고 모든 foo를 bar로 수정했음”과 같은 커밋이다. 버그 2345에서 명칭 변경이 요구되고 있다 하더라도 이런 커밋은 여러개의 패치로 분리해야 한다. 이 버그 픽스를 안정 브랜치에 적용해야 하는 상황일 때 다른 내용이 함께 있기 때문에 적용할 수가 없다. 잘못된 패치를 유용한 덩어리에 넣는 일은 누군가 직접 처리하기 전까진 프로젝트에 아무런 가치를 더해주지 못하는 일이라 가장 시간 소모적이고 짜증나는 일 중 하나다.
  • 코드 변경에 공백 변경을 함께 넣는 경우. 사막에서 바늘 찾는건 재미있는 게임이지만 패치를 들여다보고 있을 때는 전혀 아니다. 이 방법은 분명 버그를 소개하는 가장 강력한 방법이긴 하다. 비록 수백 줄의 코드에 들여쓰기를 변경해 모든 코드가 변경된 상태로 표시되고 있으니 말이다. 재미로 한건지 장점이 있어서 한 것인지는 둘째치고 어느 위치에 버그가 있었고 그 버그를 어떻게 수정했는지 거의 아무도 찾을 수 없다.
  • 너무나도 사랑스러운 코드 누락. 새로운 기능을 추가하기 위해 수백 줄 코드 패치를 작성하는 동시에 이 기능을 위해 기존에 있던 인프라 구조를 절반 이상 재작성을 한 경우다. 그 결과로 수백 줄의 코드를 리뷰해야 하고 매번 이 영역에 있던 코드와 관련된 버그를 발견하게 된다.인프라 구조를 한번에 한 조각씩 수정하는 것으로 시작하고 그 작업을 하고서 맨 위에 새로운 기능을 꼽았으면 훨씬 쉽고 적은 시간을 사용했을 것이다. 위와 같은 방법을 자주 사용해서, 즉 빈번하게 코드 덤프를 통채로 적용하는 프로젝트가 있다면 이는 외부 개발자들의 사기를 떨어뜨린다. 당신이라면 코드를 기여하는 프로젝트가 아니라 지독한 잡음에서 신호를 골라내는 일에 시간을 쓰는 프로젝트에 참여하겠는가?
  • 패치와 관계 없는 공백 변경. 리뷰어는 패치에 관한 큰 그림을 생각해야 한다. 공백만 있는 덩어리는 혼란스럽다. 리뷰어는 그 공백이 실제 변경인지 무시해도 되는지 확인하기 위해 더 추가적인 노력을 기울이게 된다. 빈 행이 추가되거나 제거되는 경우는 그렇게 나쁘지 않다. 정말 나쁜 경우는 들여쓰기 변경이다.

위와 같은 상황에는 수많은 변명도 존재하는데 대부분 다음과 같이 대답하기를 좋아한다. “그래도 동작하잖아요!” 물론 동작은 한다. 하지만 코드는 정적이지 않다. 얼마 간의 시간 내에 코드는 아마 이동하고, 다시 작성될 것이며, 다른 방법으로 호출되거나 버그가 포함되어 있다는 사실이 발견될 수도 있다. 동시에 최초의 개발자는 코드를 움직이고 누구도 왜 그 코드가 그렇게 움직였는지 모를 수도 있다. 최악의 경우는 모든 사람이 코드를 만지는 것을 두려워하는 상황인데 누구도 어떻게 실제로 동작하는지 알 수 없기 때문이다.

또 다른 일반적인 변명은 다음과 같다. “하지만 나는 이 프로젝트에 참여하는 유일한 사람입니다.” 사실이 아니다. 모든 소프트웨어 프로젝트는 (위에서 보는 것과 같이) 협동 프로젝트다. 누구도 단순히 근시안적으로 생각하지 않는다. 특히 FOSS 프로젝트는 외부의 기여자 즉, 테스터, 개발자, 우선순위를 정하는 사람, 사용자 등에 의존적이다. 그들이 참여하게 만드는 게 어렵다면 그 프로젝트는 더 쉽게 망할 것이다.

좀 덜 일반적이지만 최근에 자주 보이는 또 다른 변명은 소스 컨트롤 관리가 너무 느리다는 불평이다. 분산 소스 컨트롤 관리는 이 이슈를 해결했기 때문에 시간도 절약하고 돈 절약에도 아마 보탬이 될 것이다.


  1. 최초 개발자라는 표현이 아무리 다듬어도 어색해서 “나”로 대입해서 번역했다. 

Don’t “Push” Your Pull Requests의 번역글이다. 코드를 기여하기 전에 그 커뮤니티의 분위기를 아는 것, 커뮤니티와 소통하는 것이 얼마나 중요한가에 대한 이야기다.

Ilya! Thank you for giving me the opportunity to translate this article. 😀


문제에 직면하거나 불편한 부분을 만나게 되면 에디터를 열고 코드를 수정해 문제를 해결하려고 파고들게 된다. 선한 의도로 간단한 패치를 만든다. 패치를 풀 리퀘스트로 보내고서 의자에 편히 기대고 결과를 지켜보게 된다. 자신을 스스로 대견하다고 생각하면서 말이다. 하지만 간단한 패치라고 해도 본 코드에 합쳐지기 전에 전례에 따라 준수해야 하는 몇 가지 원칙들이 있기 마련이다.

그 후에도 가끔 우연하게 불편한 문제를 발견하게 된다. 표면적인 문제로 아주 단순하게 해결할 수 있어서 하루 잠깐 짬을 내 개선을 할 수도 있고, 더 깊이 들어가 본질적인 문제를 살펴봐야 할 수도 있다. 그 결과, 이 문제를 완벽하게 해결한 300줄 이상의 패치, 수정된 여러 파일과 함께 지구를 구한 기분으로 풀 리퀘스트 보내게 된다.

여기에 모든 문제가 있다. 당신은 여기에 투자한 모든 시간과 작업, 노력을 인정받길 원한다. 하지만 메인테이너는 공포에 질려 패치를 들여다보고는 왜 그가 이 패치를 수락할 수 없는지 나열하게 된다. 그는 어디서 시작해야 할지도 몰라서 잘못된 부분만 콕 집어 살펴보게 된다. 당신이 기여한 코드는 어떤 방향으로도 빠르게 진행될 수 없다. 이런 문제는 어느 쪽을 비난할 수도 없는 상황이다.

풀 리퀘스트를 “떠넘기지” 말 것

기여는 항상 환영해야 마땅하지만, 깜짝 놀랄 패치는 결국 짐이 될 뿐이다. 당신은 분명 도움을 주기 위해 한 일이다. 하지만 누군가는 당신보다 더 오랜 기간 동안 그 코드를 유지보수 해왔다. 그러므로 그들을 놀라게 하는 것을 피하고 그들의 방식을 먼저 따르자. 더 최악인 경우는, 협소한 문제를 해결하기 위해 지역적인 변경을 만들어 프로젝트에서의 전체적인 구현을 놓치게 되는 경우가 있다. 미리 세워둔 로드맵 계획 또는 전체적인 구조적 결정 등을 엉망으로 만든다. 좋은 아이디어라도 몇 프로젝트에는 적절하지 못한 구현이 될 수 있다. 심지어 이 과정이 다른 사람의 노력을 무의미하게 만든다는 사실을 알아차리지 못할 수도 있다. 게다가 좋지 않은 타이밍이라면 이런 여러 이유로 인해 사람들이 당신을 등질 수도 있다.

코드 리뷰는 어렵다: 빨리 시작하자

다음부터는 에디터로 뛰어들기 전에 먼저 토론을 시작하고, 내 문제와 해결책에 대한 개요를 만들어 내가 어떤 일을 하려고 하는지 먼저 알리자. 그 목표를 설명하는 것은 한 문단으로 정리되어야 한다. 만약 그 글이 에세이가 되고 있다면, 문제를 더 잘게 쪼개야 한다는 신호거나 문제가 명쾌하게 정의되지 않은 상황이라는 의미다. 그 변경폭이 크다면 리뷰어를 정해 함께 전체적인 디자인을 살펴야 한다. 초기에 도움을 요청하고 손을 빌리면 만들려는 하는 코드를 마술과도 같이 제 시간에 작성해 “합칠(당겨질)” 수 있다.

코드 리뷰는 어렵다. 사실, 코드 리뷰의 효율은 200-400줄 코드를 넘으면 극단적으로 떨어지기 때문에 리뷰 속도는 시간당 300-500줄 미만으로 볼 것을 권장하고 있다. 이 방식을 계속 기억하자. 큰 커밋은 작고 다루기 쉬운 단위로 자른다. 가장 우선되는 규칙으로, 30분 정도 짧은 시간 동안만 리뷰를 한다. 또는 200줄 보다 짧은, (똑똑하진 않더라도) 깔끔하고 좋은 코드를 보는 것이다. 여러 개의 커밋이라도 로드맵을 함께 준다면 각각의 조각들에 대한 피드백을 한번에 받을 수 있을 것이다.

좋은 작업은 “밀어내기”가 아닌 “당기기”를 얻는다

스레드를 읽는 것만큼 귀찮은 일은 없다. 하지만 그보다 더 나쁜 경우는 기여하는 과정 끝에 내 기여가 잘못된 방향으로 흘러갔다는 사실을 받아들여야 할 때다. 기여자는 코드가 수락되지 않으면 상처를 받는다. 메인테이너는 딜레마에 빠진다. 그들은 커뮤니티를 통해 코드가 만들어지고 굴러가길 원하는데 이런 광팬들의 코드를 깊이 살펴보는 과정에서 그들과 대립하게 된다. 거기엔 승자가 없다. 게다가 어느 쪽도 비난할 수 없다.

다음엔 에디터로 뛰어들기 전에 토론을 시작하고, 내 문제와 해결책에 대한 윤곽을 잡아보고, 작업을 제안하자. 그저 어둠 속에서 작업해서 풀 리퀘스트로 “떠넘기지” 말자.


GitHub을 비롯해 이전보다 쉽게 참여할 수 있는 오픈소스 커뮤니티가 많아졌다. 내가 직접 작성한 코드라는 기쁨에 눈이 가려 코드를 꼼꼼히 잘 살펴보지 않고 풀 리퀘스트를 보낸 경험이 있어 이 글을 읽으며 그때의 일이 생각이 났다. 이제 막 오픈소스에 참여하려는 사람들에게 도움이 될 만한 글인 것 같아 번역하게 되었다.

최근 #이삭줍기, #코드줍기라는 이름으로 사소한 개선점을 오픈소스에서 찾아 pull request를 보내고 있는데 각각의 오픈소스마다 가진 문화나 분위기가 달라 배우는 점이 많다. 코드를 읽는 과정은 단순히 코드와 그 구조를 이해하는 것에 그치지 않고 타인을 이해하고 넓은 시야를 갖는 데 도움이 되는 느낌이다. 앞으로도 꾸준히 참여하고 참여한 커뮤니티에서 feel the room 할 수 있도록 노력해야겠다.

코드줍기란

erlang/otp의 위키에서 Writing good commit messages라는 짧은 글을 보고 한국어로 옮겼다. 대부분의 오픈소스는 각각의 Contribute 문서를 통해 어떤 관례를 사용하는지 밝히고 있는 편이지만 대부분 아래의 방식과 크게 다르지 않았다.


좋은 커밋 메시지는 다음 세가지 중요한 목적이 있다:

  • 리뷰 프로세스를 빠르게 만든다.
  • 좋은 릴리즈 노트를 작성하게 돕는다.
  • 미래의 코드 메인테이너를 돕는다. (그 메인테이너가 당신일 수도 있다!) 어떤 코드 변경이 일어났는지, 왜 이 특정한 기능이 추가되었는지 수년 후에 이야기 해도 알 수 있도록 말이다.

커밋 메시지의 구조는 다음과 같다. 이 내용은 git scm에서 자세하게 확인할 수 있다:

(50자 미만의) 변경에 대한 짧은 요약

필요하다면 상세한 설명을 첨가한다. 행 당 72자가 넘지 않도록 유의한다. 맥락에 따라 첫 줄은 이메일의 제목처럼,
나머지는 본문처럼 다뤄지는 경우가 있다. 요약과 본문을 구분하는 빈 행은 본문을 생략하지 않는 이상 필수적이다.
rebase와 같은 도구를 사용하면 혼란을 줄 수 있기 때문이다.

추가적인 문단은 빈 행 다음에 작성한다.

- 개조식 서술 또한 괜찮음

- 블릿(bullet)으로 하이픈(-)이나 별표(*)를 사용하고, 한 칸의 공간을 띄고 각 항목 사이 빈 행을 넣는
  방식이 일반적이나 다양한 관례가 있음.

해야 할 일

  • 요약과 설명을 작성할 때는 자신이 무엇을 했는지를 명령형으로 작성한다. 즉 다른 사람에게 이 일을 지시하듯 작성한다. “고쳤다 fixed”, “추가했다 added”, “변경했다 changed” 대신 “고치다 fix”, “추가하다 add”, “변경하다 change” 식으로 작성한다.
  • 항상 두번째 빈 행을 추가한다.
  • 커밋 메시지에서 줄 바꿈을 한다. (gitk등 커맨드 환경에서 커밋 메시지를 수평 스크롤 없이 쉽게 읽을 수 있는데 도움이 된다.)

하지 말아야 할 일

  • 요약 끝에 마침표를 찍지 않는다. 요약은 제목이다. 제목은 마침표로 끝나지 않는다.

  • 커밋을 요약하는데 어려움을 느낀다면 커밋 안에 여러 로직의 변화 또는 버그 수정이 있기 때문일 것이다. 이럴땐 git add -p 명령어를 사용해 여러 커밋으로 분리하는 것이 낫다.

레퍼런스

커밋 메시지에 대한 좋은 논의를 다음 블로그 포스트에서 확인할 수 있다.

색상을 바꿔요

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

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