인강 & 책 스터디 노트/Udemy 리액트 완벽 가이드 2024

리액트에서 불변성을 유지해야 하는 이유 -기본형과 참조형이란

thisisamrd 2024. 8. 21.

 

 

 

유데미의 React - The Complete Guide 2024 (incl. Next.js, Redux) 강좌를 듣다가 중요한 개념 중 하나인 불변성에 대해 복습하고 가려고 합니다. 해당 강좌는 2024년 기준 81강에 해당합니다. 본 포스팅은 해당 강의 이외에도 아래 페이지를 참고하였습니다.

 

 

 

 

 

기본형과 참조형

먼저, 자바스크립트에는 기본형과 참조형이라는 두 가지 데이터 유형이 있습니다. 리액트의 불변성을 이해하기 위해서는 여기서부터 시작을 해야 합니다.

 

 

 

기본형

숫자, 문자열, 불리언, undefined, null 등이 기본형에 속합니다. 기본형 값은 간단한 데이터 타입으로, 메모리의 스택에 저장됩니다. 기본형 값은 복사될 때 실제 값이 복사됩니다. 예를 들어, 숫자나 문자열을 변수에 할당할 때, 그 값 자체가 복사되어 새로운 변수에 저장됩니다.

 

let age = 28;
let newAge = age; // newAge는 28이라는 값이 복사된 상태

 

 

 

참조형

객체와 배열이 참조형에 속합니다. 참조형 값은 메모리의 힙(Heap)에 저장되며, 해당 값의 실제 데이터는 힙에 저장되고, 스택에는 그 값을 가리키는 포인터(참조값)가 저장됩니다. 이 때문에 객체나 배열을 다른 변수에 할당할 때, 값 자체가 복사되는 것이 아니라, 해당 값의 메모리 주소(참조값)가 복사됩니다.

 

let person = { name: 'Max' };
let newPerson = person; // person 객체의 포인터(참조값)가 복사됨

 

 

 

스택 vs 힙

 

스택은 고정된 크기의 데이터를 빠르게 저장하고 관리하는 메모리 공간으로, 주로 함수 호출과 기본형 데이터를 저장합니다. 은 크기가 변할 수 있는 데이터를 저장하는 유연한 메모리 공간으로, 객체나 배열 같은 참조형 데이터가 여기에 저장됩니다.

 

힙에서는 데이터가 동적으로 생성되고, 더 이상 사용되지 않는 데이터는 가비지 컬렉터(Garbage Collector)에 의해 자동으로 제거됩니다. 가비지 컬렉터는 메모리 누수를 방지하기 위해, 참조되지 않는 객체나 데이터를 찾아서 메모리에서 정리해주는 역할을 합니다. 스택은 함수 실행과 관련된 데이터를 관리하고, 힙은 변화할 수 있는 데이터를 관리하는 데 사용됩니다.

 

 

 

 

 

 

참조형의 이상한 동작

 

위의 참조형 데이터에서 설명한 것처럼, 객체나 배열을 다른 변수에 할당하면 실제 데이터가 아니라 참조값(포인터)이 복사됩니다. 이로 인해 두 변수가 같은 객체나 배열을 가리키게 됩니다. 따라서, 하나의 변수를 통해 객체의 속성을 변경하면, 다른 변수에서도 그 변경이 반영됩니다.

 

let person = { name: 'Max' };
let newPerson = person;
newPerson.name = 'Anna';

console.log(person.name); // 'Anna'가 출력됩니다.

 

이 예에서 newPerson의 name을 변경하면, person의 name도 함께 변경됩니다. 왜냐하면 person과 newPerson이 동일한 객체를 가리키고 있기 때문입니다.

 

 

 

불변성과 참조형

불변성은 이러한 참조형 데이터가 변하지 않도록 관리하는 중요한 개념입니다. 객체나 배열을 직접 수정하는 대신, 새로운 객체나 배열을 생성하고, 기존 데이터의 복사본에 변경을 가한 후 새로운 객체나 배열을 반환하는 방식으로 상태를 관리해야 합니다.

이렇게 하면, 이전 상태는 그대로 유지되고, 새로운 상태만이 반영되기 때문에, 예기치 않은 버그를 예방할 수 있습니다.

 

 

 

 

불변성을 유지하는 방법

배열 복사

slice() 메서드 사용: slice() 메서드는 배열의 모든 요소를 복사하여 새로운 배열을 만듭니다.

let hobbies = ['Sports', 'Cooking'];
let copiedHobbies = hobbies.slice();

 

전개 연산자(Spread Operator) 사용: ES6에서 도입된 전개 연산자 ...를 사용해 배열의 요소를 새로운 배열로 복사할 수 있습니다.

let hobbies = ['Sports', 'Cooking'];
let copiedHobbies = [...hobbies];

 

 

 

객체 복사

Object.assign() 메서드 사용: Object.assign() 메서드는 기존 객체의 속성을 새 객체로 복사하여 새로운 객체를 생성합니다.

let person = { name: 'Max' };
let copiedPerson = Object.assign({}, person);

 

 

 

전개 연산자(Spread Operator) 사용: 객체에서도 전개 연산자를 사용하여 속성을 새로운 객체로 복사할 수 있습니다.

let person = { name: 'Max' };
let copiedPerson = { ...person };

 

 

 

 

 

 

 

깊은 복사

위에서 설명한 방법들은 객체나 배열의 최상위 속성만 복사하는 얕은 복사를 만듭니다. 하지만, 객체나 배열 안에 중첩된 객체나 배열이 있을 경우, 이를 복사하려면 깊은 복사가 필요합니다. 깊은 복사는 중첩된 모든 데이터 구조를 복사하여 완전히 새로운 객체나 배열을 생성하는 것을 의미합니다.

 

 

JSON을 이용한 깊은 복사: 가장 간단한 방법 중 하나는 객체를 JSON 문자열로 변환한 다음 다시 객체로 변환하는 방법입니다. 이 방법은 깊은 복사를 가능하게 하지만, 몇 가지 제한이 있습니다. 예를 들어, Date 객체나 undefined 값, 함수 등은 JSON으로 변환할 수 없어요.

 

const original = { name: 'Max', hobbies: ['Sports', 'Cooking'] };
const deepCopy = JSON.parse(JSON.stringify(original));

deepCopy.hobbies.push('Music');
console.log(original.hobbies); // ['Sports', 'Cooking']
console.log(deepCopy.hobbies); // ['Sports', 'Cooking', 'Music']

 

 

 

재귀적으로 복사하는 방법: 함수를 이용해서 객체나 배열을 재귀적으로 복사하는 방법도 있습니다. 이 방법은 원본 객체나 배열 안에 있는 모든 중첩된 객체와 배열까지도 복사합니다.

 

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  if (Array.isArray(obj)) {
    const copy = [];
    for (let i = 0; i < obj.length; i++) {
      copy[i] = deepClone(obj[i]);
    }
    return copy;
  }

  const copy = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepClone(obj[key]);
    }
  }
  return copy;
}

const original = { name: 'Max', hobbies: ['Sports', 'Cooking'] };
const deepCopy = deepClone(original);

deepCopy.hobbies.push('Music');
console.log(original.hobbies); // ['Sports', 'Cooking']
console.log(deepCopy.hobbies); // ['Sports', 'Cooking', 'Music']

 

 

 

Lodash 라이브러리 사용: Lodash 라이브러리의 _.cloneDeep 함수를 사용하면 쉽게 깊은 복사를 할 수 있습니다.

const _ = require('lodash');

const original = { name: 'Max', hobbies: ['Sports', 'Cooking'] };
const deepCopy = _.cloneDeep(original);

deepCopy.hobbies.push('Music');
console.log(original.hobbies); // ['Sports', 'Cooking']
console.log(deepCopy.hobbies); // ['Sports', 'Cooking', 'Music']

 

 

 

불변성을 유지하기 위해서 깊은 복사를 해야만 할까?

불변성을 유지하기 위해 항상 깊은 복사를 해야 하는 것은 아닙니다. 깊은 복사가 필요한 상황은 주로 중첩된 객체나 배열이 포함된 상태를 관리할 때입니다.

객체나 배열 안에 또 다른 객체나 배열이 있을 때, 얕은 복사를 하면 최상위 레벨만 복사되고, 중첩된 데이터는 여전히 원본을 참조하게 됩니다. 이 경우, 중첩된 데이터도 독립적으로 복사해야 불변성이 유지됩니다.

 

하지만 객체나 배열이 단순한 값(숫자, 문자열 등)만 포함하고 있을 때는 얕은 복사로도 충분히 불변성을 유지할 수 있습니다. 

댓글