Object.freeze의 마법: 불변성의 진짜 이야기
“왜 내 객체가 멋대로 바뀌는 거야?” - 저도 이 문제로 며칠을 고생했습니다.
여러분도 이런 경험 있으신가요?
시나리오: 설정 관리의 악몽
저는 최근에 웹 애플리케이션의 설정 시스템을 만들고 있었습니다. 간단해 보였죠:
// 전역 설정 객체
const config = {
theme: 'light',
language: 'ko',
notifications: true
};
// UI 초기화
function initUI() {
const currentTheme = config.theme;
applyTheme(currentTheme);
}
// 설정 변경
function changeTheme(newTheme) {
config.theme = newTheme;
applyTheme(newTheme);
}
“완벽한데? 뭐가 문제야?”
문제는 다음날 아침에 터졌습니다.
// utils.js (어느 파일인가에서...)
function someUtilityFunction() {
// 실수로 config를 수정!
config.theme = 'debug-mode';
config.apiKey = 'exposed-key'; // 새로운 속성까지 추가!
// ... 다른 로직 ...
}
// ui.js
function renderHeader() {
console.log(config.theme); // 'debug-mode' (???? 언제 바뀌었지?)
}
문제점들:
- 어디서든
config를 변경할 수 있음 - 누가 언제 바꿨는지 추적 불가능
- 예상치 못한 값으로 인한 버그
- 디버깅 지옥…
더 끔찍한 경험: 공유 참조의 함정
const originalSettings = {
volume: 50,
quality: 'high'
};
// A 모듈에서 사용
const settingsA = originalSettings;
settingsA.volume = 100;
// B 모듈에서 사용
const settingsB = originalSettings;
console.log(settingsB.volume); // 100 (???)
// "어? 나는 50이어야 하는데..."
시각화:
메모리 상황:
originalSettings ──┐
├──> { volume: 100, quality: 'high' }
settingsA ─────────┤ (같은 객체를 가리킴!)
│
settingsB ─────────┘
settingsA.volume = 100 실행
↓
모든 참조가 같은 객체를 보기 때문에
originalSettings.volume도 100!
settingsB.volume도 100!
“이게 JavaScript의 ‘공유 참조’ 문제입니다.”
Object.freeze: 마법의 얼음 주문
“얼음”을 떠올려보세요
물을 생각해봅시다.
물 (일반 객체):
┌──────────────┐
│ ~~~~~~~ │ ← 자유롭게 흐름, 모양이 계속 바뀜
│ ~~~~~~~ │
└──────────────┘
어디서든 모양을 바꿀 수 있음
얼음 (Object.freeze):
┌──────────────┐
│ ▓▓▓▓▓▓ │ ← 단단히 얼어붙음, 모양 고정
│ ▓▓▓▓▓▓ │
└──────────────┘
아무도 모양을 바꿀 수 없음!
Object.freeze의 진짜 모습
// 일반 객체 (물)
const normalObj = { name: 'Alice' };
normalObj.name = 'Bob'; // ✓ 변경 가능
normalObj.age = 25; // ✓ 추가 가능
delete normalObj.name; // ✓ 삭제 가능
console.log(normalObj); // { age: 25 }
// Object.freeze (얼음)
const frozenObj = Object.freeze({ name: 'Alice' });
frozenObj.name = 'Bob'; // ✗ 무시됨! (strict mode에선 에러)
frozenObj.age = 25; // ✗ 무시됨!
delete frozenObj.name; // ✗ 무시됨!
console.log(frozenObj); // { name: 'Alice' } (변하지 않음!)
마법 같은 일이 벌어집니다:
const config = Object.freeze({
theme: 'light',
language: 'ko'
});
// 악의적인 코드가 수정을 시도해도...
config.theme = 'dark'; // 조용히 무시됨
config.apiKey = 'secret'; // 조용히 무시됨
delete config.language; // 조용히 무시됨
console.log(config);
// { theme: 'light', language: 'ko' }
// 완벽하게 보호됨! ✨
실전 예제: Before/After
Before: 혼돈의 설정 관리
// ❌ 문제투성이 코드
let appSettings = {
theme: 'light',
volume: 50,
language: 'ko'
};
// 여러 곳에서 마음대로 수정
function featureA() {
appSettings.theme = 'dark';
appSettings.newFeature = true; // 새 속성까지 추가!
}
function featureB() {
appSettings.volume = 100;
}
function featureC() {
delete appSettings.language; // 삭제까지!
}
// 나중에 다른 곳에서...
function renderUI() {
console.log(appSettings);
// { theme: 'dark', volume: 100, newFeature: true }
// "어? language는 어디 갔어? 누가 theme 바꿨어?" 😱
}
문제점:
- 예측 불가능한 상태
- 디버깅 불가능 (누가 언제 바꿨는지 모름)
- 사이드 이펙트 폭발
- 팀 협업 시 충돌
After: Object.freeze로 안전하게
// ✅ 안전한 코드
let appSettings = Object.freeze({
theme: 'light',
volume: 50,
language: 'ko'
});
// 설정을 바꾸려면 새 객체를 만들어야 함
function changeTheme(newTheme) {
// 기존 객체는 불변, 새 객체 생성!
appSettings = Object.freeze({
...appSettings, // 기존 속성 복사
theme: newTheme // theme만 변경
});
console.log('테마 변경:', appSettings);
// 명시적인 변경, 추적 가능!
}
// 악의적인 수정 시도는 무시됨
function maliciousCode() {
appSettings.theme = 'hacked'; // ✗ 무시
appSettings.virus = true; // ✗ 무시
delete appSettings.language; // ✗ 무시
}
maliciousCode();
console.log(appSettings);
// { theme: 'light', volume: 50, language: 'ko' }
// 완벽하게 보호됨! ✨
개선 효과:
- ✅ 예측 가능한 상태
- ✅ 변경 추적 가능
- ✅ 사이드 이펙트 제거
- ✅ 안전한 협업
깊이 파보기: Object.freeze는 어떻게 작동하나?
내부 메커니즘
const obj = { name: 'Alice' };
// Object.freeze 실행 전
Object.getOwnPropertyDescriptor(obj, 'name');
// {
// value: 'Alice',
// writable: true, ← 변경 가능
// enumerable: true,
// configurable: true ← 삭제/재정의 가능
// }
// Object.freeze 실행
Object.freeze(obj);
// Object.freeze 실행 후
Object.getOwnPropertyDescriptor(obj, 'name');
// {
// value: 'Alice',
// writable: false, ← 변경 불가능!
// enumerable: true,
// configurable: false ← 삭제/재정의 불가능!
// }
시각화:
Normal Object:
┌─────────────────────────┐
│ name: 'Alice' │
│ [writable: ✓] │ ← 자물쇠 없음
│ [configurable: ✓] │
└─────────────────────────┘
After Object.freeze:
┌─────────────────────────┐
│ name: 'Alice' │
│ [writable: ✗] 🔒 │ ← 자물쇠!
│ [configurable: ✗] 🔒 │
└─────────────────────────┘
Strict Mode에서의 차이
// Non-strict mode (느슨한 모드)
const obj = Object.freeze({ x: 1 });
obj.x = 2; // 조용히 무시 (에러 없음)
console.log(obj.x); // 1
// Strict mode (엄격 모드)
'use strict';
const obj2 = Object.freeze({ x: 1 });
obj2.x = 2; // ❌ TypeError: Cannot assign to read only property
권장: 항상 'use strict' 사용!
함정과 주의사항
함정 1: 얕은 동결 (Shallow Freeze)
// ❌ 함정: 중첩된 객체는 보호 안 됨!
const user = Object.freeze({
name: 'Alice',
address: {
city: 'Seoul',
country: 'Korea'
}
});
// 최상위 속성은 보호됨
user.name = 'Bob'; // ✗ 무시됨
console.log(user.name); // 'Alice'
// 하지만 중첩된 객체는 변경 가능!
user.address.city = 'Busan'; // ✓ 변경됨! 😱
console.log(user.address.city); // 'Busan'
시각화:
Object.freeze({
name: 'Alice', 🔒 ← 얼어붙음
address: { 🔒 ← 참조는 얼어붙음
city: 'Seoul', ❌ ← 내부는 안 얼어붙음!
country: 'Korea' ❌ ← 여전히 변경 가능
}
})
해결책: Deep Freeze (깊은 동결)
// ✅ 재귀적으로 모든 것을 동결
function deepFreeze(obj) {
// 1. 최상위 객체 동결
Object.freeze(obj);
// 2. 모든 속성을 순회하며 재귀적으로 동결
Object.getOwnPropertyNames(obj).forEach(prop => {
const value = obj[prop];
// 객체이고 아직 동결되지 않았으면 재귀 호출
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
deepFreeze(value);
}
});
return obj;
}
// 사용
const user = deepFreeze({
name: 'Alice',
address: {
city: 'Seoul',
country: 'Korea'
}
});
user.name = 'Bob'; // ✗ 무시
user.address.city = 'Busan'; // ✗ 무시 (이제 보호됨!)
console.log(user.address.city); // 'Seoul'
함정 2: 배열도 객체!
// ❌ 배열의 함정
const arr = Object.freeze([1, 2, 3]);
arr[0] = 99; // ✗ 무시 (요소 변경 불가)
arr.push(4); // ❌ TypeError (배열 메서드 사용 불가)
arr.pop(); // ❌ TypeError
// 하지만 내부 객체는...
const users = Object.freeze([
{ name: 'Alice' },
{ name: 'Bob' }
]);
users[0].name = 'Charlie'; // ✓ 변경됨! (얕은 동결)
console.log(users[0].name); // 'Charlie' 😱
해결책:
// ✅ 배열과 내부 객체 모두 동결
const users = deepFreeze([
{ name: 'Alice' },
{ name: 'Bob' }
]);
users[0] = { name: 'Charlie' }; // ✗ 무시
users[0].name = 'Charlie'; // ✗ 무시
users.push({ name: 'Dave' }); // ❌ TypeError
console.log(users);
// [{ name: 'Alice' }, { name: 'Bob' }]
// 완벽하게 보호됨!
함정 3: 성능 고려사항
// ❌ 대용량 객체에 deepFreeze는 느릴 수 있음
const hugeObject = {
level1: {
level2: {
level3: {
// ... 수천 개의 중첩 객체
}
}
}
};
// 재귀적으로 모든 객체 동결 → 느림!
deepFreeze(hugeObject); // ⚠️ 성능 이슈
해결책: 선택적 동결
// ✅ 필요한 부분만 동결
const config = Object.freeze({
// 자주 변경되지 않는 부분만 동결
constants: deepFreeze({
API_URL: 'https://api.example.com',
TIMEOUT: 5000
}),
// 자주 변경되는 부분은 동결 안 함
cache: {} // 동결하지 않음
});
실전 패턴
패턴 1: 불변 상태 관리 (React/Redux 스타일)
// ✅ 상태 업데이트 패턴
let state = Object.freeze({
user: { name: 'Alice', age: 25 },
theme: 'light',
notifications: true
});
// 상태 변경 함수
function updateState(updates) {
// 기존 상태와 병합하여 새 객체 생성
state = Object.freeze({
...state,
...updates
});
console.log('State updated:', state);
return state;
}
// 사용
updateState({ theme: 'dark' });
// { user: {...}, theme: 'dark', notifications: true }
// 직접 수정은 무시됨
state.theme = 'light'; // ✗ 무시
console.log(state.theme); // 'dark'
시각화:
Initial State:
┌─────────────────────────────┐
│ state v1 (frozen) │
│ { theme: 'light', ... } │
└─────────────────────────────┘
│
│ updateState({ theme: 'dark' })
↓
┌─────────────────────────────┐
│ state v2 (frozen) │ ← 새 객체!
│ { theme: 'dark', ... } │
└─────────────────────────────┘
│
│ updateState({ notifications: false })
↓
┌─────────────────────────────┐
│ state v3 (frozen) │ ← 또 다른 새 객체!
│ { theme: 'dark', │
│ notifications: false } │
└─────────────────────────────┘
패턴 2: Config 싱글톤
// ✅ 안전한 Config 관리
const Config = (function() {
const defaultConfig = Object.freeze({
DEBUG: false,
API_URL: 'https://api.example.com',
TIMEOUT: 5000
});
let currentConfig = defaultConfig;
return {
get(key) {
return currentConfig[key];
},
set(key, value) {
// 새 객체 생성 (불변성 유지)
currentConfig = Object.freeze({
...currentConfig,
[key]: value
});
},
setMany(updates) {
currentConfig = Object.freeze({
...currentConfig,
...updates
});
},
reset() {
currentConfig = defaultConfig;
},
getAll() {
// 복사본 반환 (직접 수정 방지)
return { ...currentConfig };
}
};
})();
// 사용
console.log(Config.get('DEBUG')); // false
Config.set('DEBUG', true);
console.log(Config.get('DEBUG')); // true
// 직접 접근은 불가능
Config.currentConfig = {}; // ✗ 접근 불가 (private)
패턴 3: 이벤트 데이터 보호
// ✅ 이벤트 리스너에 전달되는 데이터 보호
class EventBus {
constructor() {
this.handlers = {};
}
emit(event, data) {
// 데이터를 동결하여 리스너가 수정 못하게
const frozenData = Object.freeze({ ...data });
if (this.handlers[event]) {
this.handlers[event].forEach(handler => {
try {
handler(frozenData);
} catch (error) {
console.error('Handler error:', error);
}
});
}
}
on(event, handler) {
if (!this.handlers[event]) {
this.handlers[event] = [];
}
this.handlers[event].push(handler);
}
}
// 사용
const bus = new EventBus();
bus.on('user:login', (data) => {
console.log('User logged in:', data.username);
// 데이터 수정 시도 (무시됨)
data.username = 'hacker'; // ✗ 무시
data.token = 'stolen'; // ✗ 무시
});
bus.emit('user:login', { username: 'Alice', token: 'secret123' });
// 원본 데이터 안전하게 보호됨!
Object.freeze vs 다른 방법들
비교표
| 방법 | 변경 방지 | 추가 방지 | 삭제 방지 | 성능 | 사용 시기 |
|---|---|---|---|---|---|
| Object.freeze | ✅ | ✅ | ✅ | 빠름 | 완전한 불변성 필요 |
| Object.seal | ✅ | ✅ | ✅ | 빠름 | 구조 고정, 값 변경 가능 |
| Object.preventExtensions | ❌ | ✅ | ❌ | 빠름 | 추가만 방지 |
| const | ❌ | ❌ | ❌ | 빠름 | 재할당만 방지 |
코드로 비교
// 1. const: 재할당만 방지
const obj1 = { x: 1 };
obj1 = { x: 2 }; // ❌ TypeError (재할당 불가)
obj1.x = 2; // ✓ 속성 변경 가능
obj1.y = 3; // ✓ 추가 가능
delete obj1.x; // ✓ 삭제 가능
// 2. Object.preventExtensions: 추가만 방지
const obj2 = Object.preventExtensions({ x: 1 });
obj2.x = 2; // ✓ 변경 가능
obj2.y = 3; // ✗ 추가 불가
delete obj2.x; // ✓ 삭제 가능
// 3. Object.seal: 구조 고정, 값 변경 가능
const obj3 = Object.seal({ x: 1 });
obj3.x = 2; // ✓ 변경 가능
obj3.y = 3; // ✗ 추가 불가
delete obj3.x; // ✗ 삭제 불가
// 4. Object.freeze: 완전한 불변
const obj4 = Object.freeze({ x: 1 });
obj4.x = 2; // ✗ 변경 불가
obj4.y = 3; // ✗ 추가 불가
delete obj4.x; // ✗ 삭제 불가
시각화:
const:
┌─────────────┐
│ obj ─┐ │ 재할당만 막음
│ ↓ │
│ { x: 1 } │ ← 내용 자유롭게 변경 가능
└─────────────┘
Object.freeze:
┌─────────────┐
│ obj ─┐ │ 재할당 막음
│ ↓ │
│ 🔒{ x: 1 }│ ← 내용도 완전히 잠김!
└─────────────┘
실전 체크리스트
Object.freeze 사용해야 할 때
- 설정 객체를 보호하고 싶을 때
- 상수 데이터 (API URL, 에러 메시지 등)
- 이벤트 데이터를 리스너가 수정 못하게 할 때
- Redux/React 상태 관리
- 함수형 프로그래밍 패턴 적용 시
주의해야 할 상황
- 대용량 객체 (성능 이슈)
- 중첩된 객체 (deepFreeze 필요)
- 자주 변경되는 데이터 (새 객체 생성 비용)
- 서드파티 라이브러리와의 호환성
디버깅 팁
// ✅ 객체가 동결되었는지 확인
const obj = Object.freeze({ x: 1 });
console.log(Object.isFrozen(obj)); // true
// ✅ 속성별로 확인
const descriptor = Object.getOwnPropertyDescriptor(obj, 'x');
console.log(descriptor.writable); // false
console.log(descriptor.configurable); // false
// ✅ Strict mode에서 테스트
'use strict';
try {
obj.x = 2;
} catch (error) {
console.log('Caught:', error.message);
// "Cannot assign to read only property"
}
실전 예제: 테마 시스템
// ✅ 완전한 테마 관리 시스템
const ThemeManager = (function() {
// 테마 정의 (완전히 동결)
const themes = deepFreeze({
light: {
background: '#ffffff',
text: '#000000',
primary: '#007bff'
},
dark: {
background: '#1a1a1a',
text: '#ffffff',
primary: '#4dabf7'
}
});
// 현재 상태
let state = Object.freeze({
currentTheme: 'light',
customColors: {}
});
// Public API
return {
getTheme(name) {
return themes[name]; // 이미 동결된 객체 반환
},
getCurrentTheme() {
return state.currentTheme;
},
setTheme(name) {
if (!themes[name]) {
throw new Error(`Unknown theme: ${name}`);
}
// 새 상태 생성
state = Object.freeze({
...state,
currentTheme: name
});
console.log(`Theme changed to: ${name}`);
return this.getTheme(name);
},
getColors() {
const theme = themes[state.currentTheme];
const custom = state.customColors;
// 병합하여 동결된 객체 반환
return Object.freeze({
...theme,
...custom
});
},
setCustomColor(key, value) {
state = Object.freeze({
...state,
customColors: Object.freeze({
...state.customColors,
[key]: value
})
});
}
};
})();
// 사용
const lightTheme = ThemeManager.getTheme('light');
console.log(lightTheme); // { background: '#ffffff', ... }
// 테마 데이터 수정 시도 (무시됨)
lightTheme.background = '#000000'; // ✗ 무시
console.log(lightTheme.background); // '#ffffff' (변하지 않음)
// 테마 변경
ThemeManager.setTheme('dark');
const colors = ThemeManager.getColors();
console.log(colors); // { background: '#1a1a1a', ... }
// 커스텀 색상 추가
ThemeManager.setCustomColor('accent', '#ff6b6b');
console.log(ThemeManager.getColors());
// { background: '#1a1a1a', ..., accent: '#ff6b6b' }
마무리
핵심 요약
Object.freeze는 무엇인가?
- 객체를 “얼음”처럼 얼려서 변경 불가능하게 만드는 함수
- 속성 변경, 추가, 삭제 모두 차단
왜 사용하나?
- ✅ 예측 가능한 코드
- ✅ 버그 추적 용이
- ✅ 사이드 이펙트 제거
- ✅ 안전한 데이터 공유
주의할 점
- ⚠️ 얕은 동결 (중첩 객체는 별도 처리)
- ⚠️ 성능 고려 (대용량 객체)
- ⚠️ Strict mode 사용 권장
실제로 느낀 변화
Before (Object.freeze 없이):
- "어? 이 값이 왜 바뀌었지?"
- 디버깅에 3시간 소요
- 팀원과 충돌 빈번
- 예측 불가능한 버그
After (Object.freeze 사용):
- "값이 안 바뀌네? 완벽!"
- 버그 즉시 발견
- 안전한 코드 공유
- 자신있는 리팩토링
다음 단계
Object.freeze를 마스터했다면:
- Immer.js: 불변성을 더 쉽게 다루는 라이브러리
- Immutable.js: Facebook의 불변 데이터 구조
- 함수형 프로그래밍: 불변성의 진짜 힘
기억하세요: 불변성은 복잡해 보이지만, 실제로는 코드를 더 간단하고 안전하게 만듭니다!
Happy Freezing! ❄️✨
참고 자료
관련 가이드
외부 자료
인터랙티브 예제
직접 테스트해보세요:
// 브라우저 콘솔에서 실행해보세요!
// 1. 일반 객체
const normal = { count: 0 };
normal.count++;
console.log('Normal:', normal.count); // 1
// 2. 동결된 객체
const frozen = Object.freeze({ count: 0 });
frozen.count++;
console.log('Frozen:', frozen.count); // 0 (변하지 않음!)
// 3. Strict mode에서
'use strict';
try {
frozen.count = 10;
} catch (e) {
console.log('Error caught:', e.message);
}
작성일: 2025-10-20 주제: Object.freeze & Immutability 난이도: Beginner to Intermediate
댓글