4. 객체: 기본

protect-me·2021년 6월 1일
0
post-thumbnail

4.1 객체


요약
객체는 몇 가지 특수한 기능을 가진 연관 배열(associative array)입니다.

객체는 프로퍼티(키-값 쌍)를 저장합니다.

  • 프로퍼티 키는 문자열이나 심볼이어야 합니다. 보통은 문자열입니다.
  • 값은 어떤 자료형도 가능합니다.

아래와 같은 방법을 사용하면 프로퍼티에 접근할 수 있습니다.

  • 점 표기법: obj.property
  • 대괄호 표기법 obj["property"]. 대괄호 표기법을 사용하면 obj[varWithKey]같이 변수에서 키를 가져올 수 있습니다.

객체엔 다음과 같은 추가 연산자를 사용할 수 있습니다.

  • 프로퍼티를 삭제하고 싶을 때: delete obj.prop
  • 해당 key를 가진 프로퍼티가 객체 내에 있는지 확인하고자 할 때: "key" in obj
  • 프로퍼티를 나열할 때: for (let key in obj)
    지금까진 '순수 객체(plain object)'라 불리는 일반 객체에 대해 학습했습니다.

자바스크립트에는 일반 객체 이외에도 다양한 종류의 객체가 있습니다.

  • Array – 정렬된 데이터 컬렉션을 저장할 때 쓰임
  • Date – 날짜와 시간 정보를 저장할 때 쓰임
  • Error – 에러 정보를 저장할 때 쓰임
  • 기타 등등
  • 객체형은 원시형과 달리 다양한 데이터를 담을 수 있음
  • 키로 구분된 데이터 집합이나 복잡한 개체(entity)를 저장
  • 중괄호 안에는 키(key): 값(value) 쌍으로 구성된 프로퍼티(property) 를 여러 개 넣을 수 있는데, 키엔 문자형, 값엔 모든 자료형이 허용됨
  • 생성
    let user = new Object(); // '객체 생성자' 문법
    let user = {}; // '객체 리터럴' 문법

리터럴과 프로퍼티

let user = {     // 객체
  name: "John",  // 키: "name",  값: "John"
  age: 30        // 키: "age", 값: 30
  "likes birds": true  // 복수의 단어는 따옴표로 묶음
};

// 프로퍼티 값 얻기 - 점 표기법(dot notation)
alert( user.name ); // John
alert( user.age ); // 30

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

// 프로퍼티 삭제
delete user.age;

🚨 const로 선언된 객체는 수정될 수 있음

대괄호 표기법

🚨 여러 단어를 조합해 프로퍼티 키를 만든 경우엔, 점 표기법을 사용해 프로퍼티 값을 읽을 수 없음

대괄호 표기법으로 대체 가능 ["something new"]

// 에러
user.likes birds = true

// set
user["likes birds"] = true;

// get
alert(user["likes birds"]); // true

// delete
delete user["likes birds"];
  • 대괄호 표기법을 사용하면 아래 예시에서 변수를 키로 사용한 것과 같이 문자열뿐만 아니라 모든 표현식의 평가 결과를 프로퍼티 키로 사용할 수 있음
  • 변수 key는 런타임에 평가되기 때문에 사용자 입력값 변경 등에 따라 값이 변경될 수 있음
  • 즉, 객체에 동적 key를 할당 가능하게 됨
let user = {
  name: "John",
  age: 30
};

let key = prompt("사용자의 어떤 정보를 얻고 싶으신가요?", "name");

// 변수로 접근
alert( user[key] ); // John (프롬프트 창에 "name"을 입력한 경우)
  • 🚨 점 표기법에서는 이런 방식이 불가능함
let user = {
  name: "John",
  age: 30
};

let key = "name";
alert( user.key ) // undefined

계산된 프로퍼티

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

// 변수 fruit을 사용해 프로퍼티 이름을 만들었습니다.
bag[fruit] = 5;

이름이 확정된 상황이고, 단순한 이름이라면 처음엔 점 표기법을 사용하다가 뭔가 복잡한 상황이 발생했을 때 대괄호 표기법으로 바꾸는 경우가 많음

단축 프로퍼티

  • 프로퍼티 이름과 값이 변수의 이름과 동일할 경우

  • 수정 전

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

let user = makeUser("John", 30);
alert(user.name); // John
  • 수정 후
function makeUser(name, age) {
  return {
    name,
    age,
  };
}

let user = makeUser("John", 30);
alert(user.name); // John

프로퍼티 이름의 제약사항

  • 프로퍼티 이름엔 특별한 제약이 없음(__proto__ 제외)
  • 문자형이나 심볼형에 속하지 않은 값은 문자열로 자동 형 변환 됨
let obj = {
  0: "test" // "0": "test"와 동일
};

// 숫자 0은 문자열 "0"으로 변환되기 때문에 두 얼럿 창은 같은 프로퍼티에 접근
alert( obj["0"] ); // test
alert( obj[0] ); // test (동일한 프로퍼티)

‘in’ 연산자로 프로퍼티 존재 여부 확인하기

  • 자바스크립트에서는 객체에 존재하지 않는 프로퍼티에 접근하려 해도 에러가 발생하지 않고 undefined를 반환
  • "key" in object 를 통해 프로퍼티 존재 여부 확인 가능
let user = { name: "John", age: 30 };

alert( "age" in user ); // user.age가 존재하므로 true가 출력
alert( "blabla" in user ); // user.blabla는 존재하지 않기 때문에 false가 출력

‘for…in’ 반복문

  • for..in 반복문을 사용하면 객체의 모든 키를 순회
for (key in object) {
  // 각 프로퍼티 키(key)를 이용하여 본문(body)을 실행
}
  • 예시
let user = {
  name: "John",
  age: 30,
  isAdmin: true
};

for (let key in user) {
  // 키
  alert( key );  // name, age, isAdmin
  // 키에 해당하는 값
  alert( user[key] ); // John, 30, true
}

객체 정렬 방식

  • 정수 프로퍼티(integer property)는 자동으로 정렬됨
  • 그 외의 프로퍼티는 객체에 추가한 순서 그대로 정렬됨

4.2 참조에 의한 객체 복사


요약
객체는 참조에 의해 할당되고 복사됩니다. 변수엔 ‘객체’ 자체가 아닌 메모리상의 주소인 '참조’가 저장됩니다. 따라서 객체가 할당된 변수를 복사하거나 함수의 인자로 넘길 땐 객체가 아닌 객체의 참조가 복사됩니다.
그리고 복사된 참조를 이용한 모든 작업(프로퍼티 추가·삭제 등)은 동일한 객체를 대상으로 이뤄집니다.
객체의 '진짜 복사본’을 만들려면 '얕은 복사(shallow copy)'를 가능하게 해주는 Object.assign이나 '깊은 복사’를 가능하게 해주는 _.cloneDeep(obj)를 사용하면 됩니다. 이때 얕은 복사본은 중첩 객체를 처리하지 못한다는 점을 기억해 두시기 바랍니다.

  • 원시값(문자열, 숫자, 불린 값)은 ‘값 그대로’ 저장·할당되고 복사되는 반면
  • 객체는 ‘참조에 의해(by reference)’ 저장되고 복사됨
  • 변수엔 객체가 그대로 저장되는 것이 아니라, 객체가 저장되어있는 '메모리 주소’인 객체에 대한 '참조 값’이 저장됨
  • 객체가 할당된 변수를 복사할 땐 객체의 참조 값이 복사되고 객체는 복사되지 않음
let user = { name: "John" };
let admin = user; // 참조값을 복사함
// 변수는 두 개이지만 각 변수엔 동일 객체에 대한 참조 값이 저장

참조에 의한 비교

  • 객체 비교 시 동등 연산자 ==와 일치 연산자 ===는 동일하게 동작함
  • 비교 시 피연산자인 두 객체가 동일한 객체인 경우에 참을 반환
let a = {};
let b = a; // 참조에 의한 복사
alert( a == b ); // true, 두 변수는 같은 객체를 참조
alert( a === b ); // true
  • 아래 예시에서 두 객체 모두 비어있다는 점에서 같아 보이지만, 독립된 객체이기 때문에 일치·동등 비교하면 거짓이 반환
let a = {};
let b = {}; // 독립된 두 객체
alert( a == b ); // false

객체 복사, 병합과 Object.assign

  • 복제. 즉, 기존에 있던 객체와 똑같으면서 독립적인 객체를 만들고 싶을 경우
  1. 반복문
let user = {
  name: "John",
  age: 30
};
let clone = {}; // 새로운 빈 객체
// 빈 객체에 user 프로퍼티 전부를 복사해 넣습니다.
for (let key in user) {
  clone[key] = user[key];
}
// 이제 clone은 완전히 독립적인 복제본이 되었습니다.
clone.name = "Pete"; // clone의 데이터를 변경합니다.
alert( user.name ); // 기존 객체에는 여전히 John이 있습니다.
  1. Object.assign
    Object.assign(dest, [src1, src2, src3...])
  • 첫 번째 인수 dest는 목표로 하는 객체입니다.
  • 이어지는 인수 src1, ..., srcN는 복사하고자 하는 객체입니다. ...은 필요에 따라 얼마든지 많은 객체를 인수로 사용할 수 있다는 것을 나타냅니다.
  • 객체 src1, ..., srcN의 프로퍼티를 dest에 복사합니다. dest를 제외한 인수(객체)의 프로퍼티 전부가 첫 번째 인수(객체)로 복사됩니다.
  • 마지막으로 dest를 반환합니다.
let user = {
  name: "John",
  age: 30
};
Object.assign(clone, user);

중첩 객체 복사

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

alert( user.sizes.height ); // 182
  • clone.sizes = user.sizes로 프로퍼티를 복사하는 것만으론 객체를 복제할 수 없음
  • user.sizes는 객체이기 때문에 참조 값이 복사되기 때문
  • clone.sizes = user.sizes로 프로퍼티를 복사하면 cloneuser는 같은 sizes를 공유
  • 깊은 복사(deep cloning) : 이 문제를 해결하려면 user[key]의 각 값을 검사하면서, 그 값이 객체인 경우 객체의 구조도 복사해주는 반복문을 사용
  • 자바스크립트 라이브러리 lodash의 메서드_.cloneDeep(obj) 사용

4.3 가비지 컬렉션


요약

  • 가비지 컬렉션은 엔진이 자동으로 수행하므로 개발자는 이를 억지로 실행하거나 막을 수 없습니다.
  • 객체는 도달 가능한 상태일 때 메모리에 남습니다.
  • 참조된다고 해서 도달 가능한 것은 아닙니다. 서로 연결된 객체들도 도달 불가능할 수 있습니다.

가비지 컬렉션 기준

  • 자바스크립트는 도달 가능성(reachability) 이라는 개념을 사용해 메모리 관리를 수행
  • ‘도달 가능한(reachable)’ 값은 쉽게 말해 어떻게든 접근하거나 사용할 수 있는 값을 의미
  • 도달 가능한 값은 메모리에서 삭제되지 않음
  • 자바스크립트 엔진 내에선 가비지 컬렉터(garbage collector)가 끊임없이 동작
  • 가비지 컬렉터는 모든 객체를 모니터링하고, 도달할 수 없는 객체는 삭제

도달 가능한 값

  • 현재 함수의 지역 변수와 매개변수
  • 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
  • 전역 변수
  • 루트가 참조하는 값
  • 체이닝으로 루트에서 참조할 수 있는 값
  • 기타 등등

연결된 객체

  • 외부로 나가는 참조는 도달 가능한 상태에 영향을 주지 않음
  • 외부에서 들어오는 참조만이 도달 가능한 상태에 영향을 줌

도달할 수 없는 섬

  • 객체들이 연결되어 섬 같은 구조를 만드는데, 이 섬에 도달할 방법이 없는 경우, 섬을 구성하는 객체 전부가 메모리에서 삭제됨

내부 알고리즘

  • 가비지 컬렉션 기본 알고리즘 : mark-and-sweep

가비지 컬렉션 수행 단계
1. 가비지 컬렉터는 루트(root) 정보를 수집하고 이를 ‘mark(기억)’ 합니다.
2. 루트가 참조하고 있는 모든 객체를 방문하고 이것들을 ‘mark’ 합니다.
3. mark 된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark 합니다. 한번 방문한 객체는 전부 mark 하기 때문에 같은 객체를 다시 방문하는 일은 없습니다.
4. 루트에서 도달 가능한 모든 객체를 방문할 때까지 위 과정을 반복합니다.
5. mark 되지 않은 모든 객체를 메모리에서 삭제합니다.

가비지 컬렉션 최적화 기법

  • generational collection(세대별 수집) – 객체를 '새로운 객체’와 '오래된 객체’로 나눕니다. 객체 상당수는 생성 이후 제 역할을 빠르게 수행해 금방 쓸모가 없어지는데, 이런 객체를 '새로운 객체’로 구분합니다. 가비지 컬렉터는 이런 객체를 공격적으로 메모리에서 제거합니다. 일정 시간 이상 동안 살아남은 객체는 '오래된 객체’로 분류하고, 가비지 컬렉터가 덜 감시합니다.
  • incremental collection(점진적 수집) – 방문해야 할 객체가 많다면 모든 객체를 한 번에 방문하고 mark 하는데 상당한 시간이 소모됩니다. 가비지 컬렉션에 많은 리소스가 사용되어 실행 속도도 눈에 띄게 느려지겠죠. 자바스크립트 엔진은 이런 현상을 개선하기 위해 가비지 컬렉션을 여러 부분으로 분리한 다음, 각 부분을 별도로 수행합니다. 작업을 분리하고, 변경 사항을 추적하는 데 추가 작업이 필요하긴 하지만, 긴 지연을 짧은 지연 여러 개로 분산시킬 수 있다는 장점이 있습니다.
  • idle-time collection(유휴 시간 수집) – 가비지 컬렉터는 실행에 주는 영향을 최소화하기 위해 CPU가 유휴 상태일 때에만 가비지 컬렉션을 실행합니다.

4.4 메서드와 this


요약
객체 프로퍼티에 저장된 함수를 '메서드’라고 부릅니다.
object.doSomthing()은 객체를 '행동’할 수 있게 해줍니다.
메서드는 this로 객체를 참조합니다.
this 값은 런타임에 결정됩니다.

함수를 선언할 때 this를 사용할 수 있습니다. 다만, 함수가 호출되기 전까지 this엔 값이 할당되지 않습니다.
함수를 복사해 객체 간 전달할 수 있습니다.
함수를 객체 프로퍼티에 저장해 object.method()같이 ‘메서드’ 형태로 호출하면 this는 object를 참조합니다.
화살표 함수는 자신만의 this를 가지지 않는다는 점에서 독특합니다. 화살표 함수 안에서 this를 사용하면, 외부에서 this 값을 가져옵니다.

  • 자바스크립트에선 객체의 프로퍼티에 함수를 할당해 객체에게 행동할 수 있는 능력을 부여할 수 있음
  • 객체 프로퍼티에 할당된 함수를 메서드(method) 라고 부름

메서드 만들기

  • 아래 예시에선 user에 할당된 sayHi가 메서드
let user = {
  name: "John",
  age: 30
};

user.sayHi = function() {
  alert("안녕하세요!");
};

user.sayHi(); // 안녕하세요!

객체 지향 프로그래밍
객체를 사용하여 개체를 표현하는 방식을 객체 지향 프로그래밍(object-oriented programming, OOP) 이라 부름

메서드 단축 구문

  • 객체 리터럴 안에 메서드를 선언할 때 사용할 수 있는 단축 문법
user = {
  sayHi: function() {
    alert("Hello");
  }
};

// 단축구문
user = {
  sayHi() { // "sayHi: function()"과 동일
    alert("Hello");
  }
};

메서드와 this

  • 대부분의 메서드가 객체 프로퍼티의 값을 활용
  • 메서드 내부에서 this 키워드를 통해 객체에 접근할 수 있음
let user = {
  name: "John",
  age: 30,
  sayHi() {
    // 'this'는 '현재 객체'를 나타냄
    alert(this.name);
  }
};
user.sayHi(); // John

자유로운 this

  • 자바스크립트에선 모든 함수에 this를 사용할 수 있습니다.
  • this 값은 런타임에 결정. 즉, 컨텍스트에 따라 달라짐
  • 동일한 함수라도 다른 객체에서 호출했다면 'this’가 참조하는 값이 달라짐
let user = { name: "John" };
let admin = { name: "Admin" };

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

// 별개의 객체에서 동일한 함수를 사용함
user.f = sayHi;
admin.f = sayHi;

// 'this'는 '점(.) 앞의' 객체를 참조하기 때문에
// this 값이 달라짐
user.f(); // John  (this == user)
admin.f(); // Admin  (this == admin)

admin['f'](); // Admin (점과 대괄호는 동일하게 동작함)
  • obj.f()를 호출했다면 thisf를 호출하는 동안의 obj
  • 위 예시에선 objuseradmin을 참조

객체 없이 호출하기

  • 엄격 모드가 아닐 때: this == window (전역 객체)
  • 엄격 모드일 때: this == undefined

🚨 자유로운 this가 만드는 결과
다른 언어를 사용하다 자바스크립트로 넘어온 개발자는 this를 혼동하기 쉬움
this는 항상 메서드가 정의된 객체를 참조할 것이라고 착각함
이런 개념을 'bound this'라고 함

자바스크립트에서 this는 런타임에 결정됨메서드가 어디서 정의되었는지에 상관없이 this는 ‘점 앞의’ 객체가 무엇인가에 따라 ‘자유롭게’ 결정됨

함수(메서드)를 하나만 만들어 여러 객체에서 재사용할 수 있다는 것은 장점이지만, 이런 유연함이 실수로 이어질 수 있다는 것은 단점

this가 없는 화살표 함수

  • 🚨 화살표 함수는 일반 함수와는 달리 ‘고유한’ this를 가지지 않음
  • 화살표 함수에서 this를 참조하면, 화살표 함수가 아닌 ‘평범한’ 외부 함수에서 this 값을 가져옴
  • 별개의 this가 만들어지는 건 원하지 않고, 외부 컨텍스트에 있는 this를 이용하고 싶은 경우 화살표 함수가 유용함

과제

객체 리터럴에서 'this' 사용하기

function makeUser() {
  return {
    name: "John",
    ref: this
  };
};

let user = makeUser();

alert( user.ref.name ); // 에러

에러가 발생하는 이유는 this 값을 설정할 땐 객체 정의가 사용되지 않기 때문입니다. this 값은 호출 시점에 결정됩니다.
위 코드에서 makeUser() 내 this는 undefined가 됩니다. 메서드로써 호출된 게 아니라 함수로써 호출되었기 때문입니다.

function makeUser() {
  return {
    name: "John",
    ref() {
      return this;
    }
  };
};

let user = makeUser();

alert( user.ref().name ); // John

이렇게 하면 user.ref()가 메서드가 되고 this는 . 앞의 객체가 되기 때문에 에러가 발생하지 않습니다.

체이닝

let ladder = {
  step: 0,
  up() {
    this.step++;
  },
  down() {
    this.step--;
  },
  showStep: function() { // 사다리에서 몇 번째 단에 올라와 있는지 보여줌
    alert( this.step );
  }
};

ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1

up, down, showStep을 수정해 아래처럼 메서드 호출 체이닝이 가능하도록 해봅시다.
ladder.up().up().down().showStep(); // 1

let ladder = {
  step: 0,
  up() {
    this.step++;
    return this;
  },
  down() {
    this.step--;
    return this;
  },
  showStep() {
    alert( this.step );
    return this;
  }
}

ladder.up().up().down().up().down().showStep(); // 1

4.5 'new' 연산자와 생성자 함수


요약
생성자 함수(짧게 줄여서 생성자)는 일반 함수입니다. 다만, 일반 함수와 구분하기 위해 함수 이름 첫 글자를 대문자로 씁니다.
생성자 함수는 반드시 new 연산자와 함께 호출해야 합니다. new와 함께 호출하면 내부에서 this가 암시적으로 만들어지고, 마지막엔 this가 반환됩니다.
유사한 객체를 여러 개 만들 때 생성자 함수가 유용합니다.

자바스크립트는 언어 차원에서 다양한 생성자 함수를 제공합니다. 날짜를 나타내는 데 쓰이는 Date, 집합(set)을 나타내는 데 쓰이는 Set 등의 내장 객체는 이런 생성자 함수를 이용해 만들 수 있습니다.

  • new 연산자와 생성자 함수를 사용하면 유사한 객체 여러 개를 쉽게 만들 수 있음

생성자 함수

  • 생성자 함수(constructor function)와 일반 함수에 기술적인 차이는 없음
  • 다만 생성자 함수는 아래 두 관례를 따름
  1. 함수 이름의 첫 글자는 대문자로 시작합니다.
  2. 반드시 new 연산자를 붙여 실행합니다.
function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");

alert(user.name); // Jack
alert(user.isAdmin); // false

new User(...)를 써서 함수를 실행하면 아래와 같은 알고리즘이 동작

  1. 빈 객체를 만들어 this에 할당합니다.
  2. 함수 본문을 실행합니다. this에 새로운 프로퍼티를 추가해 this를 수정합니다.
  3. this를 반환합니다.
function User(name) {
  // this = {};  (빈 객체가 암시적으로 만들어짐)
  // 새로운 프로퍼티를 this에 추가함
  this.name = name;
  this.isAdmin = false;
  // return this;  (this가 암시적으로 반환됨)
}
  • 생성자의 의의: 재사용할 수 있는 객체 생성 코드를 구현

new function() { … }

재사용할 필요가 없는 복잡한 객체를 만들어야 한다고 해봅시다. 많은 양의 코드가 필요할 겁니다. 이럴 땐 아래와 같이 코드를 익명 생성자 함수로 감싸주는 방식을 사용할 수 있습니다.

let user = new function() {
  this.name = "John";
  this.isAdmin = false;
  // 사용자 객체를 만들기 위한 여러 코드.
  // 지역 변수, 복잡한 로직, 구문 등의
  // 다양한 코드가 여기에 들어갑니다.
};

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

new.target과 생성자 함수

  • new.target 프로퍼티를 사용하면 함수가 new와 함께 호출되었는지 아닌지를 알 수 있음
  • 일반적인 방법으로 함수를 호출했다면 new.target은 undefined를 반환
  • 반면 new와 함께 호출한 경우엔 new.target은 함수 자체를 반환
function User() {
  alert(new.target);
}

// "new" 없이 호출함
User(); // undefined

//"new"를 붙여 호출함
new User(); // function User { ... }

생성자와 return문

  • 생성자 함수엔 보통 return 문이 없음
  • 반환해야 할 것들은 모두 this에 저장되고, this는 자동으로 반환되기 때문에 반환문을 명시적으로 써 줄 필요가 없음
  • 만약 return 문이 있을 경우 (지양)
    1. 객체를 return 한다면, this 대신 객체가 반환됨
    2. 원시형을 return 한다면, return문이 무시됨

생성자 내 메서드

  • 생성자 함수를 사용하면 매개변수를 이용해 객체 내부를 자유롭게 구성할 수 있음
  • 프로퍼티 뿐만이 아니라, 메서드를 더해주는 것도 가능
function User(name) {
  this.name = name;

  this.sayHi = function() {
    alert( "My name is: " + this.name );
  };
}
let john = new User("John");
john.sayHi(); // My name is: John

문제

계산기 만들기

function Calculator() {
  this.read = function() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
    // +를 붙여 숫자형으로 변환
  };
  this.sum = function() {
    return this.a + this.b;
  };
  this.mul = function() {
    return this.a * this.b;
  };
}

let calculator = new Calculator();
calculator.read();

alert( "Sum=" + calculator.sum() );
alert( "Mul=" + calculator.mul() );

누산기 만들기

let accumulator = new Accumulator(1); // 최초값: 1

accumulator.read(); // 사용자가 입력한 값을 더해줌
accumulator.read(); // 사용자가 입력한 값을 더해줌

alert(accumulator.value); // 최초값과 사용자가 입력한 모든 값을 더해 출력함

4.6 옵셔널 체이닝 '?.'


요약
옵셔널 체이닝 문법 ?.은 세 가지 형태로 사용할 수 있습니다.

  1. obj?.propobj가 존재하면 obj.prop을 반환하고, 그렇지 않으면 undefined를 반환함
  2. obj?.[prop]obj가 존재하면 obj[prop]을 반환하고, 그렇지 않으면 undefined를 반환함
  3. obj?.method()obj가 존재하면 obj.method()를 호출하고, 그렇지 않으면 undefined를 반환함
    여러 예시를 통해 살펴보았듯이 옵셔널 체이닝 문법은 꽤 직관적이고 사용하기도 쉽습니다. ?. 왼쪽 평가 대상이 null이나 undefined인지 확인하고 null이나 undefined가 아니라면 평가를 계속 진행합니다.

?.를 계속 연결해서 체인을 만들면 중첩 프로퍼티들에 안전하게 접근할 수 있습니다.
?.?.왼쪽 평가대상이 없어도 괜찮은 경우에만 선택적으로 사용해야 합니다.
꼭 있어야 하는 값인데 없는 경우에 ?.을 사용하면 프로그래밍 에러를 쉽게 찾을 수 없으므로 이런 상황을 만들지 말도록 합시다.

  • 옵셔널 체이닝(optional chaining) ?.을 사용하면 프로퍼티가 없는 중첩 객체를 에러 없이 안전하게 접근할 수 있음

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

  1. 사용자가 여러 명 있는데 그중 몇 명은 주소 정보를 가지고 있지 않다고 가정, user.address.street를 사용해 주소 정보에 접근하면 에러가 발생
let user = {}; // 주소 정보가 없는 사용자
alert(user.address.street); // TypeError: Cannot read property 'street' of undefined
  1. 자바스크립트를 사용해 페이지에 존재하지 않는 요소에 접근해 요소의 정보를 가져오려 할 경우
// querySelector(...) 호출 결과가 null인 경우 에러 발생
let html = document.querySelector('.my-element').innerHTML;

명세서에 ?.이 추가되기 전엔 이런 문제들을 해결하기 위해 && 연산자를 사용
AND를 연결해서 사용하면 코드가 아주 길어진다는 단점

let user = {}; // 주소 정보가 없는 사용자
alert( user && user.address && user.address.street ); // undefined, 에러가 발생하지 않습니다.

옵셔널 체이닝의 등장

  • ?.?.'앞’의 평가 대상이 undefinednull이면 평가를 멈추고 undefined를 반환
let user = {}; // 주소 정보가 없는 사용자
alert( user?.address?.street ); // undefined, 에러가 발생하지 않습니다.
  • user?.address로 주소를 읽으면 아래와 같이 user 객체가 존재하지 않더라도 에러가 발생하지 않음
let user = null;
alert( user?.address ); // undefined
alert( user?.address.street ); // undefined
  • usernull이나 undefined가 아니고 실제 값이 존재하는 경우엔 반드시 user.address 프로퍼티는 있어야 함.
  • 그렇지 않으면 user?.address.street의 두 번째 점 연산자에서 에러가 발생

🚨 옵셔널 체이닝을 남용하지 마세요.
?.는 존재하지 않아도 괜찮은 대상에만 사용해야 합니다.

사용자 주소를 다루는 위 예시에서 논리상 user는 반드시 있어야 하는데 address는 필수값이 아닙니다. 그러니 user.address?.street를 사용하는 것이 바람직합니다.

실수로 인해 user에 값을 할당하지 않았다면 바로 알아낼 수 있도록 해야 합니다. 그렇지 않으면 에러를 조기에 발견하지 못하고 디버깅이 어려워집니다.

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

변수 user가 선언되어있지 않으면 user?.anything 평가시 에러가 발생합니다.

// ReferenceError: user is not defined
user?.address;

단락 평가

  • ?.는 왼쪽 평가대상에 값이 없으면 즉시 평가를 멈춤
  • 이런 평가 방법을 단락 평가(short-circuit)라고 부름
  • 그렇기 때문에 함수 호출을 비롯한 ?. 오른쪽에 있는 부가 동작은 ?.의 평가가 멈췄을 때 더는 일어나지 않음

?.()와 ?.[]

  • 함수 관련 예시와 함께 존재 여부가 확실치 않은 함수를 호출할 경우

?.()

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

let user2 = {};

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

?.[]

let user1 = {
  firstName: "Violet"
};

let user2 = null; // user2는 권한이 없는 사용자라고 가정해봅시다.

let key = "firstName";

alert( user1?.[key] ); // Violet
alert( user2?.[key] ); // undefined

alert( user1?.[key]?.something?.not?.existing); // undefined
  • ?.은 delete와 조합해 사용할 수도 있습니다.
    delete user?.name; // user가 존재할 경우 name을 삭제

?.은 읽기나 삭제하기에는 사용할 수 있지만 쓰기에는 사용할 수 없습니다.

  • ?.은 할당 연산자 왼쪽에서 사용할 수 없음
// user가 존재할 경우 user.name에 값을 쓰려는 의도
user?.name = "Violet"; // SyntaxError: Invalid left-hand side in assignment
// undefined = "Violet" => 에러

4.7 심볼형


요약
Symbol은 원시형 데이터로, 유일무이한 식별자를 만드는 데 사용됩니다.
Symbol()을 호출하면 심볼을 만들 수 있습니다. 설명(이름)은 선택적으로 추가할 수 있습니다.
심볼은 이름이 같더라도 값이 항상 다릅니다. 이름이 같을 때 값도 같길 원한다면 전역 레지스트리를 사용해야 합니다. Symbol.for(key)는 key라는 이름을 가진 전역 심볼을 반환합니다. key라는 이름을 가진 전역 심볼이 없으면 새로운 전역 심볼을 만들어줍니다. key가 같다면 Symbol.for는 어디서 호출하든 상관없이 항상 같은 심볼을 반환해 줍니다.

심볼의 주요 유스 케이스는 다음과 같습니다.
1. 객체의 ‘숨김’ 프로퍼티 – 외부 스크립트나 라이브러리에 ‘속한’ 객체에 새로운 프로퍼티를 추가해 주고 싶다면 심볼을 만들고, 이를 프로퍼티 키로 사용하면 됩니다. 키가 심볼인 경우엔 for..in의 대상이 되지 않아서 의도치 않게 프로퍼티가 수정되는 것을 예방할 수 있습니다. 외부 스크립트나 라이브러리는 심볼 정보를 갖고 있지 않아서 프로퍼티에 직접 접근하는 것도 불가능합니다. 심볼형 키를 사용하면 프로퍼티가 우연히라도 사용되거나 덮어씌워 지는 걸 예방할 수 있습니다.
이런 특징을 이용하면 원하는 것을 객체 안에 ‘은밀하게’ 숨길 수 있습니다. 외부 스크립트에선 우리가 숨긴 것을 절대 볼 수 없습니다.
2. 자바스크립트 내부에서 사용되는 시스템 심볼은 Symbol.*로 접근할 수 있습니다. 시스템 심볼을 이용하면 내장 메서드 등의 기본 동작을 입맛대로 변경할 수 있습니다. iterable 객체에선 Symbol.iterator를, 객체를 원시형으로 변환하기에선 Symbol.toPrimitive이 어떻게 사용되는지 알아보겠습니다.

사실 심볼을 완전히 숨길 방법은 없습니다. 내장 메서드 Object.getOwnPropertySymbols(obj)를 사용하면 모든 심볼을 볼 수 있고, 메서드 Reflect.ownKeys(obj)는 심볼형 키를 포함한 객체의 모든 키를 반환해줍니다. 그런데 대부분의 라이브러리, 내장 함수 등은 이런 메서드를 사용하지 않습니다.

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

심볼

  • 생성: Symbol()
  • '심볼(symbol)'은 유일한 식별자(unique identifier)를 만들고 싶을 때 사용
// id는 새로운 심볼이 됩니다.
let id = Symbol();
  • 심볼 이름: 심볼에 대한 설명
// 심볼 id에는 "id"라는 설명이 붙습니다.
let id = Symbol("id");
  • 심볼은 유일성이 보장되는 자료형이기 때문에, 설명이 동일한 심볼을 여러 개 만들어도 각 심볼값은 다름
  • 심볼에 붙이는 설명(심볼 이름)은 어떤 것에도 영향을 주지 않는 이름표 역할만 함
let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

🚨 심볼은 문자형으로 자동 형 변환되지 않음

  • 심볼형 값은 다른 자료형으로 암시적 형 변환(자동 형 변환)되지 않음
  • 문자열과 심볼은 근본이 다르기 때문에 우연히라도 서로의 타입으로 변환돼선 안 됨
  • 자바스크립트에선 '언어 차원의 보호장치(language guard)'를 마련해 심볼형이 다른 형으로 변환되지 않게 막아줌

‘숨김’ 프로퍼티

  • 심볼을 이용하면 ‘숨김(hidden)’ 프로퍼티를 만들 수 있음
  • 숨김 프로퍼티는 외부 코드에서 접근이 불가능하고 값도 덮어쓸 수 없는 프로퍼티임
let user = { // 서드파티 코드에서 가져온 객체
  name: "John"
};
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // 심볼을 키로 사용해 데이터에 접근 가능

문자열 "id"를 키로 사용해도 되는데 Symbol("id")을 사용한 이유

  • user는 서드파티 코드에서 가지고 온 객체이므로 함부로 새로운 프로퍼티를 추가할 수 없음
  • 그런데 심볼은 서드파티 코드에서 접근할 수 없기 때문에, 심볼을 사용하면 서드파티 코드가 모르게 user에 식별자를 부여할 수 있음

Symbols in a literal

  • 객체 리터럴 {...}을 사용해 객체를 만든 경우, 대괄호를 사용해 심볼형 키를 만들어야 함
let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // "id": 123은 안됨
};

심볼은 for…in 에서 배제됩니다

  • 키가 심볼인 프로퍼티는 for..in 반복문에서 배제됨
  • Object.keys(user)에서도 키가 심볼인 프로퍼티는 배제
  • '심볼형 프로퍼티 숨기기(hiding symbolic property)'라 불리는 이런 원칙 덕분에 외부 스크립트나 라이브러리는 심볼형 키를 가진 프로퍼티에 접근하지 못함
  • 🚨 Object.assign은 키가 심볼인 프로퍼티를 배제하지 않고 객체 내 모든 프로퍼티를 복사함

전역 심볼

  • 심볼은 이름이 같더라도 모두 별개로 취급됨
  • 그런데 이름이 같은 심볼이 같은 개체를 가리키길 원하는 경우, 전역 심볼 레지스트리(global symbol registry)를 활용
  • 전역 심볼 레지스트리 안에 심볼을 만들고 해당 심볼에 접근하면, 이름이 같은 경우 항상 동일한 심볼을 반환해줌
  • 레지스트리 안에 있는 심볼을 읽거나, 새로운 심볼을 생성하려면 Symbol.for(key)를 사용
  • 이 메서드를 호출하면 이름이 key인 심볼을 반환
  • 조건에 맞는 심볼이 레지스트리 안에 없으면 새로운 심볼 Symbol(key)을 만들고 레지스트리 안에 저장
// 전역 레지스트리에서 심볼을 읽습니다.
let id = Symbol.for("id"); // 심볼이 존재하지 않으면 새로운 심볼을 만듭니다.

// 동일한 이름을 이용해 심볼을 다시 읽습니다(좀 더 멀리 떨어진 코드에서도 가능합니다).
let idAgain = Symbol.for("id");

// 두 심볼은 같습니다.
alert( id === idAgain ); // true

Symbol.keyFor

  • 전역 심볼을 찾을 때 사용되는 Symbol.for(key)와 반대로 Symbol.keyFor(sym)를 사용하면 이름을 얻을 수 있음
// 이름을 이용해 심볼을 찾음
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// 심볼을 이용해 이름을 얻음
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
  • 검색 범위가 전역 심볼 레지스트리이기 때문에 전역 심볼이 아닌 심볼에는 사용할 수 없음
  • 전역 심볼이 아닌 모든 심볼은 description 프로퍼티가 존재하기 때문에 일반 심볼에서 이름을 얻고 싶으면 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
    - 기타 등등

실무(추가) - 댓글 중

  1. 노티피케이션 기능 구현시
  • 무언가를 삭제했을 때 노티피케이션(알림)을 띄워주는경우를 생각해봅시다.
  • 빠르게 여러개를 삭제하는 경우, 새로운 알림창이 뜰 때 기존에 있던 알림창을 닫아줘야한다는 요구사항이 추가될겁니다.
  • 이때 노티피케이션 하나하나를 식별할 수 있는 값을 만들어주는 과정이 필요한데, 이럴 때 Symbol을 사용할 수 있습니다.
  • 대체할수있는 방법으론 uuid 관련 로직을 직접 구현하거나 패키지를 쓰는 방법이 있습니다.
  1. api 상태 관련 변수 값 지정시
  • api 상태를 다음과 같은 4가지 경우로 나눠서 관리한다고 가정해봅시다.
  • IDLE, PENDING, SUCCESS, ERROR
  • 이때 각 상태를 string으로 저장하게 되면 오타가 나는 등의 문제가 발생할 확률이 생깁니다.
  • 이럴때 심볼을 활용할 수 있습니다.
export const apiStatus = {
IDLE: Symbol('IDLE'),
PENDING: Symbol('PENDING'),
SUCCESS: Symbol('SUCCESS'),
ERROR: Symbol('ERROR')
}
  • 대체할 수 있는 방법으론 아래와 같이 하는 경우가 있습니다.
export const IDLE = 'IDLE'
export const PENDING = 'PENDING'
export const SUCCESS = 'SUCCESS'
export const ERROR = 'ERROR'

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


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

객체-원시형으로의 형 변환은 hint를 기준으로 세 종류로 구분할 수 있습니다.

  • "string" (alert 같이 문자열을 필요로 하는 연산)
  • "number" (수학 연산)
  • "default" (드물게 발생함)
    연산자별로 어떤 hint가 적용되는지는 명세서에서 찾아볼 수 있습니다. 연산자가 기대하는 피연산자를 '확신할 수 없을 때’에는 hint가 "default"가 됩니다. 이런 경우는 아주 드물게 발생합니다. 내장 객체는 대개 hint가 "default"일 때와 "number"일 때를 동일하게 처리합니다. 따라서 실무에선 hint가 "default"인 경우와 "number"인 경우를 합쳐서 처리하는 경우가 많습니다.

객체-원시형 변환엔 다음 알고리즘이 적용됩니다.
1. 객체에 objSymbol.toPrimitive메서드가 있는지 찾고, 있다면 호출합니다.
2. 1에 해당하지 않고 hint가 "string"이라면,
obj.toString()이나 obj.valueOf()를 호출합니다.
3. 1과 2에 해당하지 않고, hint가 "number"나 "default"라면
obj.valueOf()obj.toString()을 호출합니다.
obj.toString()만 사용해도 '모든 변환’을 다 다룰 수 있기 때문에, 실무에선 obj.toString()만 구현해도 충분한 경우가 많습니다. 반환 값도 ‘사람이 읽고 이해할 수 있는’ 형식이기 때문에 실용성 측면에서 다른 메서드에 뒤처지지 않습니다. obj.toString()은 로깅이나 디버깅 목적으로도 자주 사용됩니다.




📚 참고 : javascript.info

profile
protect me from what i want

0개의 댓글