본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.
자바스크립트에서 객체는 다양한 데이터를 담을 수 있는 자료형이다. 따라서 원시형(Primitive type)과는 구분되는 독특한 자료형이며 자바스크립트 전반에 걸쳐 쓰임이 많은 자료형이다.
객체 생성은 크게 3가지 방법으로 할 수 있다.
const user = new Object();
user.id = 'longroadhome';
user.age = 20;
// user 객체는 다음의 결과와 동일
// user = {
// id: 'longroadhome',
// age: 20,
// }
래퍼객체란 원시 타입의 값을 감싸는 형태의 객체를 말한다.
Ex. Number, String, Boolean, Symbol ...
const user = {
id: 'longroadhome',
age: 20,
};
// user 객체는 다음의 결과와 동일
// user = {
// id: 'longroadhome',
// age: 20,
// }
const User = function(id, age) {
this.id = id;
this.age = age;
}
const user = new User('longroadhome', 20);
// user 객체는 다음의 결과와 동일
// user = {
// id: 'longroadhome',
// age: 20,
// }
일반적으로 가장 많이 사용하는 방법은 2번 객체 리터럴
방식을 많이 사용한다.
객체는 프로퍼티(속성)을 키 - 값
의 형태로 저장한다. 이때 프로퍼티의 키
는 항상 문자열 또는 심볼이어야 한다. 반면 값
은 어떠한 형태의 자료형도 가능하다.
프로퍼티에 접근하는 방법은 크게 2가지가 있다.
console.log(user.id);
console.log(user.name);
// 문자열 지정
console.log(user["id"]);
// 변수 지정
const name = 'name';
console.log(user[name]);
대괄호 표기법의 경우 사용자의 입력을 받아 해당 값을 키값으로 활용하여 값을 할당하거나 접근이 가능해 유연성이 높다.
객체의 프로퍼티는 특정한 순서가 있다. 이때 정수 프로퍼티(Integer Property)는 자동으로 정렬이 적용되고, 그 외의 경우엔 객체에 추가한 순서를 유지한다.
정수 프로퍼티는 어떠한 변화 없이 정수에서 문자열로, 문자열에서 정수로 변환이 가능한 문자열을 의미한다.
// 정수 프로퍼티 예시
Math.floor(Number("12")); // 12 출력 - 정수 프로퍼티
Math.floor(Number("+12")); // 12 출력 - 변형 발생
Math.floor(Number("1.2")); // 1 출력 - 변형 발생
위에서 생성한 객체는 순수객체(plain object)
라고 불리는 일반 객체이다. 자바스크립트에는 일반 객체 이외에도 다양한 종류의 객체가 존재한다.
Array
: 정렬된 데이터 컬렉션 관련, 흔히 배열이라 부른다Date
: 날짜와 시간 정보 관련Error
: 에러 정보 관련객체와 원시 타입 자료형의 가장 큰 차이점 중에 하나는, 원시 타입의 값은 값 자체가 그대로 저장, 할당 그리고 복사가 이루어진다. 그러나 객체의 경우에는 참조의 의해(by reference) 저장, 할당 그리고 복사가 이루어진다. 여기서 참조란 객체가 저장되어 있는 메모리 어딘가의 주소를 말한다. 즉 객체는 생성 시점에 변수에 저장되는 것이 아닌 메모리 어딘 가에 저장이 되고, 변수는 해당 메모리의 주소를 저장한다.
따라서 동일한 참조값을 가지고 있는 객체는 비교 시 서로 동일하다. 그러나 독립된 객체의 참조값을 가지고 있는 두 개의 변수는 서로 동일하지 않다.
객체 비교 시 동등 연산자
==
와 일치 연산자===
는 동일하게 작동한다.
하지만 일치 연산자===
를 사용하는 습관을 들여놓자!
// a에 객체의 참조값을 담고, 해당 값을 b에도 저장
// 두 변수는 동일한 참조값을 가지므로 서로 같은 객체
const a = {};
const b = a;
console.log( a === b ) // true;
// a와 b 모두 동일한 모양의 객체를 할당하는 것은 같지만
// 두 객체는 생성시점에 각각 따로 메모리 어딘가에 저장
// 따라서 서로 다른 참조값을 가지기에 두 변수는 일치하지 않음
const a = {};
const b = {};
console.log( a === b) // false;
객체가 할당된 변수를 복사(재할당)하는 것은 결국 동일한 참조값을 넘겨주는 것과 같다는 것과 같다. 만약 객체 자체를 복사하여 기존 객체와 똑같지만 별도의 독립적인 객체를 만들고 싶은 경우에는 객체 복사를 통해 가능하다.
그러나 자바스크립트의 경우에는 객체 복제 내장 메서드를 자체 지원해주지 않기 때문에 직접 복사를 구현해야 한다. 객체 복사는 크게 두 가지 타입으로 구분할 수 있다.
복사 하고자 하는 객체의 모든 프로퍼티가 원시형인 경우엔 얕은 복사만으로도 완전히 독립된 객체를 생성할 수 있다. 즉 얕은 복사란, 해당 객체의 원시형 프로퍼티에 대해서는 독립적인 복사가 이루어지지만 만일 프로퍼티가 객체로 중첩 구조를 이루고 있는 경우에는 해당 프로퍼티 객체의 참조값을 전달하므로 완전한 독립적 복사가 이루어지지 않는다.
따라서 완전히 독립적인 객체를 복사하고자 한다면, 복사하고자 하는 객체의 프로퍼티가 모두 원시형이라는 것을 인지하고 있거나 아니면 깊은 복사(Deep Copy)를 수행해야 한다. 전자의 경우는 매우 드문 경우이기에 보통 깊은 복사를 이용한다.
얕은 복사는 크게 여러가지 방법으로 가능하다.
// 원시형 프로퍼티만 가지고 있는 객체
const user = {
id: 'longroadhome',
age: 20,
}
// 1. for...in을 통한 얕은 복사
// 비어있는 별도의 객체를 하나 생성
const copy = {};
// user 객체의 모든 키 값에 대한 값을 불러와 동일하게 할당
for(const key in user) {
copy[key] = user[key];
}
// user와 copy는 서로 동일한 형식이지만
// 두 객체는 서로 독립적이다.
console.log( user === copy ); // false
// 2. Object.assign() 메서드를 이용한 복사
// 비어있는 별도의 객체를 하나 생성
const copy = {};
// 첫 매개변수는 복사할 대상이고, 그 이후는 복사할 객체이다.
Object.assign(copy, user);
// user와 copy는 서로 동일한 형식이지만
// 두 객체는 서로 독립적이다.
console.log( user === copy); // false
// 3. 전개 연산자(...)를 이용한 복사
// 선언과 초기화를 동시에 수행
const copy = { ...user };
// user와 copy는 서로 동일한 형식이지만
// 두 객체는 서로 독립적이다.
console.log( user === copy ); // false
보통 반복문 없이 간편하게 복사할 수 있는 전개연산자
방법 또는 Object.assign()
메서드를 많이 사용한다.
하지만 복사하고자 하는 객체의 프로퍼티가 또 객체일 수도 있다. 이처럼 중첩 객체의 구조를 가지고 있는 객체는 얕은 복사로는 완전히 독립적인 별도의 객체로 복사할 수 없다. 위에서 살펴보았듯이, 객체인 프로퍼티에 대해서는 참조값을 저장하기 때문이다. 따라서 해당 참조를 가진 객체에 다시 접근하여 원시형의 프로퍼티를 만날 때까지 계속 내려가야 한다. 이는 DFS 알고리즘
과 유사한 로직이며 실제로 재귀함수를 통해 깊은 복사를 할 수 있다.
// 중첩 객체 구조의 객체
const user = {
id: 'longroadhome',
age: 20,
familiy: {
familiyName: 'LEE',
groupNumber: 4
}
}
// 1. 재귀함수를 이용한 깊은 복사
function deepCopy(object) {
// 독립적인 객체를 하나 생성하고 참조값 할당
const copy = {};
// 복사할 객체의 모든 키 값에 접근하여
for(const key in object) {
// 해당 프로퍼티가 객체라면 재귀호출
if (typeof object[key] === 'object') {
result[key] = deepCopy(object[key]);
} else {
result[key] = obj[key];
}
}
return copy;
}
const copy = deepCopy(user);
// user와 copy는 서로 동일한 형식이지만
// 두 객체는 서로 독립적이다.
console.log( user === copy ); // false;
하지만 객체의 깊이가 매우 드문 경우지만, 객체의 깊이가 매우 깊은 경우라면 자바스크립트 성능 상 재귀호출에 의해 스택 오버플로우가 터질 수 있다. 이와 관련된 글은 해당 포스트에서 좀 더 자세히 접할 수 있다.
그것과 별개로 재귀함수의 로직을 개발자가 직접 구현하여 깊은 복사를 하는 것은 다소 번거롭고 부담스럽다. 따라서 보다 편하게 JSON
데이터를 처리하기 위한 메소드를 이용해 일종의 편법으로 깊은 복사를 어느 정도 흉내낼 수 있다.
// 2. JSON 내장메서드를 이용한 깊은 복수
const copy = JSON.parse(JSON.stringify(user));
// user와 copy는 서로 동일한 형식이지만
// 두 객체는 서로 독립적이다.
console.log( user === copy ); // false;
JSON.stringify()
메서드는 인수로 전달받은 자바스크립트 객체를 모두 문자열로 변환한다. 반대로 JSON.parse()
메서드는 전달받은 문자열을 다시 자바스크립트 객체로 변환한다. 즉 JSON
내장 메서드를 통해 복사를 하는 경우는 실제로 반복을 통한 순환 구조로 값을 옮겨 닮는 과정이 아니라 문자열로 변경 후, 이를 다시 해석하여 객체로 반환하기에 원본 객체와 독립된 객체가 되는 것이다.
다만 해당 방법에는 한계가 존재하는데, 먼저 성능적으로 다소 느리다는 단점이 있다. 또한 무엇보다 치명적인 점은 JSON
값에 해당되지 않는 프로퍼티를 전달받는 경우는 제대로 된 복사를 할 수 없다. 이는 ECMA-404
명세서에서 유효한 값의 종류를 명시하고 있는데 상세히 알고 싶다면 해당 링크를 참고하자. 해당 명세서에 따르면 자바스크립트에서의 함수는 JSON
값에 해당되지 않는다. 따라서 만약 객체의 프로퍼티가 함수였다면 (함수 역시 자바스크립트에서는 특별한 값으로 취급되므로 할당이 가능, 이때 자료형은 객체로 인식한다) 이는 제대로 복사를 할 수 없다는 명백한 한계가 존재한다.
따라서 보다 정확하고 간편하게 깊은 복사를 하기 위해서는 보통 외부 라이브러리를 많이 이용한다. 가장 대표적으로 lodash
와 ramda
라이브러리가 있다. 이 두 개의 라이브러리에서 지원하는 깊은 복사 함수를 이용하면 위의 이슈를 모두 고려한 깊은 복사를 수행할 수 있다.
// 3. 외부 라이브러리(lodash, ramda) 이용
import _ from 'loadsh';
import R from 'ramda';
...
const lodash_copy = _.cloneDeep(user);
console.log( user === lodash_copy ); // false
const ramda_copy = R.clone(user);
console.log( user === ramda_copy ); // false
사실 객체를 원본과 독립된 값으로 복사해야 하는 경우가 흔한 경우는 아니다. 보통은 참조값을 통해 해결 가능한 경우가 많다.
무엇보다 자바스크립트는 자체적인 깊은 복사 관련 함수를 제공하고 있지 않다. 위에서 언급한 두 라이브러리의 경우 역시 발생 가능한 모든 경우의 수를 브루트포스 방식으로 처리하여 깊은 복사를 정상적으로 하게끔 작동하기에 성능적으로도 다소 부족한 면모가 있다. 즉 깊은 복사가 필요한 경우가 언제인지, 있다면 해당 설계가 좋은 아키텍쳐인지를 먼저 고려하는 것 역시 필요할 것 이다.