자바스크립트를 다루다 보면 ... 기호를 자주 마주친다. 많은 개발자들은 이 Spread Operator(스프레드 연산자)가 배열을 펼치는 정도의 역할만 한다고 생각하고, 인터넷에서 복사한 코드를 그냥 붙여넣는다. 다만 정확히 언제 어떻게 사용해야 하는지, 얕은 복사와 깊은 복사의 차이는 뭔지 모르는 경우가 대부분이다. 이번에는 Spread Operator의 정확한 동작 원리, 배열과 객체에서의 활용법, 그리고 함수 인자 전달까지 실무에 바로 쓸 수 있는 모든 것을 정리해서 소개하겠다.

 

1단계: Spread Operator가 정확히 뭔가?
Spread Operator(...)는 배열이나 객체를 개별 요소로 펼치는 문법이다. ES2015(ES6)에서 도입되었고, 이제는 모든 최신 브라우저에서 지원한다. 간단하게 말하면 "컨테이너를 열어서 안에 있는 물건들을 꺼내는 것"이다.

기본 개념:

[1, 2, 3] 배열이 있다면, ...[1, 2, 3]1, 2, 3으로 펼쳐진다. 객체도 마찬가지로 {a: 1, b: 2}a: 1, b: 2로 펼쳐진다.

 

배열에서의 기본 동작
// ✗ Spread 없이 배열 전달 - 배열 자체가 첫 번째 인자가 됨
const nums = [1, 2, 3];
console.log(Math.max(nums));
// 결과: NaN (배열 객체와 숫자를 비교할 수 없음)

// ✓ Spread로 배열 펼침 - 개별 인자로 전달됨
console.log(Math.max(...nums));
// 결과: 3

 

2단계: 배열 복사 및 병합

Spread Operator를 가장 많이 사용하는 곳이 배열 복사다. 특히 얕은 복사와 깊은 복사의 차이를 이해하는 것이 중요하다.

배열 복사 - 얕은 복사(Shallow Copy):

// ✗ 잘못된 방법 - 같은 참조를 가짐
const original = [1, 2, 3];
const copied = original;
copied[0] = 99;
console.log(original[0]); // 99 (원본도 변경됨 - 위험!)

// ✓ 올바른 방법 - Spread로 새 배열 생성
const original = [1, 2, 3];
const copied = [...original];
copied[0] = 99;
console.log(original[0]); // 1 (원본은 안전)
console.log(copied); // [99, 2, 3]

배열 병합:

// ✗ concat 방법 - 물론 작동하지만 더 직관적이지 않음
const arr1 = [1, 2];
const arr2 = [3, 4];
const merged = arr1.concat(arr2);
// [1, 2, 3, 4]

// ✓ Spread로 더 간결하게
const arr1 = [1, 2];
const arr2 = [3, 4];
const merged = [...arr1, ...arr2];
// [1, 2, 3, 4]

// ✓ 중간에 요소 삽입도 가능
const result = [...arr1, 99, ...arr2];
// [1, 2, 99, 3, 4]

 

3단계: 객체 복사 및 병합

객체에서도 Spread Operator는 동일하게 작동한다. 배열처럼 얕은 복사를 수행하므로 중첩 객체의 경우 주의가 필요하다.

객체 복사:

// ✗ 참조 복사 - 같은 객체를 가리킴
const user1 = {name: 'Alice', age: 30};
const user2 = user1;
user2.name = 'Bob';
console.log(user1.name); // 'Bob' (원본 변경됨)

// ✓ Spread로 새 객체 생성
const user1 = {name: 'Alice', age: 30};
const user2 = {...user1};
user2.name = 'Bob';
console.log(user1.name); // 'Alice' (원본 안전)

객체 병합 및 덮어쓰기:

// ✓ 여러 객체 병합 - 나중 객체의 속성이 앞 객체를 덮어씀
const defaults = {theme: 'light', lang: 'ko'};
const userSettings = {theme: 'dark'};
const finalConfig = {...defaults, ...userSettings};
console.log(finalConfig);
// {theme: 'dark', lang: 'ko'}

// ✓ 속성 추가/변경하면서 복사
const product = {id: 1, name: 'Laptop', price: 1000};
const discountedProduct = {...product, price: 800, discount: true};
console.log(discountedProduct);
// {id: 1, name: 'Laptop', price: 800, discount: true}
console.log(product); // 원본 변경 없음

 

4단계: 함수 인자 전달(Rest Parameters와 혼동 주의)

Spread Operator는 함수 호출 시 배열을 개별 인자로 펼치는 데 사용된다. 이는 Rest Parameters(...)와 문법은 같지만 용도가 다르다.

함수에 배열 인자 전달:

// ✗ 배열 전체를 하나의 인자로 전달
const numbers = [5, 6, 2, 8, 1];
const max = Math.max(numbers); // 배열 객체 자체 전달
console.log(max); // NaN

// ✓ Spread로 펼쳐서 개별 인자로 전달
const max = Math.max(...numbers);
console.log(max); // 8

Rest Parameters와의 비교:

// Spread: 함수 호출 시 배열 펼치기
function sum(a, b, c) {
  return a + b + c;
}
const nums = [1, 2, 3];
console.log(sum(...nums)); // 6

// Rest: 함수 정의 시 여러 인자를 배열로 받기
function sumAll(...numbers) {
  return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sumAll(1, 2, 3, 4, 5)); // 15

// 둘을 함께 사용
const moreNums = [4, 5];
console.log(sumAll(1, 2, 3, ...moreNums)); // 15

 

5단계: 주의사항 - 얕은 복사의 함정

Spread Operator는 얕은 복사(Shallow Copy)를 수행한다. 중첩된 배열이나 객체가 있다면 내부 참조는 유지된다.

중첩 객체에서의 문제:

// ✗ 중첩 객체는 여전히 참조 공유
const user = {
  name: 'Alice',
  address: {city: 'Seoul', zip: '12345'}
};
const userCopy = {...user};
userCopy.address.city = 'Busan';
console.log(user.address.city); // 'Busan' (원본도 변경됨!)

// ✓ 깊은 복사 필요 - JSON 방법
const userDeepCopy = JSON.parse(JSON.stringify(user));
userDeepCopy.address.city = 'Daegu';
console.log(user.address.city); // 'Busan' (원본 안전)

// ✓ 깊은 복사 필요 - 재귀 함수
function deepCopy(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (Array.isArray(obj)) return obj.map(item => deepCopy(item));
  return Object.keys(obj).reduce((acc, key) => {
    acc[key] = deepCopy(obj[key]);
    return acc;
  }, {});
}
const userDeepCopy2 = deepCopy(user);
userDeepCopy2.address.city = 'Incheon';
console.log(user.address.city); // 'Busan' (원본 안전)

 

6단계: 실전 예제 - React 상태 업데이트 패턴

React에서 상태 불변성을 유지하려면 Spread Operator는 필수 도구다.

// 잘못된 패턴 - 직접 수정 (React가 감지 못함)
const [todos, setTodos] = useState([{id: 1, text: 'Learn JS'}]);
const updateTodo = (id, newText) => {
  todos[0].text = newText; // ✗ 직접 수정
  setTodos(todos); // React 감지 안 함
};

// 올바른 패턴 - Spread로 새 배열 생성
const updateTodo = (id, newText) => {
  const updated = todos.map(todo =>
    todo.id === id ? {...todo, text: newText} : todo
  );
  setTodos(updated); // ✓ 새 참조로 React 감지
};

 

핵심 정리

Spread Operator의 세 가지 역할:

용도 코드 예시 핵심
배열/객체 복사 const copy = [...arr] 새로운 참조 생성 (얕은 복사)
배열/객체 병합 const merged = {...obj1, ...obj2} 뒤의 속성이 앞을 덮어씀
함수 인자 전달 func(...arr) 배열을 개별 인자로 펼침

Spread Operator는 자바스크립트에서 배열과 객체를 다루는 가장 강력하고 우아한 방법이다. 특히 React, Vue 같은 프레임워크에서 상태 불변성을 유지하려면 반드시 익혀야 한다. 다만 중첩된 구조에서는 얕은 복사의 한계를 이해하고, 필요하면 깊은 복사를 고려해야 한다는 점을 잊지 말자. 이 글의 배열 병합과 객체 복사 섹션을 참고해 실제 프로젝트에 적용해보면, 더 안전하고 읽기 쉬운 자바스크립트 코드를 작성할 수 있을 것이다.