4 분 소요

불변 객체를 만드는 간단한 방법

  • 불변 객체는 최근의 React, Vue.js, Angular 등의 라이브러리나 프레임워크에서뿐만 아니라 함수형 프로그래밍, 디자인 패턴 등에서도 매우 중요한 기초가 되는 개념이다.
  • 참조형 데이터는 데이터 자체가 아닌 내부 프로퍼티를 변경할 때만 가변이 성립된다.
  • 데이터 자체를 변경하고자 하면 참조형 데이터는 기본형 데이터와 마찬가지로 변하지 않는다.
  • 그렇다면 내부 프로퍼티를 변경할 필요가 있을 때마다 매번 새로운 객체를 만들어 재활당하기로 규칙을 정하거나, 자동으로 새로운 객체를 만드는 도구를 활용한다면 객체 역시 불변성을 확보할 수 있다. (immutable.js, immer.js, immutability-helper등의 라이브러리와 spread operator, Object.assign 메서드)

어떤 상황에서 불변 객체가 필요할까?

  • 값으로 전달받은 객체에 변경을 가하더라도 원본 객체는 변하지 않아야 하는 경우, 불변 객체가 필요하다.

(객체의 가변성에 따른 문제점)

var user = {
  name: "Jaenam",
  gender: "male",
};

var chageName = function (user, newName) {
  var newUser = user;
  newUser.name = newName;
  return newUser;
};

var user2 = changeName(user, "Jung");

if (user !== user2) {
  console.log("유저 정보가 변경되었습니다.");
}

console.log(user.name, user2.name); // Jung Jung
console.log(user === user2); // true
  • 이 예제는 객체의 가변성으로 인한 문제점을 보여주는 예시다.
  • 1번째 줄에서 user 객체를 생성하고, 12번째 줄에서 user 객체의 name 프로퍼티를 ‘Jung’으로 바꾸는 함수 changeName을 호출해서 그 결과를 user2에 담고 있다ㅏ.
  • 14번째 줄에서 user 변수와 user2 변수가 서로 같지 않다는 조건이 성립하면 15번째 줄의 내용이 출력 되겠지만, 실제로는 출력 없이 통과한다.
  • 이는 user와 user2가 동일하다고 하는 것이다.

(객체의 가변성에 따른 문제점의 해결 방법)

var user = {
  name: "Jaenam",
  gender: "male",
};

var chageName = function (user, newName) {
  return {
    name: newName,
    gender: user.gender,
  };
};

var user2 = changeName(user, "Jung");

if (user !== user2) {
  console.log("유저 정보가 변경되었습니다.");
}

console.log(user.name, user2.name); // Jaenam Jung
console.log(user === user2); // false
  • changeName 함수가 새로운 객체를 반환하도록 수정했다. 이제 user와 user2는 서로 다른 객체이므로 안전하게 변정 전과 변경 후를 비교할 수 있다.
  • changeName 함수에는 새로운 객체를 만들면서 변경할 필요가 없는 기존 객체의 프로퍼티를 하드코딩으로 입력했다.
  • 이는 변경해야할 정보들이 많아지게 되면, 수고가 늘어나는 상황이다.
  • 대상 객체의 프로퍼티 개수와 상관없이 모든 프로퍼티를 복사하는 함수를 만드는 편이 더 좋을듯 하다.

(기존 정보를 복사해서 새로운 객체를 반환하는 함수(얕은복사))

var copyObject = function (target) {
  var result = {};
  for (var prop in target) {
    result[prop] = target[prop];
  }
  return result;
};
  • copyObject는 for in 문법을 이용해 result 객체에 target 객체의 프로퍼티들을 복사하는 함수이다.

(copyObject를 이용한 객체 복사)

var copyObject = function (target) {
  var result = {};
  for (var prop in target) {
    result[prop] = target[prop];
  }
  return result;
};

var user = {
  name: "Jaenam",
  gender: "male",
};

var user2 = copyObject(user);
user2.name = "Jung";

if (user !== user2) {
  console.log("유저 정보가 변경되었습니다.");
}

console.log(user.name, user2.name); // Jaenam Jung
console.log(user === user2); // false
  • 협업하는 모든 개발자들이 user객체 내부의 변경이 필요할 때는 copyObject 함수를 사용하기로 합의 하면 user 객체가 불변 객체라고 볼 수 있다.
  • 하지만 이런 규칙들은 깨지기 쉽다. 따라서 프로퍼티 변경을 할 수 없게끔 시스템적으로 제약을 거는 편이 안전하다.
  • 또한 얕은 복사만 하는 부분에서 아쉬운 부분이 있다.

얕은 복사와 깊은 복사

  • 얕은 복사는 바로 아래 단계의 값만 복사하는 방법이다.
  • 깊은 복사는 내부의 모든 값들을 하나하나 차장서 전부 복사하는 방법이다.

  • 얕은 복사를 한다는 것은 참조형 데이터가 저장된 데이터를 복사할 때 그 주솟값만 복사한다는 의미이다.
  • 해당 프로퍼티에 대해 원본과 사본이 모두 동일한 참조형 데이터의 주소를 가리키게 된다.
  • 사본을 바꾸면 원본도 바뀌고, 원본을 바꾸면 사본도 바뀝니다.
var user = {
  name: "Jaenam",
  urls: {
    portfolio: "http://github.com/abc",
    blog: "http://blog.com",
    facebook: "http://facebook.com/abc",
  },
};

var copyObject = function (target) {
  var result = {};
  for (var prop in target) {
    result[prop] = target[prop];
  }
  return result;
};

var user2 = copyObject(user);

user2.name = "Jung";
console.log(user.name == user2.name); // false

user.urls.portfolio = "http://portfolio.com";
console.log(user.urls.portfolio === user2.urls.portfolio); // true

user2.urls.blog = "";
console.log(user.urls.blog === user2.urls.blog); // true
  • 11번째 줄에서 사본인 user2의 name 프로퍼티를 바꿔도 user의 name 프로퍼티는 바뀌지 않았다.
  • 14번째 줄과 17번째 줄에서는 원본과 사본 중 어느 쪽을 바꾸더라도 다른 한쪽의 값도 함께 바뀐 것을 확인할 수 있었다.
  • 즉 user 객체에 직접 속한 프로퍼티에 대해서는 복사해서 완전히 새로운 데이터가 만들어진 반면, 한 단계 더 들어간 urls의 내부 프로퍼티들은 기존 데이터를 그대로 참조한다.
  • 이런 현상을 발생하지 않게 하려면 user.urls 프로퍼티에 대해서도 불변 객체로 만들 필요가 있다.

(중첩된 객체에 대한 깊은 복사)

var user = {
  name: "Jaenam",
  urls: {
    portfolio: "http://github.com/abc",
    blog: "http://blog.com",
    facebook: "http://facebook.com/abc",
  },
};

var copyObject = function (target) {
  var result = {};
  for (var prop in target) {
    result[prop] = target[prop];
  }
  return result;
};

var user2 = copyObject(user);
user2.urls = copyObject(user.urls);

user.urls.portfolio = "http://portfolio.com";
console.log(user.urls.portfolio === user2.urls.portfolio); // false

user2.urls.blog = "";
console.log(user.urls.blog === user2.urls.blog); // false
  • 2번째 줄에서 urls 프로퍼티에 copyObject 함수를 실행한 결과를 할당했다. 이제 urls 내부 프로퍼티까지 복사해서 새로운 데이터가 만들어졌으므로, 5번째 줄과 8번째 줄에서 값이 서로 다르다는 결과를 얻을 수 있다.
  • 객체, 객체 내부의 모든 값을 복사해서 완전히 새로운 데이터를 만들때, 객체의 프로퍼티 중에서 그 값이 기본형 데이터일 경우에는 그대 복사하면 되지만, 참조형 데이터는 다시 그 내부의 프로퍼티들을 복사해야 한다.
  • 참조형 데이터가 있을 때마다 재귀적으로 수행해야먄 비로소 깊은 복사가 된다.

(객체의 깊은 복사를 수행하는 범용 함수)

var copyObjectDeep = function (target) {
  var result = {};
  if (typeof target === "object" && target !== null) {
    // 중첩된 참조형 데이터 경우
    for (var prop in target) {
      result[prop] = copyObjectDeep(target[prop]);
    }
  } else {
    result = target; // 기본형 데이터일 경우
  }
  return result;
};

깊은 복사 다른 방법

  • 객체를 JSON 문법으로 표현된 문자열로 전환했다가 다시 JSON 객체로 바꾸면 깊은 복사를 할 수 있다.
  • 다만 메서드(함수)나 숨겨진 프로퍼티인 proto 나 getter/setter 등과 같이 JSON으로 변경할 수 없는 프로퍼티들은 모두 무시한다.

댓글남기기