자바스크립트의 객체

객체란 무엇인가

객체는 키-값의 쌍을 각각의 구성요소로 가지는 자료형입니다.

객체는 자바스크립트에서 가장 중요한 자료형으로써, 현대 자바스크립트 코드의 빌딩 블록과도 같은 역할을 합니다.

숫자나 문자열 같은 원시 자료형을 제외하고는 자바스크립트에서는 대부분의 데이터가 객체 자료형으로 표현됩니다.

객체지향 프로그래밍(Object Oriented Programming)이라는 프로그래밍 방법론에서는 객체를 보다 더 추상적인 개념으로 설명합니다만,

일단 자료형의 측면과 코드내에서 다루는 방법론에 대해 학습을 하고, 이후 해당 방법론을 공부하면서 또 직접 사용해가면서 차근차근 추상적인 정의에 대해 이해를 높이는 것이 좋을 것 같습니다.

생김새

객체는 앞서 말씀드렸듯 키-값의 쌍들을 그 요소로 구성하는 자료형으로써, 아래와 같은 생김새를 가졌습니다.

// { ... } 로 표현된 객체를 objectSample에 할당합니다.
const objectSample = {
  a: 1,
  b: 2,
}

객체는 기본적으로 위 처럼 자바스크립트 코드내에서 { key: value, ... } 의 형태로 표현할 수 있습니다. 이를 객체의 리터럴 표기법 이라고 합니다.

중괄호(Curly brackets) 로 감싸여진 모양새를 가졌고,
각각의 요소를 나타내는 키-값의 쌍들은 반드시 쉼표(,)를 통해 구분이 되어야 합니다.

const emptyObject = {};

{} 사이에 아무 것도 표기하지 않으면, 빈 객체가 생성됩니다. 빈 객체는 null과 다릅니다.

객체는 위와 같은 리터럴 표기법으로 생성하는 방법 외에도 생성자 함수를 이용하거나, Object 객체의 정적 메소드들을 이용해 동적으로 생성하는 것이 가능합니다.

// 생성자 함수를 통한 객체 생성
function Dog(name, age) {
 this.name = name;
 this.age = age;
}
const objSample = new Dog("앵두",2);

이 부분은 후속 게시물에서 보다 자세히 다룰 수 있도록 하겠습니다.

객체에서 키(key)는 어떤 객체에 포함된 데이터의 '이름'을 나타냅니다.
이 이름을 이용해 객체 속 원하는 데이터에 직접적으로 접근이 가능합니다.

아래에 어떤 객체를 표현한 코드가 있습니다.

const objectSample = {
  a: 1,
  b: 2,
  "b.c": 3,
   var: 4
  "}": 5
};

위 코드에서 객체에 사용된 '키'들은 아래와 같습니다.

a, b, "b.c", var, "}"

객체의 키는 식별자가 아닌 "문자열"로 저장이 됩니다.

그래서 var의 경우와 같이 예약어임에도 불구하고 키값으로 사용이 가능하고, "b.c"나 "}"와 같이 프로그램 코드를 구성하는 토큰으로 사용되는 특수문자 역시도 따옴표로 묶어서 키값으로 사용이 가능한 것입니다.

객체의 '값'은 특정한 '키'에 연결된 데이터를 의미합니다.

const objSample = {
  a: 1,
  b: "abc",
  c: Symbol.for("only One"),
  d: null,
  e: undefined,
  f: [1, 2, 3],
  g: {
    a: 1,
    b: null
  }
};

객체는 값으로 무엇이든 다 저장할 수 있습니다.

숫자, 문자열, 불리언, 심볼, null, undefined와 같은 원시 자료형이 하나의 자료형만을 값으로 저장할 수 있다면, 객체는 그 값으로 모든 원시타입과 객체타입(배열과 함수 포함)의 자료형을 저장할 수 있습니다.

키를 이용해 값을 수정하고 조회하는 방법

const Dog = {};
Dog.name = "멍멍이";
Dog.age = 2;
console.log(Dog.name); // 출력 : "멍멍이"

const Dog = {};
Dog["name"] = "멍멍이";
Dog["age"] = 2;
console.log(Dog["name"]); // 출력 : "멍멍이"

위에서 줄바꿈으로 구분된 두 개의 코드뭉치는 모두 같은 결과를 나타냅니다.

키값이 일반적인 변수명처럼 식별자로 활용이 가능한 문자열이라면 점 표기법(.)을 이용해 해당 키값의 자료에 접근할 수 있습니다.
키값에 스페이스가 있거나 점이나 괄호 같은 식별자로 활용이 불가한 문자열이 포함되었다면 두번째 코드와 같이 대괄호 표기법([])을 이용해 해당 키값의 자료에 접근 가능합니다.

속성

const Dog = {};
Dog.name = "멍멍이";
Dog.age = 2;

Dog 객체에서 Dog.name과 Dog.age 와 같은 각각의 구성요소들을 Dog 객체의 속성이라고 부릅니다.

메서드

Dog.bark = function () {
  console.log("월월!");
}

Dog.bark(); // "월월!"이 출력된다.

속성은 이름이나 나이와 같이 객체의 정보나 상태를 나타내는 변수의 역할을 하기도 하지만,
위와 같이 값으로 함수를 저장해서 해당 객체와 관련된 어떤 동작을 저장하고 필요할 때 호출할 수 있습니다.
이렇게 어떤 함수가 데이터로 연결되어있는 속성을 특별하게 '메서드' 라고 부릅니다.

Dog = {
  ... 기타 속성들,
  bark () { // (*)
  console.log("월월!");
  }
}

Dog.bark(); // "월월!"이 출력된다.

메서드는 위 처럼 function 키워드를 사용하지 않고 단축하여 표기하는 것도 가능합니다.
위와 같은 표기법은 객체의 리터럴 표기법 내에서만 유효하고 function 키워드를 사용할 때와 동일한 결과를 나타냅니다.

자신이 속한 객체를 참조하는 방법

많은 경우 메서드는 자신이 속한 객체에 저장된 다른 속성을 활용하여 어떤 동작을 수행합니다.

만약 어떤 메서드 내에서 자신이 속한 객체를 참조하고자 할 경우에는 this 라는 키워드를 사용하면 됩니다.

const Dog = {
    sound: "멍멍!",
    bark: function () {
        console.log(this.sound);
    }
}

Dog.bark(); // 출력 : "멍멍!"

단, ECMAScript6부터 추가된 화살표 함수를 사용할 경우에는 이 this를 이용한 방법이 원하는대로 동작하지 않을 수 있습니다.

const Dog = {
    sound: "멍멍!",
    bark: () => {
        console.log(this.sound);
    }
}

Dog.bark();
// 오류출력 : TypeError: Cannot read property 'sound' of undefined
// this 객체에 sound 속성이 존재하지 않아 호출할 수 없다는 내용의 오류

객체의 메서드로 화살표 함수를 사용할 경우 원치않은 결과가 나타날 수 있습니다.

메서드를 표기할 때는 메서드이름() { ... }와 같은 단축 표기법이나 function 키워드를 이용한 일반 함수 표현법을 사용해주세요.
(이에 대한 자세한 내용도 이후에 다른 주제에서 살펴보도록 하겠습니다.)

getter와 setter

객체는 또한 getter와 setter라는 이름의 특수한 종류의 속성을 가질 수 있습니다.
일반적인 객체의 속성을 값 속성(Data properties)이라고 하고 getter와 setter를 접근자 속성(Accessor properties)으로 별도 분류할 수 있습니다.

용도

human1 객체에서 나이에 대한 속성을 가져올 때 human1.age와 같은 접근법을 사용합니다.
마찬가지로 나이를 새롭게 저장할때도 human1.age = 31; 과 같은 표현을 사용하지요.

메서드를 통해 같은 내용의 동작을 수행하고자 하면,
human1.getAge()의 반환값을 통해 나이 정보를 조회하고,
human1.setAge(32)와 같은 메서드를 통해 새로운 나이 정보를 저장하는 형태를 띄게 될 것입니다.

console.log(human1.age);
human1.age = 32;

console.log(human1.getAge());
human1.setAge(32);

보시다시피 전자의 예로 든 일반적인 데이터 속성은 메서드에 비해 데이터를 조회하고 할당하는 방법이 보다 직관적인 모양새를 갖췄습니다.

하지만 메서드로 데이터를 접근하고 설정할 때는 그 나름대로의 장점이 있어서,
아래와 같이 데이터에 대해 좀 더 복잡한 형태의 접근과 가공이 가능합니다.

const human1 = {
  firstName: "영재",
  lastName: "주",
  age: 30,
  getFullName() {
      return this.lastName + this.firstName;
  },
  setName(name) {
      this.lastName = name[0];
      this.firstName = name.substr(1,2);
  }
}

human1.getFullName(); // "주영재"를 반환
human1.setName("김개똥");
// human1 = { firstName: "김", lastName: "개똥", age: 30, ... }

이러한 데이터 속성과 메서드의 장단점을 섞어서 데이터 속성의 문법으로 메서드를 사용할 수 있도록 만들어진 접근자 속성을 getter와 setter라고 합니다.

const human1 = {
  firstName: "영재",
  lastName: "주",
  age: 30,
  get name() {
      return this.lastName + this.firstName;
  },
  set name(name) {
      this.lastName = name[0];
      this.firstName = name.substr(1,2);
  }
}

human1.name; // "주영재"를 반환
human1.name = "김개똥";
// human1 = { firstName: "김", lastName: "개똥", age: 30, ... }

가장 아래 두 줄의 human1 객체의 가공된 풀네임에 접근하고 이름을 새롭게 저장하는 코드에 주목해주십시오.
마치 데이터 속성에 접근하고 새로운 값을 할당하는 것 같아 보입니다만, 실제로는 해당 속성에 연결된 메서드를 동작시킵니다.

따라서 앞선 두 개의 코드는 동일한 동작을 나타냅니다.

표기방법

getter와 setter는 객체의 리터럴 표기법 상에서 [get|set] 이름(...매개변수들) { ... 내용 } 의 형태로ㅂ 표현이 가능합니다.

get function getDescription() {
  ... 내용
}

이러한 표현법은 잘못된 표현법이고 문법 에러를 발생시키며 정상적으로 동작하지 않습니다.

getter와 setter를 동적으로 추가하기

일반적으로 객체 리터럴 표기법으로 객체의 선언 단계에서 getter와 setter를 미리 설정해두고 사용 합니다만,
코드의 실행 단계에서 동적으로 새로운 setter와 getter를 추가하는 것도 가능합니다.

getter/setter를 동적으로 추가할 때는 __defineGetter__ 와 __defineSetter__ 메서드를 사용하며 용법은 아래와 같습니다.
(언더바가 두 개인 것에 유의하세요)

  • 객체.__defineGetter__(속성명, 연결될 함수)
  • 객체.__defineSetter__(속성명, 연결될 함수)
const human1 = {
  firstName: "영재",
  lastName: "주",
  age: 30,
}

human1.__defineGetter__("name", function() {
  return this.lastName + this.firstName;
});

human1.__defineSetter__("name", function(name) {
    this.lastName = name[0];
    this.firstName = name.substr(1,2);
});

human1.name;
human1.name = "김개똥";

역시 앞선 코드들과 동일한 동작을 나타냅니다.

변수에 실제로 저장되는 것

const Dog = {};
Dog.name = "멍멍이";
Dog.age = 2;

앞서보았던 코드들 중 하나입니다.
무언가 이상한 점이 느껴지시나요?

무심결에 지나쳤을수도 있습니다만, 분명 Dog 변수는 const 키워드를 이용해 재할당이 불가능하도록 설정이 되어있음에도 불구하고,
Dog 객체에 내용물을 이것저것 추가하는 모양새를 하고 있습니다.

참조가 저장된다

이는 객체를 저장하는 변수가 객체라는 데이터 그 자체가 아닌 객체의 참조를 저장하고 있기 때문에 가능한 일입니다.

원시 자료형 값들은 할당 연산자(=)를 이용해 변수에 값을 저장하고자하면 그 값을 직접 저장합니다.
변수를 어떤 자료를 보관할 수 있는 상자에 비유하면 숫자나 문자열 같은 데이터를 그대로 상자에 집어넣는 모습을 상상해보면 됩니다.

하지만, 객체는 해당 객체의 값 자체가 아닌 객체의 참조를 저장하게 되는데,
역시 변수를 상자에 비유하자면 개의 이름, 나이, 짖는방법과 같은 데이터들을 그대로 상자에 담아내는 것이 아니고,
그러한 데이터들이 저장된 곳의 '위치정보'를 상자에 담아둔다라고 생각하시면 이해하시기가 훨씬 수월할 것입니다.

즉 const로 선언된 변수 상자는 한 번 내용물을 넣어두면 '읽기 전용'이 되어버리는 것은 틀림없지만, 그 변수상자에는 객체가 어디있는지 적혀진 주소만 저장이 되어있고, 주소를 통해 찾아갈 수 있는 객체 자체는 내용을 추가하거나 삭제하는 것이 자유롭습니다.

const emptyObject = {};

위 코드가 null이 아닌 이유 역시 위 내용으로 설명이 됩니다.
객체에 구성요소가 없을 뿐이지, emptyObject 변수에는 객체의 참조가 저장이 되어있기 때문이죠.

참조 복사와 값 복사

const dog1 = { name:"앵두", age:2};
const dog2 = dog1;

첫번째 줄에서 선언된 내용으로 인해, dog1이라는 변수 상자에는 { name:"앵두" ... } 리터럴로 생성된 객체의 주소가 담기게 됩니다.
두번째 줄에서 dog1에 저장된 객체의 주소가 dog2라는 변수 상자에 저장이 됩니다.

즉, dog1과 dog2는 동일한 객체의 주소를 참조하게 되는 것입니다.

그리하여,

dog2.name = "바둑이"; // dog2의 이름을 변경.
console.log(dog1.name); // dog1의 이름을 출력.

dog2의 주소로 찾아간 객체에서 name 이라는 데이터를 "바둑이"로 변경하고, 두번째 줄에서 dog1의 주소에서 name 이라는 데이터를 접근하게 되면,
두 줄 모두 자기네 주소가 담긴 변수 상자만 서로 다를 뿐이고 결국에는 같은 주소의 데이터에 대해 수정하고 추가하고 삭제하게되는 셈인 것이죠.

dog2.name = "바둑이"; // dog2의 이름을 변경.
console.log(dog1.name); // dog1의 이름을 출력.

// 출력 : "바둑이"

위와 같이 어떤 변수에 대해 자신과 같은 주소를 복사해줘서 동일한 객체를 가르키도록 하는 것을 객체에 대한 참조를 복사한다라고 해서 참조 복사 라고 합니다.

반대로

const str1 = "abcdefg";
const str2 = str1;

str1 = "hijklmnop";
console.log(str2);

// 출럭 : "abcdefg"

이처럼 변수에 담긴 값 자체를 다른 변수에 복사해주는 것을 값 복사라고 하니 참고해주시길 바랍니다.

배열도 객체고 함수도 객체다

배열과 함수는 기술적으로 보면 모두 객체입니다.
다만, 언어 자체에서 제공하는 기능과 표현방법의 차이가 있을 뿐입니다.

그래서 다음과 같은 것이 가능합니다.

배열의 예

const arr = [1,2,3,4];
arr["4"] = 5;
console.log(arr);
// 출력 : [1,2,3,4,5]
arr.name = "1부터 4까지의 자연수";
console.log(arr.name);
// 출력 : "1부터 4까지의 자연수"

const obj = {};
const obj2 = Object.assign(obj,arr); // obj라는 빈객체에 arr의 내용을 할당
console.log(obj2);
// 출력 : Object {0: 1, 1: 2, 2: 3, 3: 4, 4: 5…}

함수의 예

function sayMyName(name) {
  console.log(name);
}

sayMyName["description"] = "이름을 말하는 함수입니다.";
console.log(sayMyName.description);
// 출력 : "이름을 말하는 함수입니다."

console.log(sayMyName.length);
// 출력 : 1 <- 함수의 매개변수 갯수를 의미하는 함수 객체의 내장함수입니다.
console.log(sayMyName.name);
// 출력 : sayMyName <- 함수의 이름을 의미하는 함수 객체의 내장함수입니다.

그래서 배열과 함수도 참조복사가 됩니다.

배열이나 함수가 저장된 변수 역시 해당 객체의 주소가 저장되어있을 뿐, 값 자체가 저장되어있지 않기 때문이지요.

const emptyArr = [];

function InsertNumberTo(arr) {
  arr.push(100);
}

InsertNumberTo(emptyArr);
console.log(emptyArr); // 출력 : [100]

위 코드와 같이 어떤 함수에 전달인자(Arguments)로 배열이나 객체, 함수와 같이 참조복사가 되는 자료형을 넘길 경우,
함수에서 전달받은 매개변수(Parameters)에 동일한 객체의 주소가 할당됨을 유념해야 합니다.

그말인 즉슨, 함수 내부에서 전달받은 배열을 별도의 복사없이 그대로 수정할 경우 외부의 배열이 동시에 수정되는 외부효과(Side effect)가 나타난다는 이야기로, 원치않은 버그의 원인이 될 수 있습니다.

함수는 일급 객체 입니다

본문의 맥락에서 살짝 빗나간 여담입니다만,
특히 자바스크립트에서 함수는 1급 객체(First-class object)라고 불리웁니다.

프로그래밍 언어에서 1급 객체의 조건은 다음과 같습니다.

  1. 변수나 데이터 구조 안에 할당할 수 있어야 한다.
  2. 함수 등에 인자로 전달 할 수 있어야 한다.
  3. 반환값으로 사용할 수 있어야한다.

자바스크립트에서 함수는 위의 모든 것이 다 가능합니다.
이를 이용해 콜백같은 것을 구현하는데 이에 관련해서는 다른 문서에서 보강하여 설명해드리겠습니다.

본문의 맥락상에서 함수와 관련해 참고해야되는 부분은, 인자로 넘겨진 함수 역시 동일한 주소를 참조하고 있다는 사실입니다.

객체는 연관된 기능과 데이터의 집합이다

보통 객체는 위의 Dog나 human1과 같이 개개의 사물로 보고 해당 사물에 연관된 변수와 함수들을 속성으로 모아서 사용합니다.

예를들어 객체의 속성은 나이나 이름같은 사물을 설명하는 정보가 되며, 객체의 메서드는 청소하기, 공부하기와 같은 사물의 행동을 의미합니다.

단순히, 짖는다는 행동과 이름과 나이라는 정보가 이곳저곳에 별개의 것으로 존재하는 것 보다는 연관성있는 것들끼리 함께 모아둘 경우 데이터와 함수를 조금 더 효율적이고 직관적으로 활용할 수 있게 됩니다.

이것에 관련된 주제가 바로 객체지향 프로그래밍 입니다.

이후 문서들에서 객체지향과 관련된 내용을 구현 측면에서 다뤄볼 것입니다만,
개념적인 측면에서는 인터넷 상에 좋은 자료가 많으니 이후 따로 공부하시는 것을 권해드립니다.