ES6 In Depth: 디스트럭처링(Destructuring)

ES6 In Depth는 ECMAScript 표준 6번째 에디션(줄여서 ES6)을 맞아 JavaScript에 새로 추가된 요소들을 살펴보는 시리즈입니다.

에디터 노트: 이 글은 원래 Firefox 개발자 도구의 담당 엔지니어 Nick Fitzgerald가 자신의 블로그에 Destructuring Assignment in ES6라는 제목으로 올렸던 글입니다.

디스트럭처링(destructuring) 할당이란 무엇인가요?

디스트럭처링을 이용하면 배열의 요소나 객체의 속성을 배열 리터럴(literal)이나 객체 리터럴과 비슷한 문법을 이용해서 변수에 할당할 수 있습니다. 아주 간결한 문법입니다. 그러면서도 훨씬 더 명확합니다.

디스트럭처링 할당이 없을 경우, 어떤 배열의 처음 3개 요소에 접근하는 코드는 다음과 같을 것입니다.

var first = someArray[0];
var second = someArray[1];
var third = someArray[2];

디스트럭처링 할당을 이용하면, 똑같은 코드가 좀 더 간결하고 읽기 쉬워집니다.

var [first, second, third] = someArray;

SpiderMonkey (Firefox의 JavaScript 엔진)는 이미 디스트럭처링의 거의 모든 것을 지원하고 있습니다. 하지만, 전부 지원하지는 않습니다. bug 694100에서 SpiderMonkey의 디스트럭처링 (그리고 ES6 일반 요소) 지원에 관한 이슈를 찾아보세요.

배열과 이터러블(iterable)을 디스트럭처링하기

우리는 이미 배열을 디스트럭처링하는 예제를 보았습니다. 이 문법의 일반형은 다음과 같습니다.

[ variable1, variable2, ..., variableN ] = array;

이 구문은 variable1부터 variableN을 각 순서에 해당하는 배열 요소들로 채웁니다. 변수의 할당과 선언을 동시에 하고 싶다면, 할당 구문 앞에 var, let, 또는 const를 추가합니다.

var [ variable1, variable2, ..., variableN ] = array;
let [ variable1, variable2, ..., variableN ] = array;
const [ variable1, variable2, ..., variableN ] = array;

사실 variable이라고 한정하는 것은 잘못입니다. 왜냐하면 원하는만큼 패턴을 중첩시켜 사용할 수 있기 때문입니다.

var [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo);
// 1
console.log(bar);
// 2
console.log(baz);
// 3

아울러, 배열을 디스트럭처링할 때 일부 요소들을 건너뛸 수도 있습니다.

var [,,third] = ["foo", "bar", "baz"];
console.log(third);
// "baz"

그리고 “레스트(rest)” 패턴을 사용해서 배열 맨뒤의 요소들 모두를 다른 배열에 할당할 수도 있습니다.

var [head, ...tail] = [1, 2, 3, 4];
console.log(tail);
// [2, 3, 4]

범위를 벗어나거나 존재하지 않는 배열 요소에 접근하면, 해당 요소를 인덱스로 접근할 때와 같은 결과를 얻습니다. undefined를 얻습니다.

console.log([][0]);
// undefined

var [missing] = [];
console.log(missing);
// undefined

일러둘 것은 배열에 대한 디스트럭처링 할당 패턴이 모든 종류의 이터러블(iterable)에도 통한다는 점입니다.

function* fibs() {
  var a = 0;
  var b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}
var [first, second, third, fourth, fifth, sixth] = fibs();
console.log(sixth);
// 5

객체를 디스트럭처링하기

객체를 디스트럭처링하면 변수에 객체의 속성값을 할당할 수 있습니다. 할당할 속성을 지정하고 그 속성값을 할당할 변수를 지정합니다.

var robotA = { name: "Bender" };
var robotB = { name: "Flexo" };

var { name: nameA } = robotA;
var { name: nameB } = robotB;

console.log(nameA);
// "Bender"
console.log(nameB);
// "Flexo"

속성의 이름과 변수의 이름이 같을 경우, 약식 문법을 사용할 수 있습니다.

var { foo, bar } = { foo: "lorem", bar: "ipsum" };
console.log(foo);
// "lorem"
console.log(bar);
// "ipsum"

그리고 배열의 경우와 똑같이 디스트럭처링 패턴을 중첩시켜 사용할 수 있습니다.

var complicatedObj = {
  arrayProp: [
    "Zapp",
    { second: "Brannigan" }
  ]
};

var { arrayProp: [first, { second }] } = complicatedObj;

console.log(first);
// "Zapp"
console.log(second);
// "Brannigan"

존재하지 않는 속성을 디스트럭처링하면 undefined를 얻습니다.

var { missing } = {};
console.log(missing);
// undefined

한가지 주의할 것은 객체를 디스트럭처링해서 선언되지 않은 변수에 할당하는 경우입니다 (let, const, 또는 var를 함께 쓰지 않는 경우입니다).

{ blowUp } = { blowUp: 10 };
// Syntax error

이 구문이 에러를 일으키는 이유는 JavaScript 문법에 따라 엔진이 {로 시작하는 모든 구문을 블록(block) 구문으로 해석하기 때문입니다. (예를 들어, { console } 은 유효한 블록 구문입니다). 구문 에러를 해결하는 방법은 문장 전체를 괄호로 감싸는 것입니다.

({ safe } = {});
// No errors

객체도, 배열도, 이터러블(iterable)도 아닌 값을 디스트럭처링 하기

null이나 undefined를 디스트럭처링하려고 하면 타입 에러(type error)가 발생합니다.

var {blowUp} = null;
// TypeError: null has no properties

boolean, number, string 같은 프리미티브 타입(primitive type)들은 에러 없이 디스트럭처링할 수 있지만 결과값으로 undefined를 얻습니다.

var {wtf} = NaN;
console.log(wtf);
// undefined

이것이 이상해 보일수도 있습니다. 하지만 좀 더 생각해보면 이유는 간단합니다. 객체 할당 패턴을 사용할 때, 디스트럭처링의 대상이되는 값은 Object로 변환 가능한 값이어야 합니다. 거의 모든 타입이 객체로 변환 가능합니다. 하지만 nullundefined는 변환되지 않습니다. 배열 할당 패턴을 적용할 때, 그 값은 반드시 이터레이터(iterator)를 갖고 있어야 합니다.

디폴트 값

디스트럭처링하려는 속성이 존재하지 않을 때를 대비해서 디폴트 값을 지정할 수 있습니다.

var [missing = true] = [];
console.log(missing);
// true

var { message: msg = "Something went wrong" } = {};
console.log(msg);
// "Something went wrong"

var { x = 3 } = {};
console.log(x);
// 3

(에디터 노트: Firefox는 처음 2개 케이스를 지원합니다. 하지만 3번째 케이스는 지원하지 않습니다. bug 932080를 참조하세요.)

디스트럭처링의 실용적 응용사례

함수의 인자 정의

개발자로서 API를 만들 때, API 사용자가 API의 파라메터 순서를 기억하도록 강요하는 것은 인간적이지 않습니다. 대신 여러개의 속성을 가진 객체를 파라메터로 전달 받는 것이 좀 더 나은 접근입니다. 디스트럭처링을 이용하면 API 함수 안에서 파라메터 속성을 참조할 때마다 파라메터 객체를 반복해서 사용하는 것을 피할 수 있습니다.

function removeBreakpoint({ url, line, column }) {
  // ...
}

이것은 Firefox DevTools JavaScript 디버거의 실제 코드를 간략하게 표시한 것입니다 (Firefox DevTools의 JavaScript 디버거는 JavaScript로 작성되었습니다). 우리는 디스트럭처링 패턴이 아주 만족스럽습니다.

설정 객체의 파라메터

앞서 소개한 예제를 확장해서, 디스트럭처링하려는 객체의 속성에 디폴트 값을 줄 수도 있습니다. 이것은 설정값을 관리하는 객체가 있고, 각 설정값에 적절한 디폴트 값이 존재할 때 특히 도움이 됩니다. 예를 들어, jQuery의 ajax 함수는 함수의 2번째 인자로 설정 객체를 전달받습니다. 이 함수를 다음과 같이 다시 쓸 수 있습니다.

jQuery.ajax = function (url, {
  async = true,
  beforeSend = noop,
  cache = true,
  complete = noop,
  crossDomain = false,
  global = true,
  // ... more config
}) {
  // ... do stuff
};

이렇게 하면 설정 객체의 각 속성마다 var foo = config.foo || theDefaultFoo; 처럼 코딩하는 것을 피할 수 있습니다.

(에디터 노트: 불행히 Firefox는 객체 안에 디폴트 값을 지정하는 이 문법을 아직 지원하지 못합니다. 제가 알기로는 작업이 진행중입니다. 작업 현황을 알고 싶다면 bug 932080를 확인하세요.)

ES6 이터레이션 프로토콜과 함께

ECMAScript 6는 이터레이션 프로토콜을 제공합니다. 시리즈 초반에 소개한 바 있습니다. Map 객체 (ES6 제정과 함께 표준 라이브러리에 추가된 객체)를 순회할 때, 우리는 일련의 [key, value] 페어(pair)를 얻습니다. 이 페어를 디스트럭처링하면 키(key)와 밸류(value)에 쉽게 접근할 수 있습니다.

var map = new Map();
map.set(window, "the global");
map.set(document, "the document");

for (var [key, value] of map) {
  console.log(key + " is " + value);
}
// "[object Window] is the global"
// "[object HTMLDocument] is the document"

순회하면서 키(keys) 값에만 접근하는 코드는 다음과 같습니다.

for (var [key] of map) {
  // ...
}

그리고, 밸류(value) 값에만 접근하는 코드는 다음과 같습니다.

for (var [,value] of map) {
  // ...
}

여러개의 값을 리턴하기

비록 여러개의 값을 리턴하는 기능이 랭귀지에 추가되지는 않았지만, 배열을 리턴하고 그 결과를 디스트럭처링할 수 있기 때문에 여러개의 값을 리턴하는 기능을 대신할 수 있습니다.

function returnMultipleValues() {
  return [1, 2];
}
var [foo, bar] = returnMultipleValues();

또, 객체를 컨테이너로 이용해서 리턴값의 이름을 지정할 수도 있습니다.

function returnMultipleValues() {
  return {
    foo: 1,
    bar: 2
  };
}
var { foo, bar } = returnMultipleValues();

이 2가지 패턴 모두 임시 컨테이너를 사용하는 코드보다 훨씬 낫습니다.

function returnMultipleValues() {
  return {
    foo: 1,
    bar: 2
  };
}
var temp = returnMultipleValues();
var foo = temp.foo;
var bar = temp.bar;

또는 continuation passing style을 사용할 수도 있습니다.

function returnMultipleValues(k) {
  k(1, 2);
}
returnMultipleValues((foo, bar) => ...);

CommonJS 모듈로부터 이름(name) 가져오기

아직 ES6 모듈(module)을 사용하지 않고 있나요? 아직 CommonJS 모듈을 사용하나요? 문제 없습니다! 어떤 CommonJS 모듈 X를 가져올(import) 때, 모듈 X는 보통 당신이 실제 사용하려는 함수보다 더 많은 함수들을 노출(export)합니다. 디스트럭처링을 이용하면, 주어진 모듈에서 어떤 함수를 사용하려고 하는지 명시적으로 지정할 수 있습니다. 이를 통해 당신의 네임스페이스(namespace)를 더럽히지 않을 수 있습니다.

const { SourceMapConsumer, SourceNode } = require("source-map");

(그리고 만약 ES6의 모듈을 사용하고 있다면, import 선언에 비슷한 문법이 존재한다는 사실을 기억하세요.)

결론

그래서, 지금까지 본 바와 같이 디스트럭처링은 여러모로 유용합니다. 모질라는 디스트럭처링을 무척 오래 사용해왔습니다. Lars Hansen가 10년전에 Opera 브라우저에 JS 디스트럭처링을 도입했고, Brendan Eich가 조금 후에 Firefox 브라우저에 JS 디스트럭처링을 구현했습니다. JS 디스트럭처링은 Firefox 2를 통해 릴리즈됐습니다. 당신은 앞으로 매일 JS 랭귀지를 사용하면서 디스트럭처링을 사용하게 될 것입니다. 디스트럭처링을 사용하면 사용할수록 당신의 코드는 더 짧아지고 더 명확해질 것입니다.

시리즈를 시작하면서, ES6가 당신의 JavaScript 코딩 방식을 바꿀 것이라고 말했습니다. 그때 염두에 뒀던 것이 바로 이런 종류의 기능입니다. 하나씩 익혀나갈 수 있는 간단한 개선 사항들 말이죠. 모든 것들을 익히고 나면, 당신이 수행하는 모든 프로젝트들이 영향 받을 것입니다. 개선를 통한 개혁입니다 (Revolution by way of evolution).

디스트럭처링을 ES6 표준에 맞춰 개발한 팀이 있습니다. Tooru Fujisawa (arai)와 Arpad Borsos (Swatinem)의 훌륭한 기여에 특별한 감사를 표합니다.

크롬 브라우저는 디스트럭처링 지원 작업이 진행중입니다. 다른 브라우저들도 틀림없이 늦지 않게 디스트럭처링을 지원할 것입니다. 지금 당장 웹에서 디스트럭처링을 사용하려면 Babel이나 Traceur를 사용해야 합니다.


이번 글을 작성해준 Nick Fitzgerald에게 감사를 전합니다.

다음 글에서는, JS에 이미 존재하는 기능을 조금 더 짧게 구현하는 문법을 다룰 것입니다. 모든 랭귀지의 기초적인 구성요소 역할을 해온 문법입니다. 관심 있나요? 조금 더 짧은 문법이 당신이 열광할만한 것인가요? 저는 확실히 그 답이 예스(yes)일 거라고 예상합니다. 하지만 제말을 곧이곧대로 믿지는 마세요. 다음 글에서 ES6의 화살표 함수(arrow function)를 함께 알아봅시다.

Jason Orendorff

ES6 In Depth 에디터

이 글은 이 쓴 ES6 In Depth: Destructuring의 한국어 번역본입니다.

작성자: ingeeKim

"누구에게나 평등하고 자유로운 웹"에 공감하는 직장인.

ingeeKim가 작성한 문서들…


2 댓글

  1. 이순빈

    러스트 문서 한글 번역(https://www.penflip.com/sarojaba/rust-doc-korean)에 기여중인데, 조금 난해했던 디스트럭처링에 대해 약간 감을 잡을 수 있게 되었네요. 항상 흥미롭고 많은 도움이 됩니다.

    9월 9th, 2015 at 1:45 오후
  2. ingeeKim

    러스트 문서 레퍼런스가 필요했습니다. 요긴한 정보, 감사합니다.

    9월 9th, 2015 at 4:39 오후

댓글 쓰기