티스토리 뷰

HTTP, JS

[JS] 스프레드(spread) 문법

안톨리니 2023. 7. 22. 15:18

포스팅을 작성하기에 앞서 spread문법은 iterable, iterator와 관련된 선수지식을 필요로 합니다.

 

[JS] 이터러블(Iterable), 이터레이터(Iterator)

ES6에서 이터레이션 프로토콜이라는게 도입되었다. 이전에는 순회 가능한 데이터들 예를 들면 배열이나, 문자열 같은 객체들은 통일된 규약 없이 나름의 개별적인 구조로 각자 다양한 방법으로

tcitr-antoliny.tistory.com


두 개의 배열을 각각 순회하여 각 배열에 있는 값들을 새로운 배열에 추가하고 싶다.

 

가장 먼저 생각나는 방법은 함수를 만든 뒤

내부에서  for문을 통해 값을 줄 배열을 순회하면서 push함수로 새로운 배열에 값을 추가하는 방식이었다.

var arrA = [1, 2, 3];
var arrB = [4, 5, 6];
var arrC = [];

const addValue = (taker, giver) => {
  for(i = 0; i < giver.length; i++) {
    taker.push(giver[i])
  }
}

addValue(arrC, arrA);
addValue(arrC, arrB);

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

또다시 다른 방법을 생각해 보자

위 예시 같은 경우에는 배열 객체의 concat메서드를 사용하면 더 쉽게 구현할 수 있다.

var arrA = [1, 2, 3];
var arrB = [4, 5, 6];

var arrC = arrA.concat(arrB);

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

이번에는 만약 여러 배열을 순회해야 하거나 특정 배열에서는 값을 제거한 뒤 순회해야 하는 등

예외적인 사항이 발생했을 때를 생각해 보자

addValue(arrC, arrA);
addValue(arrC, arrB);
addValue(arrC, arrX);
addValue(arrC, arrY);
addValue(arrC, arrZ);
...
...
...

var arrC = arrA.concat(arrB).concat(arrX).concat(arrY).concat(arrZ)...

-------------------------------------------------------------------------

var arr1 = [1, 2, 7];

var arr2 = [4, 5];

arr1.pop();
arr1.push(3);

var mergeArr = arr1.concat(arr2); [1, 2, 3, 4, 5, 6]

구현은 할 수 있지만 반복되는 코드로 가독성이 떨어지며 직관적인 형태가 될 수 없다.

그리고 마지막 부분을 보면 pop(), push()메서드를 통해 arr1배열의 7을 제거하고 3을 추가한 뒤

concat함수를 통해 결국 구현은 했지만

 

pop(), push() 메서드 같은 경우는 원본을 변경한다는 점을 생각해 보면

(※ arr1은 [1, 2, 7]이 아닌 [1, 2, 3]이 된다.)

 

그저 다른 배열을 위해서 사용한 메서드로 원본 배열이 수정 돼

다시 arr1을 사용할 때 예상치 못한 상황을 만들어내는 원인이 될 수 있다.

 

 

ES6 Spread 문법의 등장

 

ES6부터 spread문법이 등장했다.

spread문법은 ...연산자이며 하나의 집합을 전부 다 순회한 상태로 만들어준다.

 

간단하게 정말 말 그대로 "펼치다"로 피연산 객체의 값을 "펼쳐서 나열한" 상태라고 생각하면 쉽다.

 

언제나 글로 보는 것보다 실제로 사용해 보는 것이 더 이해하기 쉽기에

for문 concat으로 구현했던 문제를 이번에는 spread문법으로 구현해 보았다.

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

var spreadArr1 = [...arr1, ...arr2];

console.log(spreadArr) // [1, 2, 3, 4, 5, 6]

----------------------------------------------

var arr1 = [1, 2, 7];
var arr2 = [4, 5, 6];

var spreadArr2 = [...arr1.slice(0, 2), 3, ...arr2];

console.log(spreadArr2) // [1, 2, 3, 4, 5, 6]
console.log(arr1) // [1, 2, 7]

spread문법을 사용하면 확실히 더 직관적이다.

(※ 사실 이 말에는 약간의 오류가 있는 게 처음 접하는 사람에겐 spread가 오히려 더 어려울 수 있다.)

(※ 나만 그랬던 걸 수도 있다 😥)

(※ 하지만 어느 정도 익숙해지면 직관적인 부분은 확실한 거 같다.)

 

각 배열(집합)들을 새로운 집합 내에서 spread문법으로(...)전부 다 순회한 상태로 만들어주었다.

 

여기서 필자는 spread문법으로 값들을 순회한다고 했다.

그 의미는 spread문법을 사용하게 되는 피연산자는 당연히 순회가 가능한 iterable 한 객체여야 한다.

 

 

Iterable 객체

 

JS에서 빌트인 iterable객체는 String, Array, Set, Map 등등이 존재한다.

해당 객체들은 Symbol.iterator()함수가 구현되어 있어 순회가 가능한 iterable객체이기 때문에

spread문법을 사용하기 위한 준비가 된 상태이다.

 

spread의 피연산자로 Array, Set, Map을 사용해 보았다.

var arrayA = [1, 2, 3, 4];
var setA = new Set([1, 2, 3, 4, 5]);
var mapA = new Map([['x', 1], ['y', 2], ['z', 3]]);

console.log(...arrayA); // 1 2 3 4
console.log(...setA); // 1 2 3 4 5
console.log(...mapA); // ['x', 1] ['y', 2] ['z', 3]

세 가지 타입 다 잘 동작하는 걸 확인할 수 있다.

 

하지만 이번에는 iterable하지 않은 일반 객체에 spread문법을 사용해 보겠다.

var objectA = {'x': 1, 'y': 2, 'z': 3};

console.log(...objectA);
// TypeError: Found non-callable @@iterator

위 예시에서 알 수 있듯이 iterator함수를 찾을 수 없다고 타입에러가 발생한다.

 

사실 당연한 결과다.

필자가 위에서도 설명했듯이 spread문법은 객체를 "펼쳐서 나열한"상태로 만들어준다고 했는데

그게 가능하려면 해당 객체가 iterable로 순회가 가능해야 한다.

 

하지만 일반객체 objectA에는 Symbol.iterator함수가 존재하지 않는 iterable하지 않은 객체이기 때문에

타입에러가 발생하는 것이다.

 

그렇다면 일반객체에 Symbol.iterator 함수를 직접 구현하여 iterable한 객체로 만들어준다면

해당 객체에 spread문법이 사용될까?

var iterObj = {

  a: 1,
  b: 2,
  c: 3,
  d: 4,
  [Symbol.iterator]() {
    
    // [1, 2, 3, 4]
    let valueList = Object.keys(this).map((key) => this[key]);

    let index = -1;
    
    return {
      next() {
        index++;
        return { value: valueList[index], done: index >= valueList.length}
      }
    }
  }
}

console.log(...iterObj); // 1 2 3 4

iterObj는 일반객체지만 Symbol.iterator가 구현된 iterable한 객체이다.

 

마지막 출력결과를 확인하면 어떠한 객체든 iterable한 타입이라면

spread문법이 잘 동작하는 걸 확인할 수 있다.

 

그리고 사실 당연한 부분이지만 위 예제로 spread의 내부동작도 어느 정도 유추할 수 있다.

출력결과가 1 2 3 4 인걸 보면 결국 iterable한 객체가 iterator를 생성하고

해당 iterator를 끝까지 순회한 값의 목록이 결국 spread문법의 사용결과라고 볼 수 있다.

( 여기서 한 가지 주의할 점은 spread문법의 결과는 값이 아니라는 점이다. )

(※ 간단하게 spread문법은 값을 생성하는 게 아닌 해당 iterator의 전체 값을 펼친 것일 뿐이다.)

 

 

spread 얕은 복사, 깊은 복사??

 

spread문법과 관련하여 한 가지 알고 가면 좋은 부분이 있다.

 

spread문법으로 생성한 새로운 객체는 얕은 복사일까? 깊은 복사일까?

var arrA = [1, 2, 3, 4];

var arrB = [...arrA];

arrA[0] = -1

console.log(arrA); // [ -1, 2, 3, 4 ]
console.log(arrB); // [ 1, 2, 3, 4 ]

arrA배열의 값들을 spread 문법으로 펼친 목록을 새로 생성한 arrB 배열에 복사하고

arrA의 배열값을 수정해 보았다.

 

arrA의 수정이 arrB에 영향을 주지 않는다.

사실 원시값 같은 경우는 원본의 값을 복사하여 전달되기 때문에 당연한 결과이고

shallow copy, deep copy를 구분하는데 일차원 배열로는 확인할 수 없기에 

 

이번에는 2차원 배열에 spread문법을 사용해 보겠다.

var arrA = [0, [1, 2], [3, 4], [5, 6]];

var arrB = [...arrA];

arrA[0] = 100;
arrA[1][0] = -1;

console.log(arrA); // [100, [-1, 2], [3, 4], [5, 6]]
console.log(arrB); // [0, [-1, 2], [3, 4], [5, 6 ]]

원본인 arrA의 0번 인덱스를 100으로 그리고 arrA의 1번 인덱스 배열의 0번 인덱스를 -1로 변경했다.

 

예상대로 0번 인덱스인 원시값을 100으로 변경한 건 상관없지만

1번 인덱스인 배열객체를 변경한 건 arrA, arrB 둘 다 수정된 걸 확인할 수 있다.

 

위 예제만 봐도 이제 spread문법은 얕은 복사라는 걸 알 수 있다.

배열 같은 객체를 복사하면 원본의 참조값이 복사되기 때문에 결국은 arrA나 arrB에 있는 내부 배열은

동일한 곳을 가리킨다.

 

결론은 spread문법을 사용한다 해서 중첩 객체의 내부 객체들을 새로 생성하고 하지 않는다는 점이다.

즉 spread문법은 얕은 복사를 수행한다는 걸 의미한다.

 

 

spread문법 어디에 자주 쓰일까?

 

사실 이 부분은 개인마다 차이가 있겠지만

필자의 경험상 spread를 사용하기 좋은 상황은 첫 번째로... rest파라미터를 가진 함수에 인수를 전달할 때다.

const restFunction = (...rest) => {
  console.log(rest); // [3, 5, 7]
}

restFunction(3, 5, 7)

... rest 파라미터는 여러 인수를 받도록 설정되어 있는데

만약 인수로 전달할 값이 배열에 있다고 생각해 보자

그럴 때 배열에 있는 값들을 전부 다 펼쳐서... rest파라미터에 전달해야지 배열 자체 상태로 전달하면

내부에서 중첩배열이 돼서 함수의 의도와는 다른 결괏값이 반환될 수 있다.

 

대표적인 예시로 Math.max함수를 생각하면 쉽다.

console.log(Math.max(3, 5, 7)); // 7

 

Math.max함수는 ... rest파라미터를 가진 함수로 전달받은 인자 중에서 가장 큰 값을 리턴하는 함수인데

만약 해당 인자로 배열 자체를 전달하면

var arr = [3, 5, 7]

console.log(Math.max(arr)); // NaN
console.log(Math.max(...arr)); // 7

7이 아닌 Not a Number가 반환되어 예상하지 못한 값이 반환된다.

이에 반해 spread 문법을 사용하여 배열 내부의 값들을 펼쳐서 인자로 전달하면

7이 리턴되는 걸 확인할 수 있다.

 

다음으로 두 번째는

기존값을 유지한 상태로 특수한 부분만 변경되거나 값이 추가될 때 spread 문법을 사용하면 간단하다.

const response = ["bread", "coffee"];

const currentProductData = ["chicken", "pizza", "hamburger"];

const newProductData = [...currentProductData, ...response];

기존에 가지고 있던 데이터에서 사용자가 어떠한 데이터를 더 받아왔을 때

기존 데이터에 받아온 데이터를 추가해서 보여줘야 하는 상황이라면 위 코드처럼 spread 문법을 통해 해결할 수 있다.

 

만약 일반 객체형태에 기존 값들은 유지하면서 특수한 키값에만 변경이 생긴다면

const loginData = {
  id: "antoliny",
  password: "123",
};

const newLoginData = {...loginData, ["password"]: "12345"};

새로운 객체에 먼저 기존 객체 데이터를 복사한 뒤 새롭게 변경될 부분만 뒤에 추가해 주면 된다.

 

사실 이러한 사용예시는 위에서도 언급했듯이 필자의 개인적인 견해가 담겨있다.

(※ 필자가 생각하지 못한 더 창의적인 사용법이 많을 거라고 생각한다.)

 

필자가 예시로 보여준 패턴은 그저 기존 데이터를 유지하면서 값이 새롭게 변경되는 부분이 있거나 추가될 때

spread 문법을 사용하면 용이하다는 걸 말씀드리고 싶었다.

필자의 spread문법과 관련된 설명은 여기까지다.

 

spread를 처음 접하는 사람에겐 다소 익숙하지 않은 문법으로 다가올 수 있지만

필자는 spread문법만큼 데이터를 관리해야할 때 직관적이고 편한 게 없다고 생각한다.

 

이 글을 읽는 독자분들의 프론트 관련 코드에도 spread문법이 상황에 맞게 잘 사용됐으면 좋겠다.

댓글
공지사항