FEDev Story

객체의 참조와 재할당 본문

Javascript

객체의 참조와 재할당

지구별72 2022. 5. 4. 14:15

참조에 의한 전달이 일어나는 3가지의 데이터 타입으로는 Array, Function, Object가 있다. 사실 이 3가지는 크게 보자면 전부 객체(Objects)로 볼 수 있다.

원시타입이 아닌 값이 할당된 변수들은 그 값으로 향하는 참조를 갖게 된다. 참조는 메모리에서의 객체의 위치를 가리키고 있다. 변수는 실제로 값을 가지고 있지 않다.

참조로 할당하기

객체와 같은 참조 타입의 값이 =과 같은 키워드를 이용하여 다른 변수로 복사될 때, 그 값의 주소는 실제로 복사된다. 객체는 값 대신 참조로 복사된다.

var reference = [1];
var refCopy = reference;

각각의 변수는 이제 같은 배열로 향하는 레퍼런스를 갖는다. 이 말은 우리가 reference나 refCopy를 수정하면 다음과 같은 결과를 얻게 됨을 의미한다.

reference.push(2);
console.log(reference, refCopy);  
// [1,2], [1,2] 

참조 재할당하기

참조 값을 재할당하는 것은 오래된 참조를 대체한다.

var obj = { first: 'reference' };
obj = { second: 'ref2' };

obj 안에 저장됐던 주소 값은 변경된다. 첫번째 객체는 아직 메모리에 남아 있게 된다. 남아있는 객체를 가리키는 참조가 남아있지 않을 때, 자바스크립트 엔진은 가비지 컬렉션을 동작시킬 수 있다. 이것은 프로그래머가 모든 참조를 날리고 객체를 더이상 사용할 수 없게 된 뒤 자바스크립트 엔진은 그 주소로 가 사용되지 않는 객체를 메모리로부터 안전하게 지워버리는 것을 의미한다. 이 경우에는 객체 { first: 'reference' }가 더이상 접근 불가능하고 가비지 콜렉션될 수 있다.

순수 함수

함수 중에 함수 바깥 스코프에 아무런 영향을 미치지 않는 함수를 순수 함수라고 한다. 함수가 오직 원시 값들만을 파라미터로 이용하고 주변 스코프에서 어떠한 함수도 이용하지 않는다면, 그 함수는 자연스레 순수함수가 된다. 안에서 만들어진 모든 변수들은 함수에서 반환이 되는 즉시 가비지 콜렉션 처리가 된다.
객체를 받는 함수는 주변 스코프들의 상태를 변화시킬 수 있다. 만일 함수가 배열 참조값을 가진 변수를 받고 그 변수가 가리키는 배열에 push를 수행하면, 그 주변 스코프에 존재하는 변수들과 그 참조와 그 배열이 변하는 것을 볼 수 있다. 함수 리턴 후에, 변화된 것들은 바깥 스코프에 여전히 남아있다. 이런 현상은 우리가 원하지 않는 방향으로 부작용(side-effect)을 줄 수 있다.
Array.map과 Array.filter를 포함한 많은 네이티브 배열 함수들은 그래서 순수 함수로 작성되어 있다. 배열 참조를 받아서 내부적으로 배열을 복사하고 원본 대신 복사된 배열로 작업한다. 그래서 원본도 건드리지 않고 바깥 스코프에 영향도 미치지 않고 새로운 배열의 참조를 반환하게 된다.
여기서 순수 함수와 순수 함수가 아닌 것을 비교해보자.

function changeAgeImpure(person){
   person.age = 25;
   return person;
}
var alex = {
   name: 'Alex',
   age: 30
};
var changedAlex = changeAgeImpure(alex);

console.log(alex); //{name: 'Alex', age: 25}
console.log(changedAlex); //{name: 'Alex', age: 25}

이 비순수함수는 객체를 받아서 age 프로퍼티를 25라는 값으로 바꾼다. 객체로 받아온 값에 그대로 명령을 실행하기 때문에 이 함수는 alex 객체를 직접적으로 변화시킨다. 이 함수가 person 객체를 반환할 때, 이 함수는 받았던 객체 그대로를 반환한다. alex와 alexChanged는 같은 참조를 가진다. person 변수를 반환하고 그 참조를 다시 새로운 변수에 저장하는 것은 사실 쓸데없는 행동이다.
이제 순수 함수를 보도록 하자

function changeAgePure(person){
   var newPerson = JSON.parse(JSON.stringify(person));
   newPerson.age = 25;
   return newPerson;
}
var alex = {
   name: 'Alex',
   age: 30
};
var changedAlex = changeAgePure(alex);

console.log(alex); //{name: 'Alex', age: 30}
console.log(changedAlex); //{name: 'Alex', age: 25}

이 함수에서 우리가 넘겨받은 객체를 문자열로 변화시키기 위하여 JSON.stringify를 사용했다. 그리고 JSON.parse 함수를 이용하여 다시 객체로 만든다. 이러한 과정을 거치면서 새로운 객체를 만들고 그 결과 값을 새로운 변수에 저장한다. 새 객체는 원본과 같은 프로퍼티를 가진다. 하지만 메모리상에서는 이 두객체는 다른 주소값을 가지고 구분될 수 있다.
우리가 이 새로운 객체에서 age 프로퍼티를 변경할 때, 원본은 전혀 영향을 받지 않는다. 이 함수는 지금 순수하다. 이 함수는 바깥 스코프에 아무런 영향을 미치지 않는다. 심지어 인자로 받은 객체까지도. 새롭게 만들어진 객체는 반환이 되어야 한다. 그리고 새로운 변수에 저장되어야 한다. 그렇지 않으면 결과 값은 가비지 콜렉션 될 것이고 결과 객체는 어디에도 남지 않게 된다.

자가 테스트

값 vs 참조는 코딩 인터뷰에서 많이 출제되곤 한다. 무엇이 로그에 남겨질 지 한번 스스로 추측해보자.

function changeAgeAndReference(person){
   person.age = 25;
   person = {
      name: 'John',
      age: 50
   }
   return person;
}
var personObj1 = {
   name: 'Alex',
   age: 30
};
var personObj2 = changeAgeAndReference(personObj1);

console.log(personObj1); // ?
console.log(personObj2); // ?

함수는 처음에 넘겨진 원본 객체의 프로퍼티 age를 변경한다. 그 후에 변수 값에 새로운 객체를 다시 할당한다. 그리고 그 객체를 반환한다. 두 로그의 결과는 다음과 같다.

console.log(personObj1); // { name: 'Alex', age: 25 }
console.log(personObj2); // { name: 'John', age: 50 }

함수의 파라미터로 할당되는 것들은 = 연산자로 할당하는 것과 같다는 것을 기억하자. 함수 속 변수 person은 personObj1로 향하는 참조를 갖고 있어서 처음에 전달 받은 객체에 직접 변화를 가한다. 우리가 person을 새로운 객체로 재할당 한 뒤에는 원본 객체에 더 이상 영향을 미치지 않는다.
이 재할당은 바깥 스코프에 있는 personObj1이 가리키는 객체를 변경하지 않는다. person 변수는 새로운 참조를 갖게 된다. 왜냐하면 이 변수는 단순히 재할당 됐을 뿐이고 이 재할당은 personObj1에 아무런 영향을 미치지 않기 때문이다.
우리가 이 함수를 사용할 때 유일한 차이점은 일단 함수가 끝나고 나면 함수 내부에 있던 person이 더이상 스코프 안에 있지 않다는 점이다.









Comments