Destructuing Assignment

About

JavaScript에는 구조 분해 할당(Destructuing Assignment)이라는 편리한 문법이 있다. 굉장히 많이 쓰이는 문법인데 한 번 알아보자.

내용은 전부 MDN 문서에서 가져온 것이지만 실제 개발하면서 겪은 예시도 중간 중간 넣어보았다.

문법 (Syntax)

개요

=를 기준으로 왼쪽에 할당받을 변수를, 오른쪽에 분해할 대상을 적어서 순서대로 대입할 수 있다.

const [a, b] = [10, 20]

console.log(a) // 10
console.log(b) // 20

a에는 10, b에는 20이 담기게 된다.

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

배열 및 객체

const obj = { a: 1, b: { c: 2 } }
const { a, b: { c: d} } = obj
// Two variables are bound: `a`, `d`
const obj = { a: 1, b: { c: 2 } }
const { a } = obj // `a`는 상수(constant)
let { b: { c: d } } = obj // `d`는 재할당이 가능하다
  • const로 할 경우 내부 변수는 read-only이지만,

  • let으로 할 경우 내부 변수는 재할당이 가능하다.

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

({ a: numbers[0], b: numbers[1] } = obj)
// `a`, `b`는 `numbers`의 프로퍼티로 들어간다

안되는 경우도 있다.

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

// 위 코드는 다음과 같다:
//     const numbers[0] = obj.a
//     const numbers[1] = obj.b
// 따라서 틀리다.

디폴트 값

각 구조 분해된 프로퍼티는 디폴트 값을 가질 수 있다.

  • 프로퍼티가 존재하지 않거나 undefined이면 디폴트 값이 사용된다.

  • 프로퍼티가 null 값을 가지면 디폴트 값이 적용되지 않는다.

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

디폴트 값은 표현식(expression)이 될 수도 있다. 필요할 때만 evaluated 된다.

const { b = console.log('hey') } = { b: 2 }
// `b`가 우측에 존재하므로 디폴트 값을 evaluate 하지 않기 때문에 console.log() 되지 않는다.

Rest Property

구조 분해 패턴을 ...rest와 같이 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 property는 마지막에 있어야 하며, trailing comma (,)를 가지면 안된다.

다른 문법에서의 구조 분해 패턴

변수를 바인딩하는 다른 많은 문법에서 구조 분해 패턴을 사용할 수 있다. 예를 들자면:

  • for...in, for...of 루프의 루프 변수

  • 함수의 매개변수

  • catch 바인딩 변수

예시 (Examples)

배열 구조 분해

기본적인 배열 할당

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

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

소스보다 많은 원소 구조 분해

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 

Note: 위의 실제 예시는 Node.js로 CLI 스크립트를 작성할 때가 있을 것 같다.

다음과 같은 스크립트가 있다고 하자.

some-script.js
const [, , param1, param2] = process.argv
console.log(param1)
console.log(param2)

node 명령어로 매개변수를 전달해 출력할 수 있다.

$ node ./some-script.js hello litsynp
hello
litsynp

param1, param2는 전달되지 않으면 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]

Note: Python을 써봤다면 알겠지만 Python에서도 같은 것을 할 수 있다.

a = 1
b = 2
a, b = b, a
print(a) # 2
print(b) # 1

함수로부터 반환된 배열 파싱하기

함수로부터 반환된 배열 값을 좀더 간결하게 접근할 수 있다.

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

바인딩 패턴을 Rest Property로 사용하기

배열의 구조 분해 할당에서의 rest property는 또 다른 배열이나 객체의 바인딩 패턴이 될 수 있다. 이 방법으로 동시에 프로퍼티나 배열의 인덱스를 분리(unpack)할 수 있다.

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

이런 바인딩 패턴은 각 rest property가 마지막에 위치한 한 중첩이 가능하다.

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

Warning: 하지만 객체의 구조 분해는 identifier만이 rest property로 사용 가능하다.

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

정규 표현식으로 분리(unpacking)하기

정규표현식의 exec() 메소드가 매치를 찾으면 첫 번째 요소로 문자열에서 매치된 부분 전체 (full match)를, 그리고 나머지 요소로 ()로 그룹화된 것에 매치되는 문자열의 부분들을 갖는 배열을 반환한다.

구조 분해 할당으로 full 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"

어떤 iterable이든 배열 구조 분해 사용하기

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

배열 구조 분해는 right-hand side에서 iterable 프로토콜을 사용하기 때문에 어떤 iterable이든 구조 분해가 가능하다.

객체 구조 분해

기본 할당

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
  • pfoo로, qbar로 받는다.

Note: 이것의 예시로는 Express.jsKoa.js에서 request에서 query, body 등을 받을 때가 있다.

async function findPosts(ctx) {
  const {
    query: {
      post_user_id: userId,
      post_type: postType,
    }
  } = ctx
  
  // ...
}

장점이 많은 패턴이다.

  • Request에서 전달되는 변수 이름이 코드에서 사용되는 이름과 case가 다를 경우가 있는데, 예를 들어 snake_casecamelCase 바꿀 수 있다.

  • 변수 이름을 함수 내에서 이미 쓰고 있는 경우 email로 받았다면 emailParam으로 바꾸는 등 중복되지 않게 바꿀 수 있다.

새로운 변수 이름을 할당하고 디폴트 값 설정하기

프로퍼티에 대해서 다음 두 가지 작업을 (동시에) 할 수 있다.

  • 객체로부터 분리(unpack)해 새로운 이름의 변수로 할당하기

  • 분리된(unpacked) 값이 undefined일 경우에 대비해 디폴트 값 설정하기

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

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

함수 매개변수로 전달된 객체로부터 프로퍼티 분리하기(Unpacking)

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

// `user`에서 `id`만 분리할 수 있다.
function userId({ id }) {
  return id
}

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

// 중첩된 프로퍼티도 가능하다.
function whois({ displayName, fullName: { firstName: name } }) {
  return `${displayName} is ${name}`
}

console.log(whois(user))  // "jdoe is John"

함수 매개변수의 디폴트 값 설정하기

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

중첩된 객체와 배열 구조 분해

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"

계산된(Computed) 객체 프로퍼티 이름과 구조 분해

Note: 계산된 객체 프로퍼티 이름이라는 건 그냥 객체 리터럴에서 동적인 이름으로 참조하는 걸 뜻한다.

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

console.log(foo) // "bar"

올바르지 않은 JavaScript 식별자명을 프로퍼티 이름으로 사용하기

Note: 이것도 어려운 말은 아니다. 대부분 프로그래밍 언어에서는 kebab-case(-를 구분자로 사용)를 변수명으로 사용하지 못하게 되어 있다. 백엔드 개발을 하다 보면 요청 쿼리명으로 snake_case 말고 kebab-case를 사용하게 될 때도 있데, 이럴 땐 해당 쿼리명을 그대로 변수 이름으로 설정할 수 없다. 그런 경우를 올바르지 않은 JavaScript 식별자명 (Invalid JavaScript Identifier)이라고 한다.

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

console.log(fizzBuzz) // true

배열과 객체가 합쳐진 형태의 구조 분해

배열과 객체 구조 분해를 동시에 할 수 있다.

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

const [,, { name }] = props

console.log(name) // "FizzBuzz"

실제로 배열 안에 그냥 값 말고도 객체를 많이 담으니까 사용할 일이 많은 예시인 것 같다.

객체가 구조 분해될 때 프로토타입 체인(Prototype chain)를 따라가 찾아간다

객체를 구조 분해할 때, 프로퍼티가 해당 프로퍼티 안에 없다면(not accessed in itself) 프로토타입 체인을 따라서 계속해서 찾아갈 것(look up)이다.

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

이건 좀 어려운 말인데, 프로토타입에 대한 이해가 필요하다.

JavaScript는 특정 객체의 프로퍼티나 메소드에 접근시 객체 자신의 것 뿐 아니라 __proto__ 가 가리키는 링크를 따라서 부모 역할을 하는 프로토타입 객체의 프로퍼티나 메소드에 접근할 수 있다.

특정 객체의 프로퍼티나 메소드 접근시 만약 현재 객체의 해당 프로퍼티가 존재하지 않는다면 __proto__가 가리키는 링크를 따라 부모 역할을 하는 프로토타입 객체의 프로퍼티나 메소드를 차례로 검색해 가는 것이 프로토타입 체인이다. (종착지는 Object.prototype이다.)

한마디로 말해서 구조 분해 간에 객체 자신이 해당 프로퍼티나 메소드를 갖고 있지 않다면 부모 역할을 하는 프로토타입(Prototype)의 프로퍼티와 메소드까지 계속해서 검색해나간다는 것이다.

결론 (Conclusion)

정리하고 보니 쓰임새가 새삼 참 많다. (정리하다가 몇 번 졸았다. 🥱)

근데 보면 알겠지만 Examples 섹션에 있는 내용이 반 이상이다. 예시가 다양해서 많아 보이는 것이지, 사용법과 원리만 알면 자유롭게 사용할 수 있다.

제일 중요한 건 경험을 통해 구조 분해를 사용하면 적합한지를 체득하고 적재적소에 사용하는 것이다.

REF

Last updated