📋 얕은 복사 vs 깊은 복사
📖 정의
**얕은 복사(Shallow Copy)**는 객체의 최상위 레벨만 복사하 고 중첩된 객체는 참조를 공유합니다. **깊은 복사(Deep Copy)**는 중첩된 모든 객체를 재귀적으로 복사하여 완전히 독립적인 복사본을 만듭니다.
🎯 비유로 이해하기
아파트 비유
얕은 복사와 깊은 복사를 아파트에 비유하면:
원본 아파트
├─ 거실 (1층 데이터)
│ ├─ TV
│ └─ 소파
└─ 방 (중첩 객체)
├─ 침대
└─ 책상
얕은 복사 (Shallow Copy)
├─ 새 아파트 열쇠 만들기
├─ 거실은 새로 복사 (TV, 소파 복사)
└─ 방은 원본과 같은 방 사용 (참조 공유)
결과:
원본.방.침대 = "더블침대"
→ 복사본.방.침대도 "더블침대" (같은 방!)
깊은 복사 (Deep Copy)
├─ 완전히 새 아파트 짓기
├─ 거실도 새로 만들기
└─ 방도 새로 만들기 (가구도 모두 복제)
결과:
원본.방.침대 = "더블침대"
→ 복사본.방.침대는 변경 없음 (다른 방!)
문서 복사 비유
원본 문서 (중첩 구조)
├─ 1장: 제목 (원시값)
└─ 2장: 참고자료 (객체)
├─ 링크 목록
└─ 이미지
얕은 복사
├─ 새 문서 만들기
├─ 1장 복사 (텍스트 그대로 복사)
└─ 2장은 원본과 같은 폴더 참조
원본 참고자료 수정 시:
→ 복사본도 같은 변경 보임!
깊은 복사
├─ 새 문서 만들기
├─ 1장 복사
└─ 2장도 새 폴더에 모든 파일 복사
원본 참고자료 수정 시:
→ 복사본은 영향 없음!
연필 상자 비유
원본 상자
├─ 연필 3자루 (원시값)
└─ 작은 상자 (중첩 객체)
└─ 지우개 2개
얕은 복사
├─ 큰 상자 새로 만들기
├─ 연필 3자루 복사
└─ 작은 상자는 원본과 같은 상자
작은 상자에서 지우개 1개 꺼내면:
원본 작은 상자: 지우개 1개
복사본 작은 상자: 지우개 1개 (같은 상자!)
깊은 복사
├─ 큰 상자 새로 만들기
├─ 연필 3자루 복사
└─ 작은 상자도 새로 만들어서 지우개 복사
작은 상자에서 지우개 1개 꺼내면:
원본 작은 상자: 지우개 1개
복사본 작은 상자: 지우개 2개 (다른 상자!)
⚙️ 작동 원리
1. 참조 vs 값
// 원시 타입: 값 복사
let a = 10;
let b = a; // 값 복사
b = 20;
console.log(a); // 10 (변경 없음)
console.log(b); // 20
// 객체 타입: 참조 복사
let obj1 = { name: '철수' };
let obj2 = obj1; // 참조 복사 (같은 객체)
obj2.name = '영희';
console.log(obj1.name); // 영희 (변경됨!)
console.log(obj2.name); // 영희
// 메모리 구조
/*
원시값:
a: [10]
b: [20] ← 독립적인 메모리
객체:
obj1: [메모리 주소 0x001] ─┐
├─> { name: '영희' }
obj2: [메모리 주소 0x001] ─┘
└─ 같은 주소!
*/
// 참조 비교
console.log(obj1 === obj2); // true (같은 참조)
const obj3 = { name: '영희' };
console.log(obj1 === obj3); // false (다른 참조, 내용은 같아도)
2. 얕은 복사
// 1. Spread 연산자 (...)
const original = {
name: '철수',
age: 25,
address: {
city: '서울',
district: '강남'
}
};
const shallow1 = { ...original };
// 1단계는 복사됨
shallow1.name = '영희';
console.log(original.name); // 철수 (변경 없음)
// 2단계는 참조 공유
shallow1.address.city = '부산';
console.log(original.address.city); // 부산 (변경됨!)
// 2. Object.assign()
const shallow2 = Object.assign({}, original);
shallow2.age = 30;
console.log(original.age); // 25 (변경 없음)
shallow2.address.district = '서초';
console.log(original.address.district); // 서초 (변경됨!)
// 3. Array.slice()
const arr1 = [1, 2, [3, 4]];
const arr2 = arr1.slice();
arr2[0] = 10;
console.log(arr1[0]); // 1 (변경 없음)
arr2[2][0] = 30;
console.log(arr1[2][0]); // 30 (변경됨!)
// 4. Array.concat()
const arr3 = [1, 2, [3, 4]];
const arr4 = [].concat(arr3);
arr4[2][1] = 40;
console.log(arr3[2][1]); // 40 (변경됨!)
// 얕은 복사의 문제
const user = {
name: '철수',
hobbies: ['축구', '농구']
};
const copiedUser = { ...user };
copiedUser.hobbies.push('야구');
console.log(user.hobbies); // ['축구', '농구', '야구']
// 원본도 변경됨!
3. 깊은 복사
// 1. JSON 방법 (간단하지만 제한적)
const original = {
name: '철수',
age: 25,
address: {
city: '서울',
district: '강남'
},
hobbies: ['축구', ['농구', '야구']]
};
const deep1 = JSON.parse(JSON.stringify(original));
deep1.address.city = '부산';
deep1.hobbies[1][0] = '배구';
console.log(original.address.city); // 서울 (변경 없음!)
console.log(original.hobbies[1][0]); // 농구 (변경 없음!)
// ❌ JSON 방법의 한계
const problematic = {
date: new Date(),
regex: /test/g,
func: () => console.log('hi'),
undefined: undefined,
symbol: Symbol('id'),
circular: null
};
problematic.circular = problematic; // 순환 참조
// const copied = JSON.parse(JSON.stringify(problematic));
// - date: 문자열로 변환됨
// - regex: 빈 객체 {}
// - func: 사라짐
// - undefined: 사라짐
// - symbol: 사라짐
// - circular: 에러 발생
// 2. structuredClone() (최신, 권장!)
const deep2 = structuredClone(original);
deep2.address.city = '대구';
console.log(original.address.city); // 서울 (변경 없음!)
// structuredClone 장점:
// - Date, RegExp, Map, Set 지원
// - 순환 참조 지원
// - ArrayBuffer 등 복잡한 타입 지원
const complex = {
date: new Date(),
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
buffer: new ArrayBuffer(8)
};
const cloned = structuredClone(complex);
// 모두 제대로 복사됨!
// ❌ structuredClone 한계
const withFunction = {
func: () => console.log('hi')
};
// const copied = structuredClone(withFunction); // 에러!
// 함수는 복사 불가
// 3. 재귀 함수 (완전 제어)
function deepCopy(obj, map = new WeakMap()) {
// null, undefined, 원시값
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 순환 참조 처리
if (map.has(obj)) {
return map.get(obj);
}
// Date
if (obj instanceof Date) {
return new Date(obj);
}
// RegExp
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// Map
if (obj instanceof Map) {
const mapCopy = new Map();
map.set(obj, mapCopy);
obj.forEach((value, key) => {
mapCopy.set(key, deepCopy(value, map));
});
return mapCopy;
}
// Set
if (obj instanceof Set) {
const setCopy = new Set();
map.set(obj, setCopy);
obj.forEach(value => {
setCopy.add(deepCopy(value, map));
});
return setCopy;
}
// 배열
if (Array.isArray(obj)) {
const arrCopy = [];
map.set(obj, arrCopy);
obj.forEach((item, index) => {
arrCopy[index] = deepCopy(item, map);
});
return arrCopy;
}
// 객체
const objCopy = {};
map.set(obj, objCopy);
Object.keys(obj).forEach(key => {
objCopy[key] = deepCopy(obj[key], map);
});
return objCopy;
}
// 사용
const test = {
a: 1,
b: { c: 2 },
d: [3, { e: 4 }]
};
const testCopy = deepCopy(test);
testCopy.b.c = 20;
testCopy.d[1].e = 40;
console.log(test.b.c); // 2 (변경 없음!)
console.log(test.d[1].e); // 4 (변경 없음!)
// 4. Lodash 라이브러리
// import _ from 'lodash';
// const deep3 = _.cloneDeep(original);
// 모든 경우 잘 처리됨
4. 중첩 레벨별 복사
// 1단계 중첩
const level1 = {
name: '철수',
age: 25
};
// 얕은 복사로 충분
const copy1 = { ...level1 };
copy1.name = '영희';
console.log(level1.name); // 철수 (OK!)
// 2단계 중첩
const level2 = {
name: '철수',
address: {
city: '서울'
}
};
// ❌ 얕은 복사
const copy2Shallow = { ...level2 };
copy2Shallow.address.city = '부산';
console.log(level2.address.city); // 부산 (문제!)
// ✅ 수동 2단계 복사
const copy2Deep = {
...level2,
address: { ...level2.address }
};
copy2Deep.address.city = '대구';
console.log(level2.address.city); // 부산 (OK!)
// 3단계 중첩
const level3 = {
name: '철수',
address: {
city: '서울',
detail: {
street: '강남대로'
}
}
};
// ✅ 수동 3단계 복사
const copy3 = {
...level3,
address: {
...level3.address,
detail: {
...level3.address.detail
}
}
};
// 복잡해짐! → 깊은 복사 필요
// 배열 중첩
const nestedArray = [
[1, 2],
[3, [4, 5]],
[6, [7, [8, 9]]]
];
// ❌ 얕은 복사
const shallowArr = [...nestedArray];
shallowArr[0][0] = 10;
console.log(nestedArray[0][0]); // 10 (문제!)
// ✅ 깊은 복사
const deepArr = structuredClone(nestedArray);
deepArr[2][1][1][0] = 80;
console.log(nestedArray[2][1][1][0]); // 8 (OK!)
5. 성능 비교
// 성능 테스트
const largeObject = {
a: Array(1000).fill({ b: { c: 1 } })
};
// 1. Spread (빠름, 얕은 복사)
console.time('Spread');
const spread = { ...largeObject };
console.timeEnd('Spread'); // ~0.1ms
// 2. Object.assign (빠름, 얕은 복사)
console.time('Object.assign');
const assigned = Object.assign({}, largeObject);
console.timeEnd('Object.assign'); // ~0.1ms
// 3. JSON (중간, 깊은 복사)
console.time('JSON');
const jsonCopy = JSON.parse(JSON.stringify(largeObject));
console.timeEnd('JSON'); // ~5ms
// 4. structuredClone (중간, 깊은 복사)
console.time('structuredClone');
const structured = structuredClone(largeObject);
console.timeEnd('structuredClone'); // ~3ms
// 5. 재귀 함수 (느림, 깊은 복사)
console.time('Recursive');
const recursive = deepCopy(largeObject);
console.timeEnd('Recursive'); // ~10ms
// 6. Lodash (중간, 깊은 복사)
// console.time('Lodash');
// const lodash = _.cloneDeep(largeObject);
// console.timeEnd('Lodash'); // ~7ms
/*
성능 순위:
1. Spread / Object.assign (가장 빠름)
2. structuredClone (빠름)
3. JSON (보통)
4. Lodash (보통)
5. 재귀 함수 (느림)
선택 기준:
- 1단계만: Spread
- 깊은 복사 필요 + 최신 브라우저: structuredClone
- 깊은 복사 필요 + 구형 브라우저: JSON or Lodash
- 특수 타입 포함: 재귀 함수 or Lodash
*/
💡 실제 예시
React State 불변성
import React, { useState } from 'react';
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '공부하기', completed: false },
{ id: 2, text: '운동하기', completed: false }
]);
// ❌ 잘못된 방법: 직접 수정
const toggleWrong = (id) => {
const todo = todos.find(t => t.id === id);
todo.completed = !todo.completed;
setTodos(todos); // React가 변화 감지 못함!
};
// ✅ 올바른 방법: 얕은 복사
const toggleCorrect = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed } // 새 객체
: todo
));
};
// 중첩 객체
const [user, setUser] = useState({
name: '철수',
profile: {
age: 25,
address: {
city: '서울'
}
}
});
// ❌ 잘못된 방법
const updateCityWrong = (city) => {
user.profile.address.city = city;
setUser(user); // 변화 감지 못함!
};
// ✅ 올바른 방법: 깊은 복사
const updateCityCorrect = (city) => {
setUser({
...user,
profile: {
...user.profile,
address: {
...user.profile.address,
city: city
}
}
});
};
// 또는 Immer 라이브러리
// import { produce } from 'immer';
//
// const updateCityImmer = (city) => {
// setUser(produce(user, draft => {
// draft.profile.address.city = city;
// }));
// };
return (
<div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleCorrect(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
<div>
<p>{user.name}</p>
<p>{user.profile.address.city}</p>
<button onClick={() => updateCityCorrect('부산')}>
부산으로 이사
</button>
</div>
</div>
);
}
Redux Reducer
// Redux: 항상 새 객체 반환
// ❌ 잘못된 Reducer
function badReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
state.todos.push(action.payload); // 직접 수정!
return state; // 같은 참조
case 'UPDATE_USER':
state.user.name = action.payload;
return state;
default:
return state;
}
}
// ✅ 올바른 Reducer
function goodReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, action.payload] // 새 배열
};
case 'UPDATE_USER':
return {
...state,
user: {
...state.user,
name: action.payload // 새 객체
}
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
default:
return state;
}
}
// Redux Toolkit (간편)
import { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: {
todos: [],
user: { name: '' }
},
reducers: {
addTodo: (state, action) => {
// Immer 내장: 직접 수정처럼 보이지만 불변성 유지
state.todos.push(action.payload);
},
updateUser: (state, action) => {
state.user.name = action.payload;
},
toggleTodo: (state, action) => {
const todo = state.todos.find(t => t.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
}
}
});
배열 복사 패턴
// 배열 복사의 다양한 패턴
const numbers = [1, 2, 3, 4, 5];
// 1. 전체 복사
const copy1 = [...numbers];
const copy2 = numbers.slice();
const copy3 = Array.from(numbers);
const copy4 = numbers.map(n => n);
// 2. 요소 추가
const addStart = [0, ...numbers]; // [0,1,2,3,4,5]
const addEnd = [...numbers, 6]; // [1,2,3,4,5,6]
const addMiddle = [...numbers.slice(0, 2), 2.5, ...numbers.slice(2)];
// [1,2,2.5,3,4,5]
// 3. 요소 제거
const removeFirst = numbers.slice(1); // [2,3,4,5]
const removeLast = numbers.slice(0, -1); // [1,2,3,4]
const removeIndex = [
...numbers.slice(0, 2),
...numbers.slice(3)
]; // [1,2,4,5] (인덱스 2 제거)
// 4. 요소 수정
const updateIndex = numbers.map((n, i) =>
i === 2 ? 30 : n
); // [1,2,30,4,5]
// 5. 필터링
const evens = numbers.filter(n => n % 2 === 0); // [2,4]
// 중첩 배열
const nested = [[1, 2], [3, 4], [5, 6]];
// ❌ 얕은 복사
const shallowCopy = [...nested];
shallowCopy[0][0] = 10;
console.log(nested[0][0]); // 10 (변경됨!)
// ✅ 깊은 복사
const deepCopy = nested.map(arr => [...arr]);
deepCopy[0][0] = 100;
console.log(nested[0][0]); // 10 (변경 없음!)
// 객체 배열
const users = [
{ id: 1, name: '철수', age: 25 },
{ id: 2, name: '영희', age: 23 }
];
// ❌ 얕은 복사
const usersCopy = [...users];
usersCopy[0].age = 30;
console.log(users[0].age); // 30 (변경됨!)
// ✅ 깊은 복사
const usersDeepCopy = users.map(user => ({ ...user }));
usersDeepCopy[0].age = 35;
console.log(users[0].age); // 30 (변경 없음!)
// 객체 배열 수정
const updateUser = (users, id, updates) => {
return users.map(user =>
user.id === id
? { ...user, ...updates }
: user
);
};
const updated = updateUser(users, 1, { age: 26 });
console.log(users[0].age); // 25 (원본 유지)
console.log(updated[0].age); // 26 (복사본 수정)
객체 병합
// 객체 병합 시 복사
const defaults = {
theme: 'light',
fontSize: 14,
sidebar: {
width: 200,
position: 'left'
}
};
const userSettings = {
theme: 'dark',
sidebar: {
width: 300
}
};
// ❌ 얕은 병합
const merged1 = { ...defaults, ...userSettings };
console.log(merged1);
// {
// theme: 'dark',
// fontSize: 14,
// sidebar: { width: 300 } // position 사라짐!
// }
// ✅ 깊은 병합
function deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] instanceof Object && key in target) {
result[key] = deepMerge(target[key], source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
const merged2 = deepMerge(defaults, userSettings);
console.log(merged2);
// {
// theme: 'dark',
// fontSize: 14,
// sidebar: { width: 300, position: 'left' } // 유지됨!
// }
// Lodash merge
// import _ from 'lodash';
// const merged3 = _.merge({}, defaults, userSettings);
// 배열 포함 병합
const config1 = {
plugins: ['A', 'B'],
settings: { x: 1 }
};
const config2 = {
plugins: ['C'],
settings: { y: 2 }
};
// 기본 병합
const basic = { ...config1, ...config2 };
console.log(basic.plugins); // ['C'] (덮어씀)
// 배열 합치기
const combined = {
...config1,
...config2,
plugins: [...config1.plugins, ...config2.plugins],
settings: { ...config1.settings, ...config2.settings }
};
console.log(combined.plugins); // ['A','B','C']
console.log(combined.settings); // { x:1, y:2 }
캐싱과 메모이제이션
// 객체 복사를 활용한 캐싱
class DataCache {
constructor() {
this.cache = new Map();
}
// 데이터 저장 (깊은 복사)
set(key, value) {
this.cache.set(key, structuredClone(value));
}
// 데이터 가져오기 (깊은 복사)
get(key) {
const value = this.cache.get(key);
return value ? structuredClone(value) : undefined;
}
// 원본 보호!
has(key) {
return this.cache.has(key);
}
}
// 사용
const cache = new DataCache();
const user = {
name: '철수',
settings: { theme: 'dark' }
};
cache.set('user', user);
// 가져온 데이터 수정
const cached = cache.get('user');
cached.settings.theme = 'light';
// 캐시된 원본은 안전
const original = cache.get('user');
console.log(original.settings.theme); // dark
// React 메모이제이션
import React, { useMemo, useCallback } from 'react';
function ExpensiveComponent({ data }) {
// 데이터 복사 + 연산
const processedData = useMemo(() => {
// 원본 보호하며 처리
const copied = data.map(item => ({ ...item }));
return copied.map(item => ({
...item,
processed: heavyCalculation(item)
}));
}, [data]);
return (
<div>
{processedData.map(item => (
<div key={item.id}>{item.processed}</div>
))}
</div>
);
}
// 함수 메모이제이션
function memoize(fn) {
const cache = new Map();
return function(...args) {
// 인자를 복사하여 키로 사용
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('캐시 히트');
return structuredClone(cache.get(key));
}
const result = fn(...args);
cache.set(key, structuredClone(result));
return result;
};
}
// 사용
const expensiveFunction = memoize((data) => {
// 무거운 계산
return data.map(x => x * 2);
});
const result1 = expensiveFunction([1, 2, 3]);
const result2 = expensiveFunction([1, 2, 3]); // 캐시 히트
폼 데이터 핸들링
import React, { useState } from 'react';
function ComplexForm() {
const [formData, setFormData] = useState({
personal: {
name: '',
email: '',
phone: ''
},
address: {
street: '',
city: '',
zipcode: ''
},
preferences: {
newsletter: false,
notifications: {
email: true,
sms: false
}
}
});
// 중첩 필드 업데이트
const updateField = (path, value) => {
setFormData(prev => {
const newData = { ...prev };
const keys = path.split('.');
let current = newData;
for (let i = 0; i < keys.length - 1; i++) {
current[keys[i]] = { ...current[keys[i]] };
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
return newData;
});
};
// 또는 Immer 사용
// const updateField = (path, value) => {
// setFormData(produce(draft => {
// const keys = path.split('.');
// let current = draft;
// for (let i = 0; i < keys.length - 1; i++) {
// current = current[keys[i]];
// }
// current[keys[keys.length - 1]] = value;
// }));
// };
return (
<form>
<input
value={formData.personal.name}
onChange={(e) => updateField('personal.name', e.target.value)}
placeholder="이름"
/>
<input
value={formData.address.city}
onChange={(e) => updateField('address.city', e.target.value)}
placeholder="도시"
/>
<label>
<input
type="checkbox"
checked={formData.preferences.notifications.email}
onChange={(e) =>
updateField('preferences.notifications.email', e.target.checked)
}
/>
이메일 알림
</label>
</form>
);
}
// 폼 리셋 (초기값 복사)
function FormWithReset() {
const initialData = {
username: '',
email: '',
settings: { theme: 'light' }
};
const [formData, setFormData] = useState(structuredClone(initialData));
const handleReset = () => {
setFormData(structuredClone(initialData));
};
return (
<form>
{/* ... 폼 필드들 ... */}
<button type="button" onClick={handleReset}>
초기화
</button>
</form>
);
}
🤔 자주 묻는 질문
Q1. 언제 얕은 복사를, 언제 깊은 복사를 사용하나요?
A: 데이터 구조와 요구사항에 따라 선택합니다:
// 얕은 복사 사용 시기
const useShallowCopy = {
단순객체: `
// 중첩 없는 객체
const person = { name: '철수', age: 25 };
const copy = { ...person }; // OK!
`,
배열최상위: `
// 배열 요소가 원시값
const numbers = [1, 2, 3, 4, 5];
const copy = [...numbers]; // OK!
`,
성능중요: `
// 빈번한 복사가 필요한 경우
const items = Array(10000).fill({ id: 1 });
const copy = [...items]; // 빠름!
`,
React_Props: `
// Props 전달 시
<Child data={{ ...parentData }} />
// 1단계만 복사하면 충분한 경우
`
};
// 깊은 복사 사용 시기
const useDeepCopy = {
중첩객체: `
// 여러 단계 중첩
const user = {
name: '철수',
address: {
city: '서울',
detail: { street: '강남대로' }
}
};
const copy = structuredClone(user); // 필수!
`,
배열중첩: `
// 배열 안의 객체
const users = [
{ id: 1, profile: { age: 25 } }
];
const copy = structuredClone(users);
`,
완전독립: `
// 원본과 완전히 독립적인 복사본
const original = { data: { value: 1 } };
const copy = structuredClone(original);
copy.data.value = 2;
// original.data.value는 1 유지
`,
Redux_State: `
// Redux 같은 상태 관리
const state = {
user: { settings: { theme: 'dark' } }
};
// 불변성 유지 필요
const newState = structuredClone(state);
`
};
// 판단 기준
function shouldUseDeepCopy(obj) {
// 1. 중첩 레벨 확인
const hasNested = typeof obj === 'object' &&
Object.values(obj).some(v => typeof v === 'object');
if (!hasNested) {
return false; // 얕은 복사로 충분
}
// 2. 원본 보호 필요 여부
const needsProtection = true; // 비즈니스 로직
// 3. 성능 vs 안전성
const dataSize = JSON.stringify(obj).length;
const isCritical = needsProtection && dataSize < 100000;
return isCritical;
}
// 실전 예시
function processUserData(user) {
// 단순 필드만 수정 → 얕은 복사
if (onlyTopLevelChange) {
return { ...user, name: 'Updated' };
}
// 중첩 필드 수정 → 깊은 복사
if (nestedChange) {
const copy = structuredClone(user);
copy.settings.theme = 'dark';
return copy;
}
}
Q2. JSON 방법의 문제점은 무엇인가요?
A: 특정 타입을 제대로 복사하지 못합니다:
// JSON 방법의 한계
const problematic = {
// 1. 함수
greet: function() {
console.log('hi');
},
arrow: () => console.log('arrow'),
// 2. undefined
value: undefined,
// 3. Symbol
id: Symbol('id'),
// 4. Date
created: new Date('2024-01-01'),
// 5. RegExp
pattern: /test/gi,
// 6. Map
map: new Map([['key', 'value']]),
// 7. Set
set: new Set([1, 2, 3]),
// 8. NaN, Infinity
nan: NaN,
infinity: Infinity,
// 9. 순환 참조
self: null
};
problematic.self = problematic;
// JSON 복사 시도
try {
const copy = JSON.parse(JSON.stringify(problematic));
console.log(copy.greet); // undefined (함수 사라짐!)
console.log(copy.arrow); // undefined
console.log(copy.value); // undefined (키 자체가 사라짐)
console.log(copy.id); // undefined
console.log(copy.created); // "2024-01-01T00:00:00.000Z" (문자열!)
console.log(copy.pattern); // {} (빈 객체!)
console.log(copy.map); // {} (빈 객체!)
console.log(copy.set); // {} (빈 객체!)
console.log(copy.nan); // null (null로 변환!)
console.log(copy.infinity); // null
// copy.self는 순환 참조 에러!
} catch (error) {
console.error('순환 참조 에러:', error);
}
// ✅ 대안 1: structuredClone (권장)
const copy1 = structuredClone({
created: new Date(),
pattern: /test/,
map: new Map(),
set: new Set()
});
console.log(copy1.created instanceof Date); // true
console.log(copy1.pattern instanceof RegExp); // true
console.log(copy1.map instanceof Map); // true
console.log(copy1.set instanceof Set); // true
// ✅ 대안 2: 커스텀 deepClone
function customDeepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (map.has(obj)) {
return map.get(obj); // 순환 참조 처리
}
// Date
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// RegExp
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// Map
if (obj instanceof Map) {
const cloned = new Map();
map.set(obj, cloned);
obj.forEach((value, key) => {
cloned.set(key, customDeepClone(value, map));
});
return cloned;
}
// Set
if (obj instanceof Set) {
const cloned = new Set();
map.set(obj, cloned);
obj.forEach(value => {
cloned.add(customDeepClone(value, map));
});
return cloned;
}
// 배열
if (Array.isArray(obj)) {
const cloned = [];
map.set(obj, cloned);
obj.forEach((item, i) => {
cloned[i] = customDeepClone(item, map);
});
return cloned;
}
// 일반 객체
const cloned = {};
map.set(obj, cloned);
Object.keys(obj).forEach(key => {
cloned[key] = customDeepClone(obj[key], map);
});
return cloned;
}
// 사용
const complex = {
date: new Date(),
map: new Map([['a', 1]]),
self: null
};
complex.self = complex;
const cloned = customDeepClone(complex);
console.log(cloned.date instanceof Date); // true
console.log(cloned.map instanceof Map); // true
console.log(cloned.self === cloned); // true (순환 참조 유지)
// 언제 JSON을 사용해도 될까?
const safeForJSON = {
조건: [
'함수 없음',
'undefined 없음',
'Symbol 없음',
'Date를 문자열로 받아도 OK',
'RegExp 없음',
'Map/Set 없음',
'순환 참조 없음'
],
예시: `
// 안전한 경우
const config = {
theme: 'dark',
fontSize: 14,
colors: {
primary: '#000',
secondary: '#fff'
}
};
const copy = JSON.parse(JSON.stringify(config));
// OK!
`
};
Q3. 불변성(Immutability)이 왜 중요한가요?
A: 예측 가능하고 버그를 줄이며 성능을 최적화할 수 있습니다:
// 불변성의 중요성
// 1. 예측 가능한 코드
function mutableExample() {
const user = { name: '철수', age: 25 };
// 어딘가에서 수정
someFunction(user);
// user가 변경되었을까? 알 수 없음!
console.log(user);
}
function immutableExample() {
const user = { name: '철수', age: 25 };
// 새 객체 반환
const updatedUser = someFunction({ ...user });
// user는 그대로, updatedUser는 새 객체
console.log(user); // 원본 유지
console.log(updatedUser); // 새 객체
}
// 2. 버그 방지
const sharedState = {
count: 0,
items: []
};
function buggyFunction() {
// 여러 곳에서 같은 객체 수정
componentA(sharedState);
componentB(sharedState);
componentC(sharedState);
// 누가 무엇을 바꿨는지 추적 불가!
// 디버깅 지옥
}
function safeFunct() {
const initialState = {
count: 0,
items: []
};
// 각자 복사본 사용
const stateA = { ...initialState };
const stateB = { ...initialState };
const stateC = { ...initialState };
// 독립적으로 동작
// 디버깅 쉬움
}
// 3. React 성능 최적화
function Counter({ count }) {
// React.memo로 최적화
console.log('렌더링');
return <div>Count: {count}</div>;
}
const MemoizedCounter = React.memo(Counter);
// ❌ 객체를 직접 수정
const obj = { count: 0 };
obj.count++;
<MemoizedCounter count={obj} />
// 같은 참조 → 리렌더링 안 됨!
// ✅ 새 객체 생성
const obj1 = { count: 0 };
const obj2 = { count: 1 }; // 새 객체
<MemoizedCounter count={obj2} />
// 다른 참조 → 리렌더링됨!
// 4. 시간 여행 디버깅
class StateHistory {
constructor(initialState) {
this.history = [initialState];
this.currentIndex = 0;
}
update(newState) {
// 불변성: 새 객체를 히스토리에 추가
this.history = [
...this.history.slice(0, this.currentIndex + 1),
newState
];
this.currentIndex++;
}
undo() {
if (this.currentIndex > 0) {
this.currentIndex--;
return this.history[this.currentIndex];
}
}
redo() {
if (this.currentIndex < this.history.length - 1) {
this.currentIndex++;
return this.history[this.currentIndex];
}
}
}
// 사용
const history = new StateHistory({ count: 0 });
history.update({ count: 1 });
history.update({ count: 2 });
history.update({ count: 3 });
console.log(history.undo()); // { count: 2 }
console.log(history.undo()); // { count: 1 }
console.log(history.redo()); // { count: 2 }
// 5. 동시성 안전
// 가변 상태
let mutableCounter = 0;
async function incrementMutable() {
const current = mutableCounter;
await delay(10);
mutableCounter = current + 1; // Race Condition!
}
// 여러 번 호출
incrementMutable();
incrementMutable();
incrementMutable();
// 결과: 1 (예상: 3)
// 불변 상태
let immutableCounter = 0;
async function incrementImmutable() {
return immutableCounter + 1;
}
// 안전하게 처리
Promise.all([
incrementImmutable(),
incrementImmutable(),
incrementImmutable()
]).then(results => {
immutableCounter = Math.max(...results);
console.log(immutableCounter); // 3 (정확!)
});
Q4. Immer 라이브러리는 어떻게 동작하나요?
A: Proxy를 사용하여 불변 업데이트를 쉽게 만듭니다:
// Immer: 직접 수정하는 것처럼 보이지만 불변성 유지
// import { produce } from 'immer';
// ❌ 수동 불변 업데이트 (복잡)
const state = {
user: {
name: '철수',
profile: {
age: 25,
address: {
city: '서울'
}
}
},
posts: [
{ id: 1, title: '제목1' },
{ id: 2, title: '제목2' }
]
};
// 깊이 중첩된 필드 업데이트
const updated = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
address: {
...state.user.profile.address,
city: '부산'
}
}
}
};
// ✅ Immer 사용 (간단!)
// const updated = produce(state, draft => {
// draft.user.profile.address.city = '부산';
// });
// 동작 원리
function simpleProduce(baseState, recipe) {
// 1. Proxy로 draft 생성
const changes = {};
const draft = new Proxy(baseState, {
get(target, prop) {
return target[prop];
},
set(target, prop, value) {
changes[prop] = value; // 변경사항 기록
return true;
}
});
// 2. recipe 실행 (draft 수정)
recipe(draft);
// 3. 새 객체 생성 (변경사항 적용)
return { ...baseState, ...changes };
}
// 실전 사용 예시
// Redux Toolkit
// import { createSlice } from '@reduxjs/toolkit';
//
// const todosSlice = createSlice({
// name: 'todos',
// initialState: [],
// reducers: {
// addTodo: (state, action) => {
// // Immer 내장: 직접 push 가능!
// state.push(action.payload);
// },
// toggleTodo: (state, action) => {
// const todo = state.find(t => t.id === action.payload);
// if (todo) {
// todo.completed = !todo.completed;
// }
// }
// }
// });
// React useState
// import { useImmer } from 'use-immer';
//
// function Component() {
// const [state, updateState] = useImmer({
// user: { name: '', age: 0 },
// items: []
// });
//
// const updateName = (name) => {
// updateState(draft => {
// draft.user.name = name;
// });
// };
//
// const addItem = (item) => {
// updateState(draft => {
// draft.items.push(item);
// });
// };
// }
// 복잡한 업데이트
// const nextState = produce(currentState, draft => {
// // 여러 작업을 한 번에
// draft.user.name = '영희';
// draft.posts.push({ id: 3, title: '새 글' });
// draft.posts[0].title = '수정된 제목';
//
// // 조건부 업데이트
// if (draft.user.age < 20) {
// draft.user.status = 'minor';
// }
//
// // 필터링
// draft.posts = draft.posts.filter(p => p.id !== 2);
// });
// 성능 비교
const largeState = {
items: Array(10000).fill({ value: 1 })
};
// 수동 업데이트
console.time('Manual');
const manual = {
...largeState,
items: largeState.items.map((item, i) =>
i === 5000 ? { ...item, value: 2 } : item
)
};
console.timeEnd('Manual'); // ~5ms
// Immer
// console.time('Immer');
// const immer = produce(largeState, draft => {
// draft.items[5000].value = 2;
// });
// console.timeEnd('Immer'); // ~10ms
// Immer는 약간 느리지만, 코드가 훨씬 간단!
Q5. 복사 성능을 최적화하려면?
A: 필요한 만큼만 복사하고, 적절한 방법을 선택합니다:
// 복사 성능 최적화
// 1. 얕은 복사로 충분한 경우
const users = Array(10000).fill(null).map((_, i) => ({
id: i,
name: `User${i}`
}));
// ❌ 불필요한 깊은 복사
console.time('Deep');
const deepCopy = structuredClone(users);
console.timeEnd('Deep'); // ~50ms
// ✅ 얕은 복사로 충분
console.time('Shallow');
const shallowCopy = [...users];
console.timeEnd('Shallow'); // ~1ms
// 2. 부분 복사
const largeObject = {
metadata: { /* 큰 데이터 */ },
settings: { theme: 'dark' },
cache: { /* 매우 큰 데이터 */ }
};
// ❌ 전체 복사
const fullCopy = structuredClone(largeObject);
// ✅ 필요한 부분만
const partialCopy = {
...largeObject,
settings: { ...largeObject.settings }
};
// metadata와 cache는 참조 공유 (읽기만 함)
// 3. Copy-on-Write
class CopyOnWrite {
constructor(data) {
this.data = data;
this.isDirty = false;
}
read(key) {
return this.data[key];
}
write(key, value) {
if (!this.isDirty) {
// 첫 쓰기 시에만 복사
this.data = { ...this.data };
this.isDirty = true;
}
this.data[key] = value;
}
get() {
return this.data;
}
}
// 사용
const cow = new CopyOnWrite({ a: 1, b: 2 });
console.log(cow.read('a')); // 1 (복사 안 함)
cow.write('a', 10); // 이때 복사
cow.write('b', 20); // 이미 복사됨
// 4. 구조적 공유 (Persistent Data Structures)
// Immutable.js 같은 라이브러리
// import { Map } from 'immutable';
//
// const map1 = Map({ a: 1, b: 2, c: 3 });
// const map2 = map1.set('b', 20);
//
// // map1과 map2는 대부분의 구조를 공유
// // 변경된 부분만 새로 생성
// // 메모리 효율적!
// 5. 메모이제이션
const memoizedCopy = (() => {
const cache = new WeakMap();
return function(obj) {
if (cache.has(obj)) {
console.log('캐시 히트');
return cache.get(obj);
}
const copied = structuredClone(obj);
cache.set(obj, copied);
return copied;
};
})();
const data = { x: 1, y: 2 };
const copy1 = memoizedCopy(data); // 복사
const copy2 = memoizedCopy(data); // 캐시 (빠름)
// 6. Lazy Copy
class LazyProxy {
constructor(target) {
this.target = target;
this.copy = null;
}
get(key) {
return (this.copy || this.target)[key];
}
set(key, value) {
// 첫 수정 시 복사
if (!this.copy) {
this.copy = { ...this.target };
}
this.copy[key] = value;
}
finalize() {
return this.copy || this.target;
}
}
// 7. 벤치마크
function benchmark(fn, iterations = 1000) {
const start = performance.now();
for (let i = 0; i < iterations; i++) {
fn();
}
const end = performance.now();
return end - start;
}
const testData = { a: 1, b: { c: 2 } };
console.log('Spread:', benchmark(() => ({ ...testData })));
console.log('Assign:', benchmark(() => Object.assign({}, testData)));
console.log('JSON:', benchmark(() => JSON.parse(JSON.stringify(testData))));
console.log('structuredClone:', benchmark(() => structuredClone(testData)));
// 결과 (ms):
// Spread: ~5ms
// Assign: ~6ms
// JSON: ~15ms
// structuredClone: ~10ms
🎓 다음 단계
얕은 복사와 깊은 복사를 이해했다면, 다음을 학습해보세요:
🎬 마무리
얕은 복사와 깊은 복사는 JavaScript 개발의 필수 개념입니다:
- 얕은 복사: 최상위 레벨만 복사, 빠르지만 중첩 객체는 참조 공유
- 깊은 복사: 모든 레벨 복사, 느리지만 완전히 독립적
- 방법: Spread, Object.assign (얕은), structuredClone, JSON (깊은)
- 활용: React 불변성, Redux, 폼 핸들링, 캐싱
올바른 복사 방법을 선택하면 버그를 예방하고 성능을 최적화할 수 있습니다!