본문 바로가기

JavaScript

자바스크립트에서 불변성(Immutability)이란

사실 나는 불변성이라는 개념을 자바스크립트 언어 자체를 공부할 때 말고 리액트를 공부하면서 state를 변경할 때 처음 접했었다.

그냥 단순하게 '새로운 값을 할당하지 않으면 리렌더링이 안되니까 항상 새로운 값으로 줘야지!'의 개념에서 그쳤기 때문에 불변성을 지키지 않은 코드로 버그를 발생시키기도 했고, 디버깅할 때도 애를 먹었다.

불변성 개념은 특별히 자바스크립트 언어에 한정되는 것이 아니고, 다른 언어에서도 존재하는 개념이다. 얕은 복사(shallow copy)와 깊은복사(deep copy)의 개념까지 어느정도 이어지기 때문에 꼭 숙지하는 것이 좋다.

불변성이란 무엇인지와 왜 불변성을 지키는 것이 중요한지에 대해 알아보자.


변수에 값 할당하기

다들 알고 있겠지만 변수에 값을 할당한다는 것은 값의 메모리의 주소를 가리키는 것(참조) 하는 것임을 유의하며 살펴보자.

let a = 'hello world!';

위의 구문은 'hello world!'라는 값이 메모리에 생성되고, a라는 변수가 이 메모리의 주소를 가리키도록(참조하도록) 하였다.

Immutable type

불변성(Immutability)란 말그대로 변하지 않는 것을 의미한다. 불변 데이터는 한번 생성되고나면 그 뒤에는 변할수 없다.

자바스크립트에는 원시 타입(primitive type)으로는

  • Boolean,
  • String
  • Number
  • Null
  • undefined
  • Symbol

이 있으며 이 원시 타입은 불변한다. 이 값은 메모리영역 안에서 변경이 불가능하며 변수에 할당할 때 완전히 새로운 값이 만들어져 재 할당된다.

let name = 'foo';
name = 'bar';

첫번째 구문부터 살펴보자. 첫번째 구문은 'foo'라는 string 타입의 값이 메모리에 생성되고, name은 메모리에 생성된 'foo'를 가리킨다.

두번째 구문에서는 'bar'라는 새로운 string 타입의 값이 새로 생성되고, name은 다시 'bar'를 가리킨다.

let name = 'foo';
let newName = name;
name = 'bar';

console.log(newName); //foo
console.log(name); //bar

첫번째 구문은 'foo'라는 string 타입의 값이 메모리에 생성되고, name'foo' 메모리 값을 가리킨다.
두번째 구문은 newNamename이 가리키고 있는 주소('foo' string 값)을 가리킨다.
세번째 구문은 'bar'라는 string 타입의 값이 메모리에 생성되고, name'bar' 메모리 값을 가리킨다.

세번째 구문까지 실행되었을 때 newName은 여전히 'foo'를 가리키고 있으며 name'bar'를 가리키고 있다.


Mutable type

자바스크립트에서는 위에 나열한 immutable type을 제외하고 모든 값은 객체(Object)타입이며 변할 수 있는 값이다. 객체는 새로운 값이 만들어지지 않고 직접적으로 변경이 가능하다.

let x = {
  name: 'junimo'
};

let y = x;

x.name = 'ken';

console.log(y.name); // ken
console.log(x === y) // true

첫번째 구문은 x에 새로 만든 객체를 할당한다.

두번째 구문은 yx가 가리키고 있는 객체의 주소를 똑같이 가리킨다.

세번째 구문에서는 x가 가리키고 있는 객체의 name'ken'이라는 string 데이터를 다시 할당한다.

y.name을 찍어보면 ken이 출력되는데, yx가 가리키고있는 값, 원래는 {name: 'junimo'} 었다가 지금은 {name: 'ken'} 으로 변화한 데이터의 주소를 똑같이 참조하고 있기 때문이다.

따라서 마지막 구문에서도 xy는 똑같은 데이터를 가리키고 있기 때문에 true값이 출력된다.

이번에는 배열을 보자.
위에서 언급했다시피 자바스크립트에서 immutable type을 제외하고 모든 값은 object 타입이므로 배열도 엄밀히 말하면 object타입이지만, 이해를 돕기위해 아래 예시도 살펴보겠다.

let x = ['foo'];
let y = x;

x.push('bar');

console.log(y); // ['foo', 'bar']
console.log(x === y) // true

첫번째 구문에서 배열을 생성한 후 x에 할당한다.

두번째 구문은 x가 참조하고있는 배열을 y도 참조하도록 할당한다.

세번째 구문은 push() 함수를 사용하여 x가 참조하고 있는 데이터를 ['foo'] 에서 ['foo', 'bar']로 변화시킨다.

x가 가리키고 있는 데이터는 최종적으로 push함수를 통해 ['foo', 'bar']로 바뀌었고, xy는 동일한 데이터를 가리키고있으므로 console.log(y)로 출력해보면 ['foo', 'bar']가 출력됨을 알 수 있다.

위의 예시에서는 코드의 흐름이 한눈에 보이기 때문에 그닥 불변성의 중요성을 못느낄 수 있겠지만, 만약 x.push('bar');의 라는 코드가 다른 어딘가에 존재하고, 나는 let x = ['foo']; 만 보고 코드를 짠다면 x가 더이상 ['foo']라는 값이 아니라는 것을 알기 쉽지 않기 때문에 예상한대로 코드가 돌아가지 않을 확률이 높다.

그리고 코드가 예상한대로 돌아가지 않는다는 것을 알았을 때에는, 어디서부터 잘못된건지 디버깅하는 것도 쉽지 않다.

Array의 내장함수에는 기존의 데이터를 변경시키는 함수와 새로운 배열을 만들어서 반환하는 함수가 있으므로 의도한 바가 아니라면 불변성을 지키기 위해 이를 꼭 구별하여 사용하는 것이 좋다.

배열 값을 변경할때 주로 새로운 배열을 반환하는 map()이나 [...array, 'bar'] 이런식으로 전개 연산자를 사용하는 것도 기존의 값을 변경시키지 않고 불변성을 지키기 위함이다.


잠깐, const에 대해서도 짚고 넘어가기

위의 예시에서 볼 수 있듯이 let 변수는 어떠한 값을 가리키다가 다시 다른 값의 주소를 가리키는 재 할당이 가능하다.

하지만 const재선언 및 재할당이 불가능하다.

헷갈릴 수 있는 개념이 const로 선언한 변수는 값이 불변한다고 생각할 수 있는데,
const는 값에 대한 '참조'(가리키는 것)가 한번 변수에 할당되고 나면 변할 수 없음을 의미하는 것이지 const 변수가 참조하고 있는 '값'이 불변한다는 것을 의미하지 않는다.

const car = {
  owner : "junimo",
  type : "truck"
};
car = {
  owner : "ken",
  type : "truck"
};
//Uncaught TypeError: Assignment to constant variable.

위의 구문은 car변수에 object를 생성하여 할당한 뒤, 다시 다른 object를 생성하여 할당하려고하니 error가 발생한다.

const car = {
  owner : "junimo",
  type : "truck"
};
car.owner = "ken";
console.log(car); // {owner: "ken", type: "truck"}

위의 예시에서는 car에 object를 할당하고 난 뒤에 car.owner 값을 변경하였다. console.log로 찍어보면 owner 데이터가 바뀐 것을 알 수 있다.

엄밀히 말하면 'ken' 데이터가 새롭게 생성되고 car.owner에 재할당 되었다. 따라서 객체 내부의 깊은 곳 까지의 재할당은 제어하지 않는다는 것을 유의해야한다.


불변성을 지키는 것이 왜 중요한가요?

우리는 개발자로서 유지보수가 가능하고 가독성이 좋은 코드를 작성해야한다.

불변성을 지키지 않는다면 위의 예시처럼 사용할 데이터가 어디서 어떻게 바뀌어가는지 흐름을 쫓아가기 어렵고, 이는 곧 예기치 못한 side effects나 버그로 이어지게 만든다.

불변성을 지켜 명시적으로 작성된 코드는 다른 개발자가 코드를 보았을 때도 내가 모르는 어딘가에서 데이터가 변화했을거야! 라는 불필요한 의심없이 코드를 읽는 그대로 흐름을 따라가면서 이해할 수 있도록 돕는다.

따라서 불변성을 지키면서 데이터를 변화시킨다면, 예상가능하고 신뢰할 수 있는 코드가 될 수 있다.

애초에 불변성을 지켜야 한다는 것은 리액트가 만들어낸 새로운 컨셉이 아니라 불변성이라는 개념을 지켜가면서 state와 props를 이용할 수 있도록 하는 아이디어를 리액트에 녹여낸 것이다.

immutable한 값을 state나 props로 사용한다면 어떠한 일련의 이벤트를 통해 새롭게 만들어진 object가 변수에 할당 되는 것을 볼 수 있다. 이러한 새로운 값에 대한 참조는 기존의 값에서 값이 어떻게 변화하는지 추적하기가 쉽게 만들어준다.

단순한 불변성 개념 그 자체보다는 왜 불변성을 지켜가며 코드를 작성해야하는지 대해 좀 더 포커스를 두고 생각한다면 더 좋은 코드를 작성할 수 있을 것이다.