Menu

자바스크립트 객체 스프레드(...obj) 복사·병합 완벽 정리

자바스크립트 스프레드 연산자(...obj)로 객체를 복사하고 병합하는 방법, 속성 덮어쓰기 규칙, 그리고 실수하기 쉬운 얕은 복사 함정까지 한 번에 정리했습니다.

스프레드 연산자로 객체를 다른 객체에 펼치기

객체 앞에 점 세 개(...)를 붙이는 객체 스프레드 연산자는, 해당 객체가 직접 소유한 열거 가능한 프로퍼티를 감싸고 있는 객체 리터럴 안으로 그대로 복사해 넣습니다. 원본은 그대로 둔 채 객체를 복사하거나, 병합하거나, 일부만 바꾼 새 객체를 만들 때 가장 간결한 방법이죠.

index.js
Output
Click Run to see the output here.

copyuser와 키와 값이 똑같지만, 엄연히 다른 객체입니다. 한쪽을 수정해도 다른 쪽에는 영향이 없죠 — 적어도 최상위 레벨에서는요. 이 부분은 뒤에서 다시 짚어보겠습니다.

이렇게 이해하면 쉽습니다. { ...obj }는 "obj의 프로퍼티들을 이 새 객체 리터럴 안에 쏟아붓는다"는 뜻이에요. 스프레드와 함께 적어둔 내용도 모두 결과 객체의 일부가 됩니다.

복사하면서 동시에 값 덮어쓰기

가장 자주 쓰는 패턴은 객체를 스프레드한 다음 몇몇 프로퍼티를 추가하거나 덮어쓰는 것입니다. 뒤에 오는 키가 이기기 때문에, 먼저 스프레드를 펼치고 그 뒤에 덮어쓸 값을 적으면 됩니다:

index.js
Output
Click Run to see the output here.

user는 그대로 유지되고, updatedrole만 바뀐 완전히 새로운 객체입니다. 이런 불변 업데이트 패턴은 요즘 JavaScript 코드에서 정말 자주 보입니다 — React state 업데이트, Redux reducer, 그 외에도 원본을 직접 건드리지 않으려는 코드라면 어디서든 등장하죠.

순서를 반대로 하면 결과도 반대가 됩니다:

index.js
Output
Click Run to see the output here.

여기서는 role: "guest"가 먼저 오기 때문에 user.role이 이를 덮어씁니다. 기본값을 먼저 깔아두고 스프레드로 들어오는 객체가 덮어쓰게 하고 싶을 때 유용한 패턴이죠.

객체 병합하기

두 개 이상의 객체를 새 객체 리터럴 안에 스프레드하면 자바스크립트 객체 병합이 됩니다. 키가 겹칠 경우 뒤에 오는 객체가 앞의 값을 덮어씁니다:

index.js
Output
Click Run to see the output here.

themefontSizeuserPrefs에서 오고, debugdefaults에서 그대로 넘어옵니다. 객체가 3개든 4개든 규칙은 같아요. 왼쪽에서 오른쪽으로 읽어가며, 마지막에 쓴 값이 이깁니다.

이 방식은 Object.assign({}, defaults, userPrefs)를 대체하는 요즘 문법입니다. 동작은 똑같지만, 스프레드 버전이 훨씬 읽기 쉽고, Object.assign(defaults, userPrefs)처럼 첫 인자를 빈 객체로 두는 걸 깜빡해서 defaults를 통째로 변경시켜 버리는 흔한 실수도 피할 수 있죠.

스프레드 연산자는 얕은 복사다

여기서 많은 분들이 발목을 잡힙니다. 스프레드는 객체의 최상위 프로퍼티만 복사해요. 그 프로퍼티 값이 또 다른 객체나 배열이라면, 내용이 아니라 참조(reference)만 복사된다는 뜻입니다.

index.js
Output
Click Run to see the output here.

copy.address.city를 바꿨더니 user.address.city까지 같이 바뀌어 버렸죠. 두 객체가 같은 address 객체를 공유하고 있기 때문입니다. 스프레드 연산자는 바깥쪽 껍데기만 새로 만들어 줄 뿐이거든요.

중첩된 값을 수정하고 싶다면, 바꾸려는 단계마다 스프레드를 따로 적용해 줘야 합니다:

index.js
Output
Click Run to see the output here.

데이터 구조가 복잡해서 진짜 깊은 복사(deep clone)가 필요하다면 structuredClone(obj)을 쓰면 된다. 중첩 객체는 물론이고 배열, Date, Map, Set까지 알아서 처리해 주고, 요즘 나오는 런타임 환경이라면 기본으로 내장되어 있다.

Spread와 Rest의 차이

생김새는 점 세 개로 똑같지만, 하는 일은 정반대다. spread는 펼치고, rest는 모은다.

index.js
Output
Click Run to see the output here.

간단한 규칙 하나만 기억해두세요. ...=의 앞쪽(구조 분해)에 있으면 rest, 객체나 배열 리터럴 안쪽(뒤쪽)에 있으면 spread입니다.

프로퍼티를 불변으로 제거하기

rest 구조 분해와 spread를 함께 쓰면 특정 키 하나만 제거한 객체 복사본을 깔끔하게 만들 수 있습니다. delete를 쓰지 않아도 되고, 원본을 건드리지도 않습니다.

index.js
Output
Click Run to see the output here.

tempToken은 별도의 변수로 뽑혀 나오고(그리고 이 값은 무시), 나머지 속성들은 전부 safe에 담깁니다. 원본 user는 그대로 유지되죠.

스프레드 연산자가 복사하지 않는 것들

알아두면 좋은 몇 가지 미묘한 포인트가 있습니다.

  • 열거 불가능한(non-enumerable) 속성은 복사되지 않습니다. 직접 만든 속성은 기본적으로 열거 가능하지만, Object.defineProperty로 정의한 속성이나 일부 내장 속성들은 그렇지 않습니다.
  • 프로토타입은 복사되지 않습니다. { ...instance }의 결과는 원래 클래스의 인스턴스가 아니라 그냥 평범한 객체입니다. 클래스 프로토타입에 정의된 메서드는 복사본에 존재하지 않죠.
  • 게터(getter)는 실행되어 버립니다. 게터가 있는 객체를 스프레드하면 게터가 한 번 호출되고, 그 반환값이 새 객체에 일반 속성으로 저장됩니다.
index.js
Output
Click Run to see the output here.

copy에는 xy는 들어 있지만, 결국 일반 객체일 뿐입니다. distancePoint.prototype에 정의돼 있는데, 스프레드 연산자는 프로토타입까지 건드리지 않거든요. 클래스 인스턴스를 제대로 복제하고 싶다면 보통 그 클래스가 직접 clone 메서드를 제공해야 합니다.

다음 주제: 배열 메서드

스프레드는 불변 데이터를 다루는 도구 중 하나일 뿐입니다. 나머지 큰 축은 map, filter, reduce 같은 배열 메서드인데요, 원본을 변경하지 않고 새 배열을 만들어 돌려주는 방식이죠. 다음 페이지에서 바로 이어서 살펴보겠습니다.

자주 묻는 질문

자바스크립트에서 ...obj는 어떤 동작을 하나요?

객체 리터럴 안에서 ...obj를 쓰면 해당 객체의 열거 가능한 자기 속성(own enumerable properties)을 새 객체로 그대로 복사해 넣습니다. 예를 들어 { ...user }user와 같은 키·값을 가진 새 객체를 만들어 줍니다. 원본을 건드리지 않고 객체를 복사하거나 병합할 때 가장 많이 쓰는 방식이에요.

두 객체를 병합(merge)하려면 어떻게 하나요?

새 객체 리터럴 안에 둘 다 펼쳐 주면 됩니다. const merged = { ...a, ...b } 이렇게요. 같은 키가 있으면 뒤에 오는 b의 값이 a를 덮어씁니다(나중에 쓴 값이 이김). Object.assign({}, a, b)와 결과는 같지만 이 쪽이 훨씬 읽기 편합니다.

객체 스프레드는 깊은 복사(deep copy)인가요?

아닙니다. 스프레드는 얕은 복사(shallow copy) 예요. 최상위 속성만 복사하고, 안쪽에 있는 객체나 배열은 여전히 같은 참조를 공유합니다. 그래서 copy.address.city를 바꾸면 original.address.city도 같이 바뀌어 버리죠. 진짜 깊은 복사가 필요하면 structuredClone(obj)을 사용하세요.

스프레드(spread)와 레스트(rest)는 뭐가 다른가요?

표기는 똑같이 ...지만 하는 일은 정반대입니다. 스프레드는 객체나 배열을 개별 속성/요소로 펼치는 역할({ ...user })이고, 레스트는 남은 값들을 하나의 변수로 모으는 역할(const { name, ...others } = user)을 합니다. 쓰이는 위치만 보면 어느 쪽인지 바로 구분할 수 있어요.

Coddy로 코딩 배우기

시작하기