Rest Parameter 와 Spread Syntax

ES6 신기술을 통한 가독성 향상


들어가며

ES6의 새로운 기능들은 그동안 혼동을 주거나 가독성을 해치던 많은 요소들을 해결해주었습니다. this 바인딩을 직관적이게 해주는 화살표 함수, 메서드에 대한 명확한 정의, Rest 파라미터Spread 문법 등이 바로 그것입니다.

특히 배열은 방대한 웹 콘텐츠를 다루는 자바스크립트의 특성상 매우 중요한 자료구조입니다. 데이터베이스에서 가져온 수많은 데이터들을 브라우저를 통해 정확하게 전달해야 하고 그를 위해서는 배열을 잘 다루는 것이 필수불가결하기 때문입니다.

따라서 이 글을 통해 ES6에서 배열을 다루는 새로운 기능인 Rest ParameterSpread Syntax 에 대해 그 등장배경과 함께 사용방법에 대해 알아보고, 더 나아가 차이점을 살펴봄으로써 둘의 기능을 좀 더 명확하게 알아가는 시간을 가져보고자 합니다.

Rest Parameter

등장배경

Rest Parameter의 등장배경에 대해 MDN 에서는 다음과 같이 설명합니다.

나머지 매개변수(Rest Parameter)는 다수의 인수를 배열로 변환하는 과정의 보일러플레이트 코드를 줄이기 위해 도입됐습니다.

(출처) https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/rest_parameters

아직은 이 말이 와닿지 않을 수도 있습니다. 천천히 예시를 들어보면서 Rest Paramter의 필요성을 느껴보도록 하겠습니다.

주어지는 인자값들을 모두 더하는 함수 sum을 직접 구현해보겠습니다.

function sum() {
  let total = 0;
  for (let i=0;i<arguments.length;i++) {
    total+=arguments[i];
  }
  return total;
}
sum(1, 2, 3)  // 6

함수 인자값에 명시된 인자 값 이외의 값들을 반환해주는 arguments 객체를 사용하여 총합을 성공적으로 반환했습니다.

arguments 객체에 대해 더 알고 싶으신 분은 현기선임님 글 에서 확인하시기 바랍니다.

위와 같은 함수로도 문제 없이 총합을 구할 수 있지만, 더 나은 가독성을 위해서 array.prototype.reduce를 활용하여 코드를 작성해보겠습니다.

function sum() {
  return arguments.reduce((preValue, curValue) => preValue + curValue, 0);
}
sum(1, 2, 3)  // Uncaught TypeError: arguments.reduce is not a function

함수내의 코드를 압축하여 한 줄로 만드는 데에는 성공했지만 애석하게도 TypeError를 마주하게 됐습니다.

그 이유는 arguments 객체는 배열이 아닌 유사 배열 객체(array-like-object)이기 때문입니다.

함수 내에서 다음과 같이 확인할 수 있습니다.

console.log(Object.getPrototypeOf(arguments) === Array);  // false

Array Prototype이 없으니 Array.helper.method 인 Array.prototype.reduce 또한 사용하지 못하는 것입니다.

이러한 문제는 다음과 같이 해결할 수 있습니다.

function sum() {
  return Array.from(arguments).reduce((preValue, curValue) => preValue + curValue, 0);
  // 또는
  return Array.prototype.slice.call(arguments).reduce((preValue, curValue) => preValue + curValue, 0);
}
sum(1, 2, 3);  // 6

Array.from() 메소드는 ES6에 도입됐기 때문에 이전에는 Array.prototype.slice.call()을 통해 일일히 배열을 새로 반환했어야 했습니다.

위에서 살펴본 바와 같이 arguments 객체는 다음과 같은 불편함이 있습니다.

  1. 유사 배열 객체 사용으로 인해 Array Helper Method 를 사용할 수 없다.
  2. 따라서 여러번의 메소드 호출해야하기에 가독성이 안좋다.
  3. 심지어 이는 자주 사용돼야 하기 때문에 반복된다.

이제 다시 한번 MDN 에서 설명하는 등장배경에 대해 보면 처음보다 그 필요성이 잘 느껴집니다.

나머지 매개변수(Rest Parameter)는 다수의 인수를 배열로 변환하는 과정의 보일러플레이트 코드를 줄이기 위해 도입됐습니다.

(출처) https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/rest_parameters

여기서 말하는 보일러플레이트 코드가 바로 위 코드에서 볼 수 있는 Array.prototype.slice.call() 이라 할 수 있습니다. 결론적으로Rest Paramter는 30글자 가까이 되는 코드를 단 몇 글자로 줄이기 위해 등장한 것입니다.

사용방법

이를 해결하기 위해 바로 Rest Parameter를 사용할 수 있습니다.

Rest Parameter 는 MDN에서 다음과 같이 설명합니다.

Rest Parameter 구문을 사용하면 함수가 정해지지 않은 수의 매개변수를 배열로 받을 수 있습니다. JavaScript에서 가변항 함수를 표현할 때 사용합니다.

(출처) https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/rest_parameters

위에서 볼 수 있듯이 Rest Parameter는 가변항 함수, 즉 인자의 개수가 유동적인 함수에서 유용하게 사용될 수 있습니다.

예시를 들어보자면 다음과 같습니다.

function sum(...rest) {
  return rest.reduce((preValue, curValue) => preValue + curValue, 0);
}
sum(1, 2, 3);  // 6

함수 매개변수 앞에 ...을 붙여서 정의한 것이 Rest Parameter입니다. 이로써 함수에 전달된 인수 목록을 배열로 전달받게 됩니다.

또한 일반 매개 변수와 함께 사용 할 수도 있습니다.

function rest(a, b, ...rest) {
  console.log('a: ' + a);
  console.log('b: ' + b);
  console.log('rest: ' + rest);
}
rest('a', 'b', 'c', 'd')
/*
a: a
b: b
rest: c,d
*/

하지만 여기에 몇가지 주의할 사항이 있습니다.

우선 Rest Parameter는 매개변수 마지막에 위치해야 합니다. 그렇지 않으면 에러가 발생합니다.

function rest(...rest, a, b) {
  console.log('hi');
}
rest('a', 'b', 'c');  // Uncaught SyntaxError: Rest parameter must be last formal parameter

또한 Rest Parameter는 단 하나만 선언할 수 있습니다. 이 또한 자바스크립트 엔진은 매개변수를 마지막에 선언하지 않은 것으로 인식하여 에러를 반환합니다.

function rest(...rest1, ...rest2) {
  console.log('hi');
}
rest('a', 'b', 'c');  // Uncaught SyntaxError: Rest parameter must be last formal parameter

Spread Syntax

이번에는 Spread Syntax에 대해 살펴보겠습니다.

우선 Spread SyntaxRest Parameter와 동일하게 ...를 Array, String, Dom Collection, arguments 등과 같이 순회 가능한 이터러블 값 앞에 붙여 사용합니다.

console.log(...['a', 'b', 'c']);  // a b c
console.log(...'pxdXe');  // p x d X e

Spread Syntax는 크게 세 가지 상황에서 사용합니다.

  1. 함수 호출문의 인수 목록
  2. 배열 리터럴의 요소 목록
  3. 객체 리터럴의 프로퍼티 목록

여기서 계속 반복되는 목록에 주목해야 할 점이 있습니다. 그것은 바로 Spread Syntax의 결과는 값이 아니라는 점입니다. ...는 연산자가 아니기 때문입니다. 따라서 Spread Syntax의 결과값인 목록은 변수에 할당할 수 없습니다.

그러면 세 가지 상황에 대해 Spread Syntax를 사용하지 않았을 때와 사용했을 때를 비교해보며 그 필요성과 사용방법에 대해 알아보도록 하겠습니다.

함수 호출문

function foo(a, b, c) {}
const args = [1, 2, 3];

위와 같이 function foo(a, b, c) {} 가 있고 이곳에 const args = [1, 2, 3]; 의 요소를 순차적으로 하나씩 넣고 싶은 상황에 있습니다. 어떻게 하면 될까요?

우선 foo(args[0], args[1], args[2]); 와 같이 요소의 인덱스를 이용해 하나씩 넣는 방법이 있습니다. args를 똑같이 세번이나 사용하는 굉장히 비효율적인 방법으로 보입니다. 따라서 args를 한번만 쓰고 함수를 호출하고 싶습니다.

다음과 같은 방법이 있습니다.

foo.apply(null, args);

Function.prototype.apply() 바인딩을 통해 args를 한번만 써서 성공적으로 배열을 인자로 전달했습니다.

하지만 apply 메소드도 쓰이고, null 이라는 불필요해보이는 값이 보입니다. 이것을 없애고 깔끔하게 인자를 전달하는 방법이 바로 Spread Syntax입니다.

foo.apply(...args);

위와 같이 ...args를 통해 배열을 목록으로 만들어 함수에 인자값으로 깔끔하게 전달할 수 있습니다!

배열 리터럴

const a = [1, 2];
const b = [3, 4];
const c = [5, 6];

위와 같이 배열을 가진 세 변수가 있습니다. 이 배열들을 합쳐 새로운 배열을 만들고 싶은 상황입니다. 어떻게 해야 할까요?

splice메서드를 활용하는 등 다양한 방법이 있겠지만 우선 concat 메서드를 활용해보겠습니다. 우선 a, b 두개만 합쳐보겠습니다.

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

비교적 간단해 보입니다. 하지만 여기에 c 까지 한번에 붙이고자 한다면 어떻게 될까요?

const arr = a.concat(b).concat(c);
console.log(arr);  // [1, 2, 3, 4, 5, 6]

concat 메서드 뒤에 곧바로 concat 메서드가 한번 더 붙어버리고 맙니다. 만약 합칠 배열이 n개 라면 concat메서드는 n-1개가 붙어버리게 되는 것입니다.

이를 Spread Syntax를 통해 간단하고 깔끔하게 표현할 수 있습니다.

const arr = [...a, ...b, ...c];
console.log(arr);  // [1, 2, 3, 4, 5, 6]

각각의 배열을 목록화 하여 배열 리터럴을 통해 하나의 새로운 배열로 만들었습니다. 훨씬 더 간결하고 직관적입니다.

ES6 이전에는 배열을 복사하기 위해서 slice를 사용하였습니다.

const arr1 = [1, 2];
const arr2 = arr1.slice();

Spread Syntax를 활용하여 메서드 사용 없이 배열 복사가 가능합니다.

const arr1 = [1, 2];
const arr2 = [...arr1]  // [1, 2]

이때 복사는 얕은 복사(shallow copy)로, 1차원 배열에 효과적입니다. 2차원 배열을 복사하게되면 깊은 복사가 아니기 때문에 해당 요소 배열의 메모리를 참조하기 때문에 원본 배열이 변화하면 복사 배열 또한 바뀌게 됩니다.

const arr1 = [[1], [2]];
const arr2 = [...arr1];
arr1[0][0] = 3;
consol.log(arr2);  // [[3], [2]] -> a 내부 요소 배열 변화 시 b 또한 영향을 받는다!

따라서 배열에 대한 깊은 복사가 필요할 때는 JSON.parse(JSON.stringify(arr)) 이나 Lodash 라이브러리 사용이 권장됩니다.

깊은 복사에 대한 자세한 내용은 MDN 공식문서를 참고하시기 바랍니다. (링크 : https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy )

Spread Syntax를 사용하면 유사 배열 객체인 이터러블을 배열로 손쉽게 변환 시킬 수 있습니다.

여기서 우리는 Rest Parameter에서 만난 예제를 다시 한번 살펴볼 수 있습니다.

function sum() {
  return arguments.reduce((preValue, curValue) => preValue + curValue, 0);
}
sum(1, 2, 3)  // Uncaught TypeError: arguments.reduce is not a function

위 코드에서 우리는 arguments객체가 유사 배열 요소이기 때문에 배열 프로토타입 메서드를 사용하지 못하는 에러를 만났었습니다. 이에 Rest Paramter를 사용하여 코드를 깔끔하고 가독성있게 만들 수 있었습니다. Spread Syntax는 우리에게 다른 방법을 제시합니다.

function sum() {
  return [...arguments].reduce((preValue, curValue) => preValue + curValue, 0);
}
sum(1, 2, 3)  // 6

위와같이 ...arguments를 통해 유사 배열 객체를 배열로 변환하여 Array Helper Method를 이용할 수 있습니다! 단, 이터러블이 아닌 유사 배열 객체에는 사용할 수 없으니 주의하시길 바랍니다. 이터러블이 아닌 유사 배열 객체를 배열로 변환하기 위해서는 Array.from()메서드를 사용하시면 됩니다.

객체 리터럴

객체 리터럴에서의 Spread Syntax 사용법은 배열 리터럴에서 사용할 때와 굉장히 유사합니다. 우선 예시를 보겠습니다.

const obj1 = {a: 1, b: 2};
const obj2 = {c: 3, d: 4};
const cloneObj1 = {...obj1};  // {a: 1, b: 2}
const newObj = {...obj1, ...ojb2};  // {a: 1, b: 2, c: 3, d: 4}

보시는 바와 같이 배열 리터럴에서 사용했을 때와 같은 컨셉입니다. 해당 요소를 목록화한 뒤 넣는 것입니다.

한가지 유념해둬야 할 점은 키 값이 중복 될 때는 후에 오는 값이 우선권을 갖는 다는 점입니다.

const obj1 = {a: 1, b: 2};
const obj2 = {b: 5, c: 3};
const newOjb = {...ojb1, ...obj2};  // {a: 1, b: 5, c: 3}

차이점

Rest ParameterSpread Syntax...를 앞에 붙임으로써 사용하는 점으로 인해 모양이 같이 자칫하면 헷갈릴 수 있습니다. 하지만 이 둘은 어떻게 보면 정반대의 개념입니다. Spread Syntax는 배열을 해당 요소로 확장하는 반면, Rest Paramter는 하나의 요소로 압축하는 개념이기 때문입니다.

이 둘을 구분하는 방법은 어렵지 않습니다. Rest Paramter는 함수 정의시 인자에만 위치할 수 있기 때문입니다. Rest Parameter는 미래에 정해질 값에 대한 것이고, Spread Syntax는 이미 정해진 값을 새로운 목록으로 만드는 것입니다.

// 미래의 인자 값에 대한 정의 - 미래에 들어 오는 값들을 여기 '압축'하겠다!
function foo(...여기 있으면 Rest Paramter!!) {}

// 이미 있는 값에 대한 목록화 - 이미 있는 값을 새롭게 '확장'하겠다!
foo(...여기 있으면 Spread Syntax!!)
const arr = [...여기 있으면 Spread Syntax!!]
const obj = {...여기 있으면 Spread Syntax!!}

마치며

지금까지 Rest ParamterSpread Syntax에 대해 그 필요성을 중심으로 살펴보았습니다. 필요성은 한마디로 보일러 플레이트, 즉 ‘중복’을 줄임으로써 코드를 간결하게 만들어 가독성을 향상시키는 것입니다. 가독성이 좋아짐으로써 코딩 시 잘못된 코드로 인한 오류를 최소화 할 수 있고 후일의 유지보수 또한 용이해집니다.

긴 글 읽어주셔서 감사드리며, Rest Parameter, Spread Syntax와 함께 행복한 코딩 생활 되시기를 바라겠습니다 :)


추천 글