요즘IT
위시켓
새로 나온
인기요즘 작가들컬렉션
물어봐
새로 나온
인기
요즘 작가들
컬렉션
물어봐
개발
AI
IT서비스
기획
디자인
비즈니스
프로덕트
커리어
트렌드
스타트업
서비스 전체보기
위시켓요즘IT
고객 문의
02-6925-4867
10:00-18:00주말·공휴일 제외
yozm_help@wishket.com
요즘IT
요즘IT 소개작가 지원
기타 문의
콘텐츠 제안하기광고 상품 보기
요즘IT 슬랙봇크롬 확장 프로그램
이용약관
개인정보 처리방침
청소년보호정책
㈜위시켓
대표이사 : 박우범
서울특별시 강남구 테헤란로 211 3층 ㈜위시켓
사업자등록번호 : 209-81-57303
통신판매업신고 : 제2018-서울강남-02337 호
직업정보제공사업 신고번호 : J1200020180019
제호 : 요즘IT
발행인 : 박우범
편집인 : 노희선
청소년보호책임자 : 박우범
인터넷신문등록번호 : 서울,아54129
등록일 : 2022년 01월 23일
발행일 : 2021년 01월 10일
© 2013 Wishket Corp.
로그인
요즘IT 소개
콘텐츠 제안하기
광고 상품 보기
개발

헷갈리는 자바스크립트 객체 복사: 얕은 복사 vs 깊은 복사 쉽게 정리하기

효빈
10분
2시간 전
129
에디터가 직접 고른 실무 인사이트 매주 목요일에 만나요.
newsletter_profile0명 뉴스레터 구독 중

객체를 복사할 땐 주의하세요!

 

자바스크립트를 사용하다 보면 객체를 복사했는데 원본까지 바뀌어버리는 황당한 경험, 한 번쯤 해보셨을 겁니다. 처음에는 단순한 변수 복사라고 생각했지만, 실제로는 원본 객체와 복사본이 같은 메모리를 공유하면서 예기치 않은 결과가 발생하게 됩니다. 이러한 문제는 객체가 참조 타입이라는 특징에서 비롯되며, ‘얕은 복사(Shallow Copy)’와 ‘깊은 복사(Deep Copy)’의 차이를 이해하지 못하면 실무에서 매우 위험한 버그로 이어질 수 있습니다.

 

이번 글에서는 자바스크립트 객체의 참조 구조를 시작으로, 얕은 복사와 깊은 복사의 개념과 방법을 예제와 함께 정리하고, 실무에서의 선택 기준까지 구체적으로 살펴보겠습니다.

 

객체 복사, 왜 중요할까?

객체 복사의 개념은 단순히 변수에 값을 복제하는 것이 아니라, 메모리와 데이터 구조를 분리하는 데 중요한 역할을 합니다. 특히 상태 관리, 컴포넌트 간의 props 전달, 외부 API 응답 처리 등 다양한 상황에서 객체를 안전하게 다루려면, 복사의 개념을 정확히 이해하고 있어야 합니다.

 

1) 자바스크립트의 객체는 참조형

자바스크립트에서 객체는 원시값과 달리 참조 값으로 저장됩니다. 이는 객체를 변수에 할당하거나 다른 변수에 복사할 때, 값 그 자체가 아닌 메모리 주소가 복사된다는 의미입니다. 이 때문에 한 변수를 수정하면 다른 변수에도 영향을 미치는 것입니다.

 

const user = { name: "홍길동" };
const copy = user;

copy.name = "김효빈";

console.log(user.name); // "김효빈"

 

<출처: 작가>

 

위 코드처럼 user와 copy는 서로 다른 변수이지만 같은 객체를 가리키고 있기 때문에 copy에서 값을 변경하면 user에도 그대로 반영됩니다.

 

2) 복사를 제대로 하지 않으면 버그로 이어진다

실제 프로젝트에서는 상태 관리나 폼 데이터 처리, 컴포넌트 간 데이터 전달 시 객체를 복사해야 할 일이 많습니다. 이때 복사 방식이 적절하지 않으면 원하지 않는 곳에서 데이터가 바뀌어 디버깅하기 어려운 버그가 발생합니다. 복사란 단순한 ‘복제’가 아니라, 객체 간의 “의존성 해제”라는 점에서 매우 중요합니다.

 

 

‘얕은 복사’ 알아보기

얕은 복사는 객체를 복사할 때 가장 흔하게 사용되며, 동시에 초보 개발자가 자주 실수하는 지점이기도 합니다. 이 방식은 객체의 최상위 속성만 복사하고, 그 내부에 중첩된 객체나 배열은 기존 객체와 참조를 공유하게 됩니다. 따라서 복사본을 수정했을 때 의도하지 않게 원본까지 바뀌는 경우가 발생할 수 있어 주의가 필요합니다.

 

1) 얕은 복사의 의미

얕은 복사는 말 그대로 객체의 ‘얕은’ 층, 즉 1단계 수준의 속성만 복사합니다. 객체의 구조가 평면적일 경우에는 문제가 없지만, 객체 안에 또 다른 객체나 배열이 들어 있는 구조에서는 그 내부까지는 복사되지 않고 참조만 이어지게 됩니다.

 

쉽게 말하면 복사본이 원본 객체의 ‘껍데기’만 따로 만들어 쓰는 셈이고, 속 안에 들어 있는 재료(중첩 객체)는 원본과 공유하고 있는 것입니다. 따라서 복사본을 수정하면, 원하지 않게 원본 데이터까지 함께 변경될 수 있습니다.

 

2) spread 연산자

ES6에서 도입된 전개 연산자(...)는 얕은 복사를 수행할 때 가장 자주 쓰이는 문법입니다. 간결하고 직관적이기 때문에 많은 개발자들이 즐겨 사용합니다.

 

const user = { name: "김효빈", address: { city: "서울" } };
const copy = { ...user };

copy.name = "홍길동";
copy.address.city = "부산";

console.log(user.name); // "김효빈"
console.log(user.address.city); // "부산" ← 원본도 바뀜

 

이 예제에서 name 속성은 문자열(원시값)이기 때문에 전혀 문제없이 복사됩니다. 하지만 address는 객체이며, 복사 과정에서 내부 city 속성까지 새롭게 복제되는 것이 아니라 기존 address 객체를 그대로 참조하게 됩니다. 결과적으로 copy.address.city를 변경하면 user.address.city도 함께 바뀌는 현상이 나타납니다.

 

이러한 전개 연산자는 한눈에 보기 쉬운 문법 덕분에 많이 사용되지만, 중첩 객체를 다룰 때는 언제나 의도한 동작인지 점검하는 습관이 필요합니다.

 

3) Object.assign()

또 다른 얕은 복사 방식으로는 Object.assign() 메서드가 있습니다. 이 메서드는 기존 객체를 대상으로 새로운 객체에 속성을 복사해 주는 역할을 합니다. 사용 방식은 아래와 같습니다.

 

const copy2 = Object.assign({}, user);

 

위 코드는 빈 객체에 user의 속성을 복사합니다. 동작 원리는 spread 연산자와 거의 동일하며, 최상위 속성만 복사되고 중첩 객체는 참조로 남습니다.

 

예전 브라우저에서도 동작한다는 장점 때문에, Babel 트랜스파일을 사용하지 않는 환경이라면 Object.assign()이 더 유용할 수 있습니다. 하지만 복사 범위가 얕다는 점은 똑같이 주의해야 할 포인트입니다.

 

4) 얕은 복사의 함정

얕은 복사는 간단한 작업에는 매우 유용하지만, 객체가 조금이라도 복잡해지면 쉽게 오류를 유발할 수 있습니다. 특히 프론트엔드에서 상태 관리를 구현할 때 문제가 발생할 수 있습니다. 예를 들어, Redux나 React에서 상태 객체를 얕은 복사한 뒤 내부 값을 수정하면, 실제로는 원본 상태까지 바뀌게 되어 불변성(immutability) 원칙이 깨지게 됩니다. 이로 인해 상태 변경을 감지하지 못하거나, 예상치 못한 렌더링 문제가 발생할 수 있습니다.

 

또한 중첩된 데이터 구조를 반복적으로 복사할 경우, 어느 단계가 공유되고 있는지 파악하기 어려워 디버깅에 많은 시간이 소요되기도 합니다. 이런 경우에는 깊은 복사를 고려하거나, 구조적으로 얕은 복사만으로 충분한 구조로 리팩토링하는 것이 현명할 수 있습니다.

 

 

‘깊은 복사’ 알아보기

객체 복사에서 진짜 ‘독립적인’ 데이터를 만들고 싶다면 얕은 복사로는 부족합니다. 중첩된 객체나 배열까지 모두 새로운 메모리 공간에 복사해서, 원본과 전혀 연결되지 않은 완전한 복사본을 만들 수 있어야 합니다. 이처럼 모든 하위 구조까지 복제하는 방식이 바로 깊은 복사(deep copy)입니다.

 

1) 깊은 복사의 의미

깊은 복사는 단순히 1차원적인 속성만 복사하는 것이 아니라, 객체 내부에 또 다른 객체나 배열이 있을 경우에도 재귀적으로 복사해 주는 방식입니다. 복사본의 모든 프로퍼티는 원본과 전혀 다른 메모리 주소를 가지므로, 어떤 값을 수정하더라도 서로 영향을 주지 않습니다.

 

예를 들어, 아래와 같이 중첩된 객체를 복사한 경우를 봅시다.

 

const user = {
  name: "효빈",
  settings: {
    theme: "dark",
  }
};

const copied = JSON.parse(JSON.stringify(user));
copied.settings.theme = "light";

console.log(user.settings.theme); // "dark"

 

<출처: 작가>

 

위 코드에서 복사본을 수정해도 원본은 그대로 유지되며, 이는 얕은 복사로는 불가능한 동작입니다.

 

2) JSON 방식

가장 간단하게 깊은 복사를 할 수 있는 방법은 JSON.stringify()로 직렬화한 뒤, JSON.parse()로 다시 객체화하는 방식입니다.

 

const deepCopy = JSON.parse(JSON.stringify(user));

 

이 방식은 빠르고 간편하다는 장점이 있지만, undefined, Symbol, function, Date, Map, Set 등은 직렬화 대상이 아니므로 손실되며, 순환 참조(Circular Reference)가 있는 객체는 JSON.stringify()에서 오류를 발생시킨다는 등의 제약 사항이 있습니다.

 

실무에서는 비교적 단순한 데이터 구조(예: 백엔드 API 응답 객체 등)에 한해 안전하게 사용할 수 있지만, 모든 상황에서 쓸 수는 없습니다.

 

3) structuredClone()

structuredClone()은 최신 브라우저(Chrome, Firefox, Edge 등)에서 지원하는 내장 함수로, 거의 모든 객체 타입을 깊은 복사할 수 있는 표준적인 방법입니다. 순환 참조도 문제없이 처리하며, 대부분의 내장 객체(Date, RegExp, Blob 등)도 안정적으로 복제합니다.

 

const deepCopy2 = structuredClone(user);

 

단점은 지원 범위입니다. IE는 물론, 오래된 브라우저에서는 사용할 수 없으며, Node.js 에서는 17버전 이후에 정식 지원됩니다.

 

4) 직접 구현하는 재귀 함수

가장 확실하고 강력한 방법은 직접 재귀적으로 구현하는 것입니다. 각 프로퍼티를 순회하면서 객체나 배열인 경우, 또 한 번 객체를 만들어 복사해 주는 방식입니다.

 

function deepClone(obj) {
  if (obj === null || typeof obj !== "object") return obj;
  if (Array.isArray(obj)) return obj.map(deepClone);

  const cloned = {};
  for (const key in obj) {
    cloned[key] = deepClone(obj[key]);
  }
  return cloned;
}

 

이 함수는 원시값은 그대로 반환하고, 객체나 배열은 새로 만들어 재귀적으로 복사합니다. 물론 완전한 범용 복사를 위해서는 Map, Set, Date 등의 타입에 대한 별도 처리도 필요합니다.

 

따라서 이 방식은 강력하지만 버그 발생 가능성도 높기 때문에, 직접 구현하기보다는 신뢰할 수 있는 라이브러리를 사용하는 것이 좋습니다.

 

<출처: 작가>

 

 

실무에서는 어떻게 선택할까?

객체를 복사할 때 가장 중요한 판단 기준은 ‘이 복사본이 원본과 얼마나 독립되어야 하느냐’입니다. 얕은 복사는 단순하고 빠르지만, 중첩된 객체까지 완전히 분리해 주지 않기 때문에 실수로 원본이 변조되는 일이 자주 생깁니다. 반면, 깊은 복사는 완전한 독립을 보장하지만, 무조건적으로 쓴다고 해서 효율적인 것도 아닙니다. 실무에서는 코드의 성격과 상황에 따라 이 둘을 구분해서 사용하는 것이 핵심입니다.

 

1) 얕은 복사로 충분한 경우

객체의 구조가 단순하고, 중첩된 속성을 수정하지 않으며, 복사한 객체를 한정된 범위 내에서만 사용할 때는 얕은 복사로 충분합니다. 예를 들어, props로 전달할 단순한 옵션 객체를 수정하거나, API 요청을 만들기 위해 일부 속성만 가공할 때는 spread 연산자나 Object.assign()으로도 충분히 안전하게 처리할 수 있습니다.

 

예를 들어, 아래처럼 전개 연산자로 새로운 객체를 만든다고 할 때, 중첩된 속성이 없다면 완전히 독립된 객체처럼 동작합니다.

 

const config = { mode: "dark", theme: "blue" };
const newConfig = { ...config, theme: "red" };

 

이 경우에는 newConfig를 수정해도 config에는 아무런 영향을 주지 않기 때문에 얕은 복사로도 충분히 안전합니다. 또한 함수 내부에서 일시적으로 사용하는 객체나, 변경의 여지가 없는 불변 객체에도 얕은 복사를 활용할 수 있습니다.

 

2) 깊은 복사가 필요한 경우

반대로 객체 안에 중첩된 객체나 배열이 포함되어 있고, 그 내부 속성들을 변경해야 하는 경우에는 반드시 깊은 복사를 해야 합니다. 얕은 복사로는 최상위 객체만 새로 만들어질 뿐, 내부 중첩 객체는 여전히 원본과 참조를 공유하기 때문에 예상치 못한 버그가 발생할 수 있습니다. 예를 들어, 아래와 같은 사용자 객체가 있다고 가정해 볼게요.

 

const user = {
  name: "효빈",
  preferences: {
    theme: "light",
    language: "ko"
  }
};

const shallowCopy = { ...user };
shallowCopy.preferences.theme = "dark";

console.log(user.preferences.theme); // "dark" (원본도 바뀜)

 

이처럼 얕은 복사로는 내부 preferences 객체까지는 복사되지 않기 때문에, shallowCopy를 수정하면 user도 함께 변경됩니다. 이런 문제는 특히 상태 관리에서 치명적일 수 있습니다. 예를 들어, 리액트(React)에서 이전 상태와 새로운 상태를 비교해 변경 여부를 판단할 때, 참조가 같으면 변경되지 않은 것으로 간주하므로 올바른 렌더링이 되지 않을 수도 있습니다.

 

또한 서버에서 받은 원본 데이터를 보호하면서 조작하거나, 이전 상태로 되돌릴 수 있는 undo 기능을 구현할 때도 깊은 복사는 필수입니다. 이때는 structuredClone()이나 커스텀 재귀 함수, 혹은 신뢰할 수 있는 라이브러리를 활용해, 깊은 복사를 적용해야만 데이터의 정합성과 예측 가능성을 확보할 수 있습니다.

 

 

마치며

객체를 복사한다는 건 단순히 값을 복제하는 일이 아니라, 메모리 구조를 분리하여 서로 영향을 주지 않도록 만드는 중요한 작업입니다. 얕은 복사와 깊은 복사의 차이를 정확히 이해하고, 상황에 맞는 복사 방식을 선택해야 버그 없는 안정적인 코드를 작성할 수 있습니다.

 

자바스크립트 최신 API인 structuredClone()은 많은 불편을 해결해 주고 있지만, 브라우저 호환성에 따라 여전히 JSON 방식이나 직접 구현한 재귀 함수가 필요한 경우도 있습니다. 이처럼 복사 방식은 상태 관리, API 응답 가공, 폼 처리 등 거의 모든 프론트엔드 개발에서 등장하는 만큼, 이번 글을 통해 개념을 탄탄히 잡고, 실무에 적용해 보시면 좋겠습니다.

 

©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.