
JavaScript를 학습하다 보면 함수에서 매개변수가 어떻게 전달되는지에 대해 혼란을 겪는 경우가 많습니다. 특히 객체를 다룰 때 예상과 다른 결과가 나와서 당황하게 됩니다. 오늘은 JavaScript의 매개변수 전달 방식을 완전히 이해하고, 실무에서 활용할 수 있는 지식을 얻어보겠습니다.
먼저 다음 코드를 살펴보겠습니다.
function change(a, b, c) {
a = 'a changed'
b = { b: 'changed' };
c.c = 'changed';
}
let a = 'a unchanged';
let b = { b: 'unchanged' };
let c = { c: 'unchanged' };
change(a, b, c);
console.log(a, b, c); // 결과는?
이 코드의 실행 결과를 예측해보세요. 정답은 다음과 같습니다:
// 출력: "a unchanged" {b: 'unchanged'} {c: 'changed'}
왜 이런 결과가 나올까요? 이를 이해하기 위해서는 JavaScript의 매개변수 전달 방식을 깊이 있게 알아야 합니다.
JavaScript는 Call by Value 방식으로 매개변수를 전달합니다. 이는 함수 호출 시 값의 복사본이 전달된다는 의미입니다. 하지만 여기서 중요한 것은 "값"이 무엇인지 정확히 이해하는 것입니다.
| 타입 | 저장되는 값 | 함수 전달 시 복사되는 것 |
|---|---|---|
| 원시 타입 | 실제 데이터 값 | 데이터 값 자체 |
| 참조 타입 | 메모리 주소(참조) | 메모리 주소 값 |
// 원시 타입: 값 자체가 저장
let num = 42; // 변수에 42라는 값이 직접 저장
let str = "hello"; // 변수에 "hello"라는 값이 직접 저장
// 참조 타입: 메모리 주소가 저장
let obj = { x: 1 }; // 변수에는 객체가 있는 메모리 주소가 저장
let arr = [1, 2, 3]; // 변수에는 배열이 있는 메모리 주소가 저장
function changeString(str) {
str = 'changed'; // 새로운 값 할당
console.log('함수 내부:', str); // "changed"
}
let originalString = 'original';
changeString(originalString);
console.log('함수 외부:', originalString); // "original" (변경되지 않음)
메모리 관점에서 보기:
함수 호출 전:
originalString → "original"
함수 호출 시:
originalString → "original"
str (복사본) → "original"
함수 내부에서 str 변경 후:
originalString → "original" (변경 없음)
str (복사본) → "changed" (지역 변수만 변경)
function changeObject(obj) {
obj = { newProperty: 'new value' }; // 새로운 객체 할당
console.log('함수 내부:', obj); // {newProperty: 'new value'}
}
let originalObject = { oldProperty: 'old value' };
changeObject(originalObject);
console.log('함수 외부:', originalObject); // {oldProperty: 'old value'} (변경되지 않음)
메모리 관점에서 보기:
함수 호출 전:
originalObject → 주소A → {oldProperty: 'old value'}
함수 호출 시:
originalObject → 주소A → {oldProperty: 'old value'}
obj (복사본) → 주소A → {oldProperty: 'old value'}
함수 내부에서 obj에 새 객체 할당 후:
originalObject → 주소A → {oldProperty: 'old value'} (변경 없음)
obj (복사본) → 주소B → {newProperty: 'new value'} (새로운 주소 참조)
function modifyObject(obj) {
obj.property = 'modified'; // 기존 객체의 속성 변경
console.log('함수 내부:', obj); // {property: 'modified'}
}
let originalObject = { property: 'original' };
modifyObject(originalObject);
console.log('함수 외부:', originalObject); // {property: 'modified'} (변경됨!)
메모리 관점에서 보기:
함수 호출 전:
originalObject → 주소A → {property: 'original'}
함수 호출 시:
originalObject → 주소A → {property: 'original'}
obj (복사본) → 주소A → {property: 'original'}
함수 내부에서 객체 속성 변경 후:
originalObject → 주소A → {property: 'modified'} (같은 객체가 변경됨)
obj (복사본) → 주소A → {property: 'modified'} (같은 객체 참조)
// 배열 재할당 vs 배열 요소 변경
function arrayOperations(arr1, arr2) {
// 새로운 배열 할당 - 원본에 영향 없음
arr1 = [10, 20, 30];
// 기존 배열의 요소 변경 - 원본에 영향 있음
arr2.push(4);
arr2[0] = 100;
}
let array1 = [1, 2, 3];
let array2 = [1, 2, 3];
arrayOperations(array1, array2);
console.log('array1:', array1); // [1, 2, 3] (변경 없음)
console.log('array2:', array2); // [100, 2, 3, 4] (변경됨)
function modifyNestedObject(obj) {
// 최상위 속성 변경
obj.level1 = 'changed';
// 중첩 객체 속성 변경
obj.nested.level2 = 'also changed';
// 중첩 객체 자체 교체
obj.anotherNested = { newProp: 'new' };
}
let complexObject = {
level1: 'original',
nested: { level2: 'original nested' },
anotherNested: { oldProp: 'old' }
};
modifyNestedObject(complexObject);
console.log(complexObject);
// {
// level1: 'changed',
// nested: { level2: 'also changed' },
// anotherNested: { newProp: 'new' }
// }
function modifyFunction(fn) {
// 함수에 속성 추가 (함수도 객체이므로 가능)
fn.customProperty = 'added property';
// 함수 자체를 새로운 함수로 교체 (원본에 영향 없음)
fn = function() { console.log('new function'); };
}
function originalFunction() {
console.log('original function');
}
modifyFunction(originalFunction);
console.log(originalFunction.customProperty); // "added property"
originalFunction(); // "original function" (함수는 변경되지 않음)
// ❌ 문제가 될 수 있는 코드
function processUserData(user) {
user.isProcessed = true; // 원본 객체를 변경
user.processedAt = new Date();
// 다른 로직들...
return user;
}
const originalUser = { name: 'John', age: 30 };
const processedUser = processUserData(originalUser);
console.log(originalUser); // {name: 'John', age: 30, isProcessed: true, processedAt: ...}
// 원본 객체가 의도치 않게 변경됨!
해결책 1: 객체 복사
// ✅ 얕은 복사 (Shallow Copy)
function processUserData(user) {
const userCopy = { ...user }; // 스프레드 연산자 사용
// 또는: const userCopy = Object.assign({}, user);
userCopy.isProcessed = true;
userCopy.processedAt = new Date();
return userCopy;
}
// ✅ 깊은 복사 (Deep Copy) - 중첩 객체가 있는 경우
function processUserDataDeep(user) {
const userCopy = JSON.parse(JSON.stringify(user)); // 간단한 방법
// 또는 Lodash의 cloneDeep 사용
userCopy.isProcessed = true;
userCopy.processedAt = new Date();
return userCopy;
}
// ✅ structuredClone (최신 브라우저)
function processUserDataModern(user) {
const userCopy = structuredClone(user);
userCopy.isProcessed = true;
userCopy.processedAt = new Date();
return userCopy;
}
해결책 2: 불변성 라이브러리 사용
// Immer 라이브러리 사용 예시
import { produce } from 'immer';
function processUserData(user) {
return produce(user, draft => {
draft.isProcessed = true;
draft.processedAt = new Date();
});
}
// ❌ 원본 배열을 변경하는 메서드들
function processArray(arr) {
arr.push('new item'); // 원본 변경
arr.sort(); // 원본 변경
arr.reverse(); // 원본 변경
return arr;
}
// ✅ 새로운 배열을 반환하는 메서드들 사용
function processArraySafe(arr) {
return [...arr, 'new item'] // 스프레드로 복사 후 추가
.slice() // 복사본 생성
.sort() // 복사본 정렬
.reverse(); // 복사본 뒤집기
}
// ✅ 함수형 프로그래밍 스타일
function processArrayFunctional(arr) {
return arr
.concat(['new item']) // 새 배열 반환
.map(item => item) // 변형이 필요한 경우
.filter(item => item) // 필터링이 필요한 경우
.slice() // 복사본 생성
.sort()
.reverse();
}
// ✅ 기본값과 구조 분해를 활용한 안전한 함수
function updateUser(user = {}, updates = {}) {
// 구조 분해와 기본값을 활용하여 안전하게 처리
const {
name = 'Anonymous',
age = 0,
email = '',
...otherProps
} = user;
return {
name,
age,
email,
...otherProps,
...updates,
updatedAt: new Date()
};
}
// 사용 예시
const user = { name: 'John', age: 30 };
const updatedUser = updateUser(user, { age: 31 });
console.log(user); // 원본: {name: 'John', age: 30}
console.log(updatedUser); // 새 객체: {name: 'John', age: 31, updatedAt: ...}
// 성능 테스트 함수
function performanceTest() {
const largeObject = {
data: new Array(10000).fill(0).map((_, i) => ({ id: i, value: `item-${i}` })),
metadata: { created: new Date(), version: 1 }
};
console.time('Shallow Copy');
for (let i = 0; i < 1000; i++) {
const copy = { ...largeObject };
}
console.timeEnd('Shallow Copy');
console.time('Deep Copy (JSON)');
for (let i = 0; i < 1000; i++) {
const copy = JSON.parse(JSON.stringify(largeObject));
}
console.timeEnd('Deep Copy (JSON)');
console.time('structuredClone');
for (let i = 0; i < 1000; i++) {
const copy = structuredClone(largeObject);
}
console.timeEnd('structuredClone');
}
// 실행 결과 예시:
// Shallow Copy: 0.5ms
// Deep Copy (JSON): 45ms
// structuredClone: 15ms
// ✅ 필요한 부분만 복사
function updateUserEfficient(user, updates) {
// 변경이 필요한 속성만 복사
const needsUpdate = Object.keys(updates).some(key => user[key] !== updates[key]);
if (!needsUpdate) {
return user; // 변경이 없으면 원본 반환
}
return { ...user, ...updates };
}
// ✅ 지연 복사 (Lazy Copy)
function createLazyCopy(original) {
let copied = false;
let copy = original;
return new Proxy(original, {
set(target, property, value) {
if (!copied) {
copy = { ...original };
copied = true;
}
copy[property] = value;
return true;
},
get(target, property) {
return copied ? copy[property] : target[property];
}
});
}
// Java에서의 매개변수 전달
public class ParameterPassing {
public static void modifyPrimitive(int num) {
num = 100; // 원본에 영향 없음
}
public static void modifyObject(List<String> list) {
list.add("new item"); // 원본에 영향 있음
list = new ArrayList<>(); // 원본에 영향 없음
}
}
# Python에서의 매개변수 전달
def modify_data(num, lst, dct):
num = 100 # 원본에 영향 없음 (불변 타입)
lst.append("new") # 원본에 영향 있음 (가변 타입)
dct["key"] = "new" # 원본에 영향 있음 (가변 타입)
lst = [] # 원본에 영향 없음 (재할당)
dct = {} # 원본에 영향 없음 (재할당)
공통점: 대부분의 언어에서 매개변수 전달 방식은 유사합니다.
// ❌ 잘못된 State 업데이트
function UserProfile() {
const [user, setUser] = useState({ name: 'John', preferences: {} });
const updatePreferences = (newPrefs) => {
user.preferences = { ...user.preferences, ...newPrefs }; // 직접 변경!
setUser(user); // React가 변경을 감지하지 못함
};
// ...
}
// ✅ 올바른 State 업데이트
function UserProfile() {
const [user, setUser] = useState({ name: 'John', preferences: {} });
const updatePreferences = (newPrefs) => {
setUser(prevUser => ({
...prevUser,
preferences: { ...prevUser.preferences, ...newPrefs }
}));
};
// 또는 useCallback과 함께 사용
const updatePreferencesCallback = useCallback((newPrefs) => {
setUser(prevUser => ({
...prevUser,
preferences: { ...prevUser.preferences, ...newPrefs }
}));
}, []);
// ...
}
// Reducer에서 불변성 유지
function userReducer(state, action) {
switch (action.type) {
case 'UPDATE_PROFILE':
return {
...state,
profile: { ...state.profile, ...action.payload }
};
case 'ADD_SKILL':
return {
...state,
skills: [...state.skills, action.payload]
};
case 'REMOVE_SKILL':
return {
...state,
skills: state.skills.filter(skill => skill.id !== action.payload.id)
};
default:
return state;
}
}
// 객체 변경 감지 유틸리티
function createChangeTracker(obj, name = 'object') {
return new Proxy(obj, {
set(target, property, value) {
console.log(`${name}.${String(property)} changed from`, target[property], 'to', value);
target[property] = value;
return true;
},
deleteProperty(target, property) {
console.log(`${name}.${String(property)} deleted`);
delete target[property];
return true;
}
});
}
// 사용 예시
const trackedUser = createChangeTracker({ name: 'John', age: 30 }, 'user');
trackedUser.age = 31; // "user.age changed from 30 to 31"
// 함수 매개변수 변경 추적
function trackParameterChanges(fn, fnName) {
return function(...args) {
const originalArgs = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : arg
);
console.log(`${fnName} called with:`, originalArgs);
const result = fn.apply(this, args);
const modifiedArgs = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : arg
);
console.log(`${fnName} args after execution:`, modifiedArgs);
return result;
};
}
// 사용 예시
const trackedFunction = trackParameterChanges(
function modifyData(obj) {
obj.modified = true;
},
'modifyData'
);
// ✅ 함수 매개변수를 변경하지 말고 새로운 값 반환
function processData(data) {
return { ...data, processed: true };
}
// ✅ 배열 메서드 체이닝 시 불변성 유지
function transformArray(arr) {
return arr
.filter(item => item.active)
.map(item => ({ ...item, transformed: true }))
.sort((a, b) => a.priority - b.priority);
}
// ✅ 명확한 함수명과 문서화
/**
* 사용자 데이터를 업데이트합니다 (원본 객체는 변경되지 않음)
* @param {Object} user - 원본 사용자 객체
* @param {Object} updates - 업데이트할 속성들
* @returns {Object} 새로운 사용자 객체
*/
function updateUser(user, updates) {
return { ...user, ...updates, updatedAt: new Date() };
}
// ❌ 매개변수 직접 변경
function badProcessData(data) {
data.processed = true; // 원본 변경
return data;
}
// ❌ 배열 원본 변경 메서드 사용
function badTransformArray(arr) {
arr.push(newItem); // 원본 변경
arr.sort(); // 원본 변경
return arr;
}
// ❌ 예측하기 어려운 부수 효과
function confusingFunction(obj) {
obj.someProperty = 'changed'; // 예상치 못한 변경
// 다른 로직들...
return 'some result';
}
JavaScript의 매개변수 전달 방식을 이해하는 것은 예측 가능하고 안전한 코드를 작성하는 데 필수적입니다.
핵심 포인트 요약:
실무에서는 의도치 않은 객체 변경을 방지하기 위해 객체 복사, 불변성 라이브러리, 함수형 프로그래밍 패턴을 적극 활용하세요. 특히 React와 같은 라이브러리에서는 불변성이 성능 최적화와 직결되므로 더욱 중요합니다.
마치 요리할 때 원본 재료를 보존하면서 새로운 요리를 만드는 것처럼, 코드에서도 원본 데이터를 보존하면서 필요한 변경사항을 적용하는 습관을 기르는 것이 중요합니다.