본문으로 건너뛰기
Advertisement

5.1 객체 기초 심화

JavaScript의 객체는 단순한 키-값 저장소를 넘어, 동적 프로퍼티, 디스크립터, 게터/세터 등 강력한 기능을 제공합니다.


프로퍼티 단축 (Shorthand Property Names)

변수명과 프로퍼티명이 같을 때 단축 문법을 사용합니다.

const name = "Alice";
const age = 30;
const role = "admin";

// ES5 방식
const userOld = { name: name, age: age, role: role };

// ES6+ 단축 방식
const user = { name, age, role };

console.log(user); // { name: "Alice", age: 30, role: "admin" }

// 함수에서 여러 값을 객체로 반환할 때 편리
function getCoordinates(id) {
const x = id * 10;
const y = id * 20;
const z = id * 30;
return { x, y, z }; // { x: x, y: y, z: z }
}

const { x, y, z } = getCoordinates(5);
console.log(x, y, z); // 50 100 150

계산된 프로퍼티명 (Computed Property Names)

대괄호 [] 안에 표현식을 넣어 동적으로 프로퍼티 키를 생성합니다.

const prefix = "user";
const timestamp = Date.now();

const dynamicObj = {
[`${prefix}_id`]: 1,
[`${prefix}_name`]: "Alice",
[`created_${timestamp}`]: new Date().toISOString(),
[Symbol.iterator]: function* () { yield* Object.values(this); },
};

console.log(dynamicObj.user_id); // 1
console.log(dynamicObj.user_name); // "Alice"

// 폼 데이터 처리에서 활용
function updateField(state, fieldName, value) {
return {
...state,
[fieldName]: value, // 동적 키
[`${fieldName}Error`]: null, // 관련 에러 초기화
};
}

let formState = { email: "", emailError: "필수 항목", password: "" };
formState = updateField(formState, "email", "alice@example.com");
console.log(formState);
// { email: "alice@example.com", emailError: null, password: "" }

// 객체 키를 Enum처럼 활용
const ActionType = {
INCREMENT: "INCREMENT",
DECREMENT: "DECREMENT",
RESET: "RESET",
};

const handlers = {
[ActionType.INCREMENT]: (state) => ({ ...state, count: state.count + 1 }),
[ActionType.DECREMENT]: (state) => ({ ...state, count: state.count - 1 }),
[ActionType.RESET]: () => ({ count: 0 }),
};

let state = { count: 5 };
state = handlers[ActionType.INCREMENT](state);
console.log(state.count); // 6

스프레드/나머지 연산자 — 객체 활용

const defaults = { theme: "light", lang: "ko", fontSize: 14, timeout: 5000 };
const userPrefs = { theme: "dark", fontSize: 16 };

// 스프레드: 기본값 + 사용자 설정 병합
const config = { ...defaults, ...userPrefs };
console.log(config);
// { theme: "dark", lang: "ko", fontSize: 16, timeout: 5000 }

// 나머지 패턴으로 특정 프로퍼티 제외
const { theme, lang, ...rest } = config;
console.log(rest); // { fontSize: 16, timeout: 5000 }

// 중첩 객체 불변 업데이트
const product = {
id: 1,
name: "노트북",
specs: { ram: "16GB", storage: "512GB", cpu: "M3" },
price: 1500000,
};

const upgraded = {
...product,
specs: { ...product.specs, ram: "32GB" },
price: 1800000,
};

console.log(product.specs.ram); // "16GB" — 원본 유지
console.log(upgraded.specs.ram); // "32GB"

// 객체 복제 (얕은 복사)
const original = { a: 1, b: { c: 2 } };
const clone = { ...original };
clone.a = 99;
console.log(original.a); // 1 (독립적)

clone.b.c = 99;
console.log(original.b.c); // 99 (얕은 복사: 중첩 객체는 참조 공유)

Object 정적 메서드

Object.keys / values / entries / fromEntries

const scores = { alice: 85, bob: 92, carol: 78, dave: 95 };

// 키, 값, 엔트리 배열로 변환
console.log(Object.keys(scores)); // ["alice", "bob", "carol", "dave"]
console.log(Object.values(scores)); // [85, 92, 78, 95]
console.log(Object.entries(scores)); // [["alice", 85], ["bob", 92], ...]

// entries → 변환 → fromEntries (객체 변환 파이프)
const bonus = Object.fromEntries(
Object.entries(scores).map(([name, score]) => [name, score * 1.1])
);
console.log(bonus); // { alice: 93.5, bob: 101.2, carol: 85.8, dave: 104.5 }

// 상위 2명 추출
const top2 = Object.fromEntries(
Object.entries(scores)
.sort(([, a], [, b]) => b - a)
.slice(0, 2)
);
console.log(top2); // { dave: 95, bob: 92 }

Object.assign

// 여러 소스 객체를 타겟에 병합 (첫 번째 인수를 직접 변경!)
const target = { a: 1 };
const source1 = { b: 2, c: 3 };
const source2 = { c: 4, d: 5 }; // c는 source2가 덮어씀

Object.assign(target, source1, source2);
console.log(target); // { a: 1, b: 2, c: 4, d: 5 }

// 원본 수정 방지: 빈 객체를 타겟으로
const merged = Object.assign({}, source1, source2);

// 스프레드가 더 직관적
const mergedSpread = { ...source1, ...source2 };

Object.create

// 지정한 프로토타입을 가진 객체 생성
const animal = {
breathe() { return `${this.name}이 숨을 쉽니다`; },
eat(food) { return `${this.name}${food}를 먹습니다`; },
};

const dog = Object.create(animal);
dog.name = "바둑이";
dog.bark = function() { return "멍멍!"; };

console.log(dog.breathe()); // "바둑이이 숨을 쉽니다"
console.log(dog.bark()); // "멍멍!"
console.log(Object.getPrototypeOf(dog) === animal); // true

// null 프로토타입 객체 (순수 딕셔너리)
const pureDict = Object.create(null);
pureDict.key = "value";
// pureDict.hasOwnProperty — 존재하지 않음! (더 안전)
console.log(Object.prototype.toString.call(pureDict)); // "[object Object]"

프로퍼티 디스크립터 (Property Descriptor)

모든 프로퍼티는 내부적으로 디스크립터를 가집니다.

// 디스크립터 확인
const obj = { x: 42 };
console.log(Object.getOwnPropertyDescriptor(obj, "x"));
// { value: 42, writable: true, enumerable: true, configurable: true }

// 디스크립터 속성:
// - value: 값
// - writable: 값 변경 가능 여부
// - enumerable: for...in, Object.keys() 등에 포함 여부
// - configurable: 디스크립터 변경 및 삭제 가능 여부

// Object.defineProperty로 디스크립터 제어
const constants = {};

Object.defineProperty(constants, "PI", {
value: 3.14159265358979,
writable: false, // 값 변경 불가
enumerable: true, // 열거 가능
configurable: false, // 디스크립터 변경 불가
});

// constants.PI = 3; // TypeError (strict mode)
console.log(constants.PI); // 3.14159265358979

// 여러 프로퍼티 한번에 정의
const point = {};
Object.defineProperties(point, {
x: { value: 0, writable: true, enumerable: true, configurable: true },
y: { value: 0, writable: true, enumerable: true, configurable: true },
_private: { value: "비공개", enumerable: false }, // keys()에서 제외
});

console.log(Object.keys(point)); // ["x", "y"] (_private은 제외됨)

게터와 세터 (Getter / Setter)

프로퍼티 접근 시 함수를 실행합니다.

class Circle {
#radius; // private 필드

constructor(radius) {
this.radius = radius; // setter 호출
}

// getter: 값을 읽을 때 실행
get radius() { return this.#radius; }

// setter: 값을 쓸 때 실행 (유효성 검사 가능)
set radius(value) {
if (typeof value !== "number" || value < 0) {
throw new RangeError(`반지름은 0 이상의 숫자여야 합니다. 받은 값: ${value}`);
}
this.#radius = value;
}

// 계산된 프로퍼티 (getter만)
get diameter() { return this.#radius * 2; }
get area() { return Math.PI * this.#radius ** 2; }
get circumference() { return 2 * Math.PI * this.#radius; }
}

const circle = new Circle(5);
console.log(circle.radius); // 5
console.log(circle.diameter); // 10
console.log(circle.area.toFixed(2)); // 78.54

circle.radius = 10;
console.log(circle.diameter); // 20

// circle.radius = -1; // RangeError!

// 객체 리터럴에서도 사용 가능
const temperature = {
_celsius: 20,

get celsius() { return this._celsius; },
set celsius(value) { this._celsius = value; },

get fahrenheit() { return this._celsius * 9/5 + 32; },
set fahrenheit(value) { this._celsius = (value - 32) * 5/9; },

get kelvin() { return this._celsius + 273.15; },
};

temperature.celsius = 100;
console.log(temperature.fahrenheit); // 212
temperature.fahrenheit = 32;
console.log(temperature.celsius); // 0

실전 예제: 반응형 폼 상태 관리

function createFormState(initialValues) {
const _values = { ...initialValues };
const _errors = {};
const _touched = {};
const _validators = {};
const _changeListeners = new Set();

function notify() {
_changeListeners.forEach(fn => fn(getState()));
}

function getState() {
return {
values: { ..._values },
errors: { ..._errors },
touched: { ..._touched },
isValid: Object.keys(_errors).length === 0,
isDirty: Object.keys(_touched).some(k => _touched[k]),
};
}

return {
registerValidator(field, validatorFn) {
_validators[field] = validatorFn;
return this;
},

setValue(field, value) {
_values[field] = value;
_touched[field] = true;

if (_validators[field]) {
const error = _validators[field](value);
if (error) {
_errors[field] = error;
} else {
delete _errors[field];
}
}

notify();
return this;
},

onChange(listener) {
_changeListeners.add(listener);
return () => _changeListeners.delete(listener);
},

getState,
reset() {
Object.assign(_values, initialValues);
Object.keys(_errors).forEach(k => delete _errors[k]);
Object.keys(_touched).forEach(k => delete _touched[k]);
notify();
},
};
}

const form = createFormState({ email: "", password: "" });

form
.registerValidator("email", v =>
!v.includes("@") ? "유효한 이메일을 입력하세요" : null
)
.registerValidator("password", v =>
v.length < 8 ? "비밀번호는 8자 이상이어야 합니다" : null
);

const unsubscribe = form.onChange(({ values, errors, isValid }) => {
console.log("변경됨:", values.email, "|", Object.values(errors).join(", ") || "에러 없음");
console.log("유효:", isValid);
});

form.setValue("email", "notanemail");
// 변경됨: notanemail | 유효한 이메일을 입력하세요
// 유효: false

form.setValue("email", "alice@example.com");
// 변경됨: alice@example.com | 에러 없음
// 유효: false (password는 아직 미입력)

form.setValue("password", "secure123");
// 변경됨: alice@example.com | 에러 없음
// 유효: true

unsubscribe(); // 구독 해제

고수 팁

팁 1: Object.getOwnPropertyDescriptors로 완전한 복사

// 일반 스프레드는 getter를 값으로 복사
const source = {
get computed() { return Math.random(); }
};

const spread = { ...source };
console.log(typeof Object.getOwnPropertyDescriptor(spread, "computed").get);
// "undefined" — getter가 아닌 값으로 복사됨

// getter 유지한 완전한 복사
const fullCopy = Object.create(
Object.getPrototypeOf(source),
Object.getOwnPropertyDescriptors(source)
);
console.log(typeof Object.getOwnPropertyDescriptor(fullCopy, "computed").get);
// "function" — getter 유지됨

팁 2: Proxy로 동적 기본값 객체

function withDefaults(target, defaults) {
return new Proxy(target, {
get(obj, key) {
return key in obj ? obj[key] : defaults[key];
}
});
}

const config = withDefaults(
{ theme: "dark" },
{ theme: "light", lang: "en", fontSize: 14 }
);

console.log(config.theme); // "dark" (직접 설정된 값)
console.log(config.lang); // "en" (기본값)
console.log(config.fontSize); // 14 (기본값)

팁 3: WeakMap으로 진정한 비공개 프로퍼티 (클래스 필드 # 이전 패턴)

const _private = new WeakMap();

class SecureStorage {
constructor(key) {
_private.set(this, { key, data: {} });
}

set(name, value) {
const priv = _private.get(this);
priv.data[name] = value;
}

get(name) {
return _private.get(this).data[name];
}
}

const storage = new SecureStorage("secret");
storage.set("token", "abc123");
console.log(storage.get("token")); // "abc123"
// storage._private — 접근 불가 (WeakMap 외부에서)
Advertisement