코어 자바스크립트 - 데이터 타입 2

23290 단어 JavaScriptJavaScript

🤚 전체적인 내용은 위키북스 코어자바스크립트 도서, 정재남 지음 을 참고로 했고, 부가적인 내용과 생략된 부분이 존재할 수 있다.


불변 객체(Immutable Object)

불변 객체는 React.js, Vue.js, Angular.js 뿐만 아니라 함수형 프로그램, 디자인 패턴에서도 등장하는 중요한 기초 개념이다.

불변 객체가 필요한 이유

값으로 전달 받은 객체에 변경을 가하더라도 원본 객체는 변하지 않아야 되는 경우가 있을 수 있다. 특히 리액트에서는 상태 업데이트시 원본과 업데이트할 상태 비교를 통해 특정부분만 업데이트를 진행하므로 불변객체는 중요한 개념이다.

다음은 객체의 가변성에 따른 문제점을 설명하기 위한 코드이다.

let user = {
  name: 'Seongil',
  gender: 'male'
};

const changeName = (user, newName) => {
  let newUser = user;
  newUser.name = newName;
  return newUser;
}

const user2 = changeName(user, 'modolee');

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

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

객체를 let newUser = user; 이런식으로 복사했으니 같은 주소를 가리킬 수 밖에 없다...🤦‍♂️

불변객체를 만드는 간단한 방법

  1. 내부 프로퍼티를 변경할 필요가 있을때 마다 매번 새로운 객체를 만들어 재할당
  2. 새로운 객체를 만드는 도구를 이용. 대표적으로 immutable.js, immer.js 등이 있다. 혹은 ES6의 spread operator, Object.assign 메서드 등을 활용할 수 있다.

여기서는 첫번째 방법인 새로운 객체를 만들어 재할당하는 방법을 사용해보도록 하겠다.

let user = {
  name: 'Seongil',
  gender: 'male'
};

const changeName = (user, newName) => {
  return {  // ✅ 객체 재할당 
    name: newName,
    gender: user.gender
  };
}

const user2 = changeName(user, 'modolee');

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

console.log(user.name, user2.name); // Seongil modolee
console.log(user === user2); // false

하지만 매번 기존 객체의 프로퍼티를 하드코딩으로 입력해야하므로, 대상 객체에 대한 정보가 많을수록 수고로움이 늘어날 것이다. 따라서 그런 수고로움을 조금이나마 줄이기 위해 기존 정보를 복사해 새로운 객체를 반환하는 함수를 만들었다.

const copyObject = (target) => {
  let result = {};
  for (const prop in target) { // ✅ for...in 구문
    result[prop] = target[prop];
  }
  return result;
}

하지만 이 또한 얇은 복사만 수행하여 아쉬움이 있다.

얕은 복사와 깊은 복사

복사를 할 때 주의할 점이 있는데 복사는 크게 얇은 복사와 깊은 복사로 나눠진다.

  • 얇은 복사(shallow copy) : 바로 아래 단계의 값만 복사하는 방법
  • 깊은 복사(deep copy) : 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법

얕은 복사의 문제점

얕은 복사의 문제점은 중첩된 객체가 있을때 참조형 데이터가 저장 된 프로퍼티를 복사하는 경우 그 주소 값만 복사를 하게 된다. 따라서 중첩된 객체를 수정하고자 하면 역시 원본도 훼손되는 문제가 발생하게 된다.

다음 코드는 얕은복사를 실행했을때 나타나는 문제점을 보여주고 있다.

let user = {
  name: 'Seongil',
  urls: { // ✅ 중첩 객체
    linkedin: 'http://linkedin.com/in/modolee',
    github: 'http://github.com/modolee',
    blog: 'http://velog.io/@modolee'
  }
};

let user2 = copyObject(user);

user2.name = 'modolee';
console.log(user.name === user2.name); // false

user.urls.blog = 'https://medium.com/@modolee';
console.log(user.urls.blog === user2.urls.blog); // ✅ true

user2.urls.linkedin = '';
console.log(user.urls.linkedin === user2.urls.linkedin); // ✅ true

name을 변경했을때는 서로 다른 값이 되지만, urls이라는 중첩 객체 안에 blog나 linkedin을 변경했을때는 동일한 주소를 참조하고 있기 때문에 값을 바꾸더라도 같이 바뀌게 된다.

얕은 복사 해결방법

이를 해결하기 위해 아래 코드처럼 user.urls도 다시 그 내부의 프로포티들을 얕은복사를 하던가 해야한다.

let user2 = copyObject(user);
user2.urls = copyObject(user.urls);

혹은 spread 문법을 이용하거나, immutable.js를 활용하는 편이 좋다. 또한 책에서는 객체를 JSON 문법으로 표현 된 문자열로 전환(JSON.stringify)했다가 다시 JSON 객체로 전환(JSON.parse)하면 새로운 객체가 생성되는 방식도 예를 들며 설명하고 있다.


undefined

자바스크립트에서 '없음'을 나타내는 값이 두가지 있는데 바로 undefinednull이다. 의미는 같지만 미묘한 차이가 있다. undefined는 보통 우리가 쓰는 일은 거의 없고 자바스크립트 엔진이 많이 사용한다...

1. 자동으로 undefined이 부여

자동으로 undefined이 부여되는 경우는 다음과 같이 3가지로 볼 수 있다. 값이 없다 싶으면 무조건 undefined라고 보면 된다.

let a;
console.log(a); // (1) undefined. 값을 대입하지 않은 변수에 접근

let obj = { a: 1 };
console.log(obj.a); // 1
console.log(obj.b); // (2) undefined. 존재하지 않는 프로퍼티에 접근

const func = () => {};
const c = func(); // (3) return 값이 없으면 undefined를 반환한 것으로 간주
console.log(c); // undefined

2. undefined와 배열

값을 대입하지 않은 경우에 대해 배열의 경우는 조금 특이한 동작을 하게 된다.

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]; // 사용자가 임의로 undefined 부여
console.log(arr3); // [undefined, undefined, undefined]

empty와 undefined의 차이

empty는 순회와 관련된 많은 배열 메소드들의 순회대상에서 제외된다는 것이 가장 큰 특징이다.

let arr1 = [undefined, 1]; // undefined의 경우
let arr2 = [];
arr2[1] = 1; // 0번째는 empty의 경우

arr1.forEach((value, index) => { console.log(value, index); }); // undefined 0 / 1 1
arr2.forEach((value, index) => { console.log(value, index); }); // ✅ 1 1

결과를 보면 알겠지만 undefined와 다르게 empty는 순회에서 아예 제외시켜버렸다. 이는 지극히 당연한 현상이다.

  • empty처럼 값이 지정되지 않은 인덱스는 '아직은 존재하지 않은 프로퍼티'에 지나지 않는다. 특정 인덱스에 값을 지정할 때 비로소 빈 공간을 확보하고 인덱스를 이름으로 지정하고 데이터의 주소 값을 저장하게 된다.
  • undefined는 하나의 실존 데이터이므로 프로퍼티나 배열의 요소는 고유의 키값이 실존하게 되고, 따라서 순회의 대상이 될 수 있는 것이다.

결론

정리해보면 undefined를 조금 구분해서 볼 필요가 있다.

let a = undefined; // 경우1
let b; // 경우2
  • 경우1. 사용자가 명시적으로 부여한 경우 : 실존데이터이다. 프로퍼티나 배열의 요소는 고유의 키 값(프로퍼티 이름)이 실존하게 되어 순회의 대상이 된다. 사실 이럴일은 거의 없다.

  • 경우2. 자동으로 undefined이 부여되는 경우 : 실존하지 않는 데이터이고, 문자 그대로 값이 없음을 의미. 해당 프로퍼티 또는 배열의 키 값 자체가 존재하지 않음을 의미한다. Javscript 엔진이 하는 수 없이 undefined를 반환하는 것 뿐이다.


null

undefined은 사실 사용자가 직접 지정해서 쓸 이유는 없다. 바로 null이라는 데이터형이 있기 때문이다. null은 비어있음을 명시적으로 나타낼 때 쓰라고 애초부터 이런 용도로 만들어진 데이터 타입이다.

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

console.log(n == undefined); // true
console.log(n == null); // true
console.log(n === undefined); // false
console.log(n === null); // true

null은 한가지 주의할 점이 있는데 typeof null이 object라는 점이다. 따라서 어떤 변수의 값이 null인지 여부를 판별하기 위해서는 다른 방식으로 접근해야 한다.

또한, null과 undefined를 ==로 비교할때 서로 같다고 판단하지만 ===를 쓰면 서로 다르다고 판단한다.

좋은 웹페이지 즐겨찾기