현록

JavaScript 얕은 복사와 깊은 복사 차이

JavaScript의 객체를 다른 변수에 대입한다고 복사본이 생기지는 않는다.
얕은 복사는 최상위 구조만 새로 만들고 중첩 객체의 참조는 공유한다.
깊은 복사는 중첩된 값까지 독립적인 구조로 만든다.
어떤 방식이 필요한지는 중첩 객체를 공유해도 되는지에 따라 달라진다.

대입과 복사의 차이

객체를 대입하면 새 객체가 아니라 기존 객체를 가리키는 별칭이 생긴다.
두 변수는 같은 객체를 참조하므로 어느 쪽에서 속성을 바꾸든 동일한 결과를 본다.

const original = {
  profile: { name: 'Kim' },
}

const alias = original

alias.profile.name = 'Lee'

console.log(original.profile.name) // 'Lee'
console.log(alias === original) // true

복사 여부를 확인할 때는 최상위 객체와 중첩 객체의 일치 비교를 따로 보는 편이 좋다.

얕은 복사의 참조 구조

객체 spread와 Object.assign()은 새 최상위 객체를 만든다.
배열 spread, slice(), concat(), Array.from()도 새 배열을 만들지만 중첩 객체는 공유한다.

const original = {
  active: true,
  profile: { name: 'Kim' },
}

const copy = { ...original }

copy.active = false
copy.profile.name = 'Lee'

console.log(original.active) // true
console.log(original.profile.name) // 'Lee'
console.log(copy !== original) // true
console.log(copy.profile === original.profile) // true

최상위 속성을 재할당하는 작업은 원본에 영향을 주지 않는다.
공유된 중첩 객체의 속성을 바꾸면 원본에도 변경이 보인다.

깊은 복사와 structuredClone

structuredClone()은 구조화 복제 알고리즘으로 값의 깊은 복사본을 만든다.
일반 객체와 배열 외에도 Date, Map, Set, 순환 참조 같은 여러 값을 처리할 수 있다.

const original = {
  settings: { theme: 'dark' },
  visitedAt: new Date('2026-07-23T00:00:00Z'),
  tags: new Set(['javascript']),
}

original.self = original

const copy = structuredClone(original)

console.log(copy.settings === original.settings) // false
console.log(copy.visitedAt instanceof Date) // true
console.log(copy.tags instanceof Set) // true
console.log(copy.self === copy) // true

복사본의 중첩 객체를 변경해도 원본의 동일한 중첩 객체에는 영향을 주지 않는다.
그러나 structuredClone()이 모든 JavaScript 값의 의미를 그대로 재현하는 범용 복제 함수는 아니다.

JSON 복사 방식의 한계

JSON.stringify() 결과를 JSON.parse()하는 패턴도 중첩된 일반 객체의 참조를 분리할 수 있다.
하지만 이 방식은 JavaScript 객체 복사가 아니라 JSON으로 직렬화했다가 다시 읽는 과정이다.

const original = {
  createdAt: new Date('2026-07-23T00:00:00Z'),
  tags: new Set(['javascript']),
  note: undefined,
}

const copy = JSON.parse(JSON.stringify(original))

console.log(copy)
// { createdAt: '2026-07-23T00:00:00.000Z', tags: {} }

Date는 문자열로 바뀌고 SetMap의 항목은 기본 설정에서 보존되지 않는다.
객체의 undefined, 함수, Symbol 값은 생략되며 BigInt와 순환 참조는 오류를 발생시킨다.
JSON으로 표현할 수 있는 데이터라는 제약이 확실할 때만 사용해야 한다.

structuredClone의 제약

함수와 DOM 노드는 구조화 복제 대상이 아니며 복제를 시도하면 DataCloneError가 발생한다.
사용자 정의 클래스의 프로토타입 체인도 복제되지 않는다.

속성 설명자와 getter, setter, 클래스 private 요소 같은 메타데이터도 유지되지 않는다.
RegExp는 복제할 수 있지만 lastIndex 속성은 보존되지 않는다.
복제할 값에 이런 요소가 있다면 실행 전에 지원 범위를 확인해야 한다.

복사 방식 선택 기준

최상위 속성만 다르게 만들거나 중첩 객체를 의도적으로 공유할 때는 spread와 Object.assign()으로 충분하다.
지원되는 값의 전체 구조를 독립시켜야 할 때는 structuredClone()을 우선 검토한다.

JSON 왕복은 입력이 JSON 호환 데이터이고 직렬화에 따른 변환을 의도했을 때만 사용한다.
깊은 복사가 필요한 범위를 먼저 정하고 가능하면 변경할 가지만 새로 만드는 편이 의도를 읽기 쉽다.

map()이 새 배열을 반환해도 요소 객체는 공유될 수 있다.
배열 메서드의 원본 변경 기준은 JavaScript map과 forEach 차이에서 함께 확인할 수 있다.

참고 자료

관련 포스트
TypeScript any와 unknown 차이 thumbnail
TypeScript any와 unknown 차이
TypeScript any와 unknown의 차이를 타입 검사, 할당, 타입 좁히기, 외부 데이터 경계, 컴파일러 설정으로 정리합니다.
JavaScript find와 filter 차이 thumbnail
JavaScript find와 filter 차이
JavaScript find와 filter의 차이를 반환값, 순회 종료, 객체 참조, 일치 항목이 없을 때의 처리 기준으로 정리합니다.
JavaScript null과 undefined 차이 thumbnail
JavaScript null과 undefined 차이
JavaScript null과 undefined의 차이를 값이 생기는 경로, 비교, nullish 처리, JSON 직렬화, API 선택 기준으로 정리합니다.
TypeScript type과 interface 차이 thumbnail
TypeScript type과 interface 차이
TypeScript의 type과 interface 차이를 객체 모델링, 유니온, 선언 병합, extends와 교차 타입의 충돌 처리, 실무 선택 기준으로 나누어 정리합니다.
JavaScript ==와 === 차이 thumbnail
JavaScript ==와 === 차이
JavaScript ==와 ===의 차이를 암시적 타입 변환, null과 undefined, boolean과 문자열, 객체 참조, NaN, 비교 연산자 선택 기준으로 정리합니다.
marked.js 사용법과 기본 설정 thumbnail
marked.js 사용법과 기본 설정
marked.js 사용법을 초보자 기준으로 정리합니다. marked.parse와 parseInline, gfm과 breaks 옵션, 브라우저 출력, HTML sanitize 주의점까지 함께 봅니다.
JavaScript var let const 차이 thumbnail
JavaScript var let const 차이
JavaScript var, let, const 차이를 함수와 블록 스코프, 재할당과 재선언, 호이스팅과 TDZ, 반복문 closure, 객체 변경 기준으로 정리합니다.
URL path parameter와 query string 차이 thumbnail
URL path parameter와 query string 차이
URL path parameter와 query string의 차이를 초보자 기준으로 정리합니다. 리소스 식별자, 조회 조건, URLSearchParams, 인코딩, API 설계 기준을 함께 봅니다.
쿠키와 localStorage 차이 thumbnail
쿠키와 localStorage 차이
쿠키와 localStorage의 차이를 초보자 기준으로 정리합니다. 서버 자동 전송, JavaScript 접근, 만료 시간, HttpOnly, Secure, SameSite, 로그인 상태 저장 기준을 함께 봅니다.
script async와 defer 차이 thumbnail
script async와 defer 차이
HTML script 태그의 기본 동작, async와 defer의 다운로드 방식, 실행 순서, DOMContentLoaded와의 관계를 초보자 기준으로 정리합니다.
CORS 에러와 해결 기준 thumbnail
CORS 에러와 해결 기준
브라우저의 Same-Origin Policy, CORS 응답 헤더, preflight, 서버에서 해결해야 하는 이유를 초보자 기준으로 정리합니다.
fetch와 axios 차이 thumbnail
fetch와 axios 차이
fetch와 axios의 차이를 초보자 기준으로 정리합니다. 설치 여부, JSON 처리, HTTP 에러 처리, timeout, interceptor, 언제 어떤 방식을 쓰면 좋은지 함께 봅니다.
JavaScript map과 forEach 차이 thumbnail
JavaScript map과 forEach 차이
JavaScript의 map과 forEach 차이를 반환값, 새 배열, 부수 효과, 원본 변경, 비동기 콜백, Promise.all 사용 기준으로 나누어 정리합니다.
Vue 3 입문기 thumbnail
Vue 3 입문기
Vue의 메인 버전이 3가 된지 꽤 됐다. 작성 당시에는 Nuxt 3가 RC 단계였기 때문에 실서비스에 바로 적용하기에는 조심스러웠다. 그래서 궁금하고 심심하던 참에 간단한 Todo App을 만들어봤다.
marked.js renderer custom 하기 thumbnail
marked.js renderer custom 하기
marked.js renderer custom 방법을 정리합니다. Renderer의 역할, image와 link 렌더러 커스텀, alt와 rel 처리, sanitize-html 같은 HTML 정화 주의점까지 함께 봅니다.
가독성 있게 상수 넣기 thumbnail
가독성 있게 상수 넣기
오늘 회사 동료의 PR 리뷰 과정에서 좋은 기능을 공유해주셔서 TIL로 남겨봅니다. 아래와 같이 상수에 언더바(_)로 콤마처럼 구분을 시켜줄 수 있습니다. 앞으로 깔끔하고 좋은 코드를 작성하기 위해 자주 사용해야겠습니다 :)
TS에서 generic optional 하게 설정하기 thumbnail
TS에서 generic optional 하게 설정하기
오늘 next에서 `getStaticProps`와 `getLayout` 패턴을 함께 사용할 때, typescript generic을 넘겨주는 작업을 하고 있었는데, 기본값이 없다보니 기존 코드에 에러가 발생했었다. 이를 해결하기 위해 찾아보니 단순히 아래 예시처럼 `= {}`을 추가해주면 해결된다고 한다.
Backend에서 API Response가 snake_case인 경우엔? thumbnail
Backend에서 API Response가 snake_case인 경우엔?
안녕하세요. 프론트엔드 개발자의 경우, 가끔 백엔드의 API Response 값이 snake_case일 경우 어떻게 관리할지에 대해 고민에 빠지게 됩니다. 저도 오늘 같은 상황을 겪게 되었는데,  이번엔 네이밍 컨벤션을 맞춰주기로 했습니다. 컨벤션을 맞추는 데에는 여러가지 방법이 있겠지만, 고민 끝에 저는 axios의 interceptors를 통해 해결을 해보았습니다.
Promise 다루기 (feat. 병렬실행, 순차실행) thumbnail
Promise 다루기 (feat. 병렬실행, 순차실행)
오늘은 Promise를 통해 구문을 동기 처리 할 때, 여러 Promise들을 다루는 법을 소개해보겠습니다. Javscript를 작성하다 보면, 가끔 여러 Promise들을 다룰 때가 있습니다. 필자도 Nodejs 서버에서 동시에 여러 쿼리를 실행할 때 자주 마주쳤었는데요. 오늘은 어떻게 하면 Promise들을 유연하게 다룰 수 있는지 알아보겠습니다. 시작하기 앞서, 네 가지 Promise를 선언하고, 그들을 하나의 Array에 묶어보겠습니다.