JavaScript 데이터 타입(심화)

H_Chang·2023년 10월 16일

데이터 타입 심화

1. 데이터 타입의 종류(기본형과 참조형)

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

  • 값의 저장 방식 과, 불변성 여부 이다.

1. 복제의 방식

1-1. 기본형 : 값이 담긴 주소값을 바로 복제
1-2. 참조형 : 값이 담긴 주소값들로 이루어진 묶음을 가리키는 주소값을 복제

2. 불변성의 여부

2-1. 기본형 : 불변성을 띔
2-2. 참조형 : 불변성을 띄지 않음

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

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

1. 변수 vs 상수

1-1. 변수 : 변수 영역 메모리를 변경할 수 있음
1-2. 상수 : 변수 영역 메모리를 변경할 수 없음

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

2-1. 불변하다 : 데이터 영역 메모리를 변경할 수 없음
2-2. 불변하지 않다 : 데이터 영역 메모리를 변경할 수 있음

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

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

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

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

/** 선언과 할당을 붙여 쓴 방식 */
var str = 'test!';
  • 위의 코드를 표를 통해 이해해 보자!

값을 바로 변수에 대입하지 않는 이유

1. 데이터 자유롭게 변환하기 위해서

1-1. 만약 이미 입력한 문자열이 길어진다면?

  • 숫자는 항상 8byte로 고정이지만, 문자는 고정이 아니다.(영문 : 1byte, 한글 : 2byte)

  • 이때, 1003 주소에 할당된 데이터를 변환하려 할 때 훨씬 더 큰 데이터를 저장하려 한다면 → 1004 이후부터 저장되어있는 모든 데이터를 오른쪽으로 전부 미뤄야 하기 때문이다.

  • 별도 공간에 주소를 확보한 후에 주소를 가져오는것이 더 효율적이다.

2. 메모리를 효율적으로 관리하기 위해서

2-1. 똑같은 데이터를 여러번 저장해야 한다면?

  • 1만개의 변수를 생성해서 모든 변수에 숫자 1을 할당하는 상황을 가정해 보자.
  • 모든 변수를 별개로 인식한다고 한다면, 1만개의 변수 공간을 확보해야 한다.

a. 바로 대입하는 case) 숫자형은 8 바이트 고정일 경우
1만개 * 8byte = 8만 byte

b. 변수 영역에 저장되는 데이터는 2바이트로 가정하고 변수 영역에 별도 저장 한 경우
1만개 * 2byte = 2만 byte + 데이터 영역 8byte = 총 2만 8바이트

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

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

  1. 변수 vs 상수
    1-a. 변수 : 변수 영역 메모리를 변경할 수 있음
    1-b. 상수 : 변수 영역 메모리를 변경할 수 없음

  2. 불변하다 vs 불변하지 않다
    2-a. 불변하다 : 데이터 영역 메모리를 변경할 수 없음

  • 데이터 영역의 주소의 값은 그대로이기 때문

    2-b. 불변하지 않다 : 데이터 영역 메모리를 변경할 수 있음

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

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

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

a = a + 'def';
// 'def'라는 값이 'abc'의 데이터 영역 붙은것은 아니고 'abc'는 그대로 있고 'abcdef'라는 새로운 값이 생긴다.
//'abcdef'라는 새로운 값이 생기면서 이주소가 변수영역으로 갈아 껴진것이다. @5002 -> @5003
// 그렇기 때문에 변수이면서, 불변하다.
// 이 때, @5002는 더 이상 사용되지 않기 때문에 가비지컬렉터의 수거 대상이 된다.

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

  • 참조형 데이터의 변수 할당 과정
// 참조형 데이터는 별도 저장공간(obj1을 위한 별도 공간)이 필요하다!
var obj1 = {
	a: 1,
	b: 'bbb,
};


1. 변수 영역의 빈공간(1001번이 빈공간이다.)을 찾는다.
2. 'obj1'은 참조형 데이터이기 때문에 별도공간을 따로 마련했다.
3. a=7103, b=7104 로 세팅했다.
4. a는 1이라는 데이터가 필요하기 때문에 데이터 영역에 1=5001 값을 세팅해주었다.
5. 그후 a는 1의 주소값 @5001을 가지고 온다.
6. b는 'bbb'의 데이터가 필요하기 때문에 데이터 영역에 'bbb'=5002 값을 세팅해주었다.
7. 그후 b는 'bbb'의 주소값 @500를 가지고 온다.
8. 비로소 @7103 ~ @7104 까지의 주소를 'obj1'에 세팅한다.

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

  • 참조형 데이터가 불변하지 않다(가변하다)라고 하는 이유

var obj1 = {
	a: 1,
	b: 'bbb',
};

// 데이터를 변경해자!
obj1.a = 2;

  1. 'a'라는 값을 1에서 2로 봐꿔보자
  2. 데이터 영역에 2라는 값이 없기 때문에 새롭게 세팅한다. 2=@5003
  3. 'obj1'의 데이터 'a'주소값을 갈아 껴준다. (@5001 => @5003)
  4. @5001은 더 이상 사용되지 않기 때문에 가비지컬렉터의 수거 대상이 된다.
  • 데이터 영역에 저장된 값은 여전히 계속 불변값이지만, obj1을 위한 별도 영역은 얼마든지 변경이 가능하다
  • 이것 때문에 참조형 데이터를 흔히, 불변하지 않다(=가변하다)라고 한다.

중첩객체의 할당

  • 중첩객체란, 객체 안에 또 다른 객체가 들어가는 것을 말한다.
var obj = {
	x: 3,
	arr: [3, 4, 5],
}

// obj.arr[1]

  1. 'obj'를 위한 별도 공간에 'x', 'arr' 을 세팅한다.

  2. 'x'를 먼저 할당하기 위해서 데이터 영역에 3이라는 값을 추가한다.

  3. 그후 'x'는 3의 주소값 @5001을 가져온다.

  4. 'arr'도 참조형 데이터 이기 때문에 'arr'의 별도 공간을 마련해 준다.

  5. 'arr'의 별도공간에 3,4,5 값을 세팅하기 위해 데이터 베이스에 4,5 값을 추가한다.

  6. 그후 'arr'의 별도공간에 0번째에는 @5001, 1번째에는 @5002, 3번째에는 @5003 주소값을 가져온다.
    7.'obj'별도공간의 'arr'가 @8104 주소를 가져온다.

  7. 비로소 변수영역의 'objrk @7103 주소를 가져오면서 완성된다.

  • 참조카운트란?
    객체를 참조하는 변수나 다른 객체의 수를 나타내는 값이다.

  • 가비지컬렉터(GC, Garbage Collector)란?
    더 이상 사용되지 않는 객체를 자동으로 메모리에서 제거하는 역할을 한다.

변수 복사의 비교

// STEP01. 선언을 먼저 한후.
var a = 10; //기본형
var obj1 = { c: 10, d: 'ddd' }; //참조형

// STEP02. 복사를 수행하자!.
var b = a; //기본형
var obj2 = obj1; //참조형

  • 복사 이후 값 변경(객체의 프로퍼티 변경)
// STEP03. 복사 이후 값을 변경해 보자!
b = 15;
obj2.c = 20;

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

// 참조형 변수 복사의 결과는 같은 값이 나온다.
obj1 === obj2;
  1. 위와 같은 현상이 발생하게 된다.

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

b = 15;
obj2 = { c: 20, d: 'ddd'};

  • 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;
	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

위 예시의 문제점 개선)

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

// 이름을 변경하는 함수 정의
// 입력값 : 변경대상 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 
  • 위의 문제점 개선 예시는 최선의 답은 아니다. 이유로는,
  1. changeName 함수는 새로운 객체를 만들기 위해 변경할 필요가 없는 gender 프로퍼티를 하드코딩으로 입력했다. ⇒ 만일 이러한 속성이 10개 이거나, 그이상 이라면?

  2. 이러한 문제점을 개선하기 위해 얕은복사 방법을 사용한다.

얕은 복사

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

	// for ~ in 구문을 이용하여, 객체의 모든 프로퍼티에 접근할 수 있다.
	// 하드코딩을 하지 않아도 된다.
	// 이 copyObject로 복사를 한 다음, 복사를 완료한 객체의 프로퍼티를 변경한다.
	for (var prop in target) {
		result[prop] = target[prop];
	}
	return result;
}
  1. 위 패턴의 예시)
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);

얕은 복사 vs 깊은 복사

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

중첩된 객체에 대한 얕은 복사 예시)

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

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
  • 결국, ser.urls 프로퍼티도 불변 객체로 만들어야 한다.

중첩된 객체에 대한 깊은 복사 예시)

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);
  • 결론 객체의 프로퍼티 중, 기본형 데이터는 그대로 복사 + 참조형 데이터는 다시 그 내부의 프로퍼티를 복사한다! ⇒ 재귀적 수행!(recursive)

a. 재귀적 수행: 함수나 알고리즘이 자기 자신을 호출하여 반복적으로 실행되는 것을 말한다.

결론을 적용한 예시 코드)

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);

JSON(=JavaScript Object Notation)을 이용하는 방법

  • 장점
  1. JSON.stringify() 함수를 사용하여 객체를 문자열로 변환한 후, 다시 JSON.parse() 함수를 사용하여 새로운 객체를 생성하기 때문에, 원본 객체와 복사본 객체가 서로 독립적으로 존재한다. 따라서 복사본 객체를 수정해도 원본 객체에 영향을 미치지 않는다.
  2. JSON을 이용한 깊은 복사는 다른 깊은 복사 방법에 비해 코드가 간결하고 쉽게 이해할 수 있다.
  • 단점
  1. JSON을 이용한 깊은 복사는 원본 객체가 가지고 있는 모든 정보를 복사하지 않습니다. 예를 들어, 함수나 undefined와 같은 속성 값은 복사되지 않는다.
  2. JSON.stringify() 함수는 순환 참조(Recursive Reference)를 지원하지 않는다. 따라서 객체 안에 객체가 중첩되어 있는 경우, 이 방법으로는 복사할 수 없다.
  • 따라서 JSON을 이용한 깊은 복사는 객체의 구조가 간단하고, 함수나 undefined와 같은 속성 값이 없는 경우에 적합한 방법이다. 만약 객체의 구조가 복잡하거나 순환 참조가 있는 경우에는 다른 깊은 복사 방법을 고려해야 한다.

5. undefined와 null

  • 둘 다 없음을 나타내는 값이다.
  • 하지만 미세하게 다르고, 그 목적 또한 다르다.

1. undefined

  • 사용자(=개발자)가 직접 지정할 수도 있지만 일반적으로는 자바스크립트 엔진에서 값이 있어야 할 것 같은데 없는 경우, 자동으로 부여한다.

1-1. 변수에 값이 지정되지 않은 경우, 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때
1-2. .이나 []로 접근하려 할 때, 해당 데이터가 존재하지 않는 경우 일때
1-3. 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 문제점

2-1. 지금 undefined로 나오는 이 변수가, 필요에 의해 할당한건지 자바스크립트 엔진이 반환한건지 구분할 수가 없다.
2-2. 없다명시적으로 표현할 때는 undefined를 사용하지 말자!

2. null

  • ‘없다’를 명시적으로 표현할 때
  • 주의할점 typeof null( javascript 자체 버그 이다.)

null 예시)

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

//동등연산자(equality operator)
 //타입까지 일치하지 않아도 된다.
console.log(n == undefined); // true
console.log(n == null); // true

//일치연산자(identity operator)
 //undefined와 null의 구분을 확실하게 할수있다.
console.log(n === undefined); // false
console.log(n === null); // true
profile
프론트 엔드 시작하는 뉴비!

0개의 댓글