본문 바로가기

[자바스크립트] 불변 객체 (feat 코어 자바스크립트)

 

 

 

 

들어가며

많은 기업들이 타입스크립트와 nest.js를 활용해서 서버 개발을 하곤 합니다. 취업을 하려면, 타입스크립트와 nest.js를 공부해서 실무를 익히는 것이 중요할 것입니다. 하지만 아직 자바스크립트의 기초도 없는 상태에서 타입스크립트와 nest.js를 공부하는 것이 맞을까 하는 생각이 들었습니다. 빠르게 기술변화를 적응하고, 러닝 커브를 줄이기 위해 빠르게 공부해야 하는 것도 맞겠지만, 그전에 언어의 기반이 되는 자바스크립트부터 제대로 알아야 하지 않을까 하는 생각이 들었습니다. 이 기회에 자바스크립트의 기본에 대해 정리해보고자 합니다. 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

참조형 데이터는 기본형 데이터와 마찬가지로 데이터 자체를 변경하려고 한다면 데이터는 변하지 않습니다. 하지만 참조형 데이터가 가변적이다라고 말하는 것은, 내부 프로퍼티를 변경할 때를 말합니다.

 

만약 객체를 복사해서, 내부 프로퍼티를 변경하고 싶을 때, 복사한 객체를 변경하더라도, 원본 객체가 변하지 않아야 하는 경우가 생길 것입니다. 이런 경우에 '불변 객체'가 필요합니다. 불변 객체를 만들기 위해서는 다양한 방법을 활용할 수 있습니다.

 

내부 프로퍼티를 변경할 필요가 있을 때마다 매번 새로운 객체를 만들어 재할당하기로 규칙을 정하거나 자동으로 새로운 객체를 만드는 도구를 활용한다면 불변성을 확보할 수 있습니다. 혹은 불변성을 확보할 필요가 있을 경우에는 불변 객체로 취급하고, 그렇지 않은 경우에는 기존 방식대로 사용하는 식으로 상황에 따라 대처해도 됩니다. 그렇다면 불변 객체를 어떻게 만들 수 있는지 살펴보겠습니다.

 

 

 

 

let user = {
  name: "const",
  gender: "male"
};

function changeName(user, newName) {
  let newUser = user;
  newUser.name = newName;
  return newUser;
}

let user2 = changeName(user, "epitone");

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

console.log(user.name, user2.name); // epitone epitone
console.log(user === user2); // true

 

 

 

불변 객체를 만들기 전, 객체의 가변성으로 인해 어떤 문제가 나타날 수 있을지 알아보겠습니다. 첫 번째 줄에서 user 객체를 생성하고 user 객체의 name 프로퍼티를 epitone으로 바꿔주는 함수를 호출해서, 그 결과를 user2 변수에 할당했습니다. 

 

이때 user, user2 변수 모두 name 프로퍼티가 'epitone'으로 출력되는 것을 볼 수 있습니다. 마지막 줄에서는 user와 user2가 서로 동일하다고 나옵니다. 만약 user2와 user가 프로퍼티가 바뀌더라도, 다른 객체가 되려면 어떻게 해야 할까요?  

 

 

 

 

let user = {
  name: "const",
  gender: "male"
};

function changeName(user, newName) {
  return {
    name: newName,
    gender: user.gender
  };
}

let user2 = changeName(user, "epitone");

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

console.log(user.name, user2.name); // const epitone
console.log(user === user2); // false

 

 

 

첫 번째는 changeName 함수가 정말 새로운 객체를 반환하도록 수정했습니다. 이렇게 된다면, user와 user2는 서로 다른 객체이므로 안전하게 변경 전과 후를 비교할 수 있습니다. 하지만 문제점이 있습니다. changeName 함수는 새로운 객체를 만들면서 변경할 필요가 없는 기존 객체의 프로퍼티(gender)를 하드코딩으로 입력했습니다. 지금은 gender 프로퍼티가 하나 있어서 쉬웠을 수 있지만, 만약 프로퍼티가 많은 객체였다면, 하드코딩의 양이 더욱 많아질 것입니다. 이런 방식보다는 대상 객체의 프로퍼티 개수와 상관없이 모든 프로퍼티를 복사하는 함수를 만드는 편이 더 좋을 것입니다. 

 

 

 

let user = {
  name: "const",
  gender: "male"
};

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

let user2 = copyObject(user);
user2.name = 'epitone';

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

console.log(user.name, user2.name); // const epitone
console.log(user === user2); // false

 

 

 

위에서 copyObject 함수를 만들었습니다. copyObject 함수는 for in 문법을 이용해 result 객체에 target 객체의 프로퍼티들을 복사하는 함수입니다. copyObject 함수를 활용해서 간단하게 객체를 복사하고 내용을 수정하는 데 성공했습니다. copyObject 함수는 프로토타입 체이닝 상의 모든 프로퍼티를 복사하는 점, getter/setter는 복사하지 않는 점, 얕은 복사만을 수행한다는 점에서 아쉽지만, 문제를 모두 보완하려면 함수가 무거워질 수밖에 없지만, user 객체에 대해서는 문제가 되지 않으므로 일단 진행해보겠습니다.

 

copyObject 함수를 활용해서 객체를 만들었을 때, 가장 아쉬운 점은 이 함수는 '얕은 복사만을 수행한다'는 점입니다. 그렇다면, 얕은 복사는 무엇이고, 깊은 복사는 또 무엇일까요? 이에 대해 알아보겠습니다. 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

얕은 복사 (shallow copy)

얕은 복사는 참조형 데이터가 저장된 프로퍼티를 복사할 때 그 주솟값만 복사하는 방법입니다. 위에서 copyObject 함수는 얕은 복사만 수행했습니다. copyObject는 해당 프로퍼티에 대해 원본과 사본이 모두 동일한 참조형 데이터의 주소를 가리키게 됩니다.

 

얕은 복사에 대해 예를 들어 살펴보겠습니다. 

 

const obj = { vaule: 1 }
const newObj = obj;

newObj.vaule = 2;

console.log(obj.vaule); // 2
console.log(obj === newObj); // true

 

obj 변수에 object를 할당하고, newObj 변수에 obj 변수의 값을 할당했습니다. 그리고 newObj 프로퍼티인 value 값을 2로 설정하고, obj.value를 콘솔에 출력하면, 2로 변경된 것을 볼 수 있습니다. 왜냐면, 얕은 복사 때문에, 사본의 데이터를 변경하더라도, 동일한 참조형 데이터 주소를 가리키고 있기에, 원본의 데이터도 변경되는 것입니다. 그렇다면, 깊은 복사를 사용하려면 어떻게 해야 할까요? 이에 대해 알아보겠습니다. 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

깊은 복사 (deep copy)

깊은 복사는 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법입니다. 깊은 복사에 대해 예를 들어 살펴보겠습니다.

 

 

let a = 1;
let b = a;

b = 2;

console.log(a); // 1
console.log(b); // 2
console.log(a === b); // false

 

 

만약 변수 a의 값으로 1을 할당하고, 변수 b에 a를 할당했습니다. 그리고 변수 b에 2를 재할당하고, a와 b를 출력해보면, a는 1, b는 2가 출력됩니다. 자바스크립트에서 원시 타입은 깊은 복사가 진행됩니다. 그렇다면, 원시 타입이 아닌, 객체에서 깊은 복사는 어떻게 이뤄지는지 알아보겠습니다. 객체의 깊은 복사에는 다양한 방법이 있습니다. 

 

 

 

Object.assign()

Object.assign(생성할 객체, 복사할 객체) 메서드는 첫 번째 인수로 빈 객체를 넣어주며, 두 번째 인수로 할당할 객체를 넣으면 됩니다. 

 

const obj = { a: 1 };
const newObj = Object.assign({}, obj);

newObj.a = 2;

console.log(obj); // { a: 1 }
console.log(obj === newObj); // false

 

새로운 newObj 객체를 Object.assign() 메서드를 사용해서 생성했고, newObj.a 값을 변경해도 기존의 obj는 변하지 않았습니다. 객체 간의 비교를 해도, 서로 참조값이 다르기 때문에 false가 나옵니다. 하지만 Object.assign에서의 문제는 2차원 객체의 경우 깊은 복사가 이뤄지지 않는다는 점입니다.

 

 

const obj = {
  a: 1,
  b: {
    c: 2,
  },
};

const newObj = Object.assign({}, obj);

newObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 3 } }
console.log(obj.b.c === newObj.b.c); // true

 

 

만약 obj 변수에 b 객체가 있다고 가정했을 때, 2차원 객체를 newObj에 복사하고, newObj.b.c의 값을 변경했습니다. 그리고 obj 변수를 출력해보면, c의 값이 3이 된 것을 확인할 수 있습니다. 중복 객체의 경우 Object.assign() 메서드는 중복 객체를 깊은 복사를 하지 않는다는 한계가 있습니다. 이 문제는 전개 연산자(Spread Operator)를 활용할 경우에도 발생합니다. 

 

 

 

 

전개 연산자(Spread Operator)

const obj = {
  a: 1,
  b: {
    c: 2,
  },
};

const newObj = { ...obj };

newObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 3 } }
console.log(obj.b.c === newObj.b.c); // true

 

전개 연산자를 사용할 때도 Object.assign 메서드와 마찬가지로, 중복 객체의 경우 얕은 복사가 진행됩니다. 그럼 Object.assign 메서드와 전개 연산자가 아닌, 중복 객체에서도 깊은 복사를 할 수 있는 방법을 더 알아보겠습니다.

 

 

 

 

 

 

JSON 객체 메서드

객체의 깊은 복사를 위해 JSON 객체의 stringify(), parse() 메서드를 활용할 수 있습니다. JSON.stringify 메서드는 객체를 문자열로 치환하며, JSON.parse() 메서드는 문자열을 객체로 치환합니다. 이 부분을 예를 들어 살펴보겠습니다. 

 

 

const obj = {
  a: 1,
  b: {
    c: 2,
  },
};

const newObj = JSON.parse(JSON.stringify(obj));

newObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(obj.b.c === newObj.b.c); // false

 

만약 obj 객체를 JSON.stringify 메서드를 활용해서 문자열로 변환하고, JSON.parse 메서드를 활용해서 문자열을 객체로 변환했습니다. 이렇게 된다면, 중복 객체의 경우에도 깊은 복사가 된다는 장점이 있습니다만, 2가지 단점이 있습니다. 첫 번째, 이 방법은 다른 방법들에 비해 성능이 좋지 않습니다. 그리고 두 번째 문제점은 JSON.stringify 메서드는 함수를 만났을 때 undefined로 처리한다는 문제점이 있습니다. 

 

 

 

const obj = {
  a: 1,
  b: {
    c: 2,
  },
  func: function() {
      return this.a;
  }
};

const newObj = JSON.parse(JSON.stringify(obj));

console.log(newObj.func); // undefined

 

 

위의 예시에서, func을 출력하려고 하지만, JSON 메서드를 활용했을 때, 함수를 undefined로 출력하는 문제가 발생하고 있습니다. 그럼 객체의 깊은 복사를 하기 위해서는 어떤 방법을 활용해야 할까요? 

 

 

 

 

 

 

 

 

 

커스텀 재귀 함수

객체의 깊은 복사를 다른 문제 없이 하려면, 커스텀 재귀 함수를 활용하는 것이 좋습니다. 

 

 

function deepCopy(obj) {
  if (obj === null || typeof obj !== "object") {
    return obj;
  }

  let copy = {};
  for (let key in obj) {
    copy[key] = deepCopy(obj[key]);
  }
  return copy;
}

const obj = {
  a: 1,
  b: {
    c: 2,
  },
  func: function () {
    return this.a;
  },
};

const newObj = deepCopy(obj);

newObj.b.c = 3;
console.log(obj); // { a: 1, b: { c: 2 }, func: [Function: func] }
console.log(obj.b.c === newObj.b.c); // false

 

 

재귀 함수인 deepCopy를 활용해보겠습니다. newObj 변수에 deepCopy 함수의 인자로 obj 객체를 넣고, 이를 값으로 할당했습니다. 만약 deepCopy에 함수의 인자가 객체가 아닌 경우 바로 리턴하며, 객체인 경우 객체의 값만큼 반복하며 재귀적으로 함수를 호출해서, 복사된 값을 반환합니다. 즉, 커스텀 재귀 함수를 이용하면 중첩 객체의 깊은 복사까지 할 수 있습니다. 하지만 매번 이렇게 재귀 함수를 생성해서 사용하는 것도 좋지만, 객체의 깊은 복사를 위한 오픈 소스를 활용해서 사용한다면, 더욱 쉽고 빠르게 객체의 깊은 복사가 가능합니다. 이를 위해 lodash 모듈의 cloneDeep 메서드를 활용하면 좋습니다.

 

 

 

 

 

 

 

 

lodash 모듈의 cloneDeep()

lodash 모듈의 cloneDeep() 메서드를 활용해서, 객체의 깊은 복사를 할 수 있습니다. 먼저 lodash를 활용하기 위해서는 모듈을 설치해줘야 합니다. 

 

& npm i lodash

 

 

const lodash = require("lodash");

const obj = {
  a: 1,
  b: {
    c: 2,
  },
  func: function () {
    return this.a;
  },
};

const newObj = lodash.cloneDeep(obj);

newObj.b.c = 3;
console.log(obj); // { a: 1, b: { c: 2 }, func: [Function: func] }
console.log(obj.b.c === newObj.b.c); // false

 

 

lodash의 cloneDeep 메서드를 활용한다면, 객체의 깊은 복사를 아주 간단하게 구현할 수 있습니다. 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

마치며

자바스크립트의 기본에 대해 공부하면서, 기본도 정확하게 알지 못하고, 앞으로 나아가려 했구나 하는 생각이 들었습니다. 기초이기에 지금 공부하는 내용을 더 잘 이해해야겠다고 생각했습니다. 기본에 충실할 수 있는 개발자가 되고 싶습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

출처

 

싸니까 믿으니까 인터파크도서

 

book.interpark.com

 

[JavaScript] 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)

깂은 복사와 얕은 복사에 대해 알아보겠다. 이 글의 초반 내용은 이전 포스팅의 (원시 타입과 참조 타입의 차이과 맥락이 비슷하며, 위 포스팅은 원시 타입과 참조 타입의 차이점이라면 아래는

velog.io