본문 바로가기
Programming/4. JavaScript & React

016_pass by value, pass by reference

by @sangseophwang 2022. 4. 23.

??

최근 진행한 한 코딩테스트에서 생각한 대로 구현되지 않았던 문제가 있었다. 문제나 정답을 동일하게 작성할 수 없으니 비슷한 예를 다음과 같이 적어보겠다.

 

function solution(N, M) {
  const array = new Array(N).fill([]);
  const words = M.split(" ").map((e) => e.split(""));
  for (let i = 0; i < words.length; i++) {
    array[Number(words[i][0])].push(words[i][1]);
  }
  return array;
}

console.log(solution(2, "01 13 04"));

// 기대값: [['1', '4'], ['3']]
// 출력 : [['1', '3', '4'], ['1', '3', '4']]

 

코드를 간략히 해석해보자면 다음과 같다.

1. N개만큼의 빈 배열을 만들어준다.
2. M을 공백 기준으로 나눠주면 첫번째 숫자는 배열의 인덱스, 두번째 숫자는 해당 인덱스 배열에 들어갈 값이다.
3. M을 분리한 words 변수를 순회하며 array 변수에 각각 위치에 맞는 값을 넣어준다. 

하지만 예상과 달리 array 변수의 모든 배열에 값이 복사되는 현상이 발생했다. 왜 이런 일이 발생했을까?

 

new Array().fill([ ])의 함정

우선 이 현상의 가시적 원인은 배열 속 배열을 만드는 방법으로 new Array().fill([])을 사용한 것이다. 그런데 보통 fill 내부에 원시값을 넣어 배열을 채우듯이 하면 되는거 아닌가라고 생각할 수 있지만, 이 문제를 이해하기 위해서는 pass by value, pass by reference를 이해해야 한다.

 

🚀  pass by value (값에 의한 호출)

  • 인자로 넘기는 값을 복사해 새로운 함수에 전달하는 방식.
  • 값에 의한 호출은 원본값이 변경될 가능성이 없지만, 고비용과 메모리에 문제가 있다고 한다.
let a = 1;
const b = a;

console.log(a, b);  // 1 1
console.log(a === b);  // true

a = 10;

console.log(a, b);  // 10 1
console.log(a === b);  // false

b에 a를 할당하게 되면 a와 연결되는 것이 아니라 a가 가지고 있던 값 자체를 복사해 저장하게 된다. 그래서 a 값이 바뀌더라도 b는 기존에 전달받은 1이라는 값을 그대로 갖게 되는 것이다. 이러한 특성은 원시값(Primitive Type)인 Number, String, Boolean 등이 갖고 있다.

 

 

🚀  pass by reference (참조에 의한 호출)

  • 주소값을 인자로 전달받는 방식
  • 참조형 데이터 (배열, 객체)가 이러한 방식으로 사용된다.
  • 이 경우에 발생할 수 있는 문제는 값이 아닌 주소를 받았기 때문에 원본까지 영향을 줄 수 있다는 점이다.
let foo = { num: 10 };
const bar = foo;

console.log(foo.num, bar.num);  // 10 10
console.log(foo === bar);  // true

bar.num = 20;

console.log(foo.num, bar.num);  // 20 20
console.log(foo === bar);  // true

foo라는 객체가 있고 bar = foo로 복사를 했다. 그리고 bar.num을 변경했더니 foo.num까지 바뀌게 됐다. 이것이 바로 주소값을 인자로 전달받았을 때 생기는 문제인 것이다. 값이 아닌 값을 담고 있는 배열을 받았고, 배열 주소를 공유하고 있기 때문에 같은 값이 바뀌에 된 것이다. 이 내용을 이미지로 쉽게 표현하자면 다음과 같다.

 

최초 상태
값을 변경했을 때

 

그렇다면 다시 원래 문제로 돌아가보자. new Array().fill([])은 배열에 빈 배열을 생성하는 것이다. 이러한 경우 fill에 전달된 배열은 배열 그 자체가 전달되는 것이 아닌 배열을 가리키는 주소가 전달되어 새롭게 생성된 배열의 빈칸을 채우게 되는 것이다. 따라서 각각 다른 배열이라고 생각했지만 사실은 같은 값을 가리키고 있는 배열이었던 것이다. 그 이유는 fill 메서드는 변경자 메서드로, 복사본이 아니라 this 객체를 변형해 반환하기 때문이다. (참조만 복사) 그래서 이후 각각 다른 배열에 값을 추가하는 코드를 작성하더라도 모든 배열에 동일하게 값이 들어간 것처럼 보이게 된 것이다. 

 

🧪 해결법?

간단하다. 서로 다른 참조의 배열을 빈 배열에 채워주면 된다. 이렇게 되면 각각 가리키는 값이 다르기 때문에 추가, 삭제 시 원하는대로 작동할 것이다.

 

function solution(N, M) {
  let array = [];
  for (let i = 0; i < N; i++) {
    array.push([]);
  }
  const words = M.split(" ").map((e) => e.split(""));
  for (let i = 0; i < words.length; i++) {
    array[Number(words[i][0])].push(words[i][1]);
  }
  return array;
}

console.log(solution(2, "01 13 04"));   // [['1', '4'], ['3']]

 

이런 식으로 array 배열에 하나씩 배열을 추가해주고 기존에 원했던 방식인 '각 인덱스 배열에 값 넣기'를 실행해주면 결과가 잘 나오는 것을 확인할 수 있다.

 

반응형

댓글