sbyeol3 / articles

Learn.. Run.. 🏃
34 stars 1 forks source link

[번역] 자바스크립트에서 Value와 Reference의 차이 #10

Open sbyeol3 opened 3 years ago

sbyeol3 commented 3 years ago

원문 : Explaining Value vs. Reference in Javascript 부제 : 어떻게 차이가 있는지 컴퓨터 메모리를 살펴보자.

이 아티클은 저의 온라인 코스 Step Up Your JS: A Comprehensive Guide to Intermediate JavaScript에서 쓰여진 글입니다. 인터랙티브한 코드 플레이그라운드와 온라인 퀴즈를 이 곳에서 즐겨보세요.

자바스크립트는 _value_로 전달되는 5가지의 데이터 타입을 갖고 있습니다 : Boolean, null, undefined, String, 그리고 Number가 있습니다. 이 타입들을 원시 타입이라고 부릅니다.

그리고 _reference_로 전달되는 3가지의 데이터 타입이 있습니다 : Array, Function, Object입니다. 이 타입들은 기술적으로는 객체이기 때문에 통틀어 객체(Objects)라고 부를 것입니다.

원시 타입 (Primitives)

원시 타입이 변수에 할당되면 변수에 원시 값을 갖게 된다고 생각할 수 있습니다.

var x = 10;
var y = 'abc';
var z = null;

x10갖습니다. y'abc'갖습니다. 이 개념을 잘 알기 위해, 이 변수들과 값들이 메모리에서 어떻게 보이는지에 대한 이미지를 보겠습니다.

= 연산자를 사용하여 이 변수들에 다른 변수를 할당할 때 새로운 변수에 값을 복사합니다. 값에 의해 복사되는 것이죠.

var x = 10;
var y = 'abc';

var a = x;
var b = y;

console.log(x, y, a, b); // -> 10, 'abc', 10, 'abc'

ax 모두 10을 갖고, by 모두 'abc'를 갖습니다. 값들은 복사된 것이므로 각 변수들은 분리되어 있습니다.

특정 변수를 수정한다 해도 다른 변수에 영향을 주지 않습니다. 각 변수들끼리는 어떠한 관계도 갖지 않는다고 생각하시면 됩니다.

var x = 10;
var y = 'abc';

var a = x;
var b = y;

a = 5;
b = 'def';

console.log(x, y, a, b); // -> 10, 'abc', 5, 'def'

객체 (Objects)

혼란스러울 수도 있지만 일단 참고 읽어보세요. 읽고 나면 훨씬 쉬워보일 것입니다.

원시 값이 아닌 값을 할당받은 변수들은 값으로 _참조(reference)_를 가집니다. 해당 참조는 메모리에서 객체의 위치를 가리킵니다. 변수들은 사실 실제로 값을 가지고 있는 것은 아닙니다.

여러분의 컴퓨터 메모리의 특정 위치에서 객체가 생성됩니다. arr = []라는 코드를 작성할 때 메모리에서 배열을 하나 생성한 것입니다. 변수 arr은 배열의 위치, 즉 메모리 주소를 받습니다.

addressnumberstring 처럼 값으로 전달되는 새로운 데이터 타입이라고 생각해봅시다. address는 참조로 전달되는 값의 메모리 위치를 가리킵니다. 따옴표로 표시되는 문자열처럼 address<> 기호로 표시됩니다.

참조 타입의 변수를 할당하고 사용할 때 아래처럼 작성합니다.

1) var arr = [];
2) arr.push(1);

첫 번째 줄과 두 번째 줄은 메모리에서 아래와 같이 표현됩니다.

1.

2.

변수 arr에 포함된 값, 즉 주소는 고정되어 있습니다. 메모리에 있는 배열이 변하는 것입니다. arr 에 값을 넣는 것과 같이 배열을 사용할 때 자바스크립트 엔진은 메모리에 arr의 위치로 가서 그 곳에 저장된 정보로 작업하는 겁니다.

참조에 의한 할당 (Assigning by Reference)

참조 타입인 객체가 = 연산자를 사용하여 다른 변수로 복사될 때 해당 값의 주소는 실제로 복사되는 주소 값입니다. 객체는 값 대신에 참조 값이 복사됩니다.

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

위 코드는 메모리에서 아래와 같습니다.

각 변수는 _동일한 배열_의 참조값을 가집니다. 이 말은 reference 변수를 수정하면 refCopy도 같이 변경된다는 것입니다.

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

메모리에 있는 배열에 2를 추가했습니다. referencerefCopy는 같은 배열을 가리키고 있습니다.

참조 값의 재할당 (Reassigning a Reference)

참조 변수를 재할당하면 이전의 참조 값을 대체합니다.

var obj = { first: 'reference' };

메모리에서는 다음과 같습니다.

두 번째 라인이 있다고 해봅시다.

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

obj에 저장된 주소 값이 변경됩니다. 첫 번째 객체는 여전히 메모리에 남아있고 다음 객체도 마찬가지입니다.

#234 값에서 볼 수 있듯이 남아있는 객체에 대한 참조값이 없을 때 자바스크립트 엔진은 가비지 콜렉션을 수행할 수 있습니다. 객체의 모든 참조를 잃고 더 이상 그 객체를 사용할 수 없다는 것을 의미합니다. 그래서 엔진은 안전하게 메모리로부터 제거합니다. 이 경우에 객체 `{ first: 'reference' }는 더 이상 접근할 수 없고 가비지콜렉션의 대상이 됩니다.

== 과 ===

일치 연산자 =====가 참조 타입의 변수에 사용되는 경우 참조 값을 확인합니다. 동일한 항목의 참조값을 갖는 경우 비교 결과는 true가 됩니다.

var arrRef = [’Hi!’];
var arrRef2 = arrRef;

console.log(arrRef === arrRef2); // -> true

별개의 객체라면 동일한 프로퍼티를 갖고 있다 하더라도 비교 결과는 false가 됩니다.

var arr1 = ['Hi!'];
var arr2 = ['Hi!'];

console.log(arr1 === arr2); // -> false

두 개의 객체가 있고 프로퍼티가 같은지 확인하는 가장 쉬운 방법은 두 객체를 문자열로 변환하여 문자열을 비교하는 것입니다. 일치 연산자가 원시값을 비교할 때 단순히 값이 같으지만 비교하기 때문입니다.

var arr1str = JSON.stringify(arr1);
var arr2str = JSON.stringify(arr2);

console.log(arr1str === arr2str); // true

또 다른 선택지는 객체들 내 재귀적인 루프에서 각 프로퍼티가 같은지 비교하는 것입니다.

함수를 통한 파라미터 전달

함수에 원시 값을 전달할 때 함수는 파라미터의 값을 복사합니다. 이는 =을 사용하는 것과 사실상 동일합니다.

var hundred = 100;
var two = 2;

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

var twoHundred = multiply(hundred, two);

위의 예에서 hundred에 값 100을 주었습니다. multiply에 이 값을 전달할 때 x는 값 100을 갖게 됩니다. = 할당을 사용할 때처럼 값이 복사된 것입니다. 역시나 hundred에는 영향을 주지 않습니다. PAUSE 주석에서 메모리가 어떤 상태인지에 대한 스냅샷을 보여드리겠습니다.

순수 함수

외부 스코프에 영향을 끼치지 않는 함수를 _순수함수_라고 합니다. 파라미터로 원시값만 받고 함수를 둘러싼 외부 스코프에 있는 어떠한 변수를 사용하지 않는다면 해당 함수는 자동적으로 외부 스코프에 영향을 끼치지 않는 순수함수가 됩니다. 내부에서 생성되는 모든 변수들은 함수가 리턴될 때 가비지 콜렉터에 의해 수집됩니다.

그러나 함수가 객체를 파라미터로 받는다면 함수를 둘러싼 스코프를 변경시킬 수 있습니다. 배열의 참조값을 받아 해당 배열을 변경시킨다면 함수를 둘러싼 스코프에 있는 변수가 변경되는 것입니다. 함수가 리턴하고 나서도 그 변경사항이 외부 스코프에 유효하게 됩니다. 이는 추적하기 어렵고 원치 않은 사이드이펙트를 일으킬 수 있습니다.

그래서 Array.mapArray.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객체를 반환할 때는 전달받은 객체와 정확히 동일한 객체를 리턴하는 것입니다.alexalexChanged는 동일한 참조값을 갖고 있죠.person` 변수를 반환하고 새로운 배열에 참조값을 저장하는 것은 중복되는 일입니다.

순수 함수의 예를 봅시다.

function changeAgePure(person) {
    var newPersonObj = JSON.parse(JSON.stringify(person));
    newPersonObj.age = 25;
    return newPersonObj;
}

var alex = {
    name: 'Alex',
    age: 30
};

var alexChanged = changeAgePure(alex);

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

이 함수에서는 전달받은 객체를 문자열로 변환하기 위해 JSON.stringify를 사용합니다. 그리고 해당 문자열을 JSON.parse를 사용하여 객체로 파싱합니다. 이 변환 과정을 통해 새로운 객체를 생성했습니다. 동일한 동작을 하는 다른 방법으로는 원본 객체를 통한 반복문 내에서 각 프로퍼티를 새로운 객체에 동일하게 할당해주면 됩니다. 동일한 프로퍼티를 갖는 새로운 객체는 메모리 내에서는 전달받은 객체와는 완전히 별개의 객체입니다.

새로운 객체의 age 프로퍼티를 변경시켜도 원본 객체는 영향을 받지 않습니다. 이 함수가 순수함수이기 때문입니다. 함수 내부 스코프 밖에 있는 어떠한 객체에도 영향을 주지 않고 심지어 전달받은 객체에도 영향을 주지 않습니다. 새로운 객체가 반환되고 새로운 변수에 저장될 수도 있고 함수가 종료됐을 때 해당 객체를 더 이상 사용하지 않는다면 가비지 콜렉터가 수집합니다.

직접 실험해보세요

Value vs. reference비교는 코딩 인터뷰에서 종종 물어보는 개념입니다. 여기서 어떻게 출력될 것인지 직접 알아내보세요.

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 객체의 참조값을 가지기 때문에 처음에는 그 객체를 직접 조작합니다. 새로운 객체로 재할당되고 나서는 원본 객체에 영향을 주지 않습니다.

재할당은 외부 스코프의 personObj1 가리키는 객체를 변경시키지 않습니다. person은 새로운 참조 값을 가지지만 재할당이 personObj1을 변경시키지 않습니다.

위의 코드는 아래 코드와 같습니다.

var personObj1 = {
    name: 'Alex',
    age: 30
};

var person = personObj1;
person.age = 25;

person = {
  name: 'john',
  age: 50
};

var personObj2 = person;

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

두 코드의 차이는 함수의 사용 여부입니다. 함수가 종료되면 person은 더 이상 스코프에 존재하지 않습니다.

끝입니다. 이제 코드를 짜세요.

이 글이 유용하셨다면 하트 버튼을 눌러주시고 저의 다른 글들도 봐주세요.