구조 분해 할당

구조 분해 할당 (Destructuring assignment)

배열에서 값을, 또는 객체에서 속성(property)을 꺼내 별개의 변수에 담을 수 있게 해 주는 JavaScript 문법입니다. 데이터를 받아들이는 위치(예: 할당문의 왼쪽, 또는 새로운 식별자 바인딩을 만드는 모든 곳)에서 사용할 수 있습니다. Baseline Widely available — 이 기능은 널리 지원되며 수많은 기기와 브라우저 버전에서 동작합니다.

출처: Destructuring assignment

본문

구조 분해(destructuring)는 배열의 값이나 객체의 속성을 개별 변수로 풀어내는 JavaScript 문법입니다. 값을 받는 위치(할당문의 왼쪽이나 새로운 식별자 바인딩이 생기는 곳) 어디에서든 사용할 수 있습니다.

시도해 보기 (Try it)

let a, b, rest;
[a, b] = [10, 20];

console.log(a);
// Expected output: 10

console.log(b);
// Expected output: 20

[a, b, ...rest] = [10, 20, 30, 40, 50];

console.log(rest);
// Expected output: Array [30, 40, 50]

구문 (Syntax)

const [a, b] = array;
const [a, , b] = array;
const [a = aDefault, b] = array;
const [a, b, ...rest] = array;
const [a, , b, ...rest] = array;
const [a, b, ...{ pop, push }] = array;
const [a, b, ...[c, d]] = array;

const { a, b } = obj;
const { a: a1, b: b1 } = obj;
const { a: a1 = aDefault, b = bDefault } = obj;
const { a, b, ...rest } = obj;
const { a: a1, b: b1, ...rest } = obj;
const { [key]: a } = obj;

let a, b, a1, b1, c, d, rest, pop, push;
[a, b] = array;
[a, , b] = array;
[a = aDefault, b] = array;
[a, b, ...rest] = array;
[a, , b, ...rest] = array;
[a, b, ...{ pop, push }] = array;
[a, b, ...[c, d]] = array;

({ a, b } = obj); // parentheses are required
({ a: a1, b: b1 } = obj);
({ a: a1 = aDefault, b = bDefault } = obj);
({ a, b, ...rest } = obj);
({ a: a1, b: b1, ...rest } = obj);

설명 (Description)

객체와 배열 리터럴 표현식은 임시로 데이터를 묶는(package) 쉬운 방법을 제공합니다.

const arr = [a, b, c];

구조 분해는 이와 비슷한 문법이지만 할당문의 왼쪽에서 사용합니다. 배열 선언의 역연산을 수행하여, 컬렉션의 각 요소를 별개의 변수로 선언합니다.

const arr = [1, 2, 3];
const [a, b, c] = arr;
// a = 1, b = 2, c = 3

객체의 경우, 아래 두 줄의 쌍을 비교해 보면 각 쌍 안에 직접적인 대응 관계가 있음을 알 수 있습니다.

const obj = { a, b, c };
const { a, b, c } = obj;
// Equivalent to:
// const a = obj.a, b = obj.b, c = obj.c;

const obj = { prop1: x, prop2: y, prop3: z };
const { prop1: x, prop2: y, prop3: z } = obj;
// Equivalent to:
// const x = obj.prop1, y = obj.prop2, z = obj.prop3;

이 기능은 Perl이나 Python 같은 언어에 있는 기능과 유사합니다. 배열/객체 구조 분해에 특화된 기능은 아래의 각 예제를 참고하세요.

바인딩 패턴과 할당 패턴 (Binding and assignment)

객체 및 배열 구조 분해 모두 두 종류의 패턴이 있습니다: 바인딩 패턴(binding pattern)할당 패턴(assignment pattern) 입니다. 두 패턴은 구문이 조금 다릅니다.

바인딩 패턴에서는 패턴이 선언 키워드(var, let, const)로 시작합니다. 이후 각각의 속성은 변수에 바인딩되거나 더 구조 분해되어야 합니다.

const obj = { a: 1, b: { c: 2 } };
const {
  a,
  b: { c: d },
} = obj;
// Two variables are bound: `a` and `d`

모든 변수는 같은 선언을 공유하므로, 일부 변수는 재할당 가능하게 하고 다른 변수는 읽기 전용으로 만들고 싶다면 두 번(한 번은 let으로, 한 번은 const로) 구조 분해해야 할 수 있습니다.

const obj = { a: 1, b: { c: 2 } };
const { a } = obj; // a is constant
let {
  b: { c: d },
} = obj; // d is re-assignable

언어가 변수를 자동으로 바인딩해 주는 다른 많은 문법(예: for...in, for...of, for await...of 루프의 루프 변수, 함수 매개변수, catch 바인딩 변수)에서도 바인딩 구조 분해 패턴을 사용할 수 있습니다.

할당 패턴에서는 패턴이 키워드로 시작하지 않습니다. 각 구조 분해된 속성은 할당 대상(target)에 할당됩니다. 이 대상은 varlet으로 미리 선언되었거나 다른 객체의 속성일 수 있으며, 일반적으로 할당 표현식의 왼쪽에 올 수 있는 무엇이든 가능합니다.

const numbers = [];
const obj = { a: 1, b: 2 };
({ a: numbers[0], b: numbers[1] } = obj);
// The properties `a` and `b` are assigned to properties of `numbers`

참고: 선언 없이 객체 리터럴 구조 분해를 사용할 때는 할당문 주위의 괄호 ( ... )가 필수입니다. { a, b } = { a: 1, b: 2 }는 단독 문법으로 유효하지 않은데, 왼쪽의 { a, b }가 표현식 문(expression statement) 규칙에 따라 객체 리터럴이 아니라 블록으로 간주되기 때문입니다. 반면 ({ a, b } = { a: 1, b: 2 })는 유효하며, const { a, b } = { a: 1, b: 2 }도 유효합니다.

코딩 스타일에 문장 끝 세미콜론을 쓰지 않는다면, ( ... ) 표현식 앞에 세미콜론을 넣어 앞의 줄에서 함수를 실행하는 것으로 해석되지 않게 해야 합니다.

위 코드의 바인딩 패턴에 해당하는 문법은 유효하지 않습니다:

const numbers = [];
const obj = { a: 1, b: 2 };
const { a: numbers[0], b: numbers[1] } = obj;

// This is equivalent to:
//   const numbers[0] = obj.a;
//   const numbers[1] = obj.b;
// Which definitely is not valid.

할당 패턴은 할당 연산자의 왼쪽으로만 사용할 수 있습니다. +=*= 같은 복합 할당 연산자에는 사용할 수 없습니다.

기본값 (Default value)

각 구조 분해된 속성은 기본값(default value) 을 가질 수 있습니다. 기본값은 해당 속성이 존재하지 않거나 값이 undefined일 때 사용됩니다. 값이 null일 때는 사용되지 않습니다.

const [a = 1] = []; // a is 1
const { b = 2 } = { b: undefined }; // b is 2
const { c = 2 } = { c: null }; // c is null

기본값은 어떤 표현식이든 될 수 있으며, 필요할 때만 평가됩니다.

const { b = console.log("hey") } = { b: 2 };
// Does not log anything, because `b` is defined and there's no need
// to evaluate the default value.

나머지 속성과 나머지 요소 (Rest properties and rest elements)

구조 분해 패턴의 끝을 나머지 속성 ...rest로 끝맺을 수 있습니다. 배열 구조 분해에서는 이터러블(iterable)의 나머지 요소들을 rest(또는 원하는 이름)라는 새 배열로 모읍니다. 객체 구조 분해에서는 패턴이 이미 잡아내지 않은 객체의 모든 열거 가능한 고유 속성을 rest라는 새 객체로 복사합니다.

형식적으로는 ...rest 구문이 배열 구조 분해에서는 "rest 요소(rest elements)", 객체 구조 분해에서는 "rest 속성(rest properties)"이라 불리지만, 보통 통틀어 "rest 속성"이라고 부릅니다.

const { a, ...others } = { a: 1, b: 2, c: 3 };
console.log(others); // { b: 2, c: 3 }

const [first, ...others2] = [1, 2, 3];
console.log(others2); // [2, 3]

rest 속성은 패턴에서 마지막에 와야 하며 뒤에 쉼표가 와서는 안 됩니다.

const [a, ...b,] = [1, 2, 3];

// SyntaxError: rest element may not have a trailing comma
// Always consider using rest operator as the last element

다른 JavaScript 문법과 마찬가지로 암시적 깊은 복사(deep-copy)는 없습니다. rest 속성은 새 객체(또는 배열)를 만들지만, 그 멤버 값들이 객체 참조라면 그 참조는 그대로 유지됩니다.

예제 (Examples)

배열 구조 분해 (Array destructuring)

기본 변수 할당

const foo = ["one", "two", "three"];

const [red, yellow, green] = foo;
console.log(red); // "one"
console.log(yellow); // "two"
console.log(green); // "three"

소스보다 더 많은 요소로 구조 분해하기

할당문 오른쪽에 길이 N인 배열을 구조 분해할 때, 왼쪽에 지정한 변수 개수가 N보다 많으면 처음 N개의 변수만 값을 받습니다. 나머지 변수의 값은 undefined가 됩니다.

const foo = ["one", "two"];

const [red, yellow, green, blue] = foo;
console.log(red); // "one"
console.log(yellow); // "two"
console.log(green); // undefined
console.log(blue); // undefined

변수 값 교환 (Swapping variables)

두 변수의 값을 하나의 구조 분해 표현식으로 교환할 수 있습니다. 구조 분해 없이 두 값을 교환하려면 임시 변수(또는 일부 저수준 언어에서는 XOR-swap 기법)가 필요합니다.

let a = 1;
let b = 3;

[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1

const arr = [1, 2, 3];
[arr[2], arr[1]] = [arr[1], arr[2]];
console.log(arr); // [1, 3, 2]

함수에서 반환된 배열 파싱하기

함수에서 배열을 반환하는 것은 항상 가능했습니다. 구조 분해를 사용하면 배열 반환값을 더 간결하게 다룰 수 있습니다. 아래 예에서 f()[1, 2]를 반환하며, 이를 한 줄로 구조 분해합니다.

function f() {
  return [1, 2];
}

const [a, b] = f();
console.log(a); // 1
console.log(b); // 2

반환값의 일부 무시하기

관심 없는 반환값은 무시할 수 있습니다:

function f() {
  return [1, 2, 3];
}

const [a, , b] = f();
console.log(a); // 1
console.log(b); // 3

const [c] = f();
console.log(c); // 1

모든 반환값을 무시할 수도 있습니다:

[, ,] = f();

다만 이 경우에는 구조 분해를 쓰지 않고 함수를 호출하는 편이 더 명확할 것입니다. 반환값을 사용할 필요는 없습니다.

rest 속성으로 바인딩 패턴 사용하기

배열 구조 분해의 rest 속성은 또 다른 배열 또는 객체 바인딩 패턴일 수 있습니다. 내부 구조 분해는 rest 요소를 모은 뒤 생성된 배열에서 분해하기 때문에, 원래 이터러블에 있던 속성에는 이 방식으로 접근할 수 없습니다.

const [a, b, ...{ length }] = [1, 2, 3];
console.log(a, b, length); // 1 2 1

const [a, b, ...[c, d]] = [1, 2, 3, 4];
console.log(a, b, c, d); // 1 2 3 4

각 rest 속성이 항상 목록의 마지막이라면, 이러한 바인딩 패턴은 중첩될 수도 있습니다.

const [a, b, ...[c, d, ...[e, f]]] = [1, 2, 3, 4, 5, 6];
console.log(a, b, c, d, e, f); // 1 2 3 4 5 6

반면 객체 구조 분해에서는 rest 속성으로 식별자만 올 수 있습니다.

const { a, ...{ b } } = { a: 1, b: 2 };
// SyntaxError: `...` must be followed by an identifier in declaration contexts

let a, b;
({ a, ...{ b } } = { a: 1, b: 2 });
// SyntaxError: `...` must be followed by an assignable reference in assignment contexts

정규식 매치 결과 풀어내기

정규식의 exec() 메서드가 매치를 찾으면, 문자열에서 매치된 전체 부분을 첫 요소로, 그다음 각 괄호 그룹에 매치된 부분들을 담은 배열을 반환합니다. 구조 분해를 사용하면 이 배열에서 각 부분을 쉽게 풀어낼 수 있으며, 전체 매치가 불필요하면 무시할 수도 있습니다.

function parseProtocol(url) {
  const parsedURL = /^(\w+):\/\/([^/]+)\/(.*)$/.exec(url);
  if (!parsedURL) {
    return false;
  }
  console.log(parsedURL);
  // ["https://developer.mozilla.org/en-US/docs/Web/JavaScript",
  // "https", "developer.mozilla.org", "en-US/docs/Web/JavaScript"]

  const [, protocol, fullHost, fullPath] = parsedURL;
  return protocol;
}

console.log(
  parseProtocol("https://developer.mozilla.org/en-US/docs/Web/JavaScript"),
);
// "https"

어떤 이터러블에든 배열 구조 분해 사용하기

배열 구조 분해는 오른쪽 피연산자의 이터러블 프로토콜(iterable protocol)을 호출합니다. 따라서 배열이 아니어도 어떤 이터러블이든 구조 분해할 수 있습니다.

const [a, b] = new Map([
  [1, 2],
  [3, 4],
]);
console.log(a, b); // [1, 2] [3, 4]

이터러블이 아닌 것은 배열로 구조 분해할 수 없습니다.

const obj = { 0: "a", 1: "b", length: 2 };
const [a, b] = obj;
// TypeError: obj is not iterable

이터러블은 모든 바인딩이 할당될 때까지만 순회됩니다.

const obj = {
  *[Symbol.iterator]() {
    for (const v of [0, 1, 2, 3]) {
      console.log(v);
      yield v;
    }
  },
};
const [a, b] = obj; // Only logs 0 and 1

rest 바인딩은 지연 없이 즉시 평가되어 이전 이터러블을 쓰는 대신 새 배열을 만듭니다.

const obj = {
  *[Symbol.iterator]() {
    for (const v of [0, 1, 2, 3]) {
      console.log(v);
      yield v;
    }
  },
};
const [a, b, ...rest] = obj; // Logs 0 1 2 3
console.log(rest); // [2, 3] (an array)

객체 구조 분해 (Object destructuring)

기본 할당

const user = {
  id: 42,
  isVerified: true,
};

const { id, isVerified } = user;

console.log(id); // 42
console.log(isVerified); // true

새 변수 이름으로 할당하기

객체 속성을 풀어내 객체 속성과 다른 이름의 변수에 할당할 수 있습니다.

const o = { p: 42, q: true };
const { p: foo, q: bar } = o;

console.log(foo); // 42
console.log(bar); // true

여기서 const { p: foo } = o는 객체 o에서 이름이 p인 속성을 가져와 foo라는 지역 변수에 할당합니다.

새 변수 이름으로 할당하고 기본값 제공하기

한 속성은 다음 두 가지를 동시에 할 수 있습니다:

  • 객체에서 풀려 다른 이름의 변수에 할당되기
  • 풀려난 값이 undefined일 경우에 대비해 기본값이 지정되기
const { a: aa = 10, b: bb = 5 } = { a: 3 };

console.log(aa); // 3
console.log(bb); // 5

함수 매개변수로 전달된 객체에서 속성 풀어내기

함수 매개변수로 전달된 객체도 변수로 풀 수 있으며, 함수 본문 안에서 접근할 수 있습니다. 객체 할당과 마찬가지로 구조 분해 문법은 새 변수가 원래 속성과 같은 이름이거나 다른 이름을 가질 수 있고, 원래 객체가 그 속성을 정의하지 않을 경우를 위한 기본값을 지정할 수 있습니다.

예를 들어 사용자 정보를 담은 다음 객체를 생각해 봅시다.

const user = {
  id: 42,
  displayName: "jdoe",
  fullName: {
    firstName: "Jane",
    lastName: "Doe",
  },
};

전달된 객체의 속성을 같은 이름의 변수로 풀어내는 방법을 보여줍니다. 매개변수 값 { id }는 함수에 전달된 객체의 id 속성을 같은 이름의 변수로 풀어내 함수 안에서 사용하라는 뜻입니다.

function userId({ id }) {
  return id;
}

console.log(userId(user)); // 42

풀어낼 변수의 이름을 정의할 수도 있습니다. 여기서는 displayName 속성을 풀어 함수 본문에서 사용할 dname으로 이름을 바꿉니다.

function userDisplayName({ displayName: dname }) {
  return dname;
}

console.log(userDisplayName(user)); // "jdoe"

중첩 객체도 풀어낼 수 있습니다. 아래 예는 fullName.firstName 속성을 name이라는 변수로 풀어냅니다.

function whois({ displayName, fullName: { firstName: name } }) {
  return `${displayName} is ${name}`;
}

console.log(whois(user)); // "jdoe is Jane"

함수 매개변수의 기본값 설정하기

=를 사용해 기본값을 지정할 수 있으며, 전달된 객체에 지정된 속성이 없으면 이 값이 변수 값으로 사용됩니다. 아래는 기본 크기가 'big', 기본 좌표가 x: 0, y: 0, 기본 반지름이 25인 함수입니다.

function drawChart({
  size = "big",
  coords = { x: 0, y: 0 },
  radius = 25,
} = {}) {
  console.log(size, coords, radius);
  // do some chart drawing
}

drawChart({
  coords: { x: 18, y: 30 },
  radius: 30,
});

drawChart 함수 시그니처에서 구조 분해된 왼쪽은 빈 객체 = {}를 기본값으로 가집니다. 이 기본값 없이 함수를 작성할 수도 있습니다. 다만 기본값을 생략하면 함수는 호출 시 인수가 최소 하나 제공되기를 기대합니다. 반면 현재 형태로는 drawChart()를 인수 없이 호출할 수 있습니다. 그렇지 않다면 최소한 빈 객체 리터럴을 제공해야 합니다.

자세한 내용은 "기본 매개변수 > 기본값 할당이 있는 구조 분해 매개변수" 문서를 참고하세요.

중첩 객체와 배열 구조 분해

const metadata = {
  title: "Scratchpad",
  translations: [
    {
      locale: "de",
      localizationTags: [],
      lastEdit: "2014-04-14T08:43:37",
      url: "/de/docs/Tools/Scratchpad",
      title: "JavaScript-Umgebung",
    },
  ],
  url: "/en-US/docs/Tools/Scratchpad",
};

const {
  title: englishTitle, // rename
  translations: [
    {
      title: localeTitle, // rename
    },
  ],
} = metadata;

console.log(englishTitle); // "Scratchpad"
console.log(localeTitle); // "JavaScript-Umgebung"

for...of 반복과 구조 분해

const people = [
  {
    name: "Mike Smith",
    family: {
      mother: "Jane Smith",
      father: "Harry Smith",
      sister: "Samantha Smith",
    },
    age: 35,
  },
  {
    name: "Tom Jones",
    family: {
      mother: "Norah Jones",
      father: "Richard Jones",
      brother: "Howard Jones",
    },
    age: 25,
  },
];

for (const {
  name: n,
  family: { father: f },
} of people) {
  console.log(`Name: ${n}, Father: ${f}`);
}

// "Name: Mike Smith, Father: Harry Smith"
// "Name: Tom Jones, Father: Richard Jones"

계산된 객체 속성 이름과 구조 분해

객체 리터럴에서처럼 계산된 속성 이름을 구조 분해와 함께 사용할 수 있습니다.

const key = "z";
const { [key]: foo } = { z: "bar" };

console.log(foo); // "bar"

유효하지 않은 JavaScript 식별자인 속성 이름

유효한 JavaScript 식별자가 아닌 속성 이름도, 유효한 대체 식별자를 제공하면 구조 분해할 수 있습니다.

const foo = { "fizz-buzz": true };
const { "fizz-buzz": fizzBuzz } = foo;

console.log(fizzBuzz); // true

원시값 구조 분해 (Destructuring primitive values)

객체 구조 분해는 속성 접근과 거의 동등합니다. 즉, 원시(primitive) 값을 구조 분해하려 하면 그 값은 대응하는 래퍼(wrapper) 객체로 감싸지고, 그 래퍼 객체에서 속성에 접근합니다.

const { a, toFixed } = 1;
console.log(a, toFixed); // undefined ƒ toFixed() { [native code] }

속성 접근과 마찬가지로, null이나 undefined를 구조 분해하면 TypeError가 발생합니다.

const { a } = undefined; // TypeError: Cannot destructure property 'a' of 'undefined' as it is undefined.
const { b } = null; // TypeError: Cannot destructure property 'b' of 'null' as it is null.

패턴이 비어 있어도 이는 발생합니다.

const {} = null; // TypeError: Cannot destructure 'null' as it is null.

배열과 객체 구조 분해 결합하기

배열과 객체 구조 분해는 함께 결합할 수 있습니다. 아래 props 배열의 세 번째 요소를 가져온 다음 그 객체의 name 속성을 얻고 싶다면 다음처럼 하면 됩니다:

const props = [
  { id: 1, name: "Fizz" },
  { id: 2, name: "Buzz" },
  { id: 3, name: "FizzBuzz" },
];

const [, , { name }] = props;

console.log(name); // "FizzBuzz"

객체를 분해할 때 프로토타입 체인을 탐색합니다

객체를 분해할 때 어떤 속성이 객체 자체에 없으면 프로토타입 체인을 따라 계속 탐색합니다.

const obj = {
  self: "123",
  __proto__: {
    prot: "456",
  },
};
const { self, prot } = obj;

console.log(self); // "123"
console.log(prot); // "456"

명세 (Specifications)

구조 분해 할당은 ECMAScript 언어 명세에 정의되어 있으며, 최신 명세를 기준으로 구현되어 있습니다.

브라우저 호환성 (Browser compatibility)

이 기능은 대부분의 현대 브라우저에서 널리 지원됩니다(Baseline Widely available). 자세한 호환성 표는 MDN의 브라우저 호환성 섹션을 참고하세요.

더 알아보기