Array.fill 메서드에 객체를 전달할 때 주의하세요!

이나리·2022년 6월 26일
1

배열을 만들고, 이 배열을 특정 값으로 채우기 위해 fill 메서드를 주로 사용하는데요.
fill 메서드를 사용할 때, 한가지 주의사항이 있습니다.

이에 앞서, 배열을 만드는 방법부터 먼저 알아보겠습니다.

배열 만들기 (리터럴 vs 생성자)

배열을 만들기 위해서는, 배열 리터럴이나 생성자를 이용합니다.
리터럴과 생성자 방식은 약간의 차이가 있습니다.
먼저, 예시를 살펴보겠습니다.

  • 배열 리터럴 방식
const array = [];

array.length = 0;
array[0] = undefined;
  • 배열 생성자 방식
const array = Array(5);

array.length = 5;
array[0] = undefined;

배열을 생성한다는 점에서 두 방식은 동일하지만, 생성자는 매개변수로 숫자를 전달하게 되면, 배열의 length를 숫자만큼 변경할 수 있습니다.
단, length만큼 요소가 생성되진 않습니다. 해당 배열 안에는 아무 값도 들어있지 않습니다.

또 하나 주의해야 할 점은, 배열의 length 프로퍼티입니다.
length는 배열에 들어있는 요소의 개수가 아닙니다! 이 값은 배열 요소에서 가장 큰 인덱스 값을 나타냅니다.

그리고 이번에 공부하면서 새로 알게된 개념 중 하나가 있는데, 위의 배열 리터럴 방식에 다음과 같은 코드를 추가하면 어떤 일이 벌어질까요?

array.length = 2;

array의 length 값이 0에서 2로 변경됩니다. 하지만, 배열 안의 요소는 아무 것도 변경되지 않았습니다.

기존의 length 값이 5였는데 2로 변경된다면, 2~4번째 인덱스에 해당하는 요소들은 배열에서 모두 삭제됩니다.
length 프로퍼티는 배열을 변경시킬 수 있으니 값을 할당할 경우, 사용에 주의해야 합니다.


Array.fill(value, start, end)

배열을 생성하는 방식에 대해 알아보았으니, 이제 fill 메서드에 대해 알아보겠습니다.
start와 end 매개변수는 추가적인 개념이라 여기에서는 따로 설명하지 않겠습니다.

fill 메서드의 첫번째 매개변수 value는 해당 배열을 채울 요소입니다.

value 값이 원시값일 경우

const array = [].fill(5); // array = [5]
const newArray = Array(0).fill(5); // newArray = [5]

생성자 방식을 사용하면 length 프로퍼티를 다르게 할 수 있기 때문에, 리터럴 방식보다 다양한 배열을 만들 수 있습니다.

const newArray = Array(5).fill(5); // newArray = [5, 5, 5, 5, 5]

그런데, 만약 원시값이 아닌 객체값을 전달하게 되면 어떻게 될까요?


value 값이 객체값일 경우

const objectArray = Array(5).fill({}); // objectArray = [{}, {}, {}, {}, {}]

원시값을 전달한 것과 똑같이 객체 요소를 가진 배열로 변경됩니다.
그런데, 객체를 전달하게 되면 한가지 주의해야 할 점이 있습니다.

일단, 앞서 만든 객체 배열의 첫번째 요소에 id라는 프로퍼티를 추가하고 값을 할당해보겠습니다.

const objectArray = Array(5).fill({});
objectArray[0].id = 1;

이 객체 배열은 어떻게 변화했을까요? 결과는 다음과 같습니다.

objectArray = [{ id: 1 }, { id: 1 }, { id: 1 }, { id: 1 }, { id: 1 }];

어떻게 된걸까요? 분명 첫번째 요소만 변경했는데 다른 요소들까지 모두 변경됐습니다.

이 부분에서 원시값과 객체의 성질이 다름을 확인할 수 있습니다.


fill 메서드의 동작 원리

먼저, fill 메서드가 동작하는 방식에 대해 알아보겠습니다.
우리는 fill 메서드를 사용할 때, 배열을 채울 요소를 첫번째 매개변수로 전달합니다. 이 매개변수에는 어떤 값이든 허용됩니다.

어떤 값을 전달하든, fill 메서드는 이 값을 복사하여 해당 배열의 요소로 넣어줍니다.

여기서 포인트는 값을 복사한다입니다.

자바스크립트를 배울 때, 원시값과 객체값이 다르게 복사된다고 배웠습니다.
원시값은 값을 복사하기 때문에 값을 변경해도 항상 새로운 값을 갖지만,
객체값은 값을 복사하지 않고 그 값이 저장된 주소를 참조하기 때문에, 변경될 수 있는 값입니다.

즉, 각각 원시값과 객체값을 가진 변수를 다른 변수에 할당할 때, 할당된 다른 변수를 변경했을 때 기존의 변수 값이 변경 가능한지의 차이입니다.
원시값은 변경되지 않고, 객체값은 변경됩니다.


이를 fill 메서드에 적용해보겠습니다. 먼저, 원시값 배열입니다.

const numberArray = Array(5).fill(3); // numberArray = [3, 3, 3, 3, 3];
numberArray[0] = 5; // numberArray = [5, 3, 3, 3, 3];

변화를 준 첫번째 요소에만, 값이 변경된 것을 확인할 수 있습니다.
원시값은 항상 값을 복사하기 때문에, 해당 숫자 배열에 들어있는 요소는 모두 3이지만, 서로 다른 메모리 저장 공간에 저장된 값입니다.
따라서, 첫번째 요소에 있는 원시값을 변경하더라도, 나머지 다른 요소들은 변하지 않고 그대로 있는 것입니다.

다시 객체배열 예제를 보겠습니다.

const objectArray = Array(5).fill({}); // objectArray = [{}, {}, {}, {}, {}]
objectArray[0].id = 5;
// objectArray = [{ id: 1 }, { id: 1 }, { id: 1 }, { id: 1 }, { id: 1 }]

이 객체 배열에 들어있는 모든 객체들은 값을 복사하지 않고 참조하기 때문에, 하나의 객체의 메모리 주소를 바라보고 있습니다.
원시값처럼 모든 요소가 서로 다른 값이 아니라, 동일한 1개의 객체를 바라보기 때문에 어떤 요소를 변경하든, 다른 요소도 동일하게 변경되는 것입니다.

특별해 보이는 것 같지만, 원시값과 객체값의 차이가 불러온 코드의 변화입니다.


그러면 서로 다른 객체값을 가지려면 어떻게 해야 하나요?

하나의 객체를 참조하지 못하도록 만들면 됩니다.
그럴러면, fill 메서드에 일단 객체값을 전달하지 못할 겁니다. 전달하는 순간, 모든 값이 같아지기 때문이죠.

const array = Array(5).fill();

이제 어떻게 배열에 서로 다른 객체 5개를 추가할 수 있을까요?
fill 메서드에 전달한 매개변수의 값이 문제가 되는 경우는 배열의 length가 0이 아닐 때입니다.

배열의 length가 있을 때 반복문을 사용하여 배열의 요소를 변경하는 코드를 많이 작성해 보셨을 겁니다.
반복문을 이용해, 각 요소마다 값을 추가하면 배열에 새로운 객체값을 추가할 수 있습니다.

for (let i = 0; i < array.length; i++) {
  array[i] = {};
}

array[0].id = 1; // array = [{ id: 1 }, {}, {}, {}, {}]

이제 배열 안에 들어있는 모든 객체들은 서로 다른 객체이기 때문에 어떤 요소를 변경하더라도 다른 요소에 영향을 주지 않습니다.

for문 외에도 map과 같은 배열 메서드를 사용하는 방법도 가능합니다.

const array = Array(5).fill();
const newArray = array.map(arr => {
  arr = {};
  return arr;
});

0개의 댓글