세 개의 파일을 뒤지며 버그를 추적하다가, 아무 해가 없는 헬퍼 함수가 사용자의 이름을 바꿨다는 사실을 발견했습니다. 원본은 건드린 적이 없는데 말이죠. 적어도 그렇게 생각했습니다. JavaScript에서 변수 하나를 다른 변수에 할당하는 것이 항상 예상대로 작동하는 것은 아닙니다. 이 언어는 데이터를 두 가지 저장 전략으로 나누는데, 어떤 전략을 사용 중인지 잊어버리는 순간 소리 없는 변이(mutation)가 프로덕션 코드에 침투하게 됩니다.
이를 방지하려면 엔진이 인식하는 방식 그대로 원시 값(primitive values)과 객체(objects)의 차이를 이해해야 합니다.
원시 값(Primitives): 실제 복사본
원시 값은 더 이상 나눌 수 없는 단일 데이터입니다. 더 작은 JavaScript 값으로 분해할 수 없습니다. 이 언어는 String, Number, Boolean, Undefined, Null, Symbol, BigInt의 일곱 가지 원시 타입을 정의합니다.
원시 값은 원자적(atomic)이기 때문에 일반적으로 변수 바인딩 내부에 직접 저장됩니다. 원시 변수 하나를 다른 변수에 복사하면, 엔진은 실제 데이터를 복제합니다. 각 변수는 메모리에 자신만의 독립적인 슬롯을 할당받습니다.
let a = "Alina";
let b = a;
b = "Ali";
console.log(a); // "Alina"
console.log(b); // "Ali"
여기서 a는 그대로 유지됩니다. b에 값을 재할당하면 완전히 새로운 값이 생성되어 b가 이를 가리키게 되지만, a는 여전히 원래의 문자열을 유지합니다. 이러한 동작을 값에 의한 복사(copy by value)라고 합니다. 이는 숫자, 불리언, 심볼 및 나머지 원시 타입 계열에서도 동일하게 작동합니다. 원시 값을 함수에 전달하거나, 재할당하거나, 반환하더라도 다른 변수에 미치는 부작용(side effects)을 걱정할 필요가 없습니다.
객체(Objects): 공유된 주소
객체는 다릅니다. 객체는 여러 데이터 조각을 하나로 묶는 복합 컨테이너입니다. 이 범주에는 일반 객체(plain objects), 배열(arrays), 함수(functions), 날짜(dates) 및 기타 모든 비원시 타입이 포함됩니다. 이러한 구조는 크기가 커지거나 중첩될 수 있기 때문에, JavaScript는 변수 내부에 객체 전체를 저장하지 않습니다. 대신 변수는 참조(reference), 즉 다른 곳에 저장된 실제 데이터를 가리키는 메모리 주소를 보유합니다.
객체를 새로운 변수에 할당하면 엔진은 객체가 아닌 주소를 복사합니다. 이제 두 변수는 정확히 같은 집을 가리키게 됩니다.
const user = { name: "Alina" };
const copy = user;
copy.name = "Ali";
console.log(user.name); // "Ali"
copy.name을 변경하면 user.name도 함께 변경되는데, 이는 두 이름이 동일한 기본 객체를 가리키기 때문입니다. 이것이 참조에 의한 복사(copy by reference)입니다. 배열에서도 똑같은 놀라운 일이 발생합니다.
const scores = [82, 91, 74];
const backup = scores;
backup.push(88);
console.log(scores); // [82, 91, 74, 88]
메모리에는 단 하나의 배열만 존재합니다. scores와 backup은 단지 그 배열을 가리키는 두 개의 표지판일 뿐입니다.
const 키워드는 혼란을 가중시킵니다. const로 객체를 선언하면 변수 바인딩이 고정되어 새로운 주소를 가리키게 할 수 없습니다. 하지만 객체 자체를 고정하는 것은 아닙니다.
const settings = { theme: "dark" };
settings.theme = "light"; // Works perfectly.
settings = { theme: "dark" }; // TypeError
개발자들은 흔히 const가 불변성(immutability)을 보장할 것이라고 기대합니다. 하지만 그렇지 않습니다. const는 참조의 재할당을 막을 뿐입니다. 만약 내용물까지 고정하고 싶다면, 의도를 가지고 복사해야 합니다.
진짜 버그가 숨어 있는 곳
참조 버그는 명백한 변수 재할당의 형태로 나타나는 경우가 드뭅니다. 대신 함수 호출 내부에 숨어 있습니다.
function addTimestamp(record) {
record.timestamp = Date.now();
return record;
}
const original = { id: 1 };
addTimestamp(original);
console.log(original.timestamp); // A number now exists here. Oops.
매개변수 record는 참조의 복사본을 전달받았습니다. 함수 내부에서 발생하는 모든 속성 변이(mutation)는 호출자의 객체에 직접 기록됩니다. 함수는 단순한 변환 작업처럼 보였지만, 실제로는 스코프 경계를 넘어 상태를 유출(leak)했습니다.
이 패턴은 React와 같은 UI 프레임워크에서 특히 치명적인데, 여기서는 상태 업데이트가