
목차
JavaScript로 팀 프로젝트를 하다 보면 이런 경험 한 번쯤 있으셨을 거예요. 분명히 건드리면 안 되는 설정값이나 초깃값 객체가, 어디선가 의도치 않게 변경돼서 버그가 발생하는 상황이요.
예를 들어, 버튼 스타일 같은 디자인 토큰을 담은 객체를 여러 곳에서 사용하다가, 한 페이지에서 색상 값을 임시로 수정했더니 다른 페이지 버튼 색까지 전부 바뀌어버리는 경우처럼요.
이런 상황을 방지하려면 단순히 “이 객체 건들지 마세요!”라고 말로 약속하거나 주석으로 남기는 것만으로는 부족합니다. 코드 레벨에서 강제로 값을 고정하는 안전장치가 필요해요. 바로 그럴 때 사용하는 게 Object.freeze()
, Object.seal()
과 TypeScript의 as const
같은 기능들이죠.
이번 글에서는 객체 내부의 값이 변하지 않도록 얼릴 수 있는 방법들을 소개하려고 해요.
왜 객체를 얼려야 할까요?
JavaScript에서 객체는 기본적으로 mutable(변경 가능)해요.
const user = {
lastName: "Park"
};
user.lastName = "Kim"; // 변경 가능
console.log(user.lastName); // "Kim"
그런데 이 특성이 때로는 예기치 않은 문제를 불러오곤 해요.
실무에서 많이 겪는 상황으로 설명드리면
- 공통 설정값을 저장하는 config 객체를 여러 컴포넌트에서 참조했는데, 누군가 한 곳에서 값을 수정해버려서 전체 서비스 동작이 꼬이는 경우
- 전역 상태 관리에서 초깃값으로 사용한 객체를 직접 수정해버려, 불변성을 깨고 예상 못 한 렌더링 문제를 일으키는 경우
- 서버와 통신해서 받은 데이터를 그대로 화면에 보여주기만 할 목적이었는데, 어디선가 데이터 가공 중 원본 데이터를 실수로 수정해버리는 경우
이런 문제들은 한눈에 드러나지 않기 때문에 디버깅하기도 쉽지 않아요. 특히 협업 환경에서 여러 명이 동시에 코드를 만질 때는 더더욱요. 그래서 코드 레벨에서 “이 객체는 절대 건드릴 수 없다”라는 걸 강제해 주는 안전장치가 필요해요. 그래서 JavaScript 언어를 사용할 때는 그 안전장치 도구로 Object.freeze()
와 Object.seal()
를 사용하고, TypeScript 언어를 사용할 때는 as const
를 추가로 활용할 수 있어요.
Object.freeze()
freeze는 말 그대로 객체를 얼려버리는 것. 값 수정뿐만 아니라 속성 추가 및 삭제까지 완전히 차단해요.
const user = Object.freeze({
firstName: "Jeong Hwan",
lastName: "Park"
});
user.lastName = "Kim"; // 수정 불가능
console.log(user.lastName); // "Park"
delete user.firstName; // 삭제 불가능
console.log(user.firstName); // "Jeong Hwan"
user.job = "Frontend Developer"; // 추가 불가능
console.log(user.job); // undefined
그런데 Object.freeze()
사용 시 주의해야 할 점이 하나 있어요. 바로 freeze는 얕은(shallow) 동결이에요.
즉, 중첩된 객체까지 막아주지는 않아요.
const user = Object.freeze({
...
address: {
city: "Seoul",
postalCode: "12345"
}
});
user.address.city = "Paju"; // 수정 가능
console.log(user.address.city); // "Paju"
그래서 등장한 것이 deepFreeze
입니다.
Object.freeze()
가 기본적으로 얕은 동결만 제공하기 때문에, 중첩된 객체까지 완전히 고정하려면 직접 deepFreeze
함수를 직접 구현해서 사용해야 해요.
MDN에서도 이러한 아래와 같은 방식으로 deepFreeze
구현 방법을 소개하고 있어요.
function deepFreeze(object) {
const propNames = Reflect.ownKeys(object);
for (const name of propNames) {
const value = object[name];
if ((value && typeof value === "object") || typeof value === "function") {
deepFreeze(value);
}
}
return Object.freeze(object);
}
위 deepFreeze
함수를 구현하여 사용하면 아래 예시처럼 중첩된 객체의 값도 변경할 수 없게 됩니다.
const user = deepFreeze({
...
address: {
city: "Seoul",
postalCode: "12345"
}
});
user.address.city = "Paju"; // 수정 불가능
console.log(user.address.city); // "Seoul"
Object.seal()
seal은 “밀봉하다”라는 뜻을 갖고 있지만 Object.seal()
함수는 Object.freeze()
함수보다는 조금 느슨하게 동작해요. 속성 추가와 삭제는 Object.freeze()
와 동일하게 막고 있지만, 기존 값 수정은 가능해요.
const user = Object.seal({
firstName: "Jeong Hwan",
lastName: "Park"
});
user.lastName = "Kim"; // 수정 가능
console.log(user.lastName); // "Kim"
delete user.firstName; // 삭제 불가능
console.log(user.firstName); // "Jeong Hwan"
user.job = "Frontend Developer"; // 추가 불가능
console.log(user.job); // undefined
그래서 Object.seal()
함수는 객체의 속성 추가 및 삭제는 막고, 내부 값은 상황에 따라 바꿀 수 있도록 하고 싶을 때에 주로 사용해요.
as const
TypeScript 사용 시 객체를 상수화 하고 싶다면 as const
를 활용할 수 있어요.
const user = {
firstName: "Jeong Hwan",
lastName: "Park"
} as const;
user.lastName = "Kim"; // Type Error 발생
delete user.firstName; // Type Error 발생
user.job = "Frontend Developer"; // Type Error 발생
객체에 as const
를 선언하면 값도 literal 타입으로 고정되고, 속성도 readonly로 바뀝어요. 이렇게 선언되면 컴파일 타임에서 타입 보장을 받을 수 있어서, 런타임에서 freeze하는 것과는 또 다른 장점이 있어요. 즉 코드 작성 시 발생하는 Type Error를 통해 사전에 객체 변경을 방지할 수 있어요.
다만 주의해야할 점이 있어요. as const
는 정확하게는 Object.freeze()
나 deepFreeze
와 같은 동작을 하는 것은 아니에요. as const
는 타입 레벨에서만 readonly를 걸어주는 것이고, 런타임에서는 실제로 값이 변경 가능해요.
const user = {
firstName: "Jeong Hwan",
lastName: "Park"
} as const;
user.lastName = "Kim"; // Type Error는 발생하지만 JavaScript 런타임에서는 실제로 값이 수정됩니다.
console.log(user.lastName); // "Kim"
마무리
이제 지금까지 살펴본 객체 불변성 도구들의 특징과 적절한 사용 시점을 요약해 볼게요.
Object.freeze()
: 얕은 동결- 설정값이나 공통 상수처럼, 대부분의 경우 변경되면 안 되는 객체를 런타임에서도 안전하게 유지하고 싶을 때
Object.seal()
: 구조만 고정, 값 수정 가능- 객체의 속성에 대한 추가 및 삭제는 막고, 내부 값은 상황에 따라 바꿀 수 있도록 하고 싶을 때
- 예시: 폼 데이터 같은 임시 상태 관리
deepFreeze()
: 깊은 동결 (직접 구현 필요)- 중첩 객체까지 포함해서 완전히 읽기 전용으로 만들어야 하는 상황
- 예시: 복잡한 config 객체, 국제화 리소스
as const
: 타입 단에서 상수화, readonly 적용- TypeScript에서 컴파일 타임에 readonly를 강제하고 싶을 때
위와 같이 여러 방법으로 객체 변경을 막을 수 있지만, 모든 객체를 무조건 freeze 하는 것은 최선책은 아니에요. 각 상황에 맞게 객체 동결 여부를 판단하고, 필요하다면 얕은 동결과 깊은 동결 중 어떤 것이 적합한지 고려해여 적적한 도구를 선택해야 해요. 즉 프로젝트의 요구사항에 따라 적절한 도구를 선택하여 코드를 작성하는 것이 가장 중요해요.