About
JavaScript에는 구조 분해 할당(Destructuing Assignment) 이라는 편리한 문법이 있다. 굉장히 많이 쓰이는 문법인데 한 번 알아보자.
내용은 전부 MDN 문서에서 가져온 것이지만 실제 개발하면서 겪은 예시도 중간 중간 넣어보았다.
문법 (Syntax)
개요
=
를 기준으로 왼쪽에 할당받을 변수를, 오른쪽에 분해할 대상을 적어서 순서대로 대입할 수 있다.
Copy const [ a , b ] = [ 10 , 20 ]
console .log (a) // 10
console .log (b) // 20
a
에는 10
, b에는 20
이 담기게 된다.
Copy const x = [ 1 , 2 , 3 , 4 , 5 ]
const [ y , z ] = x
console .log (y) // 1
console .log (z) // 2
배열 및 객체
Copy const obj = { a : 1 , b : { c : 2 } }
const { a , b: { c: d } } = obj
// Two variables are bound: `a`, `d`
Copy const obj = { a : 1 , b : { c : 2 } }
const { a } = obj // `a`는 상수(constant)
let { b: { c: d } } = obj // `d`는 재할당이 가능하다
const
로 할 경우 내부 변수는 read-only이지만,
let
으로 할 경우 내부 변수는 재할당이 가능하다.
Copy const numbers = []
const obj = { a : 1 , b : 2 }
({ a : numbers[ 0 ] , b : numbers[ 1 ] } = obj)
// `a`, `b`는 `numbers`의 프로퍼티로 들어간다
안되는 경우도 있다.
Copy 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
값을 가지면 디폴트 값이 적용되지 않는다.
Copy const [ a = 1 ] = [] // a is 1
const { b = 2 } = { b : undefined } // b is 2
const { c = 2 } = { c : null } // c is null
디폴트 값은 표현식(expression)이 될 수도 있다. 필요할 때만 evaluated 된다.
Copy const { b = console .log ( 'hey' ) } = { b : 2 }
// `b`가 우측에 존재하므로 디폴트 값을 evaluate 하지 않기 때문에 console.log() 되지 않는다.
Rest Property
구조 분해 패턴을 ...rest
와 같이 rest property로 끝낼 수도 있다. 이 패턴으로 객체나 배열의 남은 프로퍼티를 새로운 객체나 배열에 할당할 수 있다.
Copy 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
루프의 루프 변수
예시 (Examples)
배열 구조 분해
기본적인 배열 할당
Copy const foo = [ 'one' , 'two' , 'three' ]
const [ red , yellow , green ] = foo
console .log (red) // "one"
console .log (yellow) // "two"
console .log (green) // "three"
소스보다 많은 원소 구조 분해
Copy 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 스크립트를 작성할 때 가 있을 것 같다.
다음과 같은 스크립트가 있다고 하자.
Copy const [ , , param1 , param2 ] = process .argv
console .log (param1)
console .log (param2)
node
명령어로 매개변수를 전달해 출력할 수 있다.
Copy $ node ./some-script.js hello litsynp
hello
litsynp
param1
, param2
는 전달되지 않으면 undefined
가 된다.
변수 스왑하기 (Swapping variables)
Copy 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에서도 같은 것을 할 수 있다.
Copy a = 1
b = 2
a , b = b , a
print (a) # 2
print (b) # 1
함수로부터 반환된 배열 파싱하기
함수로부터 반환된 배열 값을 좀더 간결하게 접근할 수 있다.
Copy function f () {
return [ 1 , 2 ]
}
const [ a , b ] = f ()
console .log (a) // 1
console .log (b) // 2
반환 값 일부 무시하기
Copy 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)할 수 있다.
Copy const [ a , b , ... { pop , push }] = [ 1 , 2 ]
console .log (a , b) // 1 2
console .log (pop , push) // [Function pop] [Function push]
Copy const [ a , b , ... [ c , d ]] = [ 1 , 2 , 3 , 4 ]
console .log (a , b , c , d) // 1, 2, 3, 4
이런 바인딩 패턴은 각 rest property가 마지막에 위치한 한 중첩이 가능하다.
Copy 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로 사용 가능하다.
Copy 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를 무시하고 나머지 부분을 분리할 수 있다.
Copy 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이든 배열 구조 분해 사용하기
Copy const [ a , b ] = new Map ([[ 1 , 2 ] , [ 3 , 4 ]])
console .log (a , b) // [1, 2] [3, 4]
배열 구조 분해는 right-hand side에서 iterable 프로토콜을 사용하기 때문에 어떤 iterable이든 구조 분해가 가능하다.
객체 구조 분해
기본 할당
Copy const user = {
id : 42 ,
isVerified : true ,
}
const { id , isVerified } = user
console .log (id) // 42
console .log (isVerified) // true
새로운 변수 이름을 할당하기
Copy const o = { p : 42 , q : true }
const { p: foo , q: bar } = o
console .log (foo) // 42
console .log (bar) // true
Note: 이것의 예시로는 Express.js 나 Koa.js 에서 request에서 query, body 등을 받을 때가 있다.
Copy async function findPosts (ctx) {
const {
query: {
post_user_id: userId ,
post_type: postType ,
}
} = ctx
// ...
}
장점이 많은 패턴 이다.
Request에서 전달되는 변수 이름이 코드에서 사용되는 이름과 case가 다를 경우가 있는데, 예를 들어 snake_case 를 camelCase 로 바꿀 수 있다.
변수 이름을 함수 내에서 이미 쓰고 있는 경우 email
로 받았다면 emailParam
으로 바꾸는 등 중복되지 않게 바꿀 수 있다.
새로운 변수 이름을 할당하고 디폴트 값 설정하기
프로퍼티에 대해서 다음 두 가지 작업을 (동시에) 할 수 있다.
객체로부터 분리(unpack)해 새로운 이름의 변수로 할당하기
분리된(unpacked) 값이 undefined
일 경우에 대비해 디폴트 값 설정하기
Copy const { a: aa = 10 , b: bb = 5 } = { a : 3 }
console .log (aa) // 3
console .log (bb) // 5
함수 매개변수로 전달된 객체로부터 프로퍼티 분리하기(Unpacking)
Copy 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"
함수 매개변수의 디폴트 값 설정하기
Copy 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 ,
})
중첩된 객체와 배열 구조 분해
Copy 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
반복과 구조 분해
Copy 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: 계산된 객체 프로퍼티 이름이라는 건 그냥 객체 리터럴에서 동적인 이름으로 참조하는 걸 뜻한다.
Copy const key = 'z'
const { [key]: foo } = { z : 'bar' }
console .log (foo) // "bar"
올바르지 않은 JavaScript 식별자명을 프로퍼티 이름으로 사용하기
Note: 이것도 어려운 말은 아니다. 대부분 프로그래밍 언어에서는 kebab-case (-
를 구분자로 사용)를 변수명으로 사용하지 못하게 되어 있다. 백엔드 개발을 하다 보면 요청 쿼리명으로 snake_case 말고 kebab-case를 사용하게 될 때도 있데, 이럴 땐 해당 쿼리명을 그대로 변수 이름으로 설정할 수 없다. 그런 경우를 올바르지 않은 JavaScript 식별자명 (Invalid JavaScript Identifier )이라고 한다.
Copy const foo = { 'fizz-buzz' : true }
const { 'fizz-buzz' : fizzBuzz } = foo
console .log (fizzBuzz) // true
배열과 객체가 합쳐진 형태의 구조 분해
배열과 객체 구조 분해를 동시에 할 수 있다.
Copy 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)이다.
Copy 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