[JS] JS는 데이터를 어떻게 처리할까?-데이터타입,얕은복사,깊은복사,불변값,가변값

고정원·2021년 6월 13일
0

1Q/1Day

목록 보기
3/13
post-thumbnail

정재남,『코어자바스크립트』를 읽고 정리한 내용입니다. 이해가 부족한 부분은 책과 동일하게 작성하였습니다.

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

1.1 변수선언

🧐변수는 어떤 원리(과정)로 선언되어질까?

//📍예제 1-1 변수선언
var a;

예제 1-1을 말로 풀어쓰면,
"변할 수 있는 데이터를 만든다. 이 데이터의 식별자는 a로 한다” 가 된다.

변수란 변경 가능한 데이터가 담길 수 있는 공간 또는 그릇
이 공간에 숫자를 담았다가 문자열을 담을 수도 있다

변수를 선언한다는 것은
메모리에서 비어있는 공간을 확보하고 그 공간의 이름을 설정하는 과정!

1.2 데이터 할당

🧐선언한 변수에 데이터는 어떻게 할당되어 질까?

//📍예제1-2
var a; //변수 a 선언
a = 'abc'; //변수 a에 데이터 할당
var a ='abc' //변수 선언과 할당을 한 문장으로 표현

데이터 할당에 대한 메모리 영역의 변화
(데이터의 성질에 따라 "변수영역"과 "데이터 영역"으로 나누어보자)

  1. 변수 영역에서 빈 공간(@1003) 확보
  2. 확보한 공간의 식별자를 a로 지정
  3. 데이터 영역의 빈 공간(@5004)에 문자열 ‘abc’를 저장
  4. 변수 영역에서는 a라는 식별자를 검색(@1003)
  5. 앞서 저장한 문자열의 주소(@5004)를 @1003의 공간에 대입

❓데이터를 왜 변수영역에 값을 직접 대입하지 않고 굳이 한 단계를 더 거치는 걸까?

⇒ 데이터 변환을 자유롭게 할 수 있게 함과 동시에 메모리를 더욱 효율적으로 관리하기 위해서
문자열 같은 데이터타입은 그 문자가 영어인지(1바이트), 한글(2바이트)인지 또는 문자열의 길이에 따라 메모리를 가변적으로 사용해야하기 때문이다.
데이터영역과 변수영역을 별도의 공간으로 나누어 저장하는 것이 최적이다.

2. 기본형데이터 vs 참조형데이터

💡 명확하게 구분하기

  • 변수와 상수 : 변수영역의 메모리가 변경 가능하냐?
  • 상수와 불변값 : 데이터 영역의 메모리가 변경 가능하냐?

2.1 불변값

기본형데이터(숫자, 문자열, boolean, null, undefined, Symbol)은 모두 불변값!

🤔기본형데이터에서
새로운 변수 b가 선언되고, 기존의 변수 a와 동일한 데이터가 할당된다면?

//📍예제1-3
var a = 10;
var b = 10;

예제 1-3의 경우에는 또 다른 변수 b를 더 선언했다. 그렇다면 b의 데이터 할당은 어떻게 될까? 여기서 중요한 것은 10이라는 값이 새로운 주소에 할당되고 b에 해당 주소가 대입되는 것이 아니라는 것이다. b는 a의 10과 같은 주소를 참조하고 새로운 데이터 영역을 더 만들지 않는다.

결국 자바스크립트 엔진은 예제 1-3을 실행할때 다음과 같은 절차를 거친다.

  1. 변수 영역에 빈 공간을 확보하여 식별자 a로 지정하고,
  2. 데이터 영역에서 10이 있는지 검색한 후 10을 찾지 못했기 때문에 새로운 주소에 10을 저장하고,
  3. a 식별자 주소에 10 데이터 주소를 대입한다.
  4. 변수 영역에 빈 공간을 확보하여 식별자 b로 지정하고,
  5. 데이터 영역에서 10이 있는지 검색한 후 10이 있는 것을 확인한 후,
    해당 데이터 주소를 b 식별자 주소에 대입한다.

🤓불변값의 성질
문자열 값도 한 번 만든 값을 바꿀 수 없고, 숫자 값도 다른 값으로 변경할 수 없다.
변경은 새로 만드는 동작을 통해서만 이루어진다.이것을 불변값의 성질이라고 한다. 한 번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않는다.

즉, 데이터의 변화가 있을 때 해당 데이터를 수정하는 것이 아니라, 새로운 수정된 데이터 영역을 새로 만든다(추가한다).

2.2 가변값

참조형 데이터의 기본적인 성질은 가변값인 경우가 많다.
하지만, 설정에 따라 변경불가능한 경우(*Object.defineProperty, Object.freeze등)도 있고, 아예 불변값으로 활용하는 방안도 있다.

🤔그럼 Object(참조형데이터)의 경우는 어떻게 데이터 할당이 이루어질까?

//📍예제1-4
var obj1 = {
  	a:1,
  	b:'bbb'
   };	

  1. 변수 영역의 빈 공간(@1003)을 확보하고, 그 주소의 이름을 obj1으로 지정
  2. 임의의 데이터 저장공간(@5002)에 데이터를 저장하려고 보니 여러 개의 프로퍼티로 이루어진 데이터 그룹이다. 내부 프로퍼티들을 저장하기 위해 별도의 변수 영역을 마련하고, 그 영역의 주소(@7103~?)를 @5002에 저장
  3. @7103과 @7104에 각각 a와 b라는 프로퍼티 이름 지정
  4. 데이터 영역에서 숫자 1을 검색하고 결과가 없으므로 @5003에 저장하고 이 주소를 @7103에 저장
  5. 문자열 ‘bbb’ 역시 임의로 5004에 저장하고, 이 주소를 @7104에 저장

📌기본형데이터와의 차이는
참조형데이터는 '객체의 변수(프로퍼티) 영역'이 별도로 존재한다
그리고 데이터 영역은 기존의 메모리공간(@5003,@5004)을 그대로 활용한다
데이터영역에 저장된 값은 모두 불변값이다. 그러나 변수 영역에는 다른 값을 얼마든지 대입 가능! 그래서 흔히 참조형 데이터는 불변하지않다(가변값이다,mutable)라고 한다.

참조형 데이터의 프로퍼티 재할당

//📍예제1-5
var obj1 = {
    a:1,
    b:'bbb'
};
obj1.a = 2;

4번째 줄까지는 예제1-4와 동일하므로 제일 마지막줄만 보면,obj1의 a프로퍼티에 숫자 2를 할당하려고 한다.
1. 데이터영역에서 숫자 2를 검색한다
2. 검색결과에 2가 없으므로 빈 공간 @5005에 저장하고
3. 이 주소를 @7103에 저장한다.
변수 obj1이 바라보고 있는 주소는 @5001로 변하지 않았다.
즉, '새로운객체'가 만들어진 것이 아니라 기존의 객체 내부의 값만 변경 되었다.

데이터 영역에 데이터를 할당하기 전에 먼저 그 데이터가 데이터영역에 있는지 부터 검사한다.
있으면 해당 데이터의 주소를 가져다 쓰고, 없으면 새로운 데이터를 데이터 영역에 생성하여 그 데이터의 주소를 쓴다.

📌기본형과 참조형의 차이는
단순히 주소를 참조하느냐가 아니라 주소를 복사하는 과정이 한 번 더 거치는지의 차이다.
다시말해, 기본형의 값 복사는 본래 갖고 있던 데이터의 주솟값만 복제를 하기 때문에 불변성이 유지가 되는 것이고!! 참조형의 값 복사는 본래 갖고 있던 데이터의 주솟값을 동일하게 갖고 있기 때문에, 복사를 한 값에서 변화를 주면 원래 갖고 있는 값도 영향을 받기 때문이다.

🔥참조형 데이터 내부의 프로퍼티를 변경 할 때만!! → 참조형 데이터가 가변값이라고 할 수 있다.

3.불변객체

3.1 불변성

불변성은 최근 React, Vue, Agnular등의 라이브러리뿐만 아니라, 디자인 패턴, 함수형 프로그래밍등에서 아주 주요한 개념으로 여긴다.
불변성은 한 객체의 내부 프로퍼티가 바뀔 때, 원본 객체는 바뀌지 않고 새로운 객체를 만들거나 만들기로 해서 확보가 가능하다. 어떻게?!

//📍객체의 가변성에 따른 문제점
let user = {
	name: '소주',
  	gender: 'female'
}

let changeName = function(user,newName){
  	let newUser = user
    newUser.name = newName
};

let user2 = changeName(user, '맥주')
if(user !==user2){
	console.log('유저정보가 변경되었습니다!')
}; //안나옴

console.log(user.name, user2.name) //맥주 맥주
console.log(user === user2) //true

위의 코드는 앞에서 본 것과 같이 user2와 user1에 할당된 객체의 주소값이 같으므로 user2.name이 업데이트 되니까 user.name을 찍었을때 바뀐 값이 출력이 된다.
따라서,

//📍객체의 가변성에 따른 문제점 해결방법 → 변경 전과 후에 서로 다른 객체를 바라보도록 만들어보자
let user = {
	name: '소주',
  	gender: 'female'
}

let changeName = function(user, newName){
	return {
      name: newName,
      gender: user.gender
};
  
let user2 = changeName(user, '맥주')
if(user !==user2){
	console.log('유저정보가 변경되었습니다!')
}; //유저정보가 변경되었습니다!

console.log(user.name, user2.name) //감자 배추
console.log(user === user2) //false

이렇게 바로 chageName함수에서 새로운 객체를 반환을 하게되면 해결을 할 수 있다. 그러나 이것도 문제가 있는것이 gender(변경할 필요가 없는 기존 객체의 프로퍼티)를 하드코딩 했기 때문이다. 만약에 이런 프러퍼티가 여러개라면? 다 하드코딩을 해야하므로 비효율적이다. 그때 for in문법을 사용하면 된다!

//📍기존 정보를 복사해서 새로운 객체를 반환하는 함수(얕은복사)
let copyObj = function(target){
	let result = {}
    for(let prop in target){
    	result[prop] = target[prop]
    }
  return result
}

//copyObj를 이용한 객체 복사
let user = {
	name: '소주',
  	gender: 'female'
}

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

console.log(user.name, user2.name) //소주 맥주
console.log(user === user2) //false

이렇게 copyObj함수를 만들어서 간단하게 객체를 복사하고, 수정 할 수 있다. 그러나 immer.js, baobab.js같은 휼륭한 라이브러리가 있으니 그것을 사용하자~

3.2 얕은복사와 깊은복사

얕은 복사는 바로 아래단계의 값만 복사하는 값이고, 깊은 복사는 내부의 모든 값들을 하나하나 찾아 전부 복사하는 방법니다. 즉 얕은복사는 참조데이터의 참조변수들의 메모리 주소까지만 복사를 하기때문에 복사한 데이터의 참조값을 바꿔도 원본 데이터가 바뀐다(기존 데이터를 그대로 참조하니까).

let deepCopyObj = function (target) {
  let result = {};
  if (typeof target === "object" && target !== null) {
    //target !== null를 넣어준 이유는, typeof 명령어가 null에 대해서도 object를 반환하는 버그가 있기 때문이다.
    for (let prop in target) {
      result[prop] = deepCopyObj(target[prop]);
    }
  } else {
    result = target;
  }
  return result;
};

let obj = {
  a:1,
  b:{
    c:null,
    d:[1,2]
  }
}

let obj2 = deepCopyObj(obj)

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

console.log(obj)
/*{
  a:1,
  b:{
    c:null,
    d:[1,3]
  }
}*/
console.log(obj2)
/*
{
  a:3,
  b:{
    c:4,
    d:[1,2]
  }
}
*/

깊은복사는 이렇게 재귀적인 로직을 통해 내부 프로퍼티들을 순회하여 target을 그대로 지정하게끔 했다.
obj와 obj2는 아예 다른 주소값을 갖는 객체들이다.따라서 저 둘은 서로 영향을 주지 않는다.
다른 방식으로는 JSON.stringfy()로 문자열로 바꾼 다음 다시 JSON화를시켜주어도 된다. 다만 prototype나 getter/setter와 같은 경우 적용이 되질 않는다.

4.undefined과 null

JS에선 없음을 의미하는 값이 두가지가 있는데 그게 바로 null과 undifiend이다. 둘의 차이는 무엇일까? 각 각 어떤 목적으로 사용될까?

4.1 undefined

undefined은 사용자가 명시적으로 지정할 수도 있으나, 값이 존재하지 않을때 JS엔진이 자동을 부여하는 경우가 있다.

JS엔진은 사용자가 어떤 값을 지정할 것이라고 예상되는 상황인데도 실제로 그렇게 안 할때 undefined를 반환한다. 아래의 세 경우가 이에 해당한다.

1) 값을 대입하지 않은 변수, 즉 데이터 영역의 메모리 주소를 지정하지 않는 식별자에 접근할 때

let a;
console.log(a) //undefined

2) 객체 내부의 존재하지 않는 프로퍼티에 접근하려고 할 때

let drink = { a:'beer'}
console.log(drink.b) // undefined

3) return문이 없거나 호출되지 않는 함수의 실행결과

let func = function(){}
console.log(func()) //undefined return값이 없으면 undefined를 반환한 것으로 간주함

그러나 1)의 경우 데이터가 배열일 때 조금 독특한 동작이 있다.

let arr1 = [];
arr1.length = 3
console.log(arr1) //[empty x 3]

let arr2 = new Array(3)
console.log(arr2) //[empty x 3]

let arr3 = [undefined, undefined, undefined]
console.log(arr3) //[undefined, undefined, undefined]

첫번째, 두번째 예를 보면,3개의 빈 요소를 확보했지만 문자 그대로 어떤 값도, undefined값 조차도 할당되어 있지 않다. 이는 '비어있는 요소'와 'undefined를 할당한 요소'가 다르다는 것을 의미한다.*'비어있는 요소'는 순회와 관련된 많은 배열 메서드들의 순회 대상에서 제외된다.
그래서 출력 결과도 당연히다르다.

let arr1 = [undefined, 1]
let arr2 = []
arr2[1] = 1;

arr1.forEach((v,i) => console.log(v,i)) 
//undefined 0 / 1 1
arr2.forEach((v,i) => console.log(v,i)) 
//1 1

arr1.map((v,i) => v+i)
//[Nan, 2]
arr2.map((v,i) => v+i)
//[empty, 2]

arr1.filter((v)=> !v)
//[undefined]
arr2.filter((v)=> !v)
//[]

arr1.reduce((p,c,i)=>p+c+i,'')
//undefined011
arr2.reduce((p,c,i)=>p+c+i,'')
//11

보다시피 arr2에서 각 메소드들이 비어있는 요소에서는 어떠한 처리도 하지 않았음을 알 수 있다.
이 이유는 배열도 객체라는 것을 생각해보면 당연한 것이다.

4.2 null

null은 비어있음을 명시적으로 나타내고 싶을때 사용하는 것이다. 그러나 typeof null이 object라고 나오는 버그가 있으니null여부는 다른식으로 접근해야하는데

let isNull = null
console.log(isNull === null) //true

바로 일치 연산자를 확인해보면 된다!!!

정리

  • 자바스크립트 데이터 타입에는 크게 기본형과 참조형이 있다.
  • 변수는 변경 가능한 데이터가 담길 수 있는 공간이고, 식별자는 그 공간(변수)의 이름을 말한다.
  • 변수를 선언하면 컴퓨터는 우선 메모리의 빈 공간에 식별자를 저장하고, 그 공간의 값은 undefined를 할당한다.후에 그 변수에 기본형 데이터를 할당하려고 하면 별도의 공간에 데이터를 저장하고, 그 공간의 주소를 변수의 값 영역에 할당한다.
  • 참조형 데이터를 할당하는 경우 컴퓨터는 참조형 데이터 내부 프로퍼티들을 위한 변수 영역을 별도로 확보해서 확보된 주소를 변수에 연결 → 다시 앞에서 확보한 변수 영역에 각 프로퍼티의 식별자를 저장 → 각 데이터를 별도의 공간에 저장해서 그 주소를 식별자들과 매칭시킨다.
  • 이렇게 할당과정에서 기본형과 참조형의 차이가 생긴 이유는 참조형 데이터가 여러 개의 객체의 프로퍼티(변수)를 모은 '그룹'이기 때문이다. 그리고 이 차이로 인해 참조형 데이터를 '가변값'으로 여기는 상황이 발생한다.
  • 참조형 데이터를 가변값으로 여겨야 하는 상황임에도 이를 불변값으로 사용하는 방법이 있긴하다. 깊은복사를 하면 된다. 즉, 내부 프로퍼티들을 일일이 복사하면 된다. 혹은 라이브러리를 사용하는 방법도 있다.
  • '없음'을 나타내는 값은 두 가지가 있는데, 각 각 아래와 같이 의미한다.
    undefined는 어떤 변수에 값이 존재하지 않을 경우를 의미하고
    null은 사용자가 명시적으로 '없음'을 표현하기 위해 대입한 값이다. 본래의 의미에 따라 사용자가 없음을 표현하기 위해 명시적으로 undefined를 대입하는 것은 지양하는 것이 좋다.

[참고한자료]
정재남, 『코어자바스크립트』, 위키북스(2019)
https://hsolemio-lee.github.io/core-javascript1/
https://velog.io/@holasim/JS%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%ED%83%80%EC%9E%85

profile
해결문제에 대해 즐겁게 대화 할 수 있는 프론트엔드 개발자

0개의 댓글