이전 튜토리얼에서 우리는 JavaScript의 객체가 무엇인지, 어떻게 작동되는지에 대한 매우 높은 수준의 개요를 보았습니다. 이 튜토리얼에서는 이전 튜토리얼을 거대한 빙산의 일각처럼 보이게 할 것입니다.
여기서 할 일은 객체를 더 자세히 살펴보고 객체 사용, 자체 사용자 정의 객체 생성, 상속, 프로토타입 및 this 키워드와 같은 고급 주제를 다루는 것입니다.
객체 만나보기
null 과 undefined를 제외한 거의 모든 항목은 객체(Object)와 직접 관련이 있거나 필요에 따라 하나가 될 수 있습니다.
Object는 우리가 속성이라고 부르는 key 와 value 쌍을 지정할 수 있는 기능이 있습니다. 이것은 hashtables, associative arrays, dictionaries 과 같은 데이터 구조를 사용하는 다른 언어에서 볼 수있는 것과 크게 다르지 않습니다.
어쨌든 글로만 보는 거는 꽤 지루합니다. 좀더 예제를 들어 살펴 보겠습니다.
객체 만들기
가장 먼저 살펴볼 것은 객체를 만드는 방법입니다. 이에 대해 여러 가지 방법이 있지만 객체 리터럴 구문을 사용하여 객체를 만드는겁니다.
let funnyGuy = {};
고지식하게 new Object () 라고 객체를 만드는 대신 {} 를 이용하여 객체를 초기화 할 수 있습니다. 위 라인은 타입이 Object 인 funnyGuy라는 객체를 만들 것입니다.
객체 리터럴 구문으로 방금 본 것보다 객체를 생성하는 데는 조금 더 많은 것이 있지만 조만간 모든 것을 다룰 것입니다.
속성 추가하기
객체가 있으면 여기에 속성을 추가하는 데 사용할 수있는 여러 경로가 있습니다. 배열에서 인덱스 할때 쓰는 브래킷 같은 방법처럼 간단한 경로가 있습니다.
funnyGuy 객체부터 다시 진행해 보겠습니다.
firstName이라는 새 속성을 추가하고 "Conan"이라는 값을 지정한다고 가정 해 보겠습니다. 이 속성을 추가하는 방법은 다음과 같이 점 표기법 구문을 사용하는 것입니다.
funnyGuy.firstName = "Conan";
간단합니다. 이 속성을 추가하면 아래와 같이 해당 속성에 액세스 할 수 있습니다.
let funnyFirstName = funnyGuy.firstName;
더 진행해 볼까요? lastName이라는 다른 속성을 추가하고 "O'Brien"의 값을 지정하겠습니다.
funnyGuy.lastName = "O'Brien";
아마 전체 코드는 다음과 같을 겁니다.
let funnyGuy = {};
funnyGuy.firstName = "Conan";
funnyGuy.lastName = "O'Brien";
이 코드가 실행되면 funnyGuy 객체를 만들고 여기에 firstName과 lastName이라는 두 가지 속성을 설정합니다.
방금 본 것은 객체를 생성하고 별도의 단계에서 속성을 설정하는 방법입니다. 처음부터 설정하려는 속성을 알고 있다면 몇 가지 단계를 함께 결합 할 수 있습니다.
let funnyGuy = {
firstName: "Conan",
lastName: "O'Brien"
};
이 코드의 최종 결과는 이전에 funnyGuy 객체를 만들고 나중에 속성을 설정했던 것과 동일합니다.
우리가 살펴 봐야 할 속성 추가에 대한 또 다른 세부 사항이 있습니다. 지금까지 속성 값이 숫자, 문자열 등으로 구성된 속성을 가진 다양한 객체를 살펴 보았습니다. 속성 값이 다른 객체 자체가 될 수 있다는 것을 알고 계십니까? content 속성에 객체를 저장하는 다음 코드의 colors 객체를 살펴보겠습니다.
let colors = {
header: "blue",
footer: "gray",
content: {
title: "black",
body: "darkgray",
signature: "light blue"
}
};
중첩 된 객체에 속성을 추가하려면 지금까지 본 모든 것을 결합하면 됩니다. 아까 보았던 중첩 된 콘텐츠 객체에 frame이라는 속성을 추가한다고 가정 해 보겠습니다. 우리가 할 수있는 방법은 다음과 같이하는 것입니다.
colors.content.frame = "yellow";
colors 객체로 시작하여 content 객체로 이동 한 다음 원하는 속성과 값을 지정합니다. content 속성에 액세스하기 위해 대괄호 표기법을 사용하려면 다음과 같이 할 수 있습니다.
colors["content"]["frame"] = "yellow";
점 표기법과 대괄호 표기법을 섞고 싶다면 다음과 같이 작동합니다.
colors.content["frame"] = "yellow";
이 작업을 마무리하기 전에 처음에 객체에 속성을 추가하는 데 사용할 수있는 여러 경로가 있다고 언급했습니다. 사용할 수있는 더 복잡한 경로에는 Object.defineProperty 및 Object.defineProperties 메서드가 있습니다.
이러한 메서드를 사용하면 속성과 해당 값을 설정할 수 있고 또 속성을 열거 할 수 있는지, 또 속성이 커스터마이징 될 수 있는지 등 더 많은 작업을 수행 할 수 있습니다.
이 방법은 좀 어려울 수 있으며, MDN 문서를 참조하시면 좀 더 다양한 예제와 함께 쉽게 이해할 수 있습니다.
Note: 브래킷 노테이션
설정 및 읽기 속성 모두에 대해 점 표기법 접근 방식을 사용했습니다. 점 대신 대괄호를 사용하는 다른 방법이 있습니다.
점 또는 대괄호를 선호하는지 여부는 본인 취향에 따라 틀리지만 대괄호 방식이 필요한 때도 있습니다. 바로 우리가 동적으로 이름을 생성해야하는 때입니다. firstName 및 lastName의 경우 이러한 속성 이름이 하드 코딩되었습니다. 다음 코드를 살펴보십시오.
myObject라는 객체가 있으며 이 객체에 속성을 설정하는 코드입니다. 하드 코딩 된 이름 목록이 없습니다. 대신 배열의 인덱스 값을 사용하여 속성 이름을 만듭니다. 속성 이름을 코드로 만들어 해당 데이터를 사용하여 myObject에 속성을 만듭니다. 생성 할 속성 이름은 data0, data1, data2, data3 및 data4입니다. 이처럼 객체에서 속성 이름을 동적으로 지정하는 이 기능은 대괄호 구문을 사용하면 쉽게 표현될 수 있습니다.
Note: 브래킷 노테이션
설정 및 읽기 속성 모두에 대해 점 표기법 접근 방식을 사용했습니다. 점 대신 대괄호를 사용하는 다른 방법이 있습니다.
let funnyGuy = {};
funnyGuy["firstName"] = "Conan";
funnyGuy["lastName"] = "O'Brien";
점 또는 대괄호를 선호하는지 여부는 본인 취향에 따라 틀리지만 대괄호 방식이 필요한 때도 있습니다. 바로 우리가 동적으로 이름을 생성해야하는 때입니다. firstName 및 lastName의 경우 이러한 속성 이름이 하드 코딩되었습니다. 다음 코드를 살펴보십시오.
let myObject = {};
for (let i = 0; i < 5; i++) {
let propertyName = "data" + i;
myObject[propertyName] = Math.random() * 100;
}
myObject라는 객체가 있으며 이 객체에 속성을 설정하는 코드입니다. 하드 코딩 된 이름 목록이 없습니다. 대신 배열의 인덱스 값을 사용하여 속성 이름을 만듭니다. 속성 이름을 코드로 만들어 해당 데이터를 사용하여 myObject에 속성을 만듭니다. 생성 할 속성 이름은 data0, data1, data2, data3 및 data4입니다. 이처럼 객체에서 속성 이름을 동적으로 지정하는 이 기능은 대괄호 구문을 사용하면 쉽게 표현될 수 있습니다.
속성 제거하기
객체에 속성을 추가하는 것이 재미 있다고 생각했다면 객체에서 속성을 제거하는 것은 약간 지루합니다. 또한 더 간단합니다. colors 객체로 계속 작업 해 봅시다.
let colors = {
header: "blue",
footer: "gray",
content: {
title: "black",
body: "darkgray",
signature: "light blue"
}
};
우리가 원하는 것은 footer 속성을 제거하는 것입니다. 대괄호 표기법을 사용하여 footer 속성에 액세스 할지 또는 점 표기법을 사용하여 액세스할지 여부에 따라 두 가지 방법으로 이를 수행 할 수 있습니다.
delete colors.footer;
// or
delete colors["footer"];
이 모든 작업을 수행하는 핵심은 delete 키워드입니다. delete 키워드를 제거하려는 속성과 같이 쓰면 그게 전부입니다.
delete 키워드를 쓸 때 알아야 사항은 성능과 관련이 있다는 겁니다. 많은 수의 객체에서 많은 속성을 자주 삭제하는 경우 속성 값을 undefined로 설정하는 것보다 delete가 훨씬 느립니다.
colors.footer = undefined;
// or
colors["footer"] = undefined;
속성을 "undefined"으로 설정하면 해당 속성이 여전히 메모리에 존재한다는 의미입니다. 여러 상황에서 트레이드 오프(속도 대 메모리)를 계산하고 자신에게 가장 적합한 것을 선택하여 최적화해야합니다.
도대체 뒤에서 무엇이 이루어지고 있을까요?
우리는 객체를 생성하고 일반적으로 추가 삭제하는 방법을 보았습니다. 실제로 객체는 자바 스크립트가 모든 Core 작업을 수행하게하는 핵심이기 때문에 무슨 일이 일어나고 있는지 더 깊이 이해하는 것이 중요합니다. JavaScript 작업의 대부분은 객체를 만들고 또 그 객체로 다른 작업을 수행하는 일이 대부분입니다.
funnyGuy 객체로 다시 시작해 보겠습니다.
let funnyGuy = {};
위의 빈 객체로 무엇을 할 수 있을까요? 일단은 funnyGuy라는 객체에 정의 된 속성이 없습니다. 우리의 funnyGuy 객체가 진정으로 혼자이고 아무것도하지 않고 고립되어 있을까요? 결과적으로 대답은 '아니오'입니다. 그 이유는 JavaScript에서 생성한 객체가 더 큰 객체 및 더 많은 기능과 자동으로 상호 연결되는 방식과 관련이 있습니다. 이러한 상호 연결을 이해하는 가장 좋은 방법은 시각화하는 것입니다. 다음을 보십시요.
이 다이어그램에서 우리는 빈 funnyGuy 객체를 만들 때 실제로 일어나는 일을 매핑했습니다.
우리의 funnyGuy는 단순히 빈 객체입니다. 우리가 정의한 속성이 없습니다. 그러나 기본적으로 생성될 때 부터 정의된 속성이 있으며 이러한 속성은 자동으로 funnyGuy 객체를 기본 Object 타입에 연결합니다. 이 링크를 통해 다음과 같이 funnyGuy의 기존 Object 객체 속성을 호출 할 수 있습니다.
let funnyGuy = {};
funnyGuy.toString(); // [object Object]
Object 객체에 연결된 링크는 비어있는 funnyGuy 객체에서 toString이 작동하도록 허용합니다. 이제 이 링크를 링크라고 부르는 것은 정확한 표현이 아닙니다. 이 링크는 실제로 다른 객체를 가리키는 프로토타입([[Prototype]]으로 표시됨)으로 불리웁니다. 다른 객체는 또 다른 객체를 가리키는 [[Prototype]]을 가질 수 있습니다. 이 모든 연결을 프로토타입 체인(prototype chain)이라고 합니다. 당신이 호출하려는 속성을 찾으려고 할 때 JavaScript가 수행하는 작업이 프로토타입 체인을 따라 이동하는 겁니다. funnyGuy 객체에서 toString을 호출하는 경우 실제로 다음과 같은 일이 발생합니다.
프로토타입 체인을 사용하면, 우리가 찾고 있는 특정 속성이 정의되어 있지 않더라도 JavaScript는 체인을 통과하여 모든 정류장에서 해당 속성이 대신 정의 되었는지 확인합니다. 이제 funnyGuy 객체의 프로토타입 체인은 그 funnyGuy 객체 자체와 Object.prototype입니다. 전혀 복잡한 사슬이 아닙니다. 더 복잡한 객체로 작업할수록 프로토타입 체인은 매우 길고 복잡해질 것입니다.
다음으로, 현재 funnyGuy 객체는 매우 기본적입니다. 좀 더 흥미롭게 만들기 위해 이전의 firstName 및 lastName 속성을 추가해 보겠습니다.
let funnyGuy = {
firstName: "Conan",
lastName: "O'Brien"
};
이 두 가지 속성이 혼합되어 있으면 이전에 시각화한 그림은 이제 다음과 같이 보일겁니다.
firstName 및 lastName 속성은 funnyGuy 객체의 일부이며 위 그림처럼 시각화됩니다. 객체에 대한 이 초기 범위를 벗어 났으므로 이제 좀 더 자세히 살펴볼 때입니다.
커스텀 객첵 만들기
일반 객체로 작업하고 거기에 속성을 추가하는 것은 꽤 유용하지만 기본적으로 동일한 객체를 여러 개 만들면 그 대단함이 정말 빨리 사라집니다. 다음 코드를 살펴봅시다.
let funnyGuy = {
firstName: "Conan",
lastName: "O'Brien",
getName: function () {
return "Name is: " + this.firstName + " " + this.lastName;
}
};
let theDude = {
firstName: "Jeffrey",
lastName: "Lebowski",
getName: function () {
return "Name is: " + this.firstName + " " + this.lastName;
}
};
let detective = {
firstName: "Adrian",
lastName: "Monk",
getName: function () {
return "Name is: " + this.firstName + " " + this.lastName;
}
};
위 코드는 funnyGuy 객체와 매우 유사한 두 개의 새로운 객체 인 theDude 및 detective를 가지고 있습니다. 이 모든 것에 대한 시각화는 이제 다음과 같이 보일 것입니다.
언뜻보기에 꽤 많은 중복이 진행되고 있는 것 같습니다. 각각의 객체에는 firstName, lastName 및 getName 속성의 자체 복사본이 있습니다. 모든 복제가 나쁜 것은 아닙니다만, 객체의 경우 복제해도 되는 속성과 그렇지 않은 속성을 파악해야합니다. 이 예에서 firstName 및 lastName 속성은 일반적으로 객체별로 고유 한 값을 갖습니다. 각 객체에 이러한 항목을 복제하는 것은 좋습니다. 그러나 getName 속성은 단순히 도우미 역할을 하고 있으며 특정 객체가 따로 고유하게 표현하려는 항목은 따로 없습니다.
getName: function () {
return "Name is: " + this.firstName + " " + this.lastName;
}
위 항목을 복제하는 것은 의미가 없으므로 복제없이 getName을 보다 일반적으로 사용할 수 있도록 해야 합니다.
getName같은 일반 속성을 포함하고 있는 부모(parent) 객체를 만들어 이를 수행하는 깨끗한 방법이 있습니다. 우리 자식(child) 객체를 Object에서 직접 상속하는 대신 이 부모 객체에서 상속 할 수 있습니다. 좀 더 구체적으로 알아보기 위해 getName을 포함하는 새 person 객체를 만들 것입니다. 우리의 funnyGuy, theDude 및 detective 객체는 person으로부터 상속됩니다. 이 상속은 복제해야 하는 속성이 복제되고 공유해야하는 속성이 공유되도록 보장합니다. 다음 시각화 그림을 보십시요.
person은 이제 프로토타입 체인의 일부이며 Object.prototype과 자식(child) 객체 사이에 자리 잡고 있습니다. 이 모든 작업을 수행하는 코드는 다음과 같습니다.
let person = {
getName: function () {
return "The name is " + this.firstName + " " + this.lastName;
}
};
let funnyGuy = Object.create(person);
funnyGuy.firstName = "Conan";
funnyGuy.lastName = "O'Brien";
let theDude = Object.create(person);
theDude.firstName = "Jeffrey";
theDude.lastName = "Lebowski";
let detective = Object.create(person);
detective.firstName = "Adrian";
detective.lastName = "Monk";
프로토타입 체인이 작동하는 방식으로 인해 funnyGuy, theDude, detective 객체에 대해 getName을 호출 할 수 있으며 잘 작동합니다.
detective.getName(); // The name is Adrian Monk
person 객체를 좀 더 향상시키기로 한다면 다음 코드를 생각해 볼 수 있습니다. 이름과 성의 첫 글자를 반환하는 getInitials 메서드를 추가한다고 가정 해 보겠습니다.
let person = {
getName: function () {
return "The name is " + this.firstName + " " + this.lastName;
},
getInitials: function () {
if (this.firstName && this.lastName) {
return this.firstName[0] + this.lastName[0];
}
}
};
이 getInitials 메소드를 person 객체에 추가합니다. 이 메서드를 사용하려면 funnyGuy와 같이 person을 상속받는 모든 객체에서 호출 할 수 있습니다.
funnyGuy.getInitials(); // CO
코드의 기능을 분할하는 데 도움이되는 중간 객체를 만드는 이와 같은 기능은 강력한 기능입니다. 이를 통해 객체를 생성하는 방법과 각각에 제공하는 기능을 보다 효율적으로 수행 할 수 있습니다.
this 키워드
이전 코드에서 눈치 채셨을 수 있는 한 가지는 this 키워드를 사용하는 것입니다. 특히 person 객체에서 대신 생성되서 자식에게 상속된 속성을 참조하기 위해 사용했을 때 더 그렇습니다. person 객체, 좀 더 구체적으로 getName 속성으로 돌아가 보겠습니다.
let person = {
getName: function () {
return "The name is " + this.firstName + " " + this.lastName;
},
getInitials: function () {
if (this.firstName && this.lastName) {
return this.firstName[0] + this.lastName[0];
}
}
};
getName을 호출 할 때 호출 한 객체에 따라 적절한 이름이 반환되는 것을 볼 수 있습니다. 예를 들어 다음을 수행한다고 가정 해 보겠습니다.
let spaceGuy = Object.create(person);
spaceGuy.firstName = "Buzz";
spaceGuy.lastName = "Lightyear";
console.log(spaceGuy.getName()); // Buzz Lightyear
이것을 실행하면 Buzz Lightyear가 콘솔에 인쇄되는 것을 볼 수 있습니다. getName 속성을 다시 보면 person 객체에 firstName 또는 lastName 속성이 전혀 존재하지 않습니다. 속성이 존재하지 않을 때, 우리는 이전에 부모에서 부모로 프로토타입 체인을 걸어 내려가는 것을 보았습니다.
우리의 경우 프로토타입 체인의 유일한 정거장은 Object.prototype입니다. Object.prototype에도 firstName 또는 lastName 속성이 없습니다. 이 getName 메서드가 어떻게 작동하고 올바른 값을 반환할까요?
대답은 getName의 return 문의 일부로 firstName 및 lastName 앞에 오는 this 키워드와 관련이 있습니다.
......
return "The name is " + this.firstName + " " + this.lastName;
......
this 키워드는 getName 메서드가 바인딩 된 객체를 참조합니다. 이 객체는 여기서는 spaceGuy입니다. 모든 프로토타입 탐색에 있어 엔트리 포인트가 되는 개체입니다.
getName 메서드가 실행되고 firstName 및 lastName 속성을 확인해야하는 지점에서 this 키워드가 가리키는 탐색이 시작됩니다. 이 탐색이 spaceGuy 객체로 시작된다는 것을 의미합니다. 실제로 spaceGuy객체는 firstName과 lastName 속성을 포함하고 있습니다. 이것이 바로 getName 메서드 (및 getInitials)가 호출 될 때 올바른 결과를 얻을 수 있는 이유입니다.
this 키워드를 이해할려면 굉장히 많은 양과 시간을 할애해야 합니다. 다음에 좀더 깊게 다루어 보겠습니다.
결론
여기에서 본 내용의 대부분은 객체가 파생되고 다른 객체를 기반으로 하는 상속을 직접 또는 간접적으로 다루었습니다. 객체의 템플릿으로 클래스를 사용하는 다른 클래스 언어와 달리 JavaScript에는 클래스라는 개념이 없습니다. JavaScript는 프로토타입 상속 모델(prototypical inheritance model)로 알려진 것을 사용합니다. 템플릿에서 객체를 인스턴스화하지 않습니다. 대신 처음부터 또는 더 일반적으로 복사하여 개체를 만듭니다.
좀 더 깊은 내용은 차차 알아가 보도록 하겠습니다.
다음편에서는 빌트인 객체에 대해 알아보도록 하겠습니다.
'코딩 > Javascript' 카테고리의 다른 글
[JS-중급편-OOP] 10.클래스 (0) | 2020.12.30 |
---|---|
[JS-중급편-OOP] 9.빌트인 객체 확장 (0) | 2020.12.27 |
[JS-중급편-OOP] 7.Getters and Setters (0) | 2020.12.26 |
[JS-중급편-OOP] 6.숫자(Numbers) (0) | 2020.12.24 |
[JS-중급편-OOP] 5.프리미티브 타입에 관하여 (0) | 2020.12.24 |