처음부터 차근차근

[코어 자바스크립트] JavaScript 데이터 타입(심화) 본문

Language/JavaScript

[코어 자바스크립트] JavaScript 데이터 타입(심화)

HangJu_95 2023. 5. 28. 20:43
728x90

JavaScript의 데이터 타입은 크게 두 가지로 나뉜다.

 

(이미지 출처 : https://velog.io/@imjkim49/자바스크립트-데이터-타입-정리 )

두 개의 구분 기준은 값의 저장 방식불변성 여부이다.

 

 [기본형과 참조형의 구분 기준]

  1. 복제의 방식
    1. 기본형 : 값이 담긴 주소값을 바로 복제
    2. 참조형 : 값이 담긴 주소값들로 이루어진 묶음을 가리키는 주소값을 복제
  2. 불변성의 여부 (데이터 값이 아닌, 메모리 관점에서 봐야 한다)
    1. 기본형 : 불변성을 띔
    2. 참조형 : 불변성을 띄지 않음

불변성의 경우, 기본형이 이해되지 않을 수 있다. 이를 위해서는 메모리와 데이터에 관한 기본 지식이 필요하다.

 

1, 메모리, 데이터

 - 비트

  • 컴퓨터가 이해할 수 있는 가장 작은 단위
  • 0,1을 가지고 있는 메모리를 구성하기 위한 작은 조각을 의미
  • 이러한 작은 조각이 모여서 메모리가 만들어진다.

- 바이트(byte)

  • 0과 1만 표현하는 비트를 모두 찾기는 부담
  • 8개의 비트를 합쳐서 새로운 단위 (byte) 형성

(이미지 출처 : https://namu.wiki/w/바이트 )

- 메모리(Memo + ry) : byte 단위로 구성

모든 데이터는 byte 단위의 식별자인 메모리 주소값을 통해서 서로 구분이 된다.

만일, 64비트(8바이트) 정수는 메모리에 어떻게 저장할 수 있을까요?

⇒ 64비트를 8개의 바이트로 분할하고, 각 바이트를 메모리에 저장해야 해요. 각 바이트는 8개의 비트를 가므로 64비트 정수는 메모리에서 8개의 연속된 바이트에 저장된답니다.

 

- javascript의 메모리 관리 방식(다른 언어와 비교)

다른 언어(java, C언어 등)들은 데이터 타입의 크기에 따라 다양하게 지정해줘야 할 만큼 개발자가 handling할 요소가 많았지만, JS는 정수의 경우 8byte를 사용하여 저장한다

 

ex) 8을 저장하는 방법

  1. JS : let a = 8(8byte)
  2. JAVA
    1. byte a = 8(1byte)
    2. short a = 8(2byte)
    3. int a = 8(4byte)
    4. long a = 8(16byte)

변수 : 변할 수 있는 수,  즉, 변할 수 있는 무언가를 뜻하며, 여기서는 데이터를 말한다

식별자 : 어떤 데이터를 식별하는 데 사용하는 이름, 즉 변수명이다.

var testValue =3

"변할 수 있는 데이터를 만든다. 이 데이터의 식별자는 testValue로 하며, 데이터는 3이다." 로 이해하면 된다.

이렇게 보면 변수란 결국 변경 가능한 데이터가 담길 수 있는 공간 또는 그릇이라고 할 수 있다.

 

2. 변수 선언과 데이터 할당

1) 할당 예시 (풀어 쓴 방식, 붙여 쓴 방식 동일하게 동작)

/** 선언과 할당을 풀어 쓴 방식 */
var str;
str = 'test!';

/** 선언과 할당을 붙여 쓴 방식 */
var str = 'test!';
주소(변수명) 1001 1002 1003 1004 1005 ...
데이터 변수명 var
데이터 @5001
         
주소(데이터) 5001 5002 5003 5004 5005 ...
데이터 'test!'          

 자바스크립트는 변수 영역에 값을 넣지 않고, 데이터 영역에 값을 넣는다.

※ 왜 값을 바로 변수에 대입하지 않는가??(= 무조건 새로 만드는 이유!)

  1. 자유로운 데이터 변환
    1. 이미 입력한 문자열이 길어진다면?
    2. 숫자는 항상 8byte로 고정이지만, 문자는 고정이 아니에요(영문 : 1byte, 한글 : 2byte). 그래서, 이미 1003 주소에 할당된 데이터를 변환하려 할 때 훨씬 더 큰 데이터를 저장하려 한다면 → 1004 이후부터 저장되어있는 모든 데이터를 오른쪽으로 다~~~ 미뤄야 하겠죠..!?
  2. 메모리의 효율적 관리
    1. 똑같은 데이터를 여러번 저장해야 한다면?
    2. 1만개의 변수를 생성해서 모든 변수에 숫자 1을 할당하는 상황을 가정해 봅시다. 모든 변수를 별개로 인식한다고 한다면, 1만개의 변수 공간을 확보해야 해요.
      1. 바로 대입하는 case) 숫자형은 8 바이트 고정이죠?
        1. 1만개 * 8byte = 8만 byte
      2. 변수 영역에 별도 저장 case)
        1. 변수 영역 : 2바이트 1만개 = 2만 byte
        2. ℹ️ 이해를 돕고자, 변수 영역에 저장되는 데이터는 2바이트로 가정했어요!
        3. 데이터 영역 : 8바이트 1개 = 8byte
        4. 총 : 2만 8바이트

 

3. 기본형 데이터와 참조형 데이터

메모리를 기준으로 다시 한번 생각해보는 두 가지 주요 개념

 

1) 변수 vs 상수

  • 변수 : 변수 영역 메모리를 변경할 수 있음
  • 상수 : 변수 영역 메모리를 변경할 수 없음

2) 불변하다 vs 불변하지 않다

  • 불변하다 : 데이터 영역 메모리를 변경할 수 없음
  • 불변하지 않다 : 데이터 영역 메모리를 변경할 수 있음
  • 여기서 변경이란, 데이터를 새로 만드는 동작을 의미하며, 이는 불변값의 성질을 의미한다.

- 불변값과 불변성(with 기본형 데이터)

// a라는 변수가 abc에서 abcdef가 되는 과정을 통해 불변성을 유추해봅시다!

// 'abc'라는 값이 데이터영역의 @5002라는 주소에 들어갔다고 가정할게요.
var a = 'abc';

// 'def'라는 값이 @5002라는 주소에 추가되는 것이 아니죠!
// @5003에 별도로 'abcdef'라는 값이 생기고 a라는 변수는 @5002 -> @5003
// 즉, "변수 a는 불변하다." 라고 할 수 있습니다.
// 이 때, @5002는 더 이상 사용되지 않기 때문에 가비지컬렉터의 수거 대상이 됩니다.
a = a + 'def';

- 가변값과 가변성(with 참조형 데이터)

1) 참조형 데이터의 변수 할당 과정

// 참조형 데이터는 별도 저장공간(obj1을 위한 별도 공간)이 필요합니다!
var obj1 = {
    a: 1,
    b: 'bbb',
};
주소(변수명) 1001 1002 1003 1004
데이터 obj1, 7103~4      
주소(데이터) 5001 5002 5003 5004
데이터 1 'bbb'    
주소(obj1) 7103 7104 7105 7106
데이터 a, 5001 b, 5002    

> 기본형 데이터의 변수 할당 과정과 차이점 : 객체의 변수(프로퍼티) 영역의 별도 존재 여부

 

> 만약 데이터가 변경된다면??

// 데이터를 변경해봅시다.
obj1.a = 2;
주소(변수명) 1001 1002 1003 1004
데이터 obj1, 7103~4      
주소(데이터) 5001 5002 5003 5004
데이터 1 'bbb' 2(새로운 값)  
주소(obj1) 7103 7104 7105 7106
데이터 a, 5003(변동) b, 5002    

※ 데이터 영역에 저장된 값은 여전히 계속 불변이지만, obj1을 위한 별도 영역은 얼마든지 변경이 가능하다.

즉, 변수에는 다른 값을 얼마든지 대입할 수 있다.

=> 참조형 데이터를 흔히 '불변하지 않다.(=가변하다)' 라고 한다.

 

2) 중첩객체의 할당

 - 중첩객체란, 객체 안에 또 다른 객체가 들어가는 것.

 - 배열을 포함하는 객체도 중첩객체로 들어간다.

 

  • 참조 카운트 : 어떤 데이터에 대해 자신의 주소를 참조하는 변수의 개수

참조 카운트가 0인 메모리 주소는 가비지 컬렉터의 수거 대상이 된다.

 

var obj = {
    x: 3,
    arr: [3, 4, 5],
}

// obj.arr[1]의 탐색과정은 어떻게 될까요? 작성하신 표에서 한번 찾아가보세요!

 

주소(변수명) 1001 1002 1003 1004 1005 ...
데이터 ojb1, 7103~4          
주소(데이터) 5001 5002 5003 5004 5005 ...
데이터 3 4 5      
주소(obj1) 7103 7104 ....
데이터 x, 5001 arr(8104~6)  
주소(arr) 8104 8105 8106
데이터 a[0],5001 a[1],5002 a[2],5003

 

3) 변수 복사의 비교

 

// STEP01. 쭉 선언을 먼저 해볼께요.
var a = 10; //기본형
var obj1 = { c: 10, d: 'ddd' }; //참조형

// STEP02. 복사를 수행해볼께요.
var b = a; //기본형
var obj2 = obj1; //참조형
주소(변수명) 1001 1002 1003 1004 1005 ...
데이터 a,5001 obj1,7103~4 b,5001 obj2,7103~4    
주소(데이터) 5001 5002 5003 5004 5005 ...
데이터 10 'ddd'        
주소(obj1) 7103 7104 ....
데이터 c, 5001 d,5002  

 

3 -1) 복사 이후 값 변경(객체 프로퍼티 변경)

 

// STEP01. 쭉 선언을 먼저 해볼께요.
var a = 10; //기본형
var obj1 = { c: 10, d: 'ddd' }; //참조형

// STEP02. 복사를 수행해볼께요.
var b = a; //기본형
var obj2 = obj1; //참조형

b = 15;
obj2.c = 20;

 

주소(변수명) 1001 1002 1003 1004 1005 ...
데이터 a,5001 obj1,7103~4 b,5003 obj2,7103~4    
주소(데이터) 5001 5002 5003 5004 5005 ...
데이터 10 'ddd' 15 20    
주소(obj1) 7103 7104 ....
데이터 c, 5004 d,5002  

 

- 기본형

  • 숫자 15라는 값을 데이터 영역에서 검색 후 없다면 생성
  • 검색한 결과주소 또는 생성한 주소를 변수 영역 b에 갈아끼움
  • a와 b는 서로 다른 데이터 영역의 주소를 바라보고 있기 때문에 영향 없음

- 참조형

  • 숫자 20이라는 값을 데이터 영역에서 검색 후 없다면 생성
  • 검색한 결과주소 또는 생성한 주소 obj2에게 지정되어 있는 별도 영역(7103~)에 갈아끼움
  • obj1도 똑같은 주소를 바라보고 있기 때문에 obj1까지 변경이 됨
  • 바로 아래와 같은 현상이 발생
// 기본형 변수 복사의 결과는 다른 값!
a !== b;

// 참조형 변수 복사의 결과는 같은 값!(원하지 않았던 결과)
obj1 === obj2;

 

3-2) 복사 이후 값 변경(객체 자체를 변경)

//기본형 데이터
var a = 10;
var b = a;

//참조형 데이터
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;

b = 15;
obj2 = { c: 20, d: 'ddd'};
주소(변수명) 1001 1002 1003 1004 1005 ...
데이터 a,5001 obj1,7103~4 b,5003 obj2,8103~4    
주소(데이터) 5001 5002 5003 5004 5005 ...
데이터 10 'ddd' 15 20    

 

주소(obj1) 7103 7104 ....
데이터 c, 5001 d,5002  
주소(obj1) 8103 8104 ....
데이터 c, 5004 d,5002  

 - obj2 변수는 참조형 데이터이고, 참조형 데이터의 값이 변경한 것임에도 불구하고 이전 케이스와는 다르게 obj1과는 바라보는 데이터 메모리 영역의 값이 달라짐.

- 참조형 데이터가 ‘가변값’이라고 할 때의 ‘가변’은 참조형 데이터 자체를 변경할 경우가 아니라, 그 내부의 프로퍼티를 변경할 때 성립

 

4. 불변 객체

1) 불변 객체의 정의

 - 객체의 속성에 접근해서 값을 변경하면 가변이 성립.

 - 반면, 객체 데이터 자체를 변경(새로운 데이터를 할당) 하고자 한다면 기존 데이터는 변경되지 않음.

 -> 즉 불변하다.

 

2) 불변 객체의 필요성

 - 아래 예시를 확인하자.

// user 객체를 생성
var user = {
    name: 'wonjang',
    gender: 'male',
};

// 이름을 변경하는 함수, 'changeName'을 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티(속성)에 접근해서 이름을 변경했네요! -> 가변
var changeName = function (user, newName) {
    var newUser = user;
    newUser.name = newName; // user 객체의 속성에 접근하여 이름 변경!
    return newUser;
};

// 변경한 user정보를 user2 변수에 할당하겠습니다.
// 가변이기 때문에 user1도 영향을 받게 될거에요.
var user2 = changeName(user, 'twojang');

// 결국 아래 로직은 skip하게 될겁니다.
if (user !== user2) {
    console.log('유저 정보가 변경되었습니다.');
}

console.log(user.name, user2.name); // twojang twojang
console.log(user === user2); // true

newUser라는 식별자에 user의 데이터 주소 값을 할당하였다.

그러나 바로 밑에 보면 데이터의 프로퍼티를 변경하였으며, 이는 이 데이터를 참조하는 모든 식별자의 값이 변경되는 결과를 만든다.

 

이것의 문제점은 아래와 같이 변경할 수 있다.

// 이름을 변경하는 함수 정의
// 입력값 : 변경대상 user 객체, 변경하고자 하는 이름
// 출력값 : 새로운 user 객체
// 특징 : 객체의 프로퍼티에 접근하는 것이 아니라, 아에 새로운 객체를 반환 -> 불변
var changeName = function (user, newName) {
    return {
        name: newName,
        gender: user.gender,
    };
};

// 변경한 user정보를 user2 변수에 할당하겠습니다.
// 불변이기 때문에 user1은 영향이 없어요!
var user2 = changeName(user, 'twojang');

// 결국 아래 로직이 수행되겠네요.
if (user !== user2) {
    console.log('유저 정보가 변경되었습니다.');
}

console.log(user.name, user2.name); // wonjang twojang
console.log(user === user2); // false 👍

changeName이라는 함수가 새로운 객체를 반환하도록 수정했다.

-> 그러나 이 방법은 문제점이 있다.

- changeName 함수는 새로운 객체를 만들기 위해 변경할 필요가 없는 gender 프로퍼티를 하드코딩으로 입력 ⇒ 만일 이러한 속성이 10개라면?

-> 얕은 복사라는 방법 제기

 

3) 얕은 복사

a. 패턴과 적용

//이런 패턴은 어떨까요?
var copyObject = function (target) {
    var result = {};

    // for ~ in 구문을 이용하여, 객체의 모든 프로퍼티에 접근할 수 있습니다.
    // 하드코딩을 하지 않아도 괜찮아요.
    // 이 copyObject로 복사를 한 다음, 복사를 완료한 객체의 프로퍼티를 변경하면
    // 되겠죠!?
    for (var prop in target) {
        result[prop] = target[prop];
    }
    return result;
}
//위 패턴을 우리 예제에 적용해봅시다.
var user = {
    name: 'wonjang',
    gender: 'male',
};

var user2 = copyObject(user);
user2.name = 'twojang';

if (user !== user2) {
    console.log('유저 정보가 변경되었습니다.');
}

console.log(user.name, user2.name);
console.log(user === user2);

- But, 얕은 복사의 경우 첩된 객체에 대해서 완벽한 복사를 할 수 없다.

  • 얕은 복사 : 바로 아래 단계의 값만 복사(위의 예제) 문제점 : 중첩된 객체의 경우 참조형 데이터가 저장된 프로퍼티를 복사할 때, 주소값만 복사
  • 깊은 복사 : 내부의 모든 값들을 하나하나 다 찾아서 모두 복사하는 방법

다음 예시를 확인해보자.

var user2 = copyObject(user);

user2.name = 'twojang';

// 바로 아래 단계에 대해서는 불변성을 유지하기 때문에 값이 달라지죠.
console.log(user.name === user2.name); // false

// 더 깊은 단계에 대해서는 불변성을 유지하지 못하기 때문에 값이 같아요.
// 더 혼란스러워 지는거죠 ㅠㅠ
user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio); // true

// 아래 예도 똑같아요.
user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog); // true

-> 결국, user.urls 프로퍼티도 불변 객체로 만들어야 한다.

 

4) 중첩된 객체에 대한 깊은 복사 살펴보기

var user = {
    name: 'wonjang',
    urls: {
        portfolio: 'http://github.com/abc',
        blog: 'http://blog.com',
        facebook: 'http://facebook.com/abc',
    }
};

// 1차 copy
var user2 = copyObject(user);

// 2차 copy -> 이렇게까지 해줘야만 해요..!!
user2.urls = copyObject(user.urls);

user.urls.portfolio = 'http://portfolio.com';
console.log(user.urls.portfolio === user2.urls.portfolio);

user2.urls.blog = '';
console.log(user.urls.blog === user2.urls.blog);

=> 객체의 프로퍼티 중, 기본형 데이터는 그대로 복사 + 참조형 데이터는 다시 그 내부의 프로퍼티를 복사

-> 재귀적 수행! (함수나 알고리즘이 자기 자신을 호출하여 반복적으로 실행되는 것)

 

※ 재귀적 수행한 코드.

var copyObjectDeep = function(target) {
    var result = {};
    if (typeof target === 'object' && target !== null) {
        for (var prop in target) {
            result[prop] = copyObjectDeep(target[prop]);
        }
    } else {
        result = target;
    }
    return result;
}
//결과 확인
var obj = {
    a: 1,
    b: {
        c: null,
        d: [1, 2],
    }
};
var obj2 = copyObjectDeep(obj);

obj2.a = 3;
obj2.b.c = 4;
obj2.b.d[1] = 3;

console.log(obj);
console.log(obj2);

-> 깊은 복사 완료.

5) 마지막 방법! JSON(=JavaScript Object Notation)을 이용하는 방법도 존재합니다. 하지만 완벽한 방법은 아니에요. 간략히 장/단점을 정리해드리니, 내용만 참고해주세요 

  • JSON.stringify() 함수를 사용하여 객체를 문자열로 변환한 후, 다시 JSON.parse() 함수를 사용하여 새로운 객체를 생성하기 때문에, 원본 객체와 복사본 객체가 서로 독립적으로 존재합니다. 따라서 복사본 객체를 수정해도 원본 객체에 영향을 미치지 않습니다.
  • JSON을 이용한 깊은 복사는 다른 깊은 복사 방법에 비해 코드가 간결하고 쉽게 이해할 수 있습니다.

단점:

  • JSON을 이용한 깊은 복사는 원본 객체가 가지고 있는 모든 정보를 복사하지 않습니다. 예를 들어, 함수나 undefined와 같은 속성 값은 복사되지 않습니다.
  • JSON.stringify() 함수는 순환 참조(Recursive Reference)를 지원하지 않습니다. 따라서 객체 안에 객체가 중첩되어 있는 경우, 이 방법으로는 복사할 수 없습니다.

따라서 JSON을 이용한 깊은 복사는 객체의 구조가 간단하고, 함수나 undefined와 같은 속성 값이 없는 경우에 적합한 방법입니다. 만약 객체의 구조가 복잡하거나 순환 참조가 있는 경우에는 다른 깊은 복사 방법을 고려해야 합니다.

그리고 hasOwnProperty 메서드를 활용하는 방법도 존재한다.

 

5. undefined와 null

둘 다 없음을 나타내는 값.

 

1) undefined

 - 일반적으로는 자바스크립트 엔진에서 값이 있어야 할 것 같은데 없는 경우, 자동 부여

  • 변수에 값이 지정되지 않은 경우, 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때
  • .이나 []로 접근하려 할 때, 해당 데이터가 존재하지 않는 경우
  • return 문이 없거나 호출되지 않는 함수의 실행 결과
var a;
console.log(a); // (1) 값을 대입하지 않은 변수에 접근

var obj = { a: 1 };
console.log(obj.a); // 1
console.log(obj.b); // (2) 존재하지 않는 property에 접근
// console.log(b); // 오류 발생

var func = function() { };
var c = func(); // (3) 반환 값이 없는 function
console.log(c); // undefined

※ '없다'를 명시적으로 표현할 때는 undefined를 사용하면 안됨!

 

배열의 경우에는 조금 특이한 동작을 한다.

비어있는 요소와 undefined를 할당한 요소는 출력 결과가 다르다.

비어있는 요소는 순회와 관련된 많은 배열 메서드들의 순회 대상에서 제외된다.

 

2) null

  • 용도 : ‘없다’를 명시적으로 표현할 때
  • 주의 : typeof null

typeof null이 object인 것은 유명한 javascript 자체 버그

var n = null;
console.log(typeof n); // object

//동등연산자(equality operator)
console.log(n == undefined); // true
console.log(n == null); // true

//일치연산자(identity operator)
console.log(n === undefined);
console.log(n === null);

 

6. 정리

데이터 타입 : 기본형과 참조형이 존재

변수 : 변경 가능한 데이터가 담길 수 있는 공간

식별자 : 그 변수의 이름

 

출처 : 코어 자바스크립트

스파르타코딩클럽 Javascript 문법 3주차

'Language > JavaScript' 카테고리의 다른 글

[코어 자바스크립트] This  (0) 2023.06.10
[코어 자바스크립트] 실행컨텍스트(Scope, Var, Object, Hoisting)  (0) 2023.06.05
Map과 Set  (0) 2023.05.25
일급 객체로서의 함수  (0) 2023.05.25
ES6 문법  (1) 2023.05.25