Harry Park's Blog

JavaScript(ES6)의 Spread 와 Rest 쉽게 설명하기

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)

MapSet 또한 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"] // 요소 추가.

위의 두 예제도 머리 속에서 각 배열 요소가 , , , 으로 펼쳐진 모습을 상상하면 이해하기 쉽다. arr2arr과 동일한 배열 요소를 가질 것이고, 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변수는 버린다.

끝.