본문 바로가기
프로그래밍/JavaScript

[Js] 구조 분해 할당(Destructuring assignment)이란?

by hi-rachel 2023. 1. 18.

 

 💡 이 글은 MDN의 문서 Destructuring assignment의 설명과 예제 코드를 공부하며 살짝 바꿔 정리한 글입니다. 좀 더 자세한 사항을 알고 싶다면 해당 문서 사이트를 방문해 주세요.

- 정확한 용어 파악을 위해 영어와 같이 명시했습니다.

 

✏️ 개념과 문법

구조 분해 할당 구문배열(arrays)이나 객체의 속성(properties from objects)을 해체(unpack)하여 그 값을 개별 변수에 담을 수 있게 하는 표현식이다.

내식대로 이해+) 사과 상자에서 제일 좋은 최상급 사과 꺼내고(A), 내가 먹을 것 꺼내고(B_특정 요소 지정), 나머지는 우르르 나머지 상자에 쏟을 수 있다(REST)

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]

위와 같이 여러 개의 array 요소가 있을 때 앞 두 개 a, b만 특정해 주고 나머지는 모두 ...rest를 통해 rest에 넣어줄 수 있다.

 

// Destructuring Assignment 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); // brackets are required
({ a: a1, b: b1 } = obj);
({ a: a1 = aDefault, b = bDefault } = obj);
({ a, b, ...rest } = obj);
({ a: a1, b: b1, ...rest } = obj);

다양한 방식으로 쓰일 수 있다. 알아 놓으면 편리한 표현이다.

 

✏️ 설명

const x = [1, 2, 3, 4, 5];
const [y, z] = x;
console.log(y); // 1
console.log(z); // 2

구조 분해 할당문의 좌변(the left-hand side of the assignment)은 원래 변수에서 어떤 값을 분해해 할당할지 정의한다.

const obj = { a: 1, b: 2 };
const { a, b } = obj;
// is equivalent to:
// const a = obj.a;
// const b = obj.b;

마찬가지로 구조 분해 할당문의 좌변에서 객체를 구조분해 할 수 있다.

 

 바인딩(Binding)과 할당(assignment)

객체, 배열 구조분해에서 약간 다른 문법으로 binding 패턴, assignment 패턴 2가지 유형의 구조 분해 패턴이 있다. 

 

- 바인딩(Binding) 패턴

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

binding 패턴에서는 패턴은 선언 키워드(var, let, const)와 함께 시작한다. 그런 다음 각각 개별 속성은 반드시 변수에 바인딩되거나 추가로 구조화되어야 한다.

 

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

모든 변수는 동일한 선언을 공유하므로 일부 변수는 재할당할 수 있지만 다른 변수는 읽기 전용이 되도록 하려면 한 번은 let으로, 한 번은 const로 두 번 분해해야 할 수 있다.

 

- 할당(assignment) 패턴

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`

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

할당 패턴에서 패턴은 키워드로 시작하지 않는다. 해체된 각 속성은 var, 일반적으로 let으로 할당 식의 왼쪽에 나타날 수 있는 모든 대상에 할당된다.

참고: ( ... ) 선언 없이 객체 리터럴 구조 분해 할당을 사용하는 경우 할당문 주변의 괄호가 필요합니다.
{ a, b } = { a: 1, b: 2 } 유효한 독립형 구문이 아닙니다. 왼쪽에 있는 {a, b}은 객체 리터럴이 아닌 블록으로 간주되기 때문입니다. 그러나 ({ a, b } = { a: 1, b: 2 })은 그대로 유효합니다 const { a, b } = { a: 1, b: 2 }.
코딩 스타일에 후행 세미콜론이 포함되지 않은 경우 ( ... )식 앞에 세미콜론이 와야 합니다. 그렇지 않으면 이전 줄에서 함수를 실행하는 데 사용될 수 있습니다.

 

기본 값(Default value)

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

각각 구조 분해된 속성은 기본 값이 될 수 있다. 기본값은 속성이 존재하지 않거나 undefined 값을 가지고 있을 때 사용된다. 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.

기본 값은 어떤 표현이든 될 수 있다. 필요할 때만 고려(evaluate)된다.

 

나머지 속성(Rest property)

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

rest property는 반드시 패턴의 마지막에 위치해야 하고 뒤에 쉼표가 없어야 한다.

 

다른 구문을 사용한 구조 분해 패턴(Destructuring patterns with other syntaxes)

언어가 변수를 바인딩하는 많은 구조에서 구조 분해 패턴도 사용할 수 있다. 아래가 포함이 된다.

  • 루프 변수 for...in 및 for...of 루프
  • 함수 매개변수
  • catch 바인딩 변수

 

✏️ 예시

📌 배열 구조분해(Array destructuring)

- 기본 변수 할당(Basic variable assignment)

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

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

 

- 원래보다 더 많은 요소로 구조 분해(Destructuring with more elements than the source)

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

할당의 오른쪽에 지정된 길이 N의 배열에서 구조 분해하는 배열에서 할당의 왼쪽에 지정된 변수의 수가 N보다 큰 경우 첫 번째 N변수에만 값이 할당된다. 나머지 변수의 값은 정의되지 않는다(undefined).

 

- 변수 교환(Swapping variables)

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]

두 개의 변수 값을 하나의 구조 분해 식으로 바꿀 수 있다.  비구조화 할당 없이 두 값을 바꾸려면 임시 변수가 필요하다.

 

- 함수에서 반환된 배열 구문 분석(Parsing an array returned from a function)

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

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

함수에서 배열을 반환하는 것은 항상 가능. Destructuring은 배열 반환 값 작업을 보다 간결하게 만들 수 있다.

이 예제에서 f()는 값 [1, 2]을 출력으로 반환하며 이는 구조 분해를 사용하여 한 줄에서 구문 분석할 수 있다.

 

- 일부 또는 전체 반환 값 무시(Ignoring some returned values)

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

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

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

// 반환된 모든 값 무시
[,,] = f();

관심이 없는 반환 값은 무시할 수 있다.

 

- 바인딩 패턴을 나머지 속성으로 사용(Using a binding pattern as the rest property)

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

배열 분해 할당의 나머지 속성 다른 배열 또는 객체 바인딩 패턴이 될 수 있다. 이렇게 하면 배열의 속성과 인덱스를 동시에 풀 수 있다.

 

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

 

 

각 나머지 속성이 목록의 마지막에 있는 한 이러한 바인딩 패턴은 중첩될 수도 있다.

 

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

반면에 객체 구조 분해는 나머지 속성으로 식별자(identifier)만 가질 수 있다.

 

- 정규식 일치에서 값 압축 풀기(Unpacking values from a regular expression match)

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"

정규식 exec() 메서드가 일치 항목을 찾으면 먼저 문자열의 전체 일치 부분을 포함하는 배열을 반환한 다음 정규식의 각 괄호 그룹과 일치하는 문자열 부분을 반환한다. Destructuring 할당을 사용하면 필요하지 않은 경우 전체 일치를 무시하고 이 배열에서 부분을 쉽게 풀 수 있다.

 

- Iterable에서 배열 구조 분해 사용(Using array destructuring on any iterable)

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

배열 분해는 오른쪽의 반복 가능한 프로토콜을 호출한다. 따라서 반드시 배열이 아닌 모든 이터러블은 구조화될 수 있다.

The iterable protocol

iterable protocol은 JavaScript 객체들이, 예를 들어 for..of 구조에서 어떠한 value들이 loop 되는 것과 같은 iteration 동작을 정의하거나 사용자 정의하는 것을 허용한다. 다른 type들(Object와 같은)이 그렇지 않은 반면에, 어떤 built-in type 들은 Array 또는 Map과 같은 default iteration 동작으로 built-in iterables이다.

iterable 하기 위해서 object는 @@iterator 메서드를 구현해야 한다. 이것은 object (또는 prototype chain의 오브젝트 중 하나) 가 Symbol.iterator key의 속성을 가져야 한다는 것을 의미한다.

Property Value
[Symbol.iterator] object를 반환하는, arguments 없는 function. iterator protocol을 따른다.

어떠한 객체가 반복(Iterate)되어야 한다면 이 객체의 @@iterator 메서드가 인수 없이 호출되고, 반환된 iterator는 반복을 통해서 획득할 값들을 얻을 때 사용된다.

 

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

이터러블이 아니면(Non-iterables) 배열로 구조분해될 수 없다.

 

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

Iterables는 모든 바인딩이 할당될 때까지만 반복된다.

 

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)

나머지 바인딩은 열심히 평가되고 이전 iterable을 사용하는 대신 새 배열을 만듭니다.

 

yield

yield 키워드는 제너레이터 함수 (function* 또는 레거시 generator 함수)를 중지하거나 재개하는 데 사용된다.

 

 


 

📌 객체 파괴(Object destructuring)

- 기본 할당(Basic assignment)

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

const { id, isVerified } = user;

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

 

- 새 변수 이름에 할당(Assigning to new variable names)

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로 지정된 로컬 변수에 할당한다.

 

- 새 변수 이름 지정 및 기본값 제공(Assigning to new variable names and providing default values)

const { a: aa = 10, b: bb = 5 } = { a: 3 };

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

속성은 아래 둘 다일 수 있다.

  • 객체에서 압축을 플고 다른 이름을 가진 변수에 할당한다.
  • 압축을 푼 값이 undefined인 경우 기본값을 할당한다.

 

- 함수 매개변수로 전달된 객체에서 속성 풀기(Unpacking properties from objects passed as a function parameter)

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

함수 매개변수로 전달된 객체는 변수로 압축을 풀 수도 있으며 함수 본문 내에서 액세스 할 수 있다. 객체 할당의 경우 구조 분해 구문을 사용하면 새 변수가 원래 속성과 동일한 이름 또는 다른 이름을 가질 수 있으며 원래 객체가 속성을 정의하지 않는 경우 기본값을 할당할 수 있다.

 

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

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

여기에서는 전달된 객체의 속성을 같은 이름의 변수로 압축 해제하는 방법을 보여준다. 매개 변수 값 { id }함수에 전달된 객체의 id속성을 동일한 이름의 변수로 압축 해제해야 함수 내에서 사용할 수 있음을 나타낸다.

 

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

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

압축을 푼 변수의 이름을 정의할 수 있다. 여기서 이름이 지정된 속성의 압축을 풀고 함수 본문 내에서 사용할 수 있도록 displayName이름을 dname로 바꾼다.

 

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

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

중첩된 개체도 압축을 풀 수도 있다. 위 예는 name변수로 압축이 풀린 fullname.firstName이라는 속성을 보여준다.

 

- 함수 매개변수의 기본값 설정(Setting a function parameter's default value)

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,
});

기본값은 = 를 사용하여 지정할 수 있으며, 전달된 개체에 지정된 속성이 없는 경우 변수 값으로 사용된다.

위에서는 기본 크기가 'big'이고 기본 좌표가 x: 0, y: 0이고 기본 반지름이 25인 함수를 보여준다.

위 함수 시그니처 drawChart에서 분해된 왼쪽의 기본값은 빈 객체 = {}.

해당 기본값 없이 함수를 작성할 수도 있다. 그러나 해당 기본값을 생략하면 함수는 호출 시 제공할 인수를 하나 이상 찾는 반면, 현재 형식에서는 매개 변수를 제공하지 않고 drawChart() 호출할 수 있다. 그렇지 않으면 최소한 빈 개체 리터럴을 제공해야 한다.

자세한 내용은 기본 매개변수 > 기본 값이 할당된 비구조화된 매개변수를 참조.

 

- 중첩 객체 및 배열 구조 분해(Nested object and array destructuring)

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 iteration and destructuring)

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"

 

- 계산된 개체 속성 이름 및 구조 분해(Computed object property names and destructuring)

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

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

 

객체 리터럴과 같은 계산된 속성 이름은 구조 분해와 함께 사용할 수 있다.

 

- 속성 이름으로 유효하지 않은 JavaScript 식별자(Invalid JavaScript identifier as a property name)

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

console.log(fizzBuzz); // true

Destructuring은 유효한 대체 식별자를 제공하여 유효한 JavaScript 식별자가 아닌 속성 이름과 함께 사용할 수 있다.

 

- 기본 값 분해(Destructuring primitive values)

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

객체 구조 분해는 속성 액세스(property accessing)와 거의 동일하다. 즉, 기본 값(primitive value)을 파괴하려고 하면 해당 값이 해당 래퍼(wrapper) 객체로 래핑 되고(감싸지고) 래퍼 객체에서 속성에 액세스 하게 된다.

 

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

// 패턴이 비었을 때도 TypeError가 발생한다.
const {} = null; // ❌ TypeError: Cannot destructure 'null' as it is null.

속성 액세스하는 것과 동일하게 null 또는 undefined를 구조분해하면 TypeError가 발생한다.

 

- 결합된 배열 및 객체 구조 분해(Combined Array and Object Destructuring)

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

const [,, { name }] = props;

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

배열 및 객체 구조 분해를 결합(combined)할 수 있다. props 배열의 아래 세 번째 요소와 그 객체에 name 속성을 원한다고 하면 위와 같이 할 수 있다.

 

- 객체가 분해될 때 프로토타입 체인을 조회합니다.(The prototype chain is looked up when the object is deconstructed)

const obj = {
  self: '123',
  __proto__: {
    prot: '456',
  },
};
const { self, prot } = obj;
// self "123"
// prot "456" (Access to the prototype chain)

객체를 분해할 때 속성 자체에 액세스 하지 않으면 프로토타입 체인을 따라 계속 조회한다.

 


 

 

mdn 문서를 정리하며 공부하는데 세 시간이 걸렸다. mdn 한글 문서를 보면 번역을 생략하는 부분이 있는지 생략된 부분이 있어 영어 문서를 확인한다. 잘못된 부분이 있다면 피드백 남겨주시면 감사합니다. 🙂 이렇게 정리해 놓으면 추가적인 공부를 하러 계속 찾는 포스팅이 된다.