변수/함수의 실행 메커니즘, 그리고 TDZ란?
이번 글에서는 자바스크립트에서 const가 가지는 불변성의 한계와, 얕은 동결(shallow freeze)과 깊은 동결(deep freeze)을 활용한 객체 불변성 관리 방법을 심층적으로 다뤄보겠습니다.
저는 최근 You Don’t Know JS 책을 통해 const가 단순히 변수를 상수로 만들어주는 것이 아니라, 객체나 배열과 결합될 때 의외의 동작을 보일 수 있다는 점을 발견했습니다.
리액트에서 상태(state)를 관리할 때, 객체나 배열을 불변하게 다루는 것은 리렌더링 효율과 버그 방지에 매우 중요합니다. 이번 글에서는 이러한 불변성 관리의 메커니즘과 실무 적용까지 깊이 살펴보겠습니다.
1. const와 불변성
const num = 10;
num = 20; // TypeError: Assignment to constant variable
-
기본 자료형(primitive)은
const를 선언하면 완전히 값이 고정됨. -
num = 20처럼 재할당 시 즉시 오류 발생.
하지만 객체나 배열과 결합하면 상황이 달라집니다.
const person = { name: "Alice", age: 25 };
person.age = 26; // 가능
person = { name: "Bob" }; // TypeError
-
const는 변수 자체의 재할당(reassignment)만 금지 -
객체 내부 프로퍼티는 여전히 변경 가능
-
즉, 객체의 주소값(reference)는 고정, 내용(content)은 mutable
결론
const ≠ 객체 내용 불변. 객체 내부를 안전하게 보호하려면 추가 처리가 필요.
2. 얕은 동결(Shallow Freeze)
자바스크립트는 Object.freeze() 메서드를 통해 객체를 얕게 동결(shallow freeze) 할 수 있습니다.
const obj = Object.freeze({ name: "Alice", age: 25 });
obj.age = 26; // 무시됨, strict mode: TypeError
console.log(obj.age); // 25
-
얕은 동결: 최상위 프로퍼티만 동결
-
객체 내부에 다른 객체가 있으면 그 내부는 여전히 변경 가능
const obj = Object.freeze({ name: "Alice", address: { city: "Seoul" } });
obj.address.city = "Busan"; // 가능
console.log(obj.address.city); // "Busan"
-
최상위 obj는 재할당 불가, 최상위 프로퍼티 수정 불가
-
중첩된 객체는 보호되지 않음 → 불변성 완전 확보 X
결론
-
얕은 동결은 얕은 구조(Flat object)에서는 충분
-
상태(state)가 중첩 구조일 때는 깊은 동결 필요
3. 깊은 동결(Deep Freeze)
깊은 동결(deep freeze)은 객체 내부의 모든 중첩 객체까지 재귀적으로 동결하는 방법입니다.
function deepFreeze(obj) {
Object.freeze(obj); // 현재 객체 동결
Object.getOwnPropertyNames(obj).forEach((prop) => {
if (
obj[prop] !== null &&
(typeof obj[prop] === "object" || typeof obj[prop] === "function") &&
!Object.isFrozen(obj[prop])
) {
deepFreeze(obj[prop]); // 재귀 동결
}
});
return obj;
}
const person = deepFreeze({
name: "Alice",
address: { city: "Seoul", zip: 12345 },
});
person.address.city = "Busan"; // 무시됨
console.log(person.address.city); // "Seoul"
-
재귀적으로 내부 객체까지 동결
-
중첩 구조에서도 불변성 유지 가능
-
단점: 객체가 깊거나 크면 성능 비용 발생
3-1. 라이브러리 활용
-
deep-freeze 같은 라이브러리를 사용하면 코드 간결
-
중첩 객체까지 쉽게 동결 가능
import deepFreeze from "deep-freeze";
const state = deepFreeze({
user: { name: "Alice", age: 25 },
todos: [{ id: 1, title: "Study JS" }],
});
3-2. Immer: 더 편리한 불변성 관리
Immer는 깊은 동결 없이도 불변성을 유지하면서 직접 수정하는 것처럼 작성할 수 있는 라이브러리입니다. [https://immerjs.github.io/immer/]
import produce from "immer";
const state = {
user: { name: "Alice", age: 25 },
todos: [{ id: 1, title: "Study JS" }],
};
const nextState = produce(state, (draft) => {
draft.user.age = 26;
draft.todos[0].title = "Practice JS";
});
console.log(state.user.age); // 25
console.log(nextState.user.age); // 26
-
원본 객체 state는 그대로 유지
-
중첩 구조도 편리하게 수정 가능
-
React 상태 관리(Redux, Recoil 등)와 매우 잘 맞음
결론
-
깊은 동결 없이도 안전하게 불변성 보장
-
코드 가독성 ↑, 개발 효율 ↑
4. const + 동결 = 안전한 상태 관리
| 방법 | 특징 | 장점 | 단점 |
|---|---|---|---|
const |
변수 재할당 금지 | 단순, 기본 자료형에 안전 | 객체 내용 mutable |
| 얕은 동결 (Object.freeze) | 최상위 프로퍼티만 동결 | 간단, 성능 부담 적음 | 중첩 객체 보호 X |
| 깊은 동결 (Deep Freeze) | 재귀적 동결, 중첩 객체까지 보호 | 완전 불변성 보장 | 성능 비용, 코드 길어짐 |
| Immer | Proxy 기반, 직접 수정 코드처럼 작성 가능 | 편리, 가독성 좋음, 깊은 구조도 안전 | 외부 라이브러리 필요 |
5. 실무적 관점: React와 상태 불변성
-
React에서 상태를 직접 수정하면 리렌더링이 제대로 일어나지 않음
-
불변성을 유지해야 컴포넌트가 상태 변화 감지 가능
-
const + shallow/deep freeze를 이해하면 불변성 기반 최적화 가능
const [user, setUser] = useState(deepFreeze({ name: "Alice", age: 25 }));
// 잘못된 접근
user.age = 26; // 동결로 인해 반영 X
// 올바른 업데이트
setUser({ ...user, age: 26 }); // 새로운 객체 생성
-
TDZ, const, freeze를 이해하면 버그와 성능 문제 예방
-
특히 중첩 상태일수록 깊은 동결 + 새 객체 생성 패턴 필수
6. Oneul Code를 정리하며…
오늘 배운 핵심은 자바스크립트에서 객체 불변성을 관리하는 다양한 방법과 한계입니다.
const
-
변수 재할당 불가
-
객체 내부 프로퍼티는 여전히 변경 가능 → 참조 불변만 보장
- 얕은 동결 (Shallow Freeze)
-
Object.freeze로 최상위 프로퍼티 보호
-
중첩 객체는 여전히 mutable
- 깊은 동결 (Deep Freeze)
-
재귀적으로 모든 중첩 객체 동결
-
중첩 구조에서도 불변성 유지
-
성능 비용 고려 필요
- 실무적 관점
-
React 상태 관리에서 필수
-
TDZ, const와 마찬가지로 안전장치 역할
-
불변성 지키면 예기치 못한 버그 예방 및 렌더링 최적화 가능
Oneul Code는 오늘 배운 내용을 기록하며,
여러분도 직접 브라우저 콘솔에서 Object.freeze()를 활용해 얕은 동결과 깊은 동결의 차이를 직접 실험해보시길 권장합니다. 이처럼 자바스크립트의 내부 실행 방식과 불변성 관리 기법을 이해하는 것이 곧 복잡한 프론트엔드 환경에서 능숙하게 코드를 다루는 개발자로 성장하는 지름길이 될 것입니다.