코딩 테스트 관련한 글은 지금까지 블로깅했던 적은 없는데, DFS/BFS 문제를 풀다가 Array.fill 함수에 대해 새롭게 알게 된 점이 있어 기록할겸 남긴다.
Array.fill 함수는 MDN 설명에 따르면, 배열의 시작 인덱스부터 끝 인덱스의 이전까지 정적인 값 하나로 채운다.
const array1 = [1, 2, 3, 4];
// fill with 0 from position 2 until position 4
console.log(array1.fill(0, 2, 4));
// expected output: [1, 2, 0, 0]
// fill with 5 from position 1
console.log(array1.fill(5, 1));
// expected output: [1, 5, 5, 5]
console.log(array1.fill(6));
// expected output: [6, 6, 6, 6]
보통은 코딩테스트 문제를 풀 때, false나 0으로 특정 길이만큼 채워진 배열을 만들고 그 배열을 돌면서 true 혹은 특정 값을 넣고 싶을 때가 있는데 그럴 때 주로 fill 함수를 사용하곤 했다.
오늘 푼 문제는 백준 1260번, DFS와 BFS라는 문제였는데 문제 풀이를 위한 블로깅은 아니니까 본론으로 빠르게 넘어가서 코드의 일부분을 먼저 보자.
const paths = [[1, 2], [1, 3], [1, 4], [2, 4], [3, 4]]
const graph = new Array(N + 1).fill([]);
for (const [from, to] of paths) {
graph[from].push(to);
graph[to].push(from);
}
먼저, 구현하려던 것은 paths라는 2차원 배열을 graph라는 2차원 배열로 원하는 형태로 가공하는 것이였는
데 코드에서 보다시피 N + 1 길이만큼의 빈 배열을 만들고 그 안에 각각의 원소에 다시 빈 배열을 넣어 2차원 배열을 만들었다. 그러고 나서 paths 배열을 돌면서 key, value 각각을 graph 배열에 넣어준다.
그래서 원하던 결과는.. [ [], [ 2, 3, 4 ], [ 1, 4 ], [ 1, 4 ], [ 1, 2, 3 ] ] 이런 형태의 배열이 결과값으로 나올 거라고 생각했지만 생각과 달리 이상한 결과가 나왔다.
[[ 2, 1, 3, 1, 4, 1, 4, 2, 4, 3 ],
[ 2, 1, 3, 1, 4, 1, 4, 2, 4, 3 ],
[ 2, 1, 3, 1, 4, 1, 4, 2, 4, 3 ],
...
]
왜 저런 결과값이 나왔는지 과정이 궁금해서, for문을 돌 때마다 console을 찍어서 확인해봤는데 뭔가 값이 들어가긴 들어가는데 graph[from], graph[to] 이렇게 지정한 배열에만 push가 되는 것이 아니라 모든 배열에 push가 되고 있었다.
뭔가.. 변수 자체를 잘못 넘겨줬나? 생각했지만 그것도 아니였고, 아래처럼 변수없이 따로 코드를 돌려 본 결과 모든 배열에 push가 되고 있는게 맞았다.
const arr = new Array(3).fill([]);
// arr = [[], [], []]
arr[0].push(1)
console.log(arr);
// [ [1], [1], [1] ] => ?
왜 특정 인덱스를 지정하는데 모든 배열에 값이 들어가는지 의문이 생겼고 빈 2차원 배열을 fill을 통해서 만들지 않고 직접 만들어서 반복문을 돌려봤다.
const arr = [[], [], []]
for (const [from, to] of paths] {
arr[from].push(to);
arr[to].push(from);
}
console.log(arr);
// [ [], [ 2, 3, 4 ], [ 1, 4 ], [ 1, 4 ], [ 1, 2, 3 ] ]
그랬더니, 원하는 결과값이 제대로 나왔다. 여기서 뭔가 Array.fill() 함수를 통해 만든 배열이 뭔가 다른 점이 있는 것 같다는 생각이 들어서 구글링을 해봤다.
찾아본 결과, fill 함수의 다른 점이 있었는데 MDN 문서에 따르면 fill 메서드는 변경자 메서드로, 복사본이 아니라 this 객체를 변형해 반환하며 value에 객체를 받을 경우 그 참조만 복사해서 배열을 채웁니다. 라고 되어있다.

이해한대로 정리해보면 value에 객체를 받을 경우 각각 다른 객체가 만들어지는게 아니라, 처음 value에 들어간 객체를 참조하여 복사하고, 복사된 객체들로 배열을 채운다는 것 같다. 그래서 내가 원하는 결과가 나오지 않고, 각 배열들이 같은 메모리 주소를 참조하여 모두 값을 공유하는 것이었다.
왜 이런 현상이 나타날까? 이를 이해하기 위해서는 Copy By Value, Copy By Reference라는 개념을 알아야 한다.
먼저, 자바스크립트의 데이터 타입은 크게 두 종류로 나뉘는데, 원시 데이터 타입(number, string, boolean, null, undefined 등)과 객체 데이터 타입(object, array, function)이다.
여기서 객체와 원시 타입의 근본적인 차이 중 하나는 객체는 '참조에 의해(by reference)' 저장되고 복사된다는 것이다. 반면에, 원시값은 '값 그대로' 저장, 할당되고 복사된다.
Copy By Value는 복사할 대상을 새롭게 만든 변수에 그 값 자체를 할당한다라는 뜻이다.
let a = "Hello!";
let b = a;
a = "Bye!";
console.log(a); // Bye!
console.log(b); // Hello!
b 변수에 a의 값을 할당한 뒤에, a의 값을 변경하였음에도 서로 영향을 끼치지 않는다. 그 이유는 a와 b는 원시값이므로 메모리가 각 변수에 할당되어 a의 메모리 주소와 b의 메모리 주소가 다르기 때문이다.
Copy By Reference는 새롭게 만든 변수에 복사할 대상이 참조하고 있는 메모리 주소값을 할당한다라는 뜻이다.
let user = {
'name': "John"
}
let admin = user;
user.name = "Pete";
console.log(user.name); // 3
console.log(admin.name); // 3
왜 위와 같은 결과가 나오게 될까?
왜냐하면, a와 b 두 변수는 객체 데이터 타입이기 때문에 참조에 의한 복사가 발생하기 때문이다. 내부적으로 같은 메모리 주소를 바라보고 있기 때문에 두 변수는 객체를 공유하고 있는 것이다.
일반적으로 값을 복사한다고 생각했을 때 '값 그대로' 복사하는 원시 데이터 타입과 달리, 객체 데이터 타입은 복사할 때 내부적으로 주소 값을 공유하는 원리를 가지고 있기 때문에 이런 결과가 발생하는 것이다.
아래의 예시를 보면, 이해하는데 더 도움이 될 것 같다.
let a = {};
let b = a; // 참조에 의한 복사
alert( a == b ); // true, 두 변수는 같은 객체를 참조합니다.
alert( a === b ); // true
두 변수가 같은 객체를 참조하고 있기 때문에 일치·동등 비교 모두에서 참이 반환된다.
let a = {};
let b = {}; // 독립된 두 객체
alert( a == b ); // false
두 변수가 비어있다는 객체라는 점에서 같아보이지만 서로 독립된 객체이기 때문에 비교연산자를 사용해보면 false가 반환된다.
https://taesung1993.tistory.com/33
https://han7096.medium.com/javascript-copy-by-reference-%EC%99%80-copy-by-value-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-9f09c9982a1
https://ko.javascript.info/object-copy