[JS 33가지 개념] 3. 값(Value) VS 참조(Reference)

2020. 6. 25. 17:55Javascript/33가지 개념

1. 원시 타입(Primitive Type)

자바스크립트 데이터는 두 가지 타입으로 나뉜다. 첫 번째는 원시타입, 두 번째는 참조타입이다. 원시 타입(Primitive Type)String, Number, Boolean, undefined, null, symbol 총 여 섯개의 타입이 있으며 다음과 같이 선언을 한다.

const num = 123;
const str = "hello";
const NULL_POINT = null;

 

 num, str, NULL_POINT 변수 모두 선언해준 값을 가지고 있다. 메모리는 이러한 변수, 값들을 다음과 같은 이미지 형태로 저장한다.

 

 

 그렇다면, 다음과 같이 새로운 변수를 만들어 기존에 선언했던 변수 값= 연산자를 이용해서 복사한다면?

let num1 = 123;
let num2 = 124;

const a = num1;
const b = num2

console.log(num1, a, num2, b); // -> Print: 123, 123, 124, 124

 

 메모리는 변수, 값들에 대해 아래와 같은 이미지를 형성할 것이다.

 

 

이제 기존에 있던 변수에 새로운 값을 복사해준다면?

let num1 = 123;
let num2 = 124;

const a = num1;
const b = num2

num1 = 125

console.log(num1, a, num2, b); // -> Print: 125, 123, 124, 124

 

 num1의 값만 125로 바뀐 것을 알 수 있다. 따라서 원시타입(Primitive) 변수들은 새로운 변수를 재할당하거나 다른 값의 변수를 복사하더라도 독립적인 고유의 영역이 있다는 것을 알 수 있다. 따라서 다른 변수에 아무런 영향을 끼치지 않는다.

 

 

2. 참조 타입(Reference Type)

 참조 타입은 객체를 나타낸다. 참조 타입종류배열, 함수, 객체 총 세 가지다. 참조 타입을 참조라고 부르는 이유는 = 연산자를 이용해서 변수에 값을 줄 때 값(value)을 주는 것이 아니라 참조 변수(Reference Address)를 주기 때문이다. 다음 예시를 보면 

const arr = [];

 

 어떻게 보면, 변수 arr에 []이라는 객체 값을 준 것 같지만, 객체 값을 준 것이 아니라 메모리 내부에 배열을 만들어 준 것이다. 좀 더 자세히 설명하면 다음과 같다. 

 

  1. const arr = []; 명령어 실행.
  2. 임의 메모리 주소 0x01에 배열 []를 만듦.
  3. 메모리 주소 값 <0x01>(주소 값을 <, > 괄호 안에 있는 형태로 만듦)를 변수 arr에 복사.
  4. arr이 갖는 값은 메모리 주소 0x01를 가리키는 값임.

 

 메모리에서는 arr을 다음과 같은 형태로 저장한다.

 

 

 그렇다면 push 메소드를 이용해서 1을 arr 배열에 넣어준다면? 

const arr = [];
arr.push(1);

 

 객체만 변한 것을 알 수 있다. 그 이유는 자바스크립트가 arr의 주소 0x01의 위치로 가서 [] 객체에 접근해 값만 추가했기 때문이다. 따라서 객체가 있는 주소 자체는 변화되지 않고 그대로다. 

 

 

3. 참조 값 할당

자바스크립트에 새 변수를 만들어서 기존에 있던 객체를 복사한다면, 새 변수의 값에는 기존 객체의 주소 값이 복사된다.

const obj = {first: 1};
const obj2 = obj;

console.log(obj);	// {first:1}
console.log(obj2);	// {first:1}

 

 메모리에서는 다음과 같은 형태로 저장한다.

 

 

그런데, obj2.first의 값을 2로 변경한다면?

const obj = { first: 1 };
const obj2 = obj;

obj.first = 2;

console.log(obj);	// { first: 2 }
console.log(obj2);	// { first: 2 }

 

 obj, obj2의 first 모두 2로 변경될 것이다. 왜냐하면 둘 다 0x02라는 동일한 주소 값을 가지고 있기 때문이다. obj2.first = 2의 명령을 실행하면 obj2는 0x02 주소로 접근한다. 그 후에 접근한 주소에서 객체의 first 속성 값을 2로 변경한다. 그런데 obj 역시 0x02의 주소값을 가지고 있기 때문에 obj, ob2 둘 다 first 속성이 2로 바뀌는 것이다. 그러면 메모리는 다음 형태로 두 변수를 저장할 것이다.

 

 

 

4. 참조 값 재 할당하기

 앞에서는 중간에 객체의 속성 값을 변경했을 때의 경우를 설명했다. 그런데 이번에는 참조 값을 중간에 재할당하면 메모리는 어떤 형태로 변수를 저장하는지 알아보도록 하겠다. 다음 코드를 보자.

let obj = { first: 1 };

obj = {second: 2};

console.log(obj); // {second:2};

 

 중간에 { second:2 } 라는 객체를 재할당 했기 때문에 obj를 출력하면 재할당한 객체가 출력 결과로 나올 것이다. 우리는 결과로 바뀐 객체를 바로 볼 수 있기 때문에 재할당 과정을 대수롭지 않게 생각할 수 있다. 하지만 이 과정은 생각보다 단순하지 않다. 이를 메모리 형태로 설명하면 다음과 같다. 밑에 그림은 변수 obj에 새 객체를 할당받을 때의 상황을 나타낸 것이다.

 

 

 객체 { second: 2 }는 메모리 주소 0x03에서 생성된다. 그리고 메모리 주소 값을 변수 obj에 할당한다. 그러면 변수 obj는 메모리 주소 0x03을 가리키게 된다. 그런데 문제가 발생한다. 메모리 낭비가 발생한다는 것이다. 그 이유주소 0x02에 있는 객체 { first:1 }가 아무도 참조하고 있지 않기 때문에 잉여 자원이 되기 때문이다.  자바스크립트에서는 이 문제를 해결하기 위해 자바스크립트 엔진으로 하여금 가비지 컬렉션이라는 동작 명령한다. 가비지 컬렉션은 사용하지 않는 잉여 메모리 자원(객체)을 안전하게 지우는 것을 의미한다. 가비지 컬렉션이 동작한 후, 메모리에서는 0x02 주소에 있는 객체가 완전히 지워진다. 따라서 변수 obj는 완전히 주소 0x03만을 가리키게 된다. 

 

 

 

5. 함수를 통한 파라미터 전달

 우리는 함수를 사용할 때 매개변수를 사용할 때가 있다. 매개변수를 원시 타입(Primitive)으로 줄 경우, 함수의 파라미터 인자는 할당받은 원시 타입(Primitive) 값을 복사한다. 다음 예를 보자.

const ten = 10;
const two = 2;

function add(p1, p2){
	return p1 + p2;
}

const result = add(ten, two);
console.log(result, ten, two) // 12, 10, 2

 

 위의 예제를 보면, add 함수에서 p1은 변수 ten의 값을 받고, p2는 two의 값을 받는다. 어떻게 보면 = 연산자를 써서 값을 복사한 것으로 볼 수 있지만, p1, p2 두 파라미터는 ten, two 아무런 영향을 끼치지 않는다. 이를 메모리 구조로 표현하면 다음과 같다.

 

 

 

6. 순수 함수

 순수 함수란, 함수 바깥 스코프에 아무런 영향도 미치지 않는 함수를 말한다. 함수 매개변수에 원시 타입(Primitive)을 매개변수로 받으면 바깥 스코프에 아무런 영향도 미치지 않는다. 따라서 위 예제 add함수는 순수 함수라고 말할 수 있다. 하지만, 매개변수로 참조 값을 받으면 경우가 다르다. 다음 예제를 보자.

function changeAge(person){
  person.age = 30;
  return person;
}

const james = {name: "James", age: 25};
const changeJames = changeAge(james);

console.log(james); // { name: 'James', age: 30 }
console.log(changeJames); // { name: 'James', age: 30 }

 

 changeAge 함수가 실행되면, 매개변수를 james 변수 값으로 받는다. 변수 james은 레퍼런스 타입이기 때문에 생성된 객체의 주소 값을 가지고 있다. 따라서 매개변수 personjames의 주소 값이 복사된다. 그리고 person james의 주소로 접근해서 age라는 속성을 30으로 바꾸고, person을 반환해서 changeJames 변수 값으로 저장한다. 근데 여기서 주목할 것이 있다. 그것은 changeAge라는 함수가 바깥 소크프에 영향을 줬다는 것이다. 함수 내부에서는 james라는 변수에 대해 따로 연산을 하지 않았는데 불구하고, 함수 바깥에 있는 james의 값에 영향을 줬다. 따라서 changeAge 함수는 순수 함수가 아니다. 그렇다면, 참조 값을 매개변수로 받고, 바깥 스코프에 영향을 끼치지 않는 순수함수가 되는 방법은 없는걸까? 다음 예를 보자.

function changeAge(person){
  const newPerson = JSON.parse(JSON.stringify(person));
  newPerson.age = 30;
  return newPerson;
}

const james = {name: "James", age: 25};
const changeJames = changeAge(james);

console.log(james); // { name: 'James', age: 25 }
console.log(changeJames); // { name: 'James', age: 30 }

 

 출력 결과만 보면, 다르다는 것을 알 수 있다. 왜냐하면 changeAge 함수에서 새 객체를 만들어서 할당했기 때문이다. 좀 더 과정을 자세히 알게 위해 changeAge 함수의 실행 과정을 설명할 필요가 있다. 실행 과정은 다음과 같다.

 

  1. person을 참조 값으로 받음
  2. person이 참조하고 있는 객체를 문자열로 변환
  3. 변환된 문자열로 명시적으로 객체로 형 변환
  4. 형 변환을 하는 과정에서 메모리의 새 주소에 새 객체가 생성됨
  5. 새로운 객체의 주소 값이 newPerson 변수에 저장
  6. newPerson 객체 age 속성을 30으로 변환
  7. newPerson 반환해서 changeJames에 저장

이런 과정에 의해서 changeJames 함수는 바깥 스코프에 아무런 영향도 끼치지 않는다. 새 객체를 생성해서 참조 값을 줬기 때문에 james와 changeJames가 가리키는 값은 다르다. 따라서 === 연산자를 이용해서 둘을 비교하면 False가 출력될 것이다. 고로 changeAge순수함수다.

 

 

7. 참고자료