JavaScript를 배우다 보면 원시(primitive) 값은 불변(immutable)하며, 그와 달리 비원시(non-primitive) 값은 가변(mutable)하다는 이야기를 자주 듣게 됩니다. 이 두 값의 유형(데이터 타입이라고도 부릅니다)의 차이를 제대로 이해하는 것은 매우 중요합니다. 이 개념을 알고 있으면 나중에 겪을 수 있는 수많은 혼란과 버그를 예방할 수 있기 때문입니다.
원시 값에는 다음과 같은 것들이 있습니다.
- 숫자(number)
- 문자열(string)
- 불리언(boolean)
반면 비원시 값은 다음과 같습니다.
- 객체(object)
- 함수(function)
가변(mutable)이란 값을 변경할 수 있다는 뜻입니다.
불변(immutable)이란 값을 변경할 수 없다는 뜻입니다.
비원시 값은 가변적이다
먼저 사람 이름 목록(배열)을 하나의 변수에 할당해 보겠습니다.
let peopleList = ['Arnold', 'Linda', 'Sylvester', 'Dolph']
이제 이 목록을 변이(mutate)해 보겠습니다. '변이'란 간단히 말해 변경을 의미합니다. 목록에서 Arnold를 Jean-Claude로 바꾸고 결과를 출력해 보죠.
let peopleList = ['Arnold', 'Linda', 'Sylvester', 'Dolph']
peopleList[0] = 'Jean-Claude'
console.log(peopleList)
성공했습니다! 기존 값을 성공적으로 변이한 것입니다. 여기서 기존 값은 배열(list), 더 정확히는 배열 객체(array object)였습니다.
즉, JavaScript에서 비원시 값은 가변적(변경 가능)입니다.
원시 값은 불변적이다
이번에는 문자열 값을 변수에 할당해 보겠습니다.
let person = 'Ernold'
앗, 이름 첫 글자를 A 대신 E로 써서 Arnold의 철자를 틀렸네요. 빠르게 수정하고 결과를 출력해 봅시다.
let person = 'Ernold'
person[0] = 'A'
console.log(person) // 출력: Ernold
잠깐, 왜 "Ernold"가 그대로 출력되는 걸까요? 분명 E를 A로 바꿨는데 말이죠!?
아닙니다. 우리가 변경하려는 값은 문자열(string), 즉 원시 값이기 때문입니다. 모든 원시 값은 불변(immutable)하여 변경할 수 없습니다.
가변적인 것은 오직 비원시 값(객체와 함수)뿐입니다.
헷갈리나요?
JavaScript 학습 초기에 이 부분에서 혼란스러워하는 것은 전혀 이상하지 않습니다. 위 예제들은 표면적으로, 그리고 어떤 상황에서는 실제로도 비슷하게 동작하기 때문입니다.
앞서 만든 배열에서 첫 번째 이름을 출력해 보겠습니다.
let peopleList = ['Arnold', 'Linda', 'Sylvester', 'Dolph']
console.log(peopleList[0]) // 출력: "Arnold"
당연한 결과입니다. "Arnold"가 출력됩니다.
이번에는 앞서 만든 문자열(철자가 틀린 이름)의 첫 글자를 출력해 보겠습니다.
let person = 'Ernold'
console.log(person[0]) // 출력: ??
결과가 어떻게 될 것 같으신가요?
...
...
정상적으로 동작했습니다! "E"가 출력됩니다!
위 코드가 동작하지 않을 것이라고 예상하셨나요?
여기서 중요한 교훈은, 원시 값에도 비원시 값과 유사한 방식으로 접근(access)할 수는 있지만, 그 값을 변경(mutate)할 수는 없다는 점입니다. 이것이 바로 핵심 차이입니다 (접근 vs. 변경).
JavaScript에서 원시 값은 읽기 전용(read-only)인 반면, 비원시 값(객체/배열, 함수)은 읽기와 쓰기가 모두 가능합니다.
그래서 앞서 배열의 첫 번째 요소를 Arnold에서 Jean-Claude로 변경할 수는 있었지만, 단순한 철자 오류조차 "Ernold"에서 "Arnold"로 고칠 수 없었던 것입니다. 문자열, 즉 원시 값은 불변이기 때문입니다.
변수(Variable)와 값(Value)의 차이
(일부 독자분들은 이미 눈치채셨을 수도 있습니다.)
원시 값은 불변이라고 결론 내렸지만, 저는 여전히 "Ernold"라는 철자 오류를 꼭 고치고 싶습니다. 새로운 방법을 시도해 볼까요?
아래 코드를 살펴보세요. (꼼꼼하게 읽어보세요.)
let person = 'Ernold'
person = 'Arnold'
console.log(person) // 출력: ??
지금까지 배운 원시 값(예: 문자열)의 특성을 고려할 때, 위 코드는 Arnold를 출력할까요, 안 할까요?
...
(잠시 생각해 보세요.)
...
정답은 Arnold가 출력된다는 것입니다!
그 이유를 이미 아는 분들도 계시겠지만, 모르셨더라도 걱정하지 마세요. 곧 "아하!" 하는 순간이 올 겁니다. 아래 문단을 주의 깊게 읽어주세요.
위 코드가 동작하는 이유는 우리가 문자열 값을 변이(mutate)한 것이 아니기 때문입니다. 문자열 자체는 손대지도 않았습니다. 대신 person 변수에 새로운 문자열 값 'Arnold'를 재할당함으로써, 변수가 더 이상 기존의 'Ernold' 문자열을 참조(가리키지 않도록 만든 것입니다.
원시 값은 변경할 수 없지만, 변수는 변경할 수 있습니다!
변수는 값이 아니므로, 가변/불변 규칙이 동일하게 적용되지 않습니다. 변수는 단지 값을 가리킬(pointer) 뿐입니다.
우리는 기존 문자열 값 'Ernold'를 직접 건드린 적이 없습니다. 그저 person 변수에게 'Arnold'라는 다른 문자열을 가리키라고 지시했고, 이후 console.log()로 결과를 출력했을 뿐입니다.
이 내용이 지겹게 느껴질 수도 있지만, JavaScript가 어떻게 동작하는지에 대한 이 통찰은 반드시 갖추셔야 합니다. 반복은 여러분의 친구입니다.
참고: 이 튜토리얼 전체에서 사용한 변수 선언 키워드는 let입니다. 앞선 코드 예제에서 확인했듯이 let으로 선언한 변수는 재할당이 가능합니다. 만약 const 변수를 사용했다면 상황이 달라졌겠지만, 이 주제는 별도의 튜토리얼에서 다루도록 하겠습니다.
정리하면
JavaScript에서는:
- 비원시 값은 참조·접근할 수 있고, 변이(mutation)도 가능합니다.
- 원시 값은 참조·접근할 수 있지만, 변이할 수 없습니다.
- 변수와 값은 서로 다른 개념이며, 적용되는 규칙도 다릅니다.