JavaScript(ES6)에서 ...
기호는 두 가지로 용법으로 쓰인다. Spread Operator와 Rest Parameter이다. 각각의 문법은 레퍼런스 문서에 상세하게 설명되어 있이며 이미 경험적으로 익숙해진 사람들에게는 그렇게 어렵지 않다. 하지만 처음 타입스크립트를 접한 사람들이 두 개의 용법이 섞인 복잡한 코드를 본다면 적잖게 혼란스러울 수 있다. 대부분 따로 설명되어 있는 이 두 개의 용법을 하나로 묶어서 쉽게 설명하는 것이 이 포스트의 목적이다.
만만한 Spread부터 파헤치자
spread operator는 iterable 객체를 함수의 인자 혹은 배열 literal의 요소로 확장한다. 조금 더 쉽게 말하면 배열과 같은 복수개의 데이터를 가진 데이터형을 , , ,
으로 구분되는 여러개의 요소로 펼치는(spread) 곳에 사용한다는 것이다.
함수의 인자로 펼치기
대표적인 사용법 중 하나는 Iterable 객체를 함수의 인자로 펼쳐서 호출하는 것이다. 각 요소가 , , ,
으로 펼쳐진 모습을 머리 속에서 상상하면, 자연스럽게 함수의 인자로 들어갈 수 있다 느껴진다.
function foo(a, b, c, d) {
console.log(a, b, c, d)
}
const arr = ["a", "b", "c", "d"]
foo(...arr)
Map
과 Set
또한 iterable 객체이므로 다음과 같이 확장이 가능하다.
function foo(a, b, c, d) {
console.log(a, b, c, d)
}
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
["d", 4],
])
foo(...map)
const set = new Set(["a", "b", "c", "d"])
foo(...set)
다른 배열의 요소로 펼치기
[1, 2, 3, 4]
와 같은 형태를 array literal이라고 한다. 배열을 정의하기 위해서 흔하게 사용하는 방법이다. spread는 이 array literal에 다른 배열의 요소를 넣기 위해서 사용된다.
const arr = ["a", "b", "c", "d"]
const arr2 = [...arr] // 배열의 얕은 복사.
const arr3 = [...arr, "e"] // 요소 추가.
위의 두 예제도 머리 속에서 각 배열 요소가 , , ,
으로 펼쳐진 모습을 상상하면 이해하기 쉽다. arr2
는 arr
과 동일한 배열 요소를 가질 것이고, arr3
은 가장 뒤쪽에 요소가 하나 추가된다.
매우 유용한 용법들
다른 배열의 요소로 펼치기는 spread operator의 존재의 이유라고 말할 수 있을 정도로 많이 활용된다. 그 이유는 대부분의 경우, 사용법도 헷갈리는 배열 조작 함수들을 대체할 수있기 때문이다. push()
, splice()
, concat()
, unshift()
배열 복사하기
const arr = ["a", "b", "c", "d"]
const arr1 = arr // 이것을 복사라고 말하는 사람은 경계 대상 1호.
const arr2 = [...arr] // 배열 복사. 새로운 배열의 생성하되 그 요소들을 arr을 펼쳐서 새로운 배열의 요소로써 채우는 것.
여러개의 베열을 연결한 새로운 배열 생성
const arr = ["a", "b"]
const arr1 = ["c", "d"]
const arr2 = [...arr, ...arr1] // 두 배열의 요소를 합친 새로운 배열 생성
배열의 중간에 다른 배열 요소 추가하기
const arr = ["b", "c"]
const arr1 = ["a", ...arr, "d"] // 중간에 다른 배열의 요소를 추가하여 새로운 배열 생성
객체 Literal 펼치기
이 또한 spread operator의 유용한 용법이다. 배열에서의 사용 예처럼 객체 또한 복사, 병합 등의 다양한 조작이 가능하다.
하나의 예로 Redux의 Immutable Update Patterns에서 Object.assign()
을 대체하고 있는 것을 알 수 있다. 훨씬 보기 좋은 방식으로 객체를 복사하고 복사한 객체에 필요한 property를 수정한다. Redux에서는 기존의 state object를 조작하는 매번 새로운 state를 생성하는 immutable update
방식을 사용하기에 이 연산자가 유용하게 사용된다.
객체의 복사
const obj = { a: 1, b: 2 }
const obj1 = { ...obj } // obj객체를 펼쳐서 새로운 객체를 생성한다.
console.log(obj === obj1) // false, 같은 요소를 가졌지만 새로 생성된 객체이므로 서로 다른 객체이다.
객체의 연결
const obj = { a: 1, b: 2 }
const obj1 = { c: 3, d: 4 }
const obj2 = { ...obj, ...obj1 } // 두 객체를 펼쳐서 새로운 객체를 생성한다.
객체에 새로운 property 추가, 기존 property 수정
const obj = { a: 1, b: 2, c: 3 }
const obj1 = {
...obj,
c: 100, // 기존의 property를 한 번더 정의해줌으로써 property를 수정할 수 있다.
d: 200, // 새로운 property를 추가할 수 있다.
}
이제 Rest Parameter다
Rest parameter는 하나의 함수에서 여러 개의 인자를 받을 때, 앞 쪽에서 받은 인자를 제외한 나머지(rest)
인자들을 배열로 합쳐서 받을 수 있게 해준다.
function foo(a, ...rest) {
console.log(rest) // ["a","b","c"]
}
foo("a", "b", "c", "d")
세상에 바보 같은 질문은 없다?
위의 단순한 예제도 혼란스러울 수 있는 이유는 그 것이 spread operator라 착각할 수 있기 때문이다. 누군가 ...rest
가 다음 예제에서 rest
변수에 받은 배열을 b
, c
, d
변수로 각각 펼쳐주는 것이 아니냐고 한다면 그럴싸하게 느껴질 것 같다.
// 혹시 여기서
function foo(a, ...rest) {
console.log(a, b, c, d)
}
foo("a", "b", "c", "d")
조금 더 생각해보면 얼마나 바보 같은지 깨달을 수 있다. 선언되지 않은 b
,c
,d
는 사용할 수 없기 때문이다. 따라서 컴파일 에러(eslint/tslint)가 발생한다. 게다가 저런 용도로 사용할 이유가 없는데, b
,c
,d
를 각각 사용하려면 rest
에 나머지 인자를 몰아서 받을 필요도 없기 때문이다.
헷갈림 유발 예제
바보 같은 예제 말고 진짜 헷갈림을 유발하는 쓰임은 따로 있다. 이름과 행위가 불일치하게도 rest parameter가 꼭 함수의 인자에서만 쓰이는게 아니기 때문이다. 다음 예제를 보자. 예제에서 사용된 ...
은 spread operator와 rest parameter 중 무엇일까?
const arr = ["a", "b", "c", "d"]
const [firstElement, ...rest] = arr
console.log(firstElement) // "a"
console.log(rest) // ["b", "c", "d"]
위 예제에서는 rest parameter가 destructing과 함께 쓰였는데 정말 간결하고도 유용한 코드이다. 단 한줄로 arr
에서 "a"
property를 뺀 새로운 배열 rest
를 얻을 수 있다. 객체에도 똑같이 적용된다. 한 객체에서 특정 property를 뺀 새로운 객체를 만들 때 사용한다.
const obj = { a: 1, b: 2, c: 3 }
const { c, ...obj2 } = obj // a와 b property만 가진 새로운 객체 obj2를 생성한다. 만들어진 c변수는 버린다.
짧은 예제들이기 때문에 읽기 쉽지만 변수명과 대입연산자의 좌우항 길어질 경우에는 구분이 어려워진다. 이 것이 rest paramter라는 몇가지 단서를 통해서 알 수 있다.
rest
가 이미 선언되어 있던 변수가 아니다.rest
는 대입 연산자의 좌항에 존재한다.- IDE에서 마우스 포인터를 변수명 위에 놓아서 배열이라는 것을 확인한다.
정리
Rest parameter가 이름을 배신하고 함수 인자가 아닌 곳에 사용되면 헷갈림을 유발할 수 있다. 간단한 예제를 보면 자신감이 충만하지만, 훨씬 복잡한 코드 중간에 삽입된 다수의 ...
들을 보면 해석이 어렵다. 이런 경우를 대비하여 이해에 도움을 주는 문장 몇 개를 기억하자.
- spread operator는 펼치고, rest parameter는 모은다.
- spread operator는 주는 쪽이고, rest parameter는 받는 쪽이다.
- spread operator는 기존의 변수를 사용하고, rest parameter는 새로운 변수를 만든다.
마지막으로 위 세가지 문장을 합쳐본다.
spread operator는 기존의 변수를 펼쳐서 주는 쪽이고, rest parameter는 여러개의 인자를 받고 그것들을 합쳐서 새로운 배열/객체를 만든다.
마지막 예제 분석
const obj = { a: 1, b: 2, c: 3 }
const { c, ...obj2 } = obj // a와 b property만 가진 새로운 객체 obj2를 생성한다. 만들어진 c변수는 버린다.
끝.