본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.
자바스크립트에서 '객체'라고 하는 것은 기본 자료형 중에 하나이기도 하지만, 보통 '객체지향 프로그래밍(OOP)'라고 불리는 패러다임에서의 객체와도 일맥상통한다.
또한 자바스크립트에서 함수(function)은 특별한 동작을 하는 값으로 취급된다. 앞선 포스팅에서는 객체에 원시형의 값 또는 객체를 저장한 구조만 살펴보았는데, 함수 역시 값(자료형은 객체)이기 때문에 객체의 프로퍼티로 할당할 수 있다. 이렇게 객체의 프로퍼티에 할당된 함수는 보통 메서드(Method)라고 칭한다.
const user = {
id: 'longroadhome',
age: 20
};
// 함수를 하나 선언하고
function sayHello() {
alert('Hello World!');
}
// user 객체의 sayHello 프로퍼티에 함수 할당
user.sayHello = sayHello;
// 다음과 같이 함수를 호출 가능하다.
// 보통 이 같은 함수를 메서드라고 부른다.
user.sayHello(); // Hello World!
또는 객체 리터럴로 바로 메서드를 할당할 수 있다. 이때 프로퍼티가 함수, 즉 메서드로 작용한다면 다음과 같은 단축문법 역시 가능하다.
const user = {
sayHello: function () {
alert('Hello World!');
}
};
// 단축문법 : 위와 동일하게 동작하는 객체 생성
const user = {
sayHello() {
alert('Hello World!');
}
}
자바스크립트의 this
는 매우 자유분방하다. 때문에 다른 언어에서의 this
와 사뭇 그 동작 방식이 다르다. 원칙적으로 자바스크립트에서는 모든 함수에 this
를 사용할 수 있다.
this
는 런타임에 컨텍스트에 따라 결정된다. 즉 동일한 함수에서 사용된 this
라고 할 지라도 어느 객체에서 호출하느냐에 따라 this
의 참조값이 달라질 수 있다.
const a = { id: 'aaa' };
const b = { id: 'bbb' };
function sayHi () {
alert(`hi ${this.name}`);
}
a.func = sayHi;
b.func = sayHi;
a.func(); // hi aaa
b.func(); // hi bbb
즉 자바스크립트에서는 this
가 런타임에 결정되기 때문에 항상 메서드가 정의된 객체를 참조하는 것이 아니다. 메서드가 어디서 정의되었는지에 상관없이 this
는 점(dot) 앞의 객체에 따라 자유롭게 결정된다. 즉 해당 객체가 this
의 컨텍스트가 된다.
이러한 방식은 장단점이 존재한다. 위의 경우와 같이, 메서드를 하나만 만들어 여러 객체에 재사용이 가능한 유연함은 장점으로 작용할 수 있지만, 오히려 그로 인한 버그나 실수로 이어질 수 있다는 것은 동시에 단점이 되기도 한다.
반면 화살표 함수는 조금 특이하다. 일반 함수와 달리 화살표 함수는 고유한 this
를 가지지 않는다. 화살표 함수 내에 있는 this
는 화살표 함수가 아닌 외부 함수에서 this
값을 가져오게 된다. 따라서 별도의 this
가 만들어지는 것이 아닌, 외부 컨텍스트에 있는 this
를 이용하고자 하는 경우 화살표 함수가 유용하다.
const user {
id: 'longroadhome',
sayHi() {
// 화살표 함수에서의 this는 외부 컨텍스트에 있는
// this와 동일하다. 즉 user 객체를 가리킨다.
const arrow = () => console.log(this.id);
arrow();
}
};
user.sayHi(); // longroadhome
this
는 자바스크립트 전반에 두루 등장하는 개념이다. 화살표 함수에서의this
에 대해 자세한 개념은 이후 포스트에서 다루어보자.
앞서 포스트에서 객체를 생성하는 방법 중에 생성자 함수를 이용하는 방법을 보았다. 해당 예시를 다시 가져오면 다음과 같다.
const User = function(id, age) {
this.id = id;
this.age = age;
}
const user = new User('longroadhome', 20);
// user 객체는 다음의 결과와 동일
// user = {
// id: 'longroadhome',
// age: 20,
// }
생성자 함수(Construction function)와 일반 함수의 기술적인 차이는 없다. 다만 생성자 함수는 다음의 컨벤션을 보통 따른다.
new
연산자와 함께 실행위의 예시에서 보면 알겠지만 new User(...)
와 같이 생성자 함수를 쓰면 다음과 같은 순서로 객체가 생성된다.
this
에 할당this
에 프로퍼티 추가this
반환또한 생성자 함수에서는 별도의 return
문 없이 객체를 반환하는 것을 볼 수 있다. 이는 자동적으로 이루어지기 때문에 별도로 return
문을 명시할 필요가 없다. 하지만 return
문을 사용한다면 다음과 같은 규칙이 적용된다.
return
하는 경우 this
대신 해당 객체 반환return
한다면, return
문 무시생성자 함수를 이용하면 매개변수를 이용해 객체 내부를 자유롭게 구성할 수 있기에 유연성이 확보된다. 따라서 유사한 객체를 여러개 만들어야 하는 경우가 있다면 생성자 함수가 유용할 수 있다.
자바와 같은 객체지향형 언어에 익숙하다면 위의 개념이
class
의 생성자(constructor)와 비슷하다는 것을 눈치챌 수 있다. 실제로 자바스크립트 역시 객체지향형 패러다임으로 작성하는 경우 위의 개념과 더불어prototype
을 이용해서 객체지향을 구현했다.
그러나ES6
에서class
가 도입되어 자바스크립트 역시 자바와 비슷한 방식으로 클래스를 구현하는 방향으로 객체지향형 프로그래밍을 구현할 수 있다.
옵셔널 체이닝 ?.
을 이용하면 프로퍼티가 없는 중첩 객체에 보다 안전하게 접근이 가능하다. 즉 존재하는지에 대한 여부를 확실하게 판단할 수 없을 때(해당 프로퍼티가 옵셔널하게 주어지는 경우) 옵셔널 체이닝을 통해 에러없이 원하는 값에 접근이 가능하다.
옵셔널 체이닝도 이전에 다룬 Nullish 병합 연산자(Nullish coalescing)와 동일하게 ES2020
에 도입된 최신 스펙의 문법이다. 즉 구버전의 브라우저에서는 폴리필이 필요할 수 있다.
옵셔널 체이닝 이전에는 다음과 같이 프로퍼티에 안전하게 접근하기 위해 &&
연산자를 이용했다.
const user = {
address: 'seoul'
}
console.log(user.address); // seoul
console.log(user.address.street); // TypeError!
console.log(user && user.address && user.address.street);
// undefined; (별도의 에러는 발생 X)
옵셔널 체이닝 ?.
을 이용하면 앞의 평가 대상이 undefined
또는 null
인 경우 평가를 멈추고 바로 undefined
를 반환한다. 따라서 그 이후의 접근은 시도하지 않기 때문에 에러가 발생하지 않는다.
// 위의 코드를 다음과 같이 줄일 수 있다.
// address.street 이 있다면 값 반환
// address.street 이 없거나 user.address 가 없다면 undefined
console.log(user?.address?.street);
하지만 옵셔널 체이닝을 너무 남용하는 것은 좋은 코딩 스타일이 아닐 수 있다. 반드시 값이 할당되어 있어야 하는 경우에는 옵셔널 체이닝을 사용하지 않도록 하자. 그렇지 않으면 초기에 에러를 발견하지 못하고 따라서 디버깅이 어려워질 수 있다.
// user 객체는 항상 값이 존재하는 값
// street 속성은 옵셔널
console.log(user.address?.street);
또한 옵셔널 체이닝은 별도의 연산자가 아닌 특별한 문법 구조체(syntax construct)에 해당한다. 즉 함수나 대괄호 등과 함께 동작할 수 있다.
// user 객체에 sayHello라는 메서드가 옵셔널하게 주어질 때
// 해당 메서드가 있다면 함수 호출
// 없다면 평가를 멈추고 undefined 반환
user.sayHello?.();
자바스크립트는 객체 프로퍼티의 키
를 오직 문자열과 Symbol
만을 허용한다. Symbol
역시 자바스크립트의 7가지 자료형 중에 하나이며 자체로는 원시형 데이터이다. 이 역시 ES6
에서 추가되었다.
심볼은 유일한 식별자를 만들고자 할 때 사용할 수 있다. 주의점은 new
연산자 없이 다음과 같이 사용한다.
const id = Symbol();
const another_id = Symbol('id');
const another_id2= Symbol('id');
id
와 another_id
는 모두 새로운 심볼값이 된다. 이때 심볼의 매개변수로 전달하는 문자열은 단순히 해당 심볼의 설명(description
)이 되며 another_id.description
으로 접근할 수 있다. 이 값이 같더라도 심볼값은 항상 다르다. 심볼은 유일한 값이기 때문이다. 따라서 another_id
와 another_id2
는 서로 다르다.
심볼을 이용하면 숨김(hidden) 프로퍼티를 만들 수 있다. 이러한 숨김 프로퍼티는 외부 코드에서 접근이 불가하고 값 역시 덮어 쓸 수 없는 프로퍼티로 작용한다.
주로 서드 파티 라이브러리 또는 외부에서 가져온 객체가 있고, 해당 객체에 어떠한 작업을 해야하는 경우 심볼을 프로퍼티 키로 사용해 특정 값을 넣어준다면, 외부에서는 해당 프로퍼티에 접근이 불가하다. 따라서 의도치않게 값이 덮어씌어 지거나 제거되는 경우 등을 방지할 수 있다.
const id = Symbol('id');
const user = {
name: 'KG',
[id]: 123,
}
// id 프로퍼티와 id 심볼은 서로 다르다.
user.id = 123;
// user = {
// name: 'KG',
// id: 123,
// Symbol(id): 123,
// }
때문에 for ... in
과 같이 순회를 통한 프로퍼티 키 값에 접근 역시 심볼은 제외된다.
위에서 같은 이름(description
)값으로 심볼을 생성하더라도 각각 다른 심볼값을 가지는 것을 살펴보았다. 하지만 만약 동일한 이름을 가진 심볼이라면 동일한 심볼값임을 보장하는 것을 필요로 할 수 있다. 이때 사용할 수 있는 것이 전역 심볼이다.
전역 심볼 레지스트리(Global Symbol Registry)안에 심볼을 만들고, 해당 심볼에 접근하면 이름이 같은 경우 항상 동일한 심볼을 반환한다.
이러한 레지스트리에 접근하여 심볼값을 조회하려면 Symbol.for(key)
또는 Symbol.keyFor(sym)
메서드를 이용할 수 있다.
// 전역 레지스트리에서 'id' 이름의 심볼에 접근
// 만약 없다면 해당 이름을 전역 레지스트리에 새로 생성 후 반환
const id = Symbol.for('id');
const next_id = Symbol.for('id');
console.log( id === next_id ); // true;
const globalSymbol = Symbol.for('name');
const localSymbol = Symbol('name');
console.log(Symbol.keyFor(globalSymbol)); // name, 전역심볼
console.log(Symbol.keyFor(localSymbol)); // undefined, 전역 심볼 X
console.log(localSymbol.description); // 전역이 아니면 이처럼 접근
그 외 자바스크립트 내부에서 build-in 된 시스템 심볼이 있다. 이를 이용하면 객체의 동작을 미세 조정할 수 있다. 시스템 심볼에는 다음과 같은 것들이 있다.
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
Symbol은 외부에서 접근할 수 없는 숨김처리된 값이라고 소개했지만 사실 내장 메서드
Object.getOwnPropertySymbols(obj)
또는Reflect.ownKeys(obj)
등을 이용해 조회할 수는 있다. 그러나 흔한 경우는 아니다.