자바스크립트 개발 중 복잡한 객체나 배열을 복사해서 다룰 때 원본 데이터가 함께 수정되는 버그를 경험해봤을 것이다.
다만 대부분의 개발자들은 이 문제를 해결하려고 JSON.parse(JSON.stringify(obj)) 같은 편법을 사용하거나, 정확한 원인과 한계를 모른 채 얕은 복사 연산자를 그대로 사용한다.
이번에는 JSON 방식의 치명적인 한계점부터 최신 브라우저 및 Node.js 표준인 structuredClone() 함수의 동작 원리, 그리고 실무 적용법까지 완벽하게 정리해서 소개하겠다.
자바스크립트에서 객체를 복사할 때는 메모리 참조 방식을 반드시 이해해야 한다. Spread 연산자(...)나 Object.assign()은 객체의 가장 겉면(1차원)만 복사하는 얕은 복사(Shallow Copy)를 수행한다. 객체 내부에 또 다른 객체나 배열이 중첩되어 있다면 참조값이 그대로 유지되어 복사본을 수정하는 순간 원본 데이터까지 함께 오염된다.
반면 깊은 복사(Deep Copy)는 중첩된 모든 구조를 새로운 메모리 공간에 독립적으로 복제한다.
| 구분 | 얕은 복사 (Shallow Copy) | 깊은 복사 (Deep Copy) |
|---|---|---|
| 주요 방법 | Spread 연산자(...), Object.assign() | structuredClone(), JSON 방식, Lodash cloneDeep |
| 중첩 객체 참조 | 원본과 참조 공유 (수정 시 원본 오염) | 완전한 독립 메모리 공간 할당 |
| 처리 속도 | 매우 빠름 | 객체 크기와 깊이에 따라 상이 |
| 주요 용도 | 1차원 데이터 복사, 단순 상태 업데이트 | 복잡한 상태 관리, 원본 보존이 필수인 작업 |
과거에는 깊은 복사를 구현하기 위해 크게 두 가지 방법을 주로 사용했다.
첫 번째는 JSON.parse(JSON.stringify(obj)) 방식이다. 별도 라이브러리 없이 빠르게 구현할 수 있지만, Date 객체가 문자열로 바뀌고, Map, Set, undefined, Symbol, RegExp 같은 데이터 타입이 손실된다. 특히 순환 참조(Circular Reference) 구조가 있으면 TypeError가 발생하며 스크립트가 멈춘다.
두 번째는 Lodash 라이브러리의 cloneDeep() 함수다. 모든 예외 상황을 완벽하게 처리하지만, 단 하나의 복사 기능을 위해 외부 라이브러리를 의존성에 추가해야 하므로 번들 사이즈가 커지는 단점이 있다.
이러한 문제를 해결하기 위해 도입된 브라우저 및 Node.js(v10.0.0+, 브라우저 전역) 표준 API가 바로 structuredClone()이다. 웹 워커(Web Worker)에서 데이터를 주고받을 때 사용하는 구조화된 복제 알고리즘(Structured Clone Algorithm)을 기반으로 작동하여, 별도의 패키지 설치 없이 안전하고 빠르게 깊은 복사를 지원한다.
Date 객체와 undefined, Set, 그리고 순환 참조가 포함된 복잡한 데이터를 복사할 때 두 방식이 어떻게 다르게 동작하는지 확인해보자.
✗ 잘못된 코드 (JSON.parse + JSON.stringify 사용)
const original = {
title: "REDINFO",
createdAt: new Date(),
missingData: undefined,
tags: new Set(["javascript", "backend"])
};
// JSON 방식으로 깊은 복사 시도
const copied = JSON.parse(JSON.stringify(original));
console.log(typeof copied.createdAt); // string (Date 타입 유실)
console.log("missingData" in copied); // false (키 자체가 삭제됨)
console.log(copied.tags); // {} (Set 구조가 빈 객체로 변환됨)✓ 올바른 코드 (structuredClone 함수 사용)
const original = {
title: "REDINFO",
createdAt: new Date(),
missingData: undefined,
tags: new Set(["javascript", "backend"])
};
// structuredClone으로 안전한 깊은 복사 수행
const copied = structuredClone(original);
console.log(copied.createdAt instanceof Date); // true (Date 타입 정상 유지)
console.log(copied.missingData); // undefined (키와 값 보존)
console.log(copied.tags instanceof Set); // true (Set 구조 보존)결과 출력값
// JSON 방식 결과:
// { title: 'REDINFO', createdAt: '2024-05-20T10:00:00.000Z', tags: {} }
// structuredClone 방식 결과:
// { title: 'REDINFO', createdAt: 2024-05-20T10:00:00.000Z, missingData: undefined, tags: Set(2) { 'javascript', 'backend' } }
structuredClone()이 매우 강력하지만 모든 자바스크립트 객체를 복사할 수 있는 것은 아니다. 실무 적용 시 다음 예외 상황을 알아두어야 한다.
- 함수(Function) 및 DOM 요소 복사 불가: 객체 내부에 함수나 메서드, HTML DOM 엘리먼트가 포함되어 있으면 DataCloneError 예외가 발생한다.
- 프로토타입 체인 미보존: 클래스 인스턴스를 복사할 경우, 인스턴스의 메서드나 프로토타입 정보는 복사되지 않고 일반 순수 객체(Plain Object)로 변환된다.
- Getter/Setter 비활성화: Getter 속성은 복사하는 시점의 계산된 값(Value)으로 고정되어 일반 속성으로 변환된다.
✗ 잘못된 예시 (함수가 포함된 객체를 structuredClone으로 복사 시도)
const userProfile = {
name: "홍길동",
getRole: function() { return "Admin"; }
};
// DataCloneError: function() {} could not be cloned. 에러 발생!
const clonedProfile = structuredClone(userProfile);✓ 올바른 예시 (함수나 메서드가 있는 경우 얕은 복사 또는 맞춤형 복사 적용)
const userProfile = {
name: "홍길동",
getRole: function() { return "Admin"; },
settings: { theme: "dark", notifications: true }
};
// 데이터 부분만 structuredClone을 적용하고 함수는 직접 유지
const clonedProfile = {
...userProfile,
settings: structuredClone(userProfile.settings)
};
console.log(clonedProfile.getRole()); // Admin (정상 호출)
structuredClone()은 자바스크립트에서 외부 라이브러리 없이 데이터 독립성을 완전하게 확보하는 핵심 API다. 무심코 사용하던 JSON.parse(JSON.stringify()) 편법을 버리는 작은 습관이 모여서 원인을 찾기 힘든 데이터 오염 버그를 예방하고 앱의 전체적인 안정성을 높인다. 이 글의 비교 예제와 주의사항을 참고해 기존 프로젝트의 객체 복사 로직을 structuredClone()으로 교체하면, 훨씬 깔끔하고 견고한 코드 베이스를 얻을 수 있을 것이다.