SDK JS 모던 딥 다이브 스터디 4주차

민겸·2023년 4월 3일
0

SDK 스터디

목록 보기
4/6

4주차 에서는 자바스크립트의 근간이 되는 객체를 겉핥기 식으로 다루어보았다.

10장(객체 리터럴) 요약

자바스크립트는 객체 기반의 프로그래밍 언어이며, 자바스크립트를 구성하는 거의 "모든 것"이 객체다.

원시 타입의 값은 변경 불가능한 값이지만 객체 타입의 값, 즉 객체는 변경 가능한 값이다.

객체는 0개 이상의 프로퍼티로 구성된 집합이며, 프로퍼티는 keyvalue로 구성된다.
key에는 빈 문자열을 포함한 문자열과 Symbol값이 올 수 있고,
value에는 자바스크립트에서 사용할 수 있는 모든 값이 올 수 있다.

일급 객체인 함수가 value로 오게 될 경우, 일반 함수와 구별하기 위해 메서드라 부른다.

const obj = {
  "프로퍼티key1": "프로퍼티value1", // 프로퍼티: 객체의 상태를 나타내는 값
  "프로퍼티key2": function () {}, // 메서드: 프로퍼티를 참조하고 조작할 수 있는 동작
}

이처럼 객체는 객체의 상태를 나타내는 값(프로퍼티)과 그 값을 참조하고 조가할 수 있는 동작(메서드)을 모두 포함할 수 있기 때문에 상태와 동작을 하나의 단위로 구조화할 수 있어서 유용하다.

객체 생성 및 프로퍼티 정의

이러한 객체를 생성하기 위한 방법에는 여러 가지가 있다.

  • 객체 리터럴
  • Object 생성자 함수
  • 생성자 함수
  • Object.create 메서드
  • 클래스(ES6)

이 중 가장 일반적이고 간편한 방법이 바로 객체 리터럴이다.
객체 리터럴은 중괄호({...}) 내에 0개 이상의 프로퍼티를 정의한다. 변수에 할당되는 시점에 자바스크립트 엔진은 객체 리터럴을 해석해 객체를 생성한다.

프로퍼티를 정의할 때, 프로퍼티 key는 빈 문자열을 포함한 문자열과 Symbol값이 올 수 있지만 일반적으로 문자열을 사용한다. 그리고 식별자 네이밍 규칙을 준수한다면 ""또는 ''를 생략할 수 있다.

프로퍼티 key를 문자열이나 Symbol이외의 값을 사용한다면, 암묵적 타입 변환을 통해 문자열이 된다.

동일한 프로퍼티 key가 중복된다면, 나중에 선언된 key가 덮어쓴다.

const obj = {
  myName: "MinGyeom",    // 식별자 네이밍 규칙 준수함
  "my-Name": "MinGyeom", // 식별자 네이밍 규칙 준수하지 않음. "" 생략 불가
  0: 1, // 0 과
  2: 3,  // 2 는 내부적으로 문자열로 변환된다.
  "": "", // 빈 문자열도 프로퍼티 키로 사용할 수 있다.
  myName: "MinGyeommm", // 중복된 myName의 value는 "MinGyeommm"이 된다.
}

만약 식별자 네이밍 규칙을 준수하지 않음에도 ""를 생략한다면, 자바스크립트 엔진은 -를 연산자로 보고 my-Name을 표현식으로 해석한다.

프로퍼티 접근, 갱신, 생성, 삭제

프로퍼티에 접근하는 방법은 2가지가 있다.

  • 마침표 표기법(Dot Notation)과
  • 대괄효 표기법(Bracket Notation)으로

key를 통해 value에 접근할 수 있는데, 프로퍼티 key가 식별자 네이밍 규칙을 준수한다면 2가지 방법으로, 준수하지 않는다면 대괄효 표기법으로만 접근할 수 있다.

대괄효 표기법으로 접근할 경우, 반드시 따옴표로 감싼 문자열이여만 한다. 따옴표로 감싸지 않으면 자바스크립트 엔진은 이를 식별자로 해석한다.

객체에 존재하지 않는 프로퍼티에 접근하면 에러가 발생하지 않고 undefined를 반환한다.

const myObj = {
  myName: "MinGyeom",
  "my-Name": "MinGyeom",
  1: 0
}
console.log(myObj.myName);     // "MinGyeom"
console.log(myObj.my-Name);    // 브라우저 환경: NaN
							   // Node.js 환경: RefenceError: Name is not defined
console.log(myObj["myName"]);  // "MinGyeom"
console.log(myObj["my-Name"]); // "MinGyeom"
console.log(myObj[myName]);    // ReferenceError: myName is not defined
console.log(myObj.name);       // undefined

프로퍼티 key가 숫자로 이뤄진 문자열인 경우 따옴표를 생략할 수 있다.

console.log(myObj[1]); // 0
console.log(myObj['1']); // 0

이미 존재하는 프로퍼티key에 값을 할당하면 프로퍼티의 중복 선언으로 인해 프로퍼티 값이 갱신(덮어쓰여짐)된다.

존재하지 않는 프로퍼티에 접근하면 undefined를 반환하지만, 값을 할당하면 동적으로 생성되어 추가된다.

const myObj = {
  myName: "MinGyeom",
}
myObj.age = 27;
console.log(myObj); // { myName: "MinGyeom", age: 27 }

delete 연산자를 사용하면 객체의 프로퍼티를 삭제한다. 존재하지 않는 프로퍼티를 삭제하면 아무런 에러 없이 무시된다.

const myObj = {
  myName: "MinGyeom",
  age: 27
}
delete myObj.myName;
console.log(myObj); // { age: 27 }

ES5, ES6에서 추가된 객체 리터럴의 확장 기능

ES5부터는 계산된 프로퍼티 이름으로 프로퍼티 키를 동적 생성이,
ES6부터는 프로퍼티 축약 표현과 객체 리터럴 내부에서도 프로퍼티 키 동적 생성이
가능해졌다.

// ES5
let name = "key";
let i = 0;

const myObj1 = {};
// 계산된 프로퍼티 이름으로 key 동적 생성
myObj1[name + '-' + ++i] = i;
myObj1[name + '-' + ++i] = i;
myObj1[name + '-' + ++i] = i;
console.log(myObj1); // { key-1: 1, key-2: 2, key-3: 3 }

// ES6

let j = 0;
// 객체 내부에서 계산된 프로퍼티 이름으로 key 동적 생성
const myObj2 = {
	[`${name}-${++i}`]: i,
  	[`${name}-${++i}`]: i,
  	[`${name}-${++i}`]: i,
};
console.log(myObj2); // { key-1: 1, key-2: 2, key-3: 3 }

// 프로퍼티 정의 시, 변수명과 key이름이 같을 때 프로퍼티 key 생략
const myObj3 = { name };
console.log(myObj3); // { name: "key" }

const myObj4 = {
  name: "MinGyeom",
  // 메서드 축약 표현
  sayHi() {
    console.log("Hello, " + this.name);
  }
};

myObj4.sayHi(); // "Hello, MinGyeom"

ES6의 메서드 축약 표현으로 정의한 메서드는 프로퍼티에 할당한 함수와 다르게 동작한다는 사실에 유의하자.

11장(원시 값과 객체의 비교) 요약

11장에서는 원시 타입의 값과 객체 타입의 값이 불변성과 가변성이 띄는 이유와 실제 메모리에 어떻게 할당되며 복사했을 때 어떻게 다른지에 대해 나와있다.

원시 타입의 값은 불변성을 지닌다.

원시 타입의 값들은 문자열 타입은 글자 당 2바이트, 숫자 타입은 숫자의 크기에 상관없이 항상 8바이트이다. 그 이외의 타입들은 브라우저 제조사의 구현에 따라 다르지만 일정한 크기를 지니고 있으며 불변성을 지닌다. 그래서 원시 값이 할당된 변수에 새로운 원시 값을 할당하면 기존의 원시 값이 변하는 게 아니라 새로운 메모리 공간을 확보하여 원시 값을 할당하고 확보된 메모리 공간의 주소가 변수에 할당된다. 이는 데이터의 신뢰성을 보장한다.

ECMAScript 사양에는 변수를 통해 메모리를 어떻게 관리해야 하는지 명확하게 정의되어 있지 않다. 따라서 실제 자바스크립트 엔진을 구현하는 제조사에 따라 실제 내부 동작 방식은 미묘한 차이가 있을 수 있다.
딥 다이브 책에서는 변수에 원시 값을 갖는 변수를 할당하면 원시 값이 복사되는 것으로 표현되어 있다. 하지만 변수에 원시 값을 갖는 변수를 할당하는 시점에는 두 변수가 같은 원시 값을 참조하다가 어느 한쪽의 변수에 재할당이 이뤄졌을 때 비로소 새로운 메모리 공간에 재할당된 값을 저장하도록 동작할 수도 있다. 참고로 파이썬은 이처럼 동작한다.
총 두 가지로 나누어 추측할 수 있다.

  1. 할당 시점부터 새로운 메모리 공간을 확보한 후 기존 변수의 메모리 주소에 접근하여 값을 복사한 후 확보한 메모리 공간에 할당하고 그 메모리 주소를 새로운 변수에 할당한다.

  2. 할당 시점에 두 변수가 같은 메모리 공간의 원시 값을 참조하다가 어느 한쪽의 변수에 재할당이 이루어졌을 때 비로소 새로운 메모리 공간에 재할당된 값을 할당한다.

중요한 것은, 결국에 두 변수의 원시 값은 서로 다른 메모리 공간에 저장된 별개의 값이 되어 어느 한쪽에서 재할당을 통해 값을 변경하더라도 서로 간섭할 수 없다는 것이다.

그렇다면 객체 타입의 값은 왜 변할 수 있는 값일까?

원시 타입의 값들은 크기가 정해져 있고 상대적으로 적은 메모리를 소비하지만 객체는 경우에 따라 크기가 매우 클 수 있기 때문이다. 객체를 생성하고 프로퍼티에 접근하는 것도 원시 값과 비교할 때 비용이 많이 드는 일이다.

이러한 객체를 변경할 때 마다 원시 값처럼 이전 값을 복사 후 생성한다면 신뢰성이 보장되겠지만 객체는 크기가 매우 클 수도 있고 크기가 일정하지도 않다. 여기서 프로퍼티의 value가 객체라면 더 복잡해지고 비용도 많이 든다.

따라서 메모리의 효율적인 소비를 위해, 객체를 복사해 생성하는 비용을 절약하여 성능을 향상시키기 위해 객체는 변경 가능한 값으로 설계되어 있다.

객체는 이런 구조적 단점에 따른 부작용이 있다. 그것은
원시 값과는 다르게 여러 개의 식별자가 하나의 객체를 공유할 수 있다는 것이다.

4주차 스터디 회고

이번 4주차에서는 얻거나 상기시킨 내용들이 꽤 많았다.

  • 객체의 얕은 복사와 깊은 복사
  • 좋은 코드를 위한 비동기 처리와 React Suspense의 조합

객체를 깊은 복사할 수 있는 새로운 방법을 알게 되었다. 기존에 알고 있던 깊은 복사의 방법은 다음과 같다.

  • JSON 객체의 stringify(), parse() 사용
  • lodash_.cloneDeep() 사용
  • 재귀함수 사용

새로 알게된 객체의 깊은 복사 방법은 structuredClone이다.

structuredClone으로 객체를 복사하는 방법은 JSON 객체를 이용해 깊은 복사를 하는 것의 단점을 어느 정도 보완하는 방법이다.

  • 객체의 크기가 클 때, structuredCloneJSON 보다 속도가 빠르다.
  • 순환 그래프를 포함하는 객체의 직렬화에서 structuredCloneJSON보다 더 효율적이다.

자세한 내용은 객체의 깊은 복사 방법들에 대해 기술한 이 글을 참고하자.

다음 주는 내가 고향으로 내려갈 일이 생겨 오프라인 스터디를 참여하지 못하게 되었다.
그래서 다음 주 분량을 다다음주로 미뤄 총 4개의 장 분량과 함께 스터디 시간을 1시간 늘려서 진행하는 것으로 결정되었다.

profile
기술부채상환중...

0개의 댓글