Published on

es6) 매개변수, rest parameter, spread 문법

Authors
  • avatar
    Name
    Hoehyeon Jung
    Twitter

1. Argument

ES6로 들어서면서 자바스크립트에서는 매개변수에 대해서 좀 더 우아하게 처리하는 것이 가능하다. 다음의 예를 살펴보면서 이를 확인해보자.

1-1. 매개변수 기본 값

const sum = (a, b) => a + b;

console.log(sum(1)); // 1 + undefined = NaN
console.log(sum(4,5)); // 9

기존의 자바스크립트에서는 함수에서 인수를 전달받지 않은 경우에는 이러한 변수들을 undefined로 설정하여 처리하였다. 따라서 이로 인해서 위의 예제처럼 함수에서 원하지 않는 값이 출력될 수 있었다.

const sum = (a = 0, b = 0) => a + b;

console.log(sum());
console.log(sum(1));
console.log(sum(3,4));

하지만 ES6에서는 인수로 넘어가지 않은 값들에 대해서 기본값을 넣어줄 수 있게 되었다. 따라서 이러한 인수로 인한 문제를 매끄럽게 처리 할 수 있게 되었다.

function sum(a, b = 0) {
  console.log(arguments);
}

console.log(sum.length); // 1

console.log(sum(1)); // Arguments[1]
console.log(sum(2, 3)); // Arguments[2, 3]

이러한 매개변수 기본값 설정은 Arguments 유사 배열에 영향을 주지 않는다. 따라서 값이 매개변수 기본값은 예제와 같이 Arguments에 들어가지 않고 함수 인자 길이에도 영향을 주지 않는다.

2. Rest parameter

기존의 ES6 이전에는 함수의 매게변수를 다룰 때 arguments 객체나 함수의 .length 메서드를 통해 다뤘지만, ES6 이후로는 rest parameter를 통해서 좀 더 우아하게 처리할 수 있다.

const sum = (param1, param2, ...args) => {
  console.log(Array.isArray(args)); // true
  console.log(args); // [3, 4, 5] 
  return param1 + param2 + args.reduce((acc, cur) => acc + cur);
}

sum(1, 2, 3, 4, 5); // 15

rest parameter는 매개변수 선언 시 나머지 매개변수들에 대해 지정해줄 수 있다. 이러한 rest parameter는 기존의 arguments 유사배열과 다르게 실제 배열로 선언되어 배열로의 변환 같은 중간과정 없이 배열의 메서드 등을 자유롭게 쓸 수 있다.

function foo(...rest, param1, param2) {
  // business logic
}
foo (1, 2, 3, 4); // Syntax Error

const bar = (...args) => {
  console.log(args);
};
console.log(bar.hasOwnProperty('arguments')); // false

bar (1, 2, 3);

rest parameter는 인자의 가장 뒤에서 선언되어야 한다. 화살표 함수의 경우, arguments 객체가 존재하지 않기 때문에 매개변수들을 이용하기 위해서는 rest parameter를 이용해야 한다.

3. Spread 문법

Spread 문법은 기본적으로 선언된 자료형들을 spread하게 펼쳐주는 기능을 수행한다. 이러한 spread 문법을 사용하기 위해서는 해당 자료형이 이터러블이어야 한다.

// 배열 문자열은 이터러블
console.log(...[1, 2, 3]); // 1, 2, 3
console.log(...'hello'); // h e l l o

// Map, Set 역시 이터러블
console.log(...new Map([['a', 1], ['b', 2]])); // ['a','1'] ['b', '2']
console.log(...new Set([1, 2, 3])); // 1 2 3

// 일반 객채는 이터러블이 아님
console.log(...{a: 1, b: 2}); // TypeError: Found non-callable @@iterator

배열이나 문자열, 그리고 ES6에서 추가된 Map, Set과 같은 자료형들은 기본적으로 선언 시 이터러블이 내장되게 된다. 따라서 spread 문법을 사용할 수 있으나 일반 객체는 이터러블하지 않기 때문에 spread 문법 사용 시 에러가 발생한다.

3.1 함수의 인수로 사용

const foo = (x, y, z) => {
  console.log(x, y, z);
}

const arr = [1, 2, 3];

// ES5
foo.apply(null, arr); // foo.call(null, 1, 2, 3);

// ES6
foo(...arr);

ES5까지만 해도 배열 요소를 나누기 위해서는 Function.prototype.apply의 기능을 사용해야 했다. 하지만 ES6 이후로는 해당 자료형이 이터러블이라면 spread를 통해 좀 더 읽기 쉽고 간결하게 표현이 가능하다.

// rest parameter
const foo = (param, ...rest) => console.log(rest);

// spread 문법
foo(0, ...[1, 2, 3], 4); // [1, 2, 3, 4]

표현법이 유사하기 때문에 rest paramter와 spread 문법은 헷갈릴 수 있다. rest parameter의 경우 함수의 인자 중 ...을 통해 나머지 인자를 받아 이를 배열로 묶는 것이다. spread 문법의 경우 이터러블한 자료형을 분해하는 것으로 rest parameter와 다르게 선언의 위치가 자유롭다. 따라서 위의 예제에선 배열 [1, 2, 3]이 분해되어 전달되고 이를 rest paramter는 배열로 받아들여 최종적으로 [1, 2, 3, 4]가 출력된다.

3.2 배열에서 사용

3.1 push

기존의 두 배열을 합쳐서 하나의 배열에 합친 값을 넣고자 할 경우 push 메소드를 사용한다.

const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
const arr3 = [4, 5, 6];

// ES5
Array.prototype.push.apply(arr1, arr3);
console.log(arr1); // [1, 2, 3, 4, 5, 6];

// ES6
arr2.push(...arr3);
console.log(arr2); // [1, 2, 3, 4, 5, 6];

ES5에서는 push를 통해 개별 인수를 더하기 위해 Function.prototype.apply 메소드를 사용했다. 하지만 ES6에서는 spread 문법을 통해 좀 더 간결하게 처리가 가능하다.

3.2 concat

배열을 합치는 데에는 push와 같이 기존의 변수 값을 수정하는 방식도 있지만, 이로 인한 상태 추정이 어려워질 수 있는 문제가 발생할 수 있다. 따라서 기존 배열에 새로운 값을 더해서 합치는 메소드인 concat을 좀 더 선호하여 사용한다. 이러한 concat도 spread 문법을 통해 간결하게 새 배열을 선언할 수 있다.

const arr = [1, 2, 3];

// ES5
console.log(arr.concat([4, 5, 6])); // [1, 2, 3, 4, 5, 6]

// ES6
console.log([...arr, 4, 5, 6]); // [1, 2, 3, 4, 5, 6]

3.3 splice

배열의 일부를 수정하는 메서드로 splice가 있다. 이 역시 다른 배열 입력을 위해 다음과 같은 방식을 이용한다.

const arr1 = [1, 2, 3, 6];
const arr2 = [4, 5];

// ES5
// arr1.splice(3, 0, 4, 5)
Array.prototype.splice.apply(arr1, [3, 0].concat(arr2)); // [1, 2, 3, 4, 5, 6]

// ES6
arr1.splice(3, 0, ...arr2);

ES5에서는 위와 같이 새로운 배열을 분해해서 인수로 넣기 위해 Function.prototype.apply를 사용해야 해서 코드도 장황해지고 이해하기 힘들 수 있다. 하지만 ES6에서는 splice를 통해 좀 더 간결하면서 읽기 쉽게 처리가 가능하다.

3.4 copy

배열의 경우 유용한 메서드가 많아서 처리가 잦다보니 이러한 데이터를 복사해서 사용하는 경우가 잦다. 이를 위해서는 다음과 같은 메서드를 사용한다.

const todos = [
  {id: 1, content: 'HTML', completed: true},
  {id: 2, content: 'CSS', completed: false},
  {id: 3, content: 'Javascript', completed: true},
];

// ES5
const todos1 = todos.slice(todos);
todos1[1].completed = true;

// ES6
const todos2 = [...todos]; // [..., {id:2, ..., completed: true}, ...]

console.log(todos); // [..., {id:2, ..., completed: true}, ...]

기본적으로 ES5의 slice(), ES6의 spread 문법은 복사할 배열을 shallow copy한다. 따라서 내부의 객체와 같은 참조 값들은 변경이 이루어지면 원본에도 변경이 발생하게 된다. 이를 방지하기 위해서는 array의 경우 배열의 내부값이 객체와 같이 참조 값이 아닌 경우에는 slice(), spread 등을 이용하여 복사한다. 하지만 객체 등 참조값이 포함 될 경우 deep copy를 통해 배열을 복사한다.

const todos = [
  {id: 1, content: 'HTML', completed: true},
  {id: 2, content: 'CSS', completed: false},
  {id: 3, content: 'Javascript', completed: true},
];

// ES5
const todos1 = JSON.parse(JSON.stringify(todos));
todos1[1].completed = true; // only modified at todos1

// ES6
const clone = (items) => items.map(item => {
  return Array.isArray(item)
  ? clone(item)
  : typeof item === 'object'
    ? {...item}
    : item
});
const todos2 = clone(todos); // same as todos [ ... ]

console.log(todos); // same as initialized [ ... ]

Deep Copy를 위해 ES5 이전에는 JSON으로 직렬화한 후 parsing하면 된다. 아니면 함수형으로 좀 더 선언적으로 선언하려면 위와 같이 clone이라는 메서드를 재귀적으로 선언하여 사용하면 된다. clone의 경우가 10프로 가량 성능적으로 이득이면서 선언적인 프로그래밍이 가능하다.

참고