22. 10. 16 자바스크립트) 객체 기본

divedeepp·2022년 10월 15일
0

JavaScript

목록 보기
5/11

자바스크립트엔 여덟 가지 자료형이 있고, 이 중 일곱 개는 오직 하나의 데이터만 담을 수 있어 원시형(primitive type)이라 부른다.


객체 기본

객체형은 원시형과 달리 키로 구분된 데이터 집합이나 복잡한 객체 등 다양한 데이터를 담을 수있다.

객체 생성하기

객체를 만드는 방법은 두 가지가 있다.

// 객체 생성자 문법
let user = new Object();

// 객체 리터럴 문법
let user = {};

객체 리터럴과 프로퍼티

객체 리터럴의 중괄호 안에는 키:값 쌍으로 구성된 프로퍼티가 들어간다. 키에는 문자형과 심볼형, 값에는 모든 자료형이 허용된다.

let user = {
  name: "Yong",
  age: 27
};

프로퍼티는 점 표기법을 이용해 값을 읽거나 추가, 삭제할 수 있다.

// 프로퍼티 값 읽기
user.name;  // Yong
user.age; // 27

// 프로퍼티 추가하기
user.isAdmin = true;

// 프로퍼티 삭제하기. delete 연산자를 사용.
delete user.age;

// 여러 단어를 조합해 프로퍼티 만들기. 복수의 단어는 따옴표로 묶어야 한다.
let user = {
  name: "Yong",
  age: 27,
  "likes dog": true
};

여러 단어를 조합해 프로퍼티 키를 만든 경우에는 점 표기법을 사용해서 프로퍼티 값을 읽을 수 없다. 대신 대괄호 표기법을 사용해서 읽을 수 있다. 마찬가지로 대괄호 표기법을 사용해서 프로퍼티 추가와 삭제도 가능하다. 대괄호 표기법 안의 프로퍼티 키는 따옴표로 감싸야 한다. 또, 프로퍼티 키를 변수에 할당하여 접근할 수도 있다.

let user = {};

// 대괄호 표기법을 사용해 프로퍼티 추가하기
user["likes dog"] = true;

// 대괄호 표기법을 사용해 프로퍼티 읽기
user["likes dog"]); // true

// 키를 변수에 할당하여 프로퍼티에 접근하기
let key = "likes dog";
user[key]; // true

// 대괄호 표기법을 사용해 프로퍼티 삭제하기
delete user["likes dog"];

객체 리터럴 방식으로 객체를 만들 때, 프로퍼티 키가 대괄호로 둘러싸여 있는 경우, 해당 프로퍼티 키를 계산된 프로퍼티(computed property)라고 부른다.

let fruit = prompt("어떤 과일을 구매하시겠습니까?", "apple");

let bag = {
  [fruit]: 5, // 변수 fruit에서 프로퍼티 이름을 동적으로 받아 옵니다.
};

alert( bag.apple ); // fruit에 "apple"이 할당되었다면, 5가 출력됩니다.

위 예시에서 [fruit]는 프로퍼티 이름을 변수 fruit에서 가져오겠다는 것을 의미한다.

const, let과 객체 선언

앞 선 내용에서 const로 선언된 상수는 수정할 수 없다고 배웠다.

하지만, const로 선언된 객체는 수정할 수 있다. 엄밀히 말하면, 객체 안의 프로퍼티만 수정할 수 있다.

const user = {
  name: "Yong"
};

user.name = "Dean";

user.name; // Dean

물론, let으로 선언한 객체는 객체 자체도 수정할 수 있다.

단축 프로퍼티

프로퍼티 값을 매개변수에서 받아와 사용하는 경우가 종종 있다.

function makeUser(name, age) {
  return {
    name: name,
    age: age
  };
}

let user = makeUser("Yong", 27);
user.name; // Yong

위 예시의 프로퍼티들은 키와 값의 이름이 매개변수와 동일하다. 이렇게 매개변수를 사용해 프로퍼티를 만드는 경우에는 프로퍼티 값 단축 구문(property value shorthand)을 사용해서 코드를 짧게 줄일 수 있다.

function makeUser(name, age) {
  return {
    name,	// == name: name
    age,	// == age: age
  };
}

프로퍼티 존재 여부 확인하기

자바스크립트 객체는 존재하지 않는 프로퍼티에 접근해도 에러가 발생하지 않고, undefined가 반환된다.

이런 특징을 응용해서 프로퍼티 존재 여부를 쉽게 확인할 수 있다.

let user = {};

user.noSuchProperty === undefined;	// true

다만 이러한 방식은 프로퍼티는 존재하지만 값이 없을 경우에도 존재하지 않는다고 출력되므로, 존재 여부 확인이 정확하지 않을 수 있다.

이런 문제점은 in 연산자를 사용해 해결할 수 있다.

// in 연산자 사용하기
let user = { name: "Yong", age: 27 };

"age" in user;	// true
"blabla" in user;	// false

객체 순회하기

for .. in 반복문을 사용해서 객체의 모든 키를 순회할 수 있다.

for (let key in object) {
  // 각 프로퍼티 키를 이용해서 본문을 실행한다.
}

객체의 프로퍼티 정렬 방식

정수 프로퍼티는 자동으로 정렬되고, 그 외의 프로퍼티는 객체에 추가한 순서대로 정렬된다.

// 정수 프로퍼티 예시
let codes = {
  "49": "독일",
  "41": "스위스",
  "1": "미국"
};

for (let code in codes) {
  code;	// 1, 41, 49
}

// 정수가 아닌 프로퍼티 예시
let user = {
  name: "Yong",
  surname: "JunHyun"
};
user.age = 27;

for (let prop in user) {
  prop;	// name, surname, age
}

객체는 참조에 의해 복사된다

객체형과 원시형의 근본적인 차이 중 하나는 변수를 다른 변수에 할당할 때, 원시형은 값 자체가 복사되어 저장되고, 객체는 참조에 의해 복사되고 저장된다는 것이다.

쉽게 말해 값 자체가 아닌, 값을 저장하고 있는 객체에 접근할 수 있는 메모리 주소가 복사된다.

이러한 특징을 활용하면 여러 변수를 사용하여 하나의 객체에 접근하거나 조작할 수 있다.

// 원시형 예시
let message = "Hello!";
let phrase = message;

phrase = "bye!";

alert(message);	// Hello!

// 객체형 예시
let user = { name: "John" };
let admin = user;

admin.name = "Dean";

user.name	// Dean

참조에 의해 복사된 객체는 동일하다. 따라서 객체 비교 시에 동등 연산자 ==와 일치 연산자 ===는 동일하게 동작한다.

비교 시 피연산자인 두 객체가 동일한 객체인 경우에 참을 반환한다.

let a = {};
let b = a;

a == b	// true;
a === b // true;

객체 복제와 병합

객체가 할당된 변수를 복사하면 동일한 객체에 대한 참조 값이 하나 더 만들어지는 것을 살펴보았다.

그런데 객체를 복제하고 싶다면 즉, 기존에 있던 객체와 똑같지만 독립적인 객체를 만들고 싶다면 어떻게 할까?

새로운 객체를 만든 다음 기존 객체의 프로퍼티들을 순회하여 프로퍼티를 복사하면 된다.

let user = {
  name: "Yong",
  age: 27
};

let clone = {};

for (let key in user) {
  clone[key] = user[key];
}

clone.name = "Dean";

user.name;	// Yong

또는 Object.assign 메소드를 사용해도 된다.

Object.assign(dest, [src1, src2, ...]);
  • 첫 번째 인수 dest : 목적지 객체이다.
  • 이어지는 인수들 src1, src2, ... : 복사하고자 하는 객체이다.
  • 복사하고자하는 객체의 프로퍼티를 dest에 복사한다.
    • 목적지 객체에 동일한 이름을 가진 프로퍼티가 있는 경우에는 기존 값이 덮어씌워 진다.
  • dest를 반환한다.
let user = { name: "Yong" };
let permission1 = { canView: true };
let permission2 = { canEdit: true };

Object.assign(user, permission1, permission2);

// user = { name: "Yong", canView: true, canEdit: true }

위 메소드를 사용하여, 순회해서 복제했던 객체를 다시 만들어보자. 훨씬 간단하다.

let user = {
  name: "Yong",
  age: 27
};

let clone = Object.assign({}, user);

위 방식들은 중첩된 객체까지는 복사하지 못하므로, 얕은 복사라 한다.

중첩 객체 복제

프로퍼티가 중첩 객체일 때는 어떻게 할까?

let user = {
  name: "Yong",
  sizes: {
    height: 190,
    width: 100
  }
};

단순히 clone.sizes = user.sizes로는 객체를 복제할 수 없다. 왜냐하면 중첩된 user.sizes는 객체이기 때문에 참조 값이 복사되어, cloneuser는 같은 sizes 객체를 공유하게 된다.

이 문제를 해결하려면 user의 각 키를 검사하면서, 그 키가 객체인 경우에는 해당 객체의 구조도 순회하는 반복문을 사용해야 한다.

이런 방식을 깊은 복사라 한다.


메서드(method)

객체는 사용자, 주문 등과 같이 실제 존재하는 개체를 표현하고자 할 때 생성된다.

사용자는 현실에서 장바구니에서 물건 선택하기, 로그인하기 등의 행동을 한다.

이와 마찬가지로 사용자를 나타내는 객체도 특정한 행동을 할 수 있다.

자바스크립트에선 객체의 프로퍼티에 함수를 할당하여 객체에 행동을 할 수 있는 기능을 부여한다.

메서드 생성하기

let user = {
  name: "Yong",
  age: 27
};

user.sayHi = function() {
  alert("Hello!");
};

user.sayHi();	// Hello!

함수 표현식으로 함수를 만들고, 객체 프로퍼티에 함수를 할당해 주었다.

이렇게 객체 프로퍼티에 할당된 함수를 메서드라 한다.

이미 정의된 함수를 이용해 메서드를 만들 수도 있다.

let user = {
  ...
};
  
function sayHi() {
  alert("Hello!");
}

user.sayHi = sayHi;

user.sayHi();	// Hello!

메서드 단축 구문

객체 리터럴 안에 메서드를 단축 구문으로 선언할 수 있다.

// 일반적인 메서드 선언
user = {
  sayHi: function() {
    alert("Hello");
  }
};

// 단축 구문을 이용한 메서드 선언
user = {
  sayHi() {
    alert("Hello");
  }
};

메서드와 this

메서드는 객체에 저장된 정보(프로퍼티)에 접근할 수 있어야 제 역할을 할 수 있다.

메서드 내부에서 this 키워드를 사용하면 메서드를 호출한 객체에 접근할 수 있다.

this는 점(.) 앞의 객체를 참조한다.

let user = {
  name: "Yong",
  age: 27,
  
  sayHi() {
    alert(this.name);
  }
  
};

user.sayHi()  // Yong

자유로운 this

자바스크립트에선 모든 함수에 this 키워드를 사용할 수 있다.

this 값은 런타임에 결정된다. 즉, 런타임의 컨텍스트에 따라 가리키는 객체가 달라진다.

동일한 함수라도 다른 객체에서 호출했다면 this가 참조하는 값이 달라진다.

let user = { name: "Yong" };
let admin = { name: "Dean" };

function sayHi() {
  alert( this.name ) ;
}

user.f = sayHi;
admin.f = sayHi;

user.f();  // Yong
admin.f();  // Dean

this가 없는 화살표 함수

화살표 함수는 일반 함수와 달리 고유한 this를 가지지 않는다.

화살표 함수에서 this를 참조하면, 화살표 함수가 아닌 외부 함수에서 this 값을 가져온다.

아래 예시에서 함수 arrow()this는 외부 함수 sayHi()this가 된다.

let user = {
  firstName: "준현",
  sayHi() {
    let arrow = () => alert(this.firstName);
  }
};

user.sayHi();	// 준현

이러한 특징은 별개의 this가 만들어지는 건 원하지 않고 외부 컨텍스트에 있는 this를 이용하고 싶은 경우에 유용하다.


new 연산자와 생성자 함수

객체 리터럴을 사용하면 객체를 쉽게 만들 수 있지만, 개발을 하다 보면 유사한 객체를 여러 개 만들어야 할 때가 생길 수 있다.

그럴 때 new 연산자와 생성자 함수를 사용하면 유사한 객체 여러 개를 쉽게 만들 수 있다.

생성자 함수(constructor function)

생성자 함수는 두 가지 관례를 따른다.

  • 함수 이름의 첫 글자는 대문자로 시작한다.
  • new 연산자를 붙여 실행한다.

생성자 함수를 실행하면 다음과 같은 알고리즘이 동작한다.

  1. 빈 객체를 만들어 this에 할당한다.
  2. 함수 본문을 실행한다. this에 새로운 프로퍼티를 추가해 this를 수정한다.
  3. this를 반환한다.
function User(name) {
  // this = {};		빈 객체가 내부적으로 만들어짐
  
  // 새로운 프로퍼티를 this에 추가함
  this.name = name;
  this.isAdmin = false;
  
  // return this;	this가 내부적으로 반환됨
}

let user = new User("준현");

user.name;	// 준현
user.isAdmin;	// false

위 방식은 객체 리터럴 문법으로 일일이 객체를 만드는 방법보다 간단하게 여러 객체를 만들 수 있다. 생성자 함수의 의의는 바로 여기에 있다. 재사용할 수 있는 객체 생성 코드를 구현하는 것이다.

재사용할 필요가 없는 복잡한 객체를 만들어야 할 때는 어떻게 할까? 이럴 때는 익명 생성자 함수로 감싸주는 방식을 사용할 수 있다.

let user = new function() {
  this.name = "Yong";
  this.isAdmin = false;
};

위 생성자 함수는 익명 함수이기 때문에 어디에도 저장되지 않는다. 만들 때부터 단 한 번만 호출되기 때문에 재사용도 불가능하다. 이렇게 익명 생성자 함수를 이용하면, 재사용은 막으면서 코드를 캡슐화할 수 있다.

생성자 함수와 return문

생성자 함수엔 보통 return문이 없다. 반환해야 할 것들은 모두 this에 저장되고, this는 자동으로 반환되기 때문에 반환문을 명시적으로 써 줄 필요가 없다.

만약 return 문이 있다면 아래와 같은 규칙이 적용된다.

  • 객체를 return 한다면 this 대신에 명시한 객체가 반환된다.
  • 원시형을 return한다면 return문이 무시되고, this가 반환된다.
// 객체를 리턴할 때
function BigUser() {

  this.name = "원숭이";

  return { name: "고릴라" };  // <-- this가 아닌 새로운 객체를 반환함
}

alert( new BigUser().name );  // 고릴라

// 직접 명시하여 아무것도 리턴하지 않을 때
function SmallUser() {

  this.name = "원숭이";

  return; // <-- this를 반환함
}

alert( new SmallUser().name );  // 원숭이

생성자 함수 내 메서드

생성자 함수를 사용하면 매개변수를 이용해 객체 내부를 자유롭게 구성할 수 있다.

function User(name) {
  this.name = name;
  
  this.sayHi = function() {
    alert( "제 이름은 " + this.name + "입니다." );
  };
}

let yong = new User("용준현");

yong.sayHi();	// 제 이름은 용준현입니다.

옵셔널 체이닝(optional chaining) ?.

옵셔널 체이닝을 사용하여 프로퍼티가 없는 객체를 에러 없이 안전하게 접근할 수 있다.

옵셔널 체이닝이 필요한 이유

브라우저에서 동작하는 코드를 개발할 때, 다음과 같은 문제가 발생할 때가 있다.

자바스크립트를 사용해 페이지에 존재하지 않는 요소에 접근해서 요소의 정보를 가져오려 하면 문제가 발생한다.

// querySelector 호출 결과가 null인 경우에 에러 발생
let html = document.querySelector(".my-element").innerHTML;

명세서에 옵셔널 체이닝이 추가되기 전에 이런 문제들을 해결하기 위해 첫 번째 falsy를 찾는 && 연산자를 사용하곤 했다.

let user = {}; // 주소 정보가 없는 사용자

alert( user && user.address && user.address.street ); // undefined, 에러가 발생하지 않습니다.

중첩 객체의 특정 프로퍼티에 접근하기 위해 거쳐야 할 요소들을 AND로 연결해 실제 해당 객체나 프로퍼티가 있는지 확인하는 방법을 사용했다. 이런 방식은 코드가 길어진다는 단점이 있다.

옵셔널 체이닝의 등장

?.은 앞의 평가 대상이 undefinednull이면 평가를 멈추고, undefined를 반환한다.

옵셔널 체이닝을 사용해 user.address.street에 안전하게 접근해보자.

let user = {};

alert( user?.address?.street );	// undefined. 에러가 발생하지 않는다.

옵셔널 체이닝을 사용하면 아예 객체가 존재하지 않더라도 에러가 발생하지 않는다.

let user = null;

alert( user?.address );	// undefined
alert( user?.address.street );	// undefined

?.은 앞 평가 대상에만 동작하고, 확장은 되지 않는다.

쉽게 말해, 위 예시에서 user?.usernull이나 undefined인 경우만 처리할 수 있다. user가 실제 값이 존재할 때, user.address 프로퍼티가 반드시 존재해야 한다. 그렇지 않으면, 위 경우에서 옵셔널 체이닝을 사용하지 않은 두 번째 점 연산자에서 에러가 발생한다.

옵셔널 체이닝을 남용하지 말자

?.는 존재하지 않아도 괜찮은 대상에만 사용해야 한다.

사용자 주소를 다루는 위 예시에서 논리상 user는 반드시 존재해야 하는데, address는 필수값이 아니다.

그러니 user.address?.street를 사용하는 것이 바람직하다.

이유는 user에 값을 할당하지 않았다면 바로 알아낼 수 있어야 하기 때문이다. 그렇지 않으면 에러를 조기에 발견하지 못해 디버깅이 어려워진다.

?. 앞의 변수는 꼭 선언되어 있어야 한다.

옵셔널 체이닝은 선언이 완료된 변수를 대상으로만 동작한다.

옵셔널 체이닝과 단락 평가

?.는 왼쪽 평가대상이 undefined이거나 null이면 즉시 평가를 멈춘다.

이런 평가 방법을 단락 평가라고 부른다.

이러한 특성 때문에 ?. 오른쪽에 있는 부가 동작은 ?.의 평가가 멈췄을 때 더는 일어나지 않는다.

let user = null;
let x = 0;

user?.sayHi(x++);	// 아무 일도 일어나지 않는다.
alert(x);	// 0, x는 증가하지 않는다.

?.()와 ?.[]

?.은 연산자가 아니고, 함수나 대괄호와 함께 동작하는 특별한 문법 구조체이다.

존재 여부가 확실치 않은 함수를 호출할 때의 활용 방법을 살펴보자.

한 객체엔 admin 메서드가 있지만 다른 객체에는 없는 상황이다.

let user1 = {
  admin() {
    alert("관리자 계정입니다.");
  }
}

let user2 = {};

user1.admin?.();	// 관리자 계정입니다.
user2.admin?.();	//

user1에는 admin이 정의되어 있기 때문에 메서드가 제대로 호출되었다.

반면 user2에는 admin이 정의되어 있지 않았음에도, 메서드를 호출했을 때 에러 없이 평가가 멈추었다.

또, 점 표기법 대신 대괄호 표기법[]를 사용하여 객체 프로퍼티에 접근하는 경우에도 ?.[]를 사용할 수 있다.

옵셔널 체이닝을 사용하면 객체의 존재 여부가 확실치 않은 경우에도 대괄호 표기법을 사용해 프로퍼티를 읽을 수 있다.

let user1 = {
  firstName: "Violet"
};

let user2 = null;

let key = "firstName";

alert( user1?.[key] );	// violet
alert( user2?.[key] );	// undefined 에러가 발생하지 않음

옵셔널 체이닝을 이용한 프로퍼티 삭제

옵셔널 체이닝과 delete를 조합해 객체의 프로퍼티를 삭제할 수도 있다.

delete user?.name;	// user가 존재하면 user.name을 삭제한다.

옵셔널 체이닝을 이용한 프로퍼티 쓰기

당연하게도 옵셔널 체이닝은 읽기나 삭제를 할 순 있지만, 쓸 수는 없다.

옵셔널 체이닝의 앞 쪽이 undefinednull 인 곳에 값을 쓸 수는 없기 때문이다.


심볼형

자바스크립트는 객체 프로퍼티 키로 오직 문자형과 심볼형만을 허용한다.

심볼(symbol)

심볼은 원시형 데이터로, 유일한 식별자(unique identifier)를 만들고 싶을 때 사용한다.

Symbol()을 사용하여 심볼값을 만들 수 있다.

심볼 이름이라 불리는 설명을 붙일 수도 있다. 심볼 이름은 디버깅 시 유용하다.

// 심볼 선언
let id = Symbol();


// 심볼이름이 붙은 심볼 선언
let id = Symbol("id");

심볼은 유일성이 보장되는 자료형이기 때문에, 설명이 동일한 심볼을 여러 개 만들어도 각 심볼값은 다르다. 심볼 이름은 심볼에 붙이는 설명일 뿐, 어떤 것에도 영향을 주지 않는 이름표 역할만 한다.

let id1 = Symbol("id");
let id2 = Symbol("id");

id1 == id2;	// false

심볼은 문자형으로 자동 형 변환 되지 않는다.

자바스크립트에선 문자형으로의 내부적 형 변환이 비교적 자유롭게 일어난다.

let id = Symbol("id");
alert(id);	// TypeError: Cannot convert a Symbol value to a string

문자열과 심볼은 근본이 다르기 때문에 우연히라도 서로의 타입으로 변환돼선 안된다.

자바스크립트에선 언어 차원의 보호장치(language guard)를 마련해 심볼이 다른 형으로 변환되지 않게 막아준다.

description 프로퍼티를 이용해서 심볼 이름만 보여주는 것도 가능하다.

let id = Symbol("id");
alert( id.description );	// id

숨김 프로퍼티

심볼을 이용하면 숨김 프로퍼티를 만들 수 있다.

숨김 프로퍼티는 외부 코드에서 접근이 불가능하고, 갚도 덮어쓸 수 없는 프로퍼티이다. 이를 활용해 현재 스크립트에서 외부 스크립트에 속한 객체에 새로운 프로퍼티를 추가할 수 있다.

서드파티 코드에서 가지고 온 user라는 객체가 여러 개 있고, user를 이용해 어떤 작업을 해야 하는 하는 상황이라고 가정하자. user에 심볼을 이용한 식별자를 붙일 수 있다.

let user = {
  name: "Yong"
};

let id = Symbol("id");

user[id] = 1;

// 심볼을 키로 사용해 데이터에 접근
alert( user[id] );	// 1 

문자열 "id"를 키로 사용하지 않고, Symbol("id")를 사용한 이유가 무엇일까?

user는 서드파티 코드에서 가지고 온 객체이므로 함부로 새로운 프로퍼티를 추가할 수 없다.

그런데 심볼은 서드파티 코드에서 접근할 수 없기 때문에, 심볼을 사용하면 서드파티 코드가 모르게 user에 식별자를 부여할 수 있다.

또 다른 상황을 살펴보자.

제 3의 스크립트에서 user를 식별해야 하는 상황이 벌어졌다고 가정하자. user의 원천인 서드파티 코드, 현재 작성 중인 스크립트, 제 3의 스크립트가 각자 서로의 코드도 모른 채 user를 식별해야 하는 상황이 벌어졌다.

제 3의 스크립트에서도 아래와 같이 Symbol("id")를 이용해 전용 식별자를 만들어 사용할 수 있다.

let id = Symbol("id");

user[id] = "어떤 스크립트 id 값";

심볼은 유일성이 보장되므로 이름이 같더라도 우리가 만든 식별자와 어떤 스크립트에서 만든 식별자가 충돌하지 않는다.

만약 심볼 대신 문자열 "id"를 사용해 식별자를 만들었다면 충돌이 발생하고 덮어씌어질 가능성이 있다.

리터럴에서의 심볼

객체 리터럴을 사용해 객체를 만든 경우, 대괄호를 사용해 심볼형 키를 만들어야 한다.

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123
};

키가 심볼인 프로퍼티는 for in 반복문에서 배제된다.

let id = Symbol("id");

let user = {
  name: "Yong",
  age: 27,
  [id]: 123
};

for (let key in user) alert(key);	// name, age

Object.keys(user) 에서도 키가 심볼인 프로퍼티는 배제된다.

심볼형 프로퍼티 숨기기(hiding symbolic property)라 불리는 원칙 덕분에 외부 스크립트나 라이브러리는 심볼형 키를 가진 프로퍼티에 접근하지 못한다.

하지만 Object.assgin은 키가 심볼이 프로퍼티를 배제하지 않고 객체 내 모든 프로퍼티를 복사한다.

let id = Symbol("id");
let user = {
  [id]: 123
};

let clone = Object.assgin({}, user);

alert( clone[id] );	// 123

전역 심볼

심볼은 이름이 같더라도 모두 별개로 취급된다. 하지만 이름이 같은 심볼이 같은 개체를 가리키기를 원할 때도 있다.

전역 심볼 레지스트리(global symbol registry)는 이런 경우를 위해 만들어졌다.

전역 심볼 레지스트리 안에 심볼을 만들고 해당 심볼에 접근하면, 이름이 같은 경우 항상 동일한 심볼을 반환해준다.

전역 심볼 레지스트리 안에 있는 심볼을 읽거나, 새로운 심볼을 생성하려면 Symbol.for(key)를 사용하면 된다.

이 메서드를 호출하면 심볼 이름이 key인 심볼을 반환한다. 조건에 맞는 심볼이 레지스트리 안에 없으면 새로운 심볼 Symbol(key)을 만들고 레지스트리 안에 저장한다.

// 전역 심볼 레지스트리에서 심볼 읽기. 심볼이 존재하지 않으면 새로운 심볼을 만듦
let id = Symbol.for("id");

// 동일한 이름을 이용해 심볼을 다시 읽기
let idAgain = Symbol.for("id");

alert( id === idAgain) );	// true

전역 심볼 레지스트리 안에 있는 심볼을 전역 심볼이라 한다. 애플리케이션에서 광범위하게 사용해야 하는 심볼일 경우 전역 심볼을 사용하면 된다.

전역 심볼을 찾을 때 사용되는 Symbol.for(key)에 반대되는 메서드도 있다.

Symbol.keyFor(심볼)를 사용하면 심볼을 사용하여 심볼 이름을 얻을 수 있다.

let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

alert( Symbol.keyFor(sym) );	// name
alert( Symbol.keyFor(sym2) );	// id

Symbol.keyFor는 전역 심볼 레지스트리를 검색해서 해당 심볼의 이름을 얻어낸다. 검색 범위가 전역 심볼 레지스트리이기 때문에 전역 심볼이 아닌 심볼에는 사용할 수 없다. 전역 심볼이 아닌 인자가 넘어오면 undefined를 반환한다.

전역 심볼이 아닌 모든 심볼은 description 프로퍼티가 있다. 일반 심볼에서 심볼 이름을 얻고 싶으면 해당 프로퍼티를 사용하면 된다.

let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");

alert ( Symbol.keyFor(globalSymbol) );	// name
alert ( Symbol.keyFor(localSymbol) );	// undefined

alert ( localSymbol.description );	// name

시스템 심볼(system symbol)

자바스크립트 내부에서 사용되는 심볼로, 이를 활용하여 객체를 미세 조정할 수 있다.

  • Symbol.hasInstance
  • Symbol.isConcatSpreadable
  • Symbol.iterator
  • Symbol.toPrimitive
  • etc ...

사실 심볼을 완전히 숨길 방법은 없다.

Object.getOwnPropertySymbols(obj)를 사용하면 모든 심볼을 볼 수 있고, Reflect.ownKeys(obj)는 심볼형 키를 포함한 객체의 모든 키를 반환해준다.

하지만 대부분의 라이브러리, 내장 함수 등은 이런 메서드를 사용하지 않는다.


객체를 원시형으로 변환하기

객체에 원시값을 기대하는 내장 함수나 연산자를 사용할 때, 객체-원시형으로의 형 변환이 자동으로 일어난다.

  • 불리언으로의 형 변환에서 객체는 논리 평가 시 항상 true를 반환한다.
  • 숫자형으로의 형 변환은 객체끼리 빼는 연산을 할 때나 수학 관련 함수를 적용할 때 일어난다. 예를 들어, 객체 Date 끼리 빼는 연산을 수행하면, 두 날짜의 시간 차이가 반환된다.
  • 문자형으로의 형 변환은 대개 alert(obj) 같이 객체를 출력하려고 할 때 일어난다.

특수 객체 메서드를 사용한 객체의 형 변환

특수 객체 메서드를 사용하면 숫자형이나 문자형으로의 형 변환을 원하는 대로 조절할 수 있다.

객체 형 변환은 세 종류로 구분되는데, "hint"라 불리는 값이 구분 기준이 된다. "hint"는 목표로 하는 자료형 정도로 이해하면 된다.

  • "string"

alert 함수 같이 문자열을 기대하는 연산을 수행할 때는, hint가 string이 된다.

// 객체 출력
alert(obj);

// 객체를 프로퍼티 키로 사용
anotherObj[obj] = 123;
  • "number"
    수학 연산을 적용하려 할 때, hint는 number가 된다.
// 명시적 형 변환
let num = Number(obj);

// 수학 연산(이항 덧셈 연산 제외)
let n = +obj;
let delta = date1 - date2;

// 크기 비교
let greater = user1 > user2;
  • "default"
    연산자가 기대하는 자료형이 확실치 않을 때, hint는 default가 된다. 이런 상황은 아주 드물게 발생한다.

hint가 "number일 때 이항 덧셈 연산은 제외하였는데, 이항 덧셈 연산자 +는 피연산자의 자료형에 따라 문자열을 합치는 연산일 수도 있고, 숫자를 더해주는 연산을 할 수도 있기 때문이다.

동등 연산자 ==를 사용해 객체와 원시형을 비교할 때도, 객체를 어떤 자료형으로 바꿔야 할지 확신이 안 서므로 hint는 default가 된다.

// 이항 덧셈 연산
let total = obj1 + obj2;

// 동등 연산
if (user == 1) { ... };

Date 객체를 제외한 모든 내장 객체는 hint가 default인 경우와 number인 경우를 동일하게 처리한다. 따라서, 커스텀 객체를 만들 때 이러한 규칙을 따르면 된다.

결론적으로 객체와 원시형의 형 변환은 두 종류만 남게된다.

  • 객체 - 문자형
  • 객체 - 숫자형

자바스크립트는 형 변환이 필요할 때, 아래와 같은 알고리즘에 따라 원하는 메서드를 찾고 호출한다.

  1. 객체에 obj[Symbol.toPrimitive](hint) 메서드가 있는지 찾는다. 있다면 메서드를 호출한다.
    • Symbol.toPrimitive는 시스템 심볼로, 심볼형 키로 사용된다.
  2. 1에 해당하지 않고 hint가 "string" 이라면,
    • obj.toString() 이나 obj.valueOf()를 호출한다.
  3. 1과 2에 해당하지 않고 hint가 "number""default" 라면
    • obj.valueOf()obj.toString() 을 호출한다.

Symbol.toPrimitive

자바스크립트에는 Symbol.toPrimitive라는 내장 심볼이 존재한다.

이 심볼은 아래와 같이 hint를 명명하는 데 사용된다.

obj[Symbol.toPrimitive] = function(hint) {
  // 반드시 원시값을 반환해야 함
  // hint는 "string", "number", "default" 중 하나가 될 수 있음
};

// 실제 예시
let user = {
  name: "Yong",
  money: 1000,
  
  [Symbol.toPrimitive](hint) {
    alert(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};

alert(user);	// hint: string => {name: "Yong"}
alert(+user);	// hint: number => 1000
alert(user + 500);	// hint: default => 1500

이렇게 메서드를 구현해 놓으면 객체 user는 hint에 따라 문자열로 변환되기도 하고, 숫자로 변환되기도 한다.

toString과 valueOf

toStringvalueOf는 심볼이 생기기 이전부터 존재했던 메서드이다.

이 메서드를 이용하면 구식이긴 하지만 형 변환을 직접 구현할 수 있다.

객체에 Symbol.toPrimitive가 없으면 자바스크립트는 아래 규칙에 따라 toString이나 valueOf를 호출한다.

  • hint가 "string"인 경우 : toString, valueOf 순으로 호출
  • hint가 "number, "default"인 경우 : valueOf, toString 순으로 호출

이 메서드들은 반드시 원시값을 반환해야 한다. 객체를 반환하면 그 결과는 무시된다.

일반 객체는 기본적으로 다음 규칙을 따른다.

  • toString은 문자열 "[object Object]"을 반환한다.
  • valueOf는 객체 자신을 반환한다.
let user = {name: "Yong"};

alert(user);	// [object Object]
alert(user.valueOf() === user);	// true

이런 이유 때문에 alert에 객체를 넘기면 [object Object]가 출력되는 것이다.

valeOf는 객체 자신을 반환하기 때문에 그 결과가 무시된다. 우리는 그냥 이 메서드가 존재하지 않는다고 생각하면 된다.

이제 직접 이 메서드들을 사용한 예시를 구현해보자.

let user = {
  name: "Yong",
  money: 1000,
  
  // hint가 "string"인 경우
  toString() {
    return `{name: "${this.name}"}`;
  },
  
  // hint가 "number" 나 "default" 인 경우
  valueOf() {
    return this.money;
  }
  
};

alert(user);	// {name: "Yong"}
alert(+user);	// 1000
alert(user + 500);	// 1500

출력 결과가 Symbol.toPrimitive를 사용한 예제와 완전히 동일하다.

간혹 모든 형 변환을 한 곳에서 처리해야 하는 경우도 생긴다.

이럴 땐 아래와 같이 toString만 구현하면된다.

let user = {
  name: "Yong",
  
  toString() {
    return this.name;
  }
};

alert(user);	// Yong
alert(user + 500);	// Yong500

객체에 Symbol.toPrimitivevalueOf가 없으면, toString이 모든 형 변환을 처리한다.

반환 타입

위에서 살펴 본 세 개의 메서드는 hint에 명시된 자료형으로의 형 변환을 보장해주지 않는다.

확신할 수 있는 단 한 가지는 객체가 아닌 원시값을 반환해 준다는 것 뿐이다.

추가 형 변환

상당수의 연산자와 함수가 피연산자의 형을 변환시킨다.

객체가 피연산자일 때는 다음과 같은 단계를 거쳐 형 변환이 일어난다.

  1. 객체는 원시형으로 변환된다.
  2. 변환 후 원시간이 원하는 형이 아닌 경우에 또 다시 형 변환이 일어난다.

obj.toString()만 사용해도 모든 변환을 다 다룰 수 있기 때문에, 실무에선 obj.toString()만 구현해도 충분한 경우가 많다. 반환값도 사람이 읽고 이해할 수 있는 형식이기 때문에 실용성 측면에서 다른 메서드에 뒤처지지 않는다.

또, obj.toString()은 로깅이나 디버깅 목적으로도 자주 사용된다.


참고 문헌

https://ko.javascript.info/object-basics

profile
더깊이

0개의 댓글