학습 공간 개념 · 판단 · 실험

Frontend Performance Lab

JavaScript

복사, 불변 갱신과 배열 변환

얕은 복사는 중첩된 내부 객체의 참조를 그대로 공유하므로, 상태가 바뀌는 경로의 객체만 새로 만들고 배열 변환 메서드도 목적에 맞게 선택합니다.

이럴 때 필요해요

원본 데이터가 의도치 않게 함께 바뀌는 부작용을 막고 상태 변경 감지와 데이터 파이프라인을 명확하게 유지할 수 있습니다.

어떻게 동작하나요?

객체를 담은 변수 before를 복사하려고 const copy = { ...before }처럼 전개 연산자(Spread)를 쓴 뒤, copy.user.name = '지수'로 안쪽 값을 바꾸면 원본인 before.user.name까지 함께 바뀌어 버립니다. 전개 연산자나 Object.assign()은 가장 바깥쪽 객체만 새로 만들고 그 안의 중첩 객체 참조는 그대로 공유하는 얕은 복사(Shallow Copy)이기 때문입니다. 따라서 원본 상태를 보존하며 불변 갱신(Immutable Update)을 하려면 아래 예제처럼 값이 바뀌는 경로에 있는 중첩 객체(...before.user)도 함께 새로 만들어 주어야 합니다.

중첩된 모든 계층을 완전히 분리된 새 객체로 복제하는 깊은 복사(Deep Copy)가 필요할 때는 브라우저와 Node.js에 내장된 structuredClone()을 사용할 수 있습니다. structuredClone은 중첩 객체와 배열, Date, Map/Set, 순환 참조까지 복사해 주지만 함수나 DOM 노드는 복사할 수 없습니다. 또한 흔히 쓰던 JSON.parse(JSON.stringify(obj)) 방식은 undefined나 함수 프로퍼티가 증발하고 Date가 문자열로 바뀌는 손실이 있으므로 주의해야 합니다.

배열 데이터를 다룰 때도 목적에 맞는 메서드를 선택하면 의도가 분명해집니다. 요소를 1:1로 변환해 새 배열을 얻을 때는 map, 반환값 없이 부수 효과만 실행할 때는 forEach, 배열 전체를 하나의 요약값(합계, 객체 등)으로 줄일 때는 초기값을 지정한 reduce를 사용합니다. 아울러 점 세 개(...) 문법은 배열·객체 리터럴이나 함수 호출에서는 요소를 펼치는 Spread로 동작하고, 함수 매개변수나 구조 분해의 왼쪽에서는 남은 요소를 모으는 Rest로 동작합니다.

코드 예제

동작을 설명하는 읽기 전용 예제입니다.

example.js
const before = { user: { name: '민수' }, tags: ['js'] };
const after = { ...before, user: { ...before.user, name: '지수' } };
const lengths = after.tags.map(tag => tag.length);
const total = lengths.reduce((sum, n) => sum + n, 0);
javascript · 읽기 전용4줄

이름이 바뀐 user 경로만 새 객체로 만들어 불변성을 지키고 변경되지 않은 tags 배열 참조는 그대로 재사용합니다. 이어서 map과 초기값을 준 reduce로 길이의 총합을 구합니다.

자주 헷갈리는 포인트

  • map이 새 배열을 반환하더라도 콜백 내부에서 원소로 들어 있는 객체의 프로퍼티를 직접 수정하면 원본 객체도 함께 변경됩니다.
  • forEach에 async 콜백을 전달해도 각 Promise가 완료되기를 기다려 주지 않으므로 비동기 순차 처리에는 사용할 수 없습니다.

내 말로 설명해 보기

중첩된 객체를 { ...obj }로 한 번만 얕은 복사한 뒤 안쪽 객체의 프로퍼티를 수정하면 원본 객체에는 어떤 일이 일어날까요?

실전 시나리오에 적용해 보기

함께 읽으면 좋은 개념

관련 면접 질문 4개

얕은 복사와 깊은 복사는 어떤 참조를 공유하나요?

얕은 복사(Shallow Copy)는 가장 바깥쪽 객체나 배열만 새로 만들고, 그 안에 들어 있는 중첩 객체들은 기존 참조를 그대로 공유하는 방식입니다. 대표적으로 객체·배열의 전개 연산자(...)와 Object.assign()이 얕은 복사를 수행합니다. 반면 깊은 복사(Deep Copy)는 중첩된 객체들까지 모두 새로운 참조로 복제하여 원본과 복사본이 서로 영향을 주지 않게 만드는 방식이며, 현대 환경에서는 내장 structuredClone()을 주로 활용합니다.

꼬리질문과 참고자료 →

객체 상태의 불변성을 어떻게 유지할까요?

JavaScript에서 객체 상태의 불변성을 유지하려면 기존 객체나 배열을 직접 수정(mutation)하지 않고, 변경이 필요한 경로마다 새로운 객체나 배열을 만들어 교체해야 합니다. 예를 들어 객체는 전개 연산자(...)로 변경될 계층을 새로 만들고, 배열은 push나 sort 대신 map, filter, toSorted 같은 비변이 메서드로 새 배열을 반환받습니다. 이때 변경되지 않은 나머지 하위 참조는 그대로 재사용(구조적 공유)하면 불필요한 전체 복사 비용을 줄이면서도 참조 비교(===)로 상태 변경 여부를 빠르게 감지할 수 있습니다.

꼬리질문과 참고자료 →

map·forEach·reduce는 어떤 의도로 선택하나요?

배열을 순회할 때 각 요소를 1:1로 변환해 같은 길이의 새로운 배열을 만들고 싶다면 map을 선택하고, 반환값 없이 로그 출력이나 외부 상태 변경 같은 부수 효과(side effect)만 실행할 때는 forEach를 선택합니다. 그리고 여러 요소를 순회하며 합계, 그룹화 객체, 단일 요약값처럼 하나의 누적 결과로 줄여 나가고 싶을 때는 reduce를 사용합니다. 특히 reduce를 쓸 때는 빈 배열이 들어와도 안전하게 동작하고 결과 타입이 명확해지도록 두 번째 인자인 초기값(initialValue)을 항상 지정하는 것이 좋습니다.

꼬리질문과 참고자료 →
나머지 질문 1개

공식 참고 자료

← JavaScript