본문 바로가기
Javascript

JavaScript - 값 타입(Value Type)과 참조 타입(Reference Type)

by Su1993 2020. 9. 7.
반응형

자바스크립트는 값에 의한 전달(passed by value)이 일어나는 원시 타입(Primitive Types)참조에 의한 전달(passed by reference)이 일어나는 데이터 타입이 있다. 데이터 타입은 객체(Object)로 볼 수 있다.

 


 

원시 타입(Primitives)

원시 타입이 변수에 할당된다면, 그 변수를 원시 타입을 가진 변수라 생각할 수 있다.

 

let a = 1;
let b = 'bb';
let c = null;

 

이런 경우 a는 1이란 값을 가지고 있고, b는 'bb'라는 값을 가지고 있다. 이걸 메모리 상에 존재하는 변수들을 다음과 같이 표로 나타낼 수 있다.

 

Variables Values
a 1
b 'bb'
c null

 

이 변수들을 다른 변수에 =를 이용해 할당할 때, 새로운 변수에 값을 복사(copy)하게 된다. 이 변수들은 값에 의해 복사된다.

 

let a = 1;
let b = 'abc';

let c = a;
let d = b;

console.log(a, b, c, d); // 1, 'abc', 1, 'abc'

 

a와 c 그리고 b와 d는 같은 값을 가지고 있지만 분리되어있다. 즉, 값들이 복사된 것이다.

 

Variables Values
a 1
b 'abc'
c 1
d 'abc'

 

같은 값을 가진 변수 하나를 다른 값으로 바꿔도 다른 같은 값을 가진 다른 변수에는 아무런 영향이 없다. 각각 변수들이 아무 관계가 없기 때문이다.

 

객체(Object)

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

객체는 컴퓨터 메모리 어딘가에 생성된다. 만약 arr = []를 작성한다면 메모리 내부에 배열을 만든다. 변수 arr이 갖는 것은 그 배열이 위치한 주소이다. 참조 타입의 변수를 할당하고 사용할 때, 다음과 같은 코드이다.

 

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

 

메모리 안에 표기는 다음과 같다.

 

Variables Values Addresses Objects
arr <#001> #001 []
Variables Values Addresses Objects
arr <#001> #001 [1]

Addresses는 원시 타입과 같이 값에 의한 전달을 하는 새로운 종류의 데이터 타입이라 가정. Addresses는 참조에 의한 전달을 하는 위치를 가리키게 될 것. 문자열이 ' ' 또는 " " 사이에 표기되듯 Addresses는 < > 사이에 표기.

 

arr이 가진 변수의 값(주소)은 정적이다. 변수의 값이 바뀌는 게 아닌 메모리 속의 배열 값만 바뀌는 것이다.

arr을 사용해 값의 push 같은 일을 할 때, 자바스크립트 엔진은 메모리 속의 arr의 위치로 간다. 그리고 거기에 저장된 정보를 이용해 작업을 수행한다.

 

참조로 할당하기

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

 

let a = [1];
let b = a;

 

위 코드는 메모리상에서 다음과 같다.

 

Variables Values Addresses Objects
a <#001> #001 [1]
b <#001>    

 

각각의 변수는 같은 배열로 향하는 레퍼런스를 갖는다. 즉,

 

a.push(2);
console.log(a, b); // [1, 2], [1, 2]

 

2를 메모리 속에 있는 배열에 push를 하면 a와 b를 사용할 때 같은 배열을 가리키게 된다.

 

참조 재할당하기

참조 값을 재할당 하면 먼저 할당 되었던 참조를 대체한다.

 

let obj = { one: 'blue'};

 

메모리 상태

Variables Values Addresses Objects
obj <#234> #234 { one: 'blue' }

 

let obj = { one: 'blue'};
obj = { two: 'yellow'};

 

obj 안에 저장됐던 주소 값이 변경된다.

 

Variables Values Addresses Objects
obj <#678> #234 { one: 'blue' }
    #678 { two: 'yellow' }

 

남아있는 객체를 가리키는 참조가 남아있지 않을 때, 위 표에 주소 값 #234처럼, 자바스크립트 엔진은 가비지 컬렉션(garbage collection)을 동작시킬 수 있다. 이건 프로그래머가 모든 참조를 날리고 객체를 더 이상 사용할 수 없게 된 뒤 자바스크립트 엔진은 그 주소로 가서 사용되지 않는 객체를 메모리로부터 안전하게 지워버리는 것을 의미한다. 이 경우에는 객체 { one: 'blue' }가 더 이상 접근 불가능하고 가비지 콜렉션 될 수 있다. 

 

== 와 ===

동등함을 비교하는 연산자 ==와 ===는 참조 타입의 변수를 비교할 때 사용된다. 이 연산자들은 참조를 체크한다. 만일 변수가 같은 item에 대한 참조를 갖고 있다면, 비교 결과는 true가 나오게 된다.

 

let arr = ['test'];
let arr2 = arr;

console.log(arr === arr2); // true

 

만일 2개의 구분 가능한 객체들을 갖고 프로퍼티가 동일한지 확인하고 싶다면, 가장 쉬운 방법은 2개의 객체들을 문자열로 바꾸고 문자열을 비교하는 것이다. 동등 연산자가 원시 타입을 비교할 때는, 단순히 값이 같은지만 확인한다.

 

let arrStr = JSON.stringify(arr);
let arrStr2 = JSON.stringify(arr2);

console.log(arrStr === arrStr2); //true

 

또 다른 선택지로는 객체를 이용해 재귀적으로 반복을 도는 것이다. 그리고 각각의 프로퍼티가 동일한지 확인할 수 있다.

 

함수를 통한 파라미터의 전달

원시 값들을 함수로 전달할 때, 함수는 값들을 복사해서 파라미터로 전달한다. =연산자를 이용하는 것과 같다.

 

let a = 10;
let b = 20;

function test(x, y) {
	//PAUSE
    return x * y;
}

let c = test(a, b);

 

위 예에서 a라는 변수에 10이라는 값을 주었다. 이 값을 test로 넘겼을 때, 변수 x는 10을 갖게 된다. 값은 =연산자를 써서 할당한 것처럼 복사된다. 또한, 인자로 넘겨진 a라는 변수에는 아무런 영향도 미치지 않는다. 위 PAUSE 상태에서 메모리가 어떻게 구성되는지는 아래와 같다.

 

Variables Values Addresses Objects
a 10 #333 function(x, y)...
b 20    
test <#333>    
x 10    
y 20    

 

Pure Function(순수 함수)

함수 중에 함수 바깥 스코프에 아무런 영향도 미치지 않는 함수를 순수 함수(Pure Function)라고 한다. 함수가 오직 원시 값들만을 파라미터로 이용하고 주변 스코프에서 어떠한 함수도 이용하지 않는다면, 그 함수는 자연스레 순수함수가 된다. 바깥 스코프에는 영향을 끼칠 수 없고, 안에서 만들어진 모든 변수들은 함수에서 반환(return)이 실행되는 즉시 가비지 콜렉션 처리가 된다.

 

객체를 받는 함수는 주변 스코프들의 상태를 변화시킬 수 있다. 만약 함수가 배열 참조값을 가진 변수를 받고 그 변수가 가리키는 배열에 push를 수행하면, 그 주변 스코프에 존재하는 변수들과 그 참조(reference)와 그 배열이 변하는 것을 볼 수 있다. 함수 리턴 후에, 변화된 것들은 바깥 스코프에 여전히 남아있게된다. 이런 현상은 원하지 않는 방향으로 부작용(side-effect)을 줄 수 있다.

 

그래서 Array.map과 Array.filter를 포함한 많은 네이티브 배열 함수들은 순수 함수로 작성되어 있다. 배열 참조를 받아서 내부적으로 배열을 복사하고 원본 대신 복사된 배열로 작업한다. 그래서 원본도 건드리지 않고 바깥 스코프에 영향도 미치지 않고 새로운 배열의 참조를 반환하게 된다.

 

[순수 함수와 아닌 함수 비교]

 

(순수 함수가 아닌 함수)

function a(person) {
    person.age = 20;
    return person;
}

let sam = {
    name: 'Sam',
    age: 30
};

let changedSam = a(sam);

console.log(sam); // {name: 'Sam', age: 20}
console.log(changedSam); // {name: 'Sam', age: 20}

 

이 비순수함수는 객체를 받아서 age 프로퍼티를 20이라는 값으로 변경한다. 객체로 받아온 값에 그대로 명령을 실행하기 때문에 이 함수는 sam 객체를 직접적으로 변화시킨다. 이 함수가 person 객체를 반환할 때, 받았던 객체 그대로를 반환한다. sam과 changedSam은 같은 참조를 가진다. person 변수를 반환하고 그 참조를 다시 새로운 변수에 저장하는 것은 불필요하다.

 

(순수 함수)

function b(person) {
    let newPerson = JSON.parse(JSON.stringify((person));
    newPerson.age = 20;
    return newPerson;
};

let sam = {
    name: 'Sam',
    age: 25
};

let samChanged = b(sam);

console.log(sam); // {name: 'sam', age:25}
console.log(samChanged); // {name: 'sam', age:20}

 

이 함수에서 넘겨받은 객체를 문자열로 변화시키기 위해 JSON.stringify 함수를 사용한다. 그리고 JSON.parse 함수를 이용하여 다시 객체로 만든다. 이러한 과정을 거치면서 새로운 객체를 만들고 그 결과 값을 새로운 변수에 저장한다. 원본 객체의 프로퍼티를 반복해서 새로운 객체에 할당하는 방법도 있다. 하지만 위 방법이 가장 간단하다. 새 객체는 원본과 같은 프로퍼티들을 가진다. 하지만 메모리 상에서는 이 두 객체는 다른 주소 값을 가지고 구분될 수 있다.

 

순수 함수이기 때문에 새로운 객체에서 age 프로퍼티를 변경할 때, 원본은 전혀 영향을 받지 않는다. 새롭게 만들어진 객체는 반환되어야 하고, 새로운 변수에 저장되어야 한다. 그렇지 않으면 결과 값은 가비지 콜렉션 될 것이고 결과 객체는 남지 않게 된다.

반응형

'Javascript' 카테고리의 다른 글

JavaScript - == vs === vs Typeof  (0) 2020.09.14
JavaScript - Type Coercion(형 변환)  (0) 2020.09.08
JavaScript - Primitive Type  (0) 2020.09.06
JavaScript - Call Stack(호출 스택)  (0) 2020.09.03
JavaScript - JSON  (0) 2020.09.02

댓글