[underscore] extend 올바르게 사용하기

객체를 확장하는 extend()는 복사용도로도 쓸 수 있는 정말 편리한 메소드지만, 사용하면서 주의할 점이 하나 있는데, 바로 인자 순서이다.

예를 들어, 여러 명의 사람 정보를 저장한 배열에 똑같은 프로퍼티를 추가하는 로직을 짠다고 가정하자. 물론 이 경우 extend()를 사용해야할 필요는 없지만, 굳이 사용해야만 하는 상황이라고 가정하자. 그러면 다음과 같이 코드를 짤 수 있을 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const _ = require('underscore');

let people = [{
name: '이현섭',
age: 27
}, {
name: '이승민',
age: 28
}, {
name: '오유근',
age: 26
}];

let base = { home: '한강빌리지' };

people = people.map((person) => _.extend(base, person));

언뜻 봐서는 적절한 코드로 보인다. 하지만 이 코드를 실행시키면 배열 내 모든 객체의 데이터가 동일한 데이터로 변경되는 문제가 생긴다.

왜일까? 이유는 인자의 순서에 따라 extend()의 결과물이 다르기 때문이다. underscore의 문서를 다시보면,

extend _.extend(destination, *sources)

source 객체에 있는 모든 프로퍼티를 destination 객체에 복사하고, destination 객체를 리턴합니다. source는 순서대로 처리하므로, 마지막 source의 프로퍼티가 앞의 인자들이 가진 같은 이름의 프로퍼티를 덮어쓸 수 있습니다.

이 서술에 의하면, 앞에서 예로 든 코드는 base라는 변수를 확장하고 리턴하게 된 것이다. 게다가 extend() 메소드는 새로운 객체를 만들어 리턴하지 않고, 주어진 객체를 확장하기 때문에 이 경우에는 base가 확장되고 리턴되는 것만 세 번 반복되는 것이다. 따라서 새롭게 만들어진 배열은 그냥 base가 세 번 연속 extend()된 결과물 일 뿐이다.

코드가 의도한 대로 동작하도록 바꾸려면 그냥 인자 순서만 교체해주면 된다.

1
people = people.map((person) => _.extend(person, base));

위의 사항은 동일한 용도인 lodash의 assignIn(), jQuery의 extend()에서도 동일하다.

“extend는 매개변수의 순서에 유의해서 사용해라”라는 말로 축약될 수 있는 이야기를 이렇게 늘어 놓을 필요가 있나 싶지만, 이 문제로 몇 시간 가량을 날려먹었기 때문에 이렇게 포스팅 기록을 남긴다. 역시 API 문서는 영어라고 읽기 싫다고 넘기지말고 주의 깊게 보아야겠다.