본문 바로가기

스터디/모던 자바스크립트 딥 다이브

[딥 다이브 스터디 11장] 원시 값과 객체 (feat. 참조와 복사)

<모던 자바스크립트 딥 다이브> (이웅모, 위키북스) 를 읽고 공부한 내용입니다. 책의 내용을 그대로 적어 놓은 것이 아니기 때문에 오류가 있을 수 있습니다. 오류가 있다면 댓글을 통해 피드백 부탁드립니다.

들어가기 전에

1. 아래 코드는 어떤 결과를 반환할까?

let strings = "hello";

strings[strings.length - 1] = "u";

console.log(strings); // 결과는?

2. array는 const로 선언되었는데, 변경되었다. 이유는 무엇일까?

const array = [1, 2, 3];

array[2] = 4;

console.log(array); // [1, 2, 4]

3. 아래 코드는 어떤 결과를 반환할까?

const a = 1;
const b = 1;
console.log(a === b);

const array1 = [1, 2, 3];
const array2 = [1, 2, 3];
console.log(array1 == array2)
console.log(array1 === array2)

🚩 이 글을 읽고 나면...

✔ 변수와 상수, 메모리와 가비지 콜렉터의 동작 메커니즘을 알 수 있다.
✔ 원시 값과 객체의 차이를 알 수 있다.
✔ 참조가 무엇인지 설명할 수 있다.
✔ 얕은 복사와 깊은 복사에 관해 설명할 수 있다.


[1] 변수(상수)의 선언, 할당, 재할당, 가비지 콜렉터

자바스크립트는 컴파일 과정이 없다.(현대의 자바스크립트는 자바스크립트 엔진 제조사에 따라 엔진 내부에서 컴파일 과정을 거친다) 인터프리터 언어이다. 자바스크립트의 인터프리터는 자바스크립트 엔진이다. 그리고 자바스크립트는 바로 실행된다. 하지만 실행되기 이전에 거치는 단계가 하나 있다. 바로 자바스크립트 엔진이 모든 스크립트를 읽어들이는 해석과정이다.

이 과정에서 자바스크립트 엔진은 let strings를 읽어들인다.(변수의 선언과정) 그리고 실행컨텍스트에 strings라는 변수가 있다는 것을 알고 있는다. 그리고 런타임 과정에서 = "hello"를 만났을 때, strings가 "hello"를 가리키고 있다는 것을 알고 strings"hello"로 할당한다. (변수의 할당과정) 그리고 strings는 let 키워드로 선언되었기 때문에 재할당이 가능하다. 따라서 strings = "world"를 만났을 때 strings"world"를 가리키도록 한다. (이 때 메모리 공간 어딘가에 "world"가 생성되고 strings는 이 메모리 공간 위치를 기억하는 것이다.) 그리고 원래 메모리 공간 어딘가에 저장되어있던 "hello"는 더 이상 사용되지 않는다면 어떤 시점(언제가 될지는 모른다)에 가비지 콜렉터에 의해 메모리 공간에서 사라진다.

[2] 자바스크립트의 자료형(데이터 타입)

다시 자바스크립트의 자료형을 생각해보자. 자바스크립트의 자료형은 크게 원시 자료형과 객체 자료형 2가지로 나눌 수 있다. 원시 자료형은 숫자, 문자열, 불리언, undefined, null, Symbol 로 총 6가지이고, 객체 타입은 객체, 함수, 배열 등이다.

원시 값은 변경 불가능(immutable) 하고 객체 타입은 변경이 가능하다. 따라서 값을 변경할 수 없다는 것은 위 예제에서 보았던 "hello"와 "world"에 해당하는 이야기 이다.

[2-1] '원시 값'에 대한 퀴즈

아래 코드는 어떤 결과를 반환할까?

let strings = "hello";

strings[strings.length - 1] = "u";

console.log(strings); // 결과는?

이 코드를 실행하면 에러를 출력한다. (브라우저에서 실행하면 정상동작하고 strings 값은 변경되지 않는다) 왜냐하면 "hello" 는 변경불가능(immutable) 한 원시 값이기 때문에 문자열 자체를 변경할 수 없다. 변경이 불가능하다는 것은 이런 것이다.

[2-2]const는 값을 변경할 수 없다 → 재할당을 할 수 없다

const로 상수(정확히 말하자면 상수)를 선언하면 값을 변경할 수 없다. '값을 변경한다' 라는 표현 보다는 '재할당이 안된다' 라고 하는 것이 더 적합할 듯 하다. 아래 예시에서, array는 const 키워드로 선언되어 [1, 2, 3] 이라는 값(객체 타입의 값)이 할당되었다.

const array = [1, 2, 3];

array[2] = 4;

console.log(array); // [1, 2, 4]

자바스크립트를 처음 공부한다면, const로 선언된 값이 바뀌었으므로 의문을 가질 수 있을 것이다.

[2-3] 원시 타입이 아닌 값은 변경할 수 있는(mutable) 값이다.

앞서 설명했듯이, const는 재할당이 불가능한 것이다. 그리고 const로 생성된 식별자(상수명)는 그 존재 자체가 '값'이 아니라 메모리 어딘가에 저장되어있는 '값'을 가리키고 있는 포인터이다. 변수(let)는 포인터를 변경(재할당)할 수 있는 것이고 상수(const)는 변경할 수 없는 것이다.

const array[1, 2, 3]이라는 메모리 어딘가에 저장된 객체를 가리키고 있는 것이고, 이 객체는 원시 값이 아니므로 변경이 가능하다.

[3] 값의 복사와 참조

원시 값은 그대로 복사된다. 하지만 객체는 조금 특별하다.

[3-1] 원시 값

const a = 10;
let b = a;
console.log(a === b) // true
console.log(a, b) // 10 10

let b = a에서, b에 a를 할당했다. a는 10을 가리키고 있고, 10은 원시 값이다. 따라서 메모리에는 또 다른 10이 생기고, b는 그 10을 가리킨다. 따라서 두 10은 다른 메모리에 저장되어있다. 하지만 두 10은 원시 값이므로 똑같다. 그래서 a === b는 참이다. b를 let으로 선언했으니 b의 값을(정확히는 가리키는 포인터를) 변경해보면

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

너무나도 당연한 이야기 같다. 그런데 이것은 원시값이기 때문이다.

const a = 1;
const b = 1;
console.log(a === b); // true (당연한 결과?)

const array1 = [1, 2, 3];
const array2 = [1, 2, 3];
console.log(array1 == array2) // false
console.log(array1 === array2) // false

이유는 무엇일까? 객체는 원시 값이 아니기 때문에 서로 다른 객체인 것이다. 객체에 대해서 조금 더 탐구해보자.

[3-2] 객체

const a = [1, 2, 3];
const b = a;
console.log(a === b); // true (당연한 결과?)
console.log(a, b); // [ 1, 2, 3 ]  [ 1, 2, 3 ]

b[2] = 100;

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

이번에는 만들어진 [1, 2, 3]을 a에 할당하고 a를 그대로 b에 할당했다. 좀 전에는 똑같은 [1, 2, 3]이라도 두 번 생성을 했으니 서로 다른 메모리 공간에 생성된 객체였지만, 이번에는 한번 생성된 객체를 그대로 할당한 것이니 같은 메모리 주소를 공유하고 있다는 것이다.

따라서 a와 b는 같은 객체를 가리키고 있다. 그래서 b[2]를 변경했음에도 a까지 변경되어버렸다. 정확히 말하자면 a, b가 변경된 것이 아니라 메모리 공간 어딘가에 있던 [1, 2, 3] 이라는 객체가 변경되었다는 것을 이해할 수 있을 것이다.

[3-3] 참조

원시 값은 변수로 할당해도 복사가 되었지만 객체는 그렇지 않다. 객체의 경우 복사가 아닌 참조라고 한다.

여기서 잠깐) '복사'와 '참조'라는 표현에 대해

복사와 참조라는 용어는 '표현'에 차이가 있을 것 같다.

 const a = [1, 2, 3];
 const b = a;

를 두고 b가 a를 복사했다(이 경우 얕은 복사) 라고 하는 사람도 있고 b가 a를 참조했다 라고 하는 사람도 있을 것이다.

나는 '복사'는 복사를 했다면 원본이 변경되지 않아야 한다고 생각한다. 따라서 '참조'라고 표현하겠다.

[4] 객체의 복사

복사되었다면 원본이 변경되지 않아야 한다. 객체의 복사에는 소위 얕은 복사와 깊은 복사가 있다.

얕은 복사와 깊은 복사는 객체의 깊이에서 차이를 느낄 수 있다.

[4-1] 객체의 얕은복사 - 스프레드 문법

스프레드 문법 말고도 몇 가지가 더 있지만, 스프레드 문법이 가장 활용도가 높다.

const person1 = {
  name: "Yong",
  age: 20,
  info: {
    graduation: false,
  },
};

const person2 = { ...person1 };
person2.name = "Lee";
person2.info.graduation = true;

console.log(person1); // { age: 20, info: { graduation: true }, name: 'Yong' }
  1. 스프레드 문법은 얕은복사가 된다. 깊이가 1인 name은 잘 복사되었기 때문에 원본이 유지됐다.
  2. 깊이가 1보다 커지는 person1.info와 person2.info는 같은 메모리 주소를 공유한다. 그래서 두 객체의 info.graduation의 값은 참조가 된다. 따라서 원본이 유지되지 않는다.

[4-2] 깊은 복사의 방법 3가지

1. 복사와 관련된 라이브러리를 사용한다

대표적으로 유명한 자바스크립트 유틸 라이브러리인 lodash가 있다.

2. JSON.parse와 JSON.stringify

JSON.stringify는 자바스크립트 코드 형태의 객체를 JSON "문자열"로 변환한다. 따라서 원시 값이 된다. 그리고 JSON.parse를 이용해서 다시 객체로 만들어서 다른 변수(상수)에 담는다면 새로운 객체가 되었으므로 완전 복사 되었다고 할 수 있다. (하지만 성능이 좋지 않으므로 객체의 복사를 위해 사용하는 것은 지양하는 것이 좋다.)

3. 재귀함수 사용

재귀함수를 사용해서 깊은 복사(새로운 메모리주소를 가지도록 새롭게 객체를 생성하는 것)를 할 수 있다.

function deepCopy(obj) {
  const clone = {};
  for (const key in obj) {
    // value가 object라면
    if (typeof obj[key] === "object" && obj[key]) {
      clone[key] = deepCopy(obj[key]);
    } // 다른 원시값이라면
    else {
      clone[key] = obj[key];
    }
  }
  return clone;
}

const person3 = deepCopy(person1);
console.log(person1); // { age: 20, info: { graduation: true }, name: 'Yong' }
console.log(person3); // { age: 20, info: { graduation: true }, name: 'Yong' }
person3.name = "Kim";
person3.info.graduation = false;

// 깊은 복사로 다른 객체(다른 메모리 주소를 참조하는 객체)가 되었으므로 원본 객체 person1의 값은 바뀌지 않았다.
console.log(person1); // { age: 20, info: { graduation: true }, name: 'Yong' }
console.log(person3); // { age: 20, info: { graduation: false }, name: 'Kim' }

정리

객체를 다룰 땐 조심하자...

💡 오늘의 (스터디) 회고

팀원들과 첫 스터디를 진행했다. 첫 스터디인 만큼 아직 부족한 점이 많았지만, 분위기는 좋았다!😆 나는 한번도 스터디라는 것을 해본 적이 없어서 스터디가 정말 좋은가? 라는 의문이 아직 있긴 하다. 그런데 오늘 스터디를 진행하면서 내가 정리한 내용에서 내가 잘 못 알고 있는 내용이 몇 가지를 발견했다. 피드백을 받았으니 다시 알아본 결과, 정말 내가 잘 못 알고 있는 내용이었다. 자바스크립트는 컴파일 과정이 없다라는 것이었는데, 스터디 때 피드백을 받고 다시 알아보니 현대 자바스크립트는 제조사(V8의 경우 구글)마다 어느정도 컴파일 과정이 있다는 것이었다. 예전에 썼던 글에서도 컴파일 과정이 없다라고 못박아 뒀었는데, 혼자 공부했다면 계속 단호박처럼 자바스크립트는 컴파일 과정 없음. 이라고 알고 있었을 것 같다. 피드백 해주신 승희님께 감사드린다.
오늘 지은 멘토님과 커피챗도 있었다. 코드 리뷰에 대해서 여쭤봤는데, 나는 무의식중에 코드리뷰나 스터디에서 '지적'을 받을까 두려워하던 것이 있었던 것 같다. '지적'이 아니라 '피드백'이라는 점을 새겨야 할 것 같다. 그리고 전혀 두려워하지 않아야 할 것 같다. (그렇다고 대충 공부한다는 것은 아니고...) 피드백을 받으면 정말 감사한 것이다. 나 혼자 공부했으면 몰랐던 내용을 알게된 것이니... 아직 성장하려면 갈 길이 멀다. 그래도 동료와 함께 성장한다는 것이 무엇인지 알게되는 것 같아 다행히고 또 너무 좋다 :)