조컴퓨터

챕터 6. 변경 가능한 데이터 구조를 가진 언어에서 불변성 유지하기 본문

책읽기/쏙쏙 들어오는 함수형 코딩

챕터 6. 변경 가능한 데이터 구조를 가진 언어에서 불변성 유지하기

챠오위 2022. 8. 10. 17:32

챕터 6에서 중점적으로 볼 내용은 ? 

- 데이터가 바뀌지 않도록 하기 위해 카피-온-라이트를 적용하기

- 배열과 객체를 데이터에 쓸 수 있는 카피-온-라이트 동작을 만들기

- 깊이 중첩된 데이터도 카피-온-라이트가 잘 동작하게 만들기

 

 

모든 동작을 불변형으로 만들 수 있나요 ?

앞 챕터에서 장바구니 동작 일부에 카피-온-라이트 원칙을 적용하여 구현해 봤다. 구현 방식은 배열을 복사하고 값을 바꾼 다음 리턴하는 것이다. 다음은 추가적인 카피-온-라이트 적용 가능성이 있는 목록이다. (진행한 목록 포함)

 

장바구니에 대한 동작 제품에 대한 동작
1. 제품 개수 가져오기
2. 제품 이름으로 제품 가져오기
3. 제품 추가하기
4. 제품 이름으로 제품 빼기
5. 제품 이름으로 제품 구매 수량 바꾸기
    (→ 중첩된 데이터에 대한 동작)
1. 가격 설정하기
2. 가격 가져오기
3. 이름 가져오기

 

모두 불변형으로 만들 수 있을까 ? 특히 5번의 경우는 장바구니 안에 제품의 정보를 바꿔야 하기 때문에 어려워 보인다. 

이런 것을 중첩된 데이터(nested data) 라고 한다. 어떻게 하면 중첩된 데이터에 대한 불변 동작을 구현 할 수 있을까 ?

 

* 중첩(nested) 된 데이터: 데이터 구조 안에 데이터 구조가 있는 경우

 

 

동작을 읽기, 쓰기 또는 둘 다로 분류하기

읽기 동작은 데이터를 바꾸지 않고 정보를 꺼내는 것이다. 데이터가 바뀌지 않기 때문에 다루기 쉽다. 만약 인자에만 의존해 정보를 가져오는 읽기 동작이라면 계산이라 할 수 있다.

 

쓰기 동작은 어떻게든 데이터를 바꾼다. 바뀌는 값은 어디서 사용될지 모르기 때문에 바뀌지 않도록 원칙이 필요하다.

 

장바구니 동작

1. 제품 개수 가져오기 → 읽기2. 제품 이름으로 제품 가져오기 → 읽기3. 제품 추가하기 → 쓰기4. 제품 이름으로 제품 빼기 → 쓰기5. 제품 이름으로 제품 구매 수량 바꾸기 → 쓰기

 

장바구니 동작 중 세 개가 쓰기 동작이다. 쓰기 동작은 불변성 원칙에 따라 구현해야 한다. 

불변성 원칙은 카피-온-라이트(copy-on-write) 라고 한다.

 

자바스크립트는 기본적으로 변경 가능한 데이터 구조를 사용하기 때문에 불변성 원칙을 적용하려면 직접 구현해야 한다.

 

읽으면서 쓰는 동작은 어떨까 ? 어떤 경우에는 데이터를 바꾸면서(쓰기) 동시에 정보를 가져오는(읽기) 경우도 있다. 

 

제품에 대한 동작

1. 가격 설정하기 → 쓰기

2. 가격 가져오기 → 읽기

3. 이름 가져오기 → 읽기

 

 

카피-온-라이트 원칙 세 단계

다음의 세 단계로 카피-온-라이트를 적용하면 불변성을 유지하면서 값을 바꿀 수 있다.

 

1. 복사본 만들기

2. 복사본 변경하기(원하는 만큼)
3. 복사본 리턴하기

 

다음 add_element_last() 함수를 예로 들어 확인해 보자.

 

function add_element_last(array, elem) {   // 배열을 변경해보자

   var new_array = array.slice();   // 1. 복사본 만들기

   new_array.push(elem);   // 2. 복사본 변경하기

   return new_array;   // 3. 복사본 리턴하기

}

 

이 함수는 어떻게 동작하는가 ? 어떻게 기존 배열을 변경하지 않았는가 ? 

 

1. 배열을 복사했고 기존 배열은 변경하지 않았다.

2. 복사본은 함수 범위에 있기 때문에 다른 코드에서 값을 바꾸기 위해 접근할 수 없다.

3. 복사본을 변경하고 나서 함수를 리턴한다. 이후에는 값을 바꿀 수 없다.

 

즉, add_element_last() 함수는 데이터를 바꾸지 않고 정보를 리턴했기 때문에 읽기이다. 

 

 

카피-온-라이트로 쓰기를 읽기로 바꾸기

다음은 제품 이름으로 장바구니에서 제품을 빼는 함수이다.

 

function remove_item_by_name(cart, name) {

   var idx = null;

   for (var i = 0; i < cart.length; i++) {

      if (cart[i].name === name) {

         idx = i;

      }

   }

   if (idx !== null) {

      cart.splice(idx, 1);   // 장바구니 변경

   }

}

 

여기서 cart.splice() 가 하는 일이 무엇인가 ? .splice() 메서드는 배열에서 항목을 삭제하는 메서드이다.

 

cart.splice(idx, 1); 

이는 `idx 위치에 있는 항목을 하나 삭제한다.` 는 뜻이다. 

 

이 함수는 장바구니를 변경한다. 만약 remove_item_by_name() 함수에 전역변수 shopping_cart 를 넘기면 전역변수인 장바구니가 변경된다.

 

하지만 우리는 장바구니가 바뀌지 않았으면 한다. 장바구니를 변경 불가능한 데이터로 쓰려고 한다. 그럼 remove_item_by_name() 에 카피-온-라이트를 적용해 보자.

 

가장 먼저 할 일은 장바구니를 복사하는 일이다. 

 

현재 코드 인자를 복사하도록 바꾼 코드
function remove_item_by_name(cart, name) {
   var idx = null;
   for (var i = 0; i < cart.length; i++) {
      if (cart[i].name === name) {
         idx = i;
      }
   }
   if (idx !== null) {
      cart.splice(idx, 1);
   }
}
function remove_item_by_name(cart, name) {
   var new_cart = cart.splice();
   var idx = null;
   for (var i = 0; i < cart.length; i++) {
      if (cart[i].name === name) {
         idx = i;
      }
   }
   if (idx !== null) {
      cart.splice(idx, 1);
   }
}

 

다음은 cart 인자를 복사한 값을 사용하도록 바꾸자.

 

현재 코드 바꾼 코드
function remove_item_by_name(cart, name) {
   var new_cart = cart.splice();
   var idx = null;
   for (var i = 0; i < cart.length; i++) {
      if (cart[i].name === name) {
         idx = i;
      }
   }
   if (idx !== null) {
      cart.splice(idx, 1);
   }
}
function remove_item_by_name(cart, name) {
   var new_cart = cart.splice();
   var idx = null;
   for (var i = 0; i < new_cart.length; i++) {
      if (new_cart[i].name === name) {
         idx = i;
      }
   }
   if (idx !== null) {
      new_cart.splice(idx, 1);
   }
}

 

이제 복사본을 리턴하도록 바꾸자.

 

현재 코드 바꾼 코드
function remove_item_by_name(cart, name) {
   var new_cart = cart.splice();
   var idx = null;
   for (var i = 0; i < new_cart.length; i++) {
      if (new_cart[i].name === name) {
         idx = i;
      }
   }
   if (idx !== null) {
      new_cart.splice(idx, 1);
   }
}
function remove_item_by_name(cart, name) {
   var new_cart = cart.splice();
   var idx = null;
   for (var i = 0; i < new_cart.length; i++) {
      if (new_cart[i].name === name) {
         idx = i;
      }
   }
   if (idx !== null) {
      new_cart.splice(idx, 1);
   }
   return new_cart;
}

 

remove_item_by_name() 함수를 카피-온-라이트 버전으로 바꿨다. 이제 이 함수를 사용하던 곳을 바꾸는 일만 남았다.

 

remove_item_by_name() 함수를 사용하는 곳은 살짝 바꿔줘야 한다. 예를 들어 장바구니 항목을 삭제하는 버튼을 누를 때 이 함수를 부르는데, 더 이상 전역변수인 장바구니를 직접 바꾸지 않기 때문에 전역변수에 할당해 줘야 한다. 이는 다음과 같이 바꾸면 된다.

 

현재 코드 카피-온-라이트를 적용한 코드
function delete_handler(name) {
   remove_item_by_name(shopping_cart, name);
   var total = calc_total(shopping_cart);
   set_cart_total_dom(total);
   update_shipping_icons(shopping_cart);
   update_tax_dom(total);
}
function delete_handler(name) {
   shopping_cart = remove_item_by_name(shopping_cart,
                                                                         name);
   var total = calc_total(shopping_cart);
   set_cart_total_dom(total);
   update_shipping_icons(shopping_cart);
   update_tax_dom(total);
}

 

remove_item_by_name() 함수를 사용하는 곳을 찾아 리턴값에 shopping_cart 전역변수를 할당해야 한다. 

 

 

원래 버전과 카피-온-라이트 버전의 차이 보기

지금까지 고친 코드와 원래 코드를 함께 보자.

값을 바꾸던 원래 버전 카피-온-라이트를 적용한 버전
function remove_item_by_name(cart, name) {
   var idx = null;
   for (var i = 0; i < cart.length; i++) {
      if (cart[i].name === name) {
         idx = i;
      }
   }
   if (idx !== null) {
      cart.splice(idx, 1);
   }
}

function delete_handler(name) {
   remove_item_by_name(shopping_cart, name);
   var total = calc_total(shopping_cart);
   set_cart_total_dom(total);
   update_shipping_icons(shopping_cart);
   update_tax_dom(total);
}
function remove_item_by_name(cart, name) {
   var new_cart = cart.splice();
   var idx = null;
   for (var i = 0; i < new_cart.length; i++) {
      if (new_cart[i].name === name) {
         idx = i;
      }
   }
   if (idx !== null) {
      new_cart.splice(idx, 1);
   }
   return new_cart;
}

function delete_handler(name) {
   shopping_cart = remove_item_by_name(shopping_cart, 
                                                                         name);
   var total = calc_total(shopping_cart);
   set_cart_total_dom(total);
   update_shipping_icons(shopping_cart);
   update_tax_dom(total);
}

 

 

앞에서 만든 카피-온-라이트 동작은 일반적이다

앞으로 적용할 카피-온-라이트 동작도 앞에서 만든 동작과 비슷하다. add_element_last() 함수처럼 재사용하기 쉽도록 일반화할 수 있다.

 

그럼 배열에 .splice() 메서드를 일반화해 보자. .splice() 메서드는 remove_item_by_name() 함수에 있었다.

 

원래 코드 카피-온-라이트를 적용한 코드
function removeItems(array, idx, count) {
   array.splice(idx, count);
}
function removeItems(array, idx, count) {
   var copy = array.slice();
   copy.splice(idx, count);
   return copy;
}

 

이제 고친 removeItems() 함수에 맞게 remove_item_by_name() 함수를 고쳐보자.

 

원래 카피-온-라이트 버전 splice() 사용한 카피-온-라이트 버전
function remove_item_by_name(cart, name) {
   var new_cart = cart.splice();
   var idx = null;
   for (var i = 0; i < new_cart.length; i++) {
      if (new_cart[i].name === name) {
         idx = i;
      }
   }
   if (idx !== null) {
      new_cart.splice(idx, 1);
   }
   return new_cart;
}
function remove_item_by_name(cart, name) {
   var idx = null;
   for (var i = 0; i < cart.length; i++) {
      if (cart[i].name === name) {
         idx = i;
      }
   }
   if (idx !== null) {
      return removeItems(cart, idx, 1);
   }
   return cart;
}

 

이런 작업은 많이 사용하기 때문에 재사용할 수 있도록 만들면 나중에 고생을 하지 않아도 된다. 더이상 배열이나 객체를 복사하는 코드 패턴을 반복해서 쓰지 않아도 된다.

 

 

자바스크립트 배열 훑어보기

배열(array) 은 자바스크립트에 기본 컬렉션(collection) 이다. 자바스크립트 배열은 순서 있는 값을 나타내는 컬렉션이다. 배열은 다른 타입의 항목을 동시에 가질 수 있다. 그리고 인덱스(index) 로 접근한다. 자바스크립트 배열은 자바나 C 에서 말하는 배열과 다르게 크기를 늘리거나 줄일 수 있다.

 

인덱스로 값 찾기[idx]
idx 위치에 있는 값을 가져온다. 인덱스는 0 부터 시작한다.

> var array = [1, 2, 3, 4];
> array[2];
3
값 할당하기 []=
값을 할당하는 동작은 배열을 변경한다.

> var array = [1, 2, 3, 4];
> array[2] = "abc";
"abc"
> array
[1, 2, "abc", 4]
길이 .length
배열에 항목이 몇 개 있는지 알려준다. 메서드가 아니기 때문에 괄호를 쓰지 않는다.

> var array = [1, 2, 3, 4];
> array.length
4
끝에 추가하기 .push(e1)
배열을 변경하는 메서드이다. 배열 끝에 e1 을 추가하고 새로운 길이를 리턴한다.

> var array = [1, 2, 3, 4];
> array.push(10);
5
> array
[1, 2, 3, 4, 10]
끝에 있는 값을 지우기 .pop()
배열을 변경하는 메서드이다. 배열 끝에 있는 값을 지우고 지운 값을 리턴한다.

> var array = [1, 2, 3, 4];
> array.pop();
4
> array
[1, 2, 3]
앞에 추가하기 .unshift(e1)
배열을 변경하는 메서드이다. 배열 맨 앞에 e1 을 추가하고 새로운 길이를 리턴한다.

> var array = [1, 2, 3, 4];
> array.unshift(10);
5
> array
[10, 1, 2, 3, 4]
앞에 있는 값을 지우기 .shift()
배열을 변경하는 메서드이다. 배열 맨 앞에 있는 값을 지우고 지운 값을 리턴한다.

> var array = [1, 2, 3, 4];
> array.shift();
1
> array
[2, 3, 4]
배열 복사하기 .slice()
배열을 얇게 복사*해서 새로운 배열을 리턴한다. 

> var array = [1, 2, 3, 4];
> array.slice();
[1, 2, 3, 4]
항목 삭제하기 .splice(idx, num)
배열을 변경하는 메서드이다. idx 위치에서 num 개 항목을 지운다. 그리고 지운 항목을 리턴한다.

> var array = [1, 2, 3, 4, 5, 6];
> array.splice(2, 3);  // [3, 4, 5] 3개의 항목을 삭제
> array
[1, 2, 6]

 

* 얇게 복사 : 배열에 항목이 다른 배열이나 객체를 참조한다면 .slice() 해도 참조하는 배열이나 객체는 복사되지 않는다.

 

 

연습문제) 메일링 리스트에 연락처를 추가하는 코드이다. 이 코드를 카피-온-라이트 형식으로 바꾸어 보라. (추가 예정)

 

 

쓰기를 하면서 읽기도 하는 동작은 어떻게 해야 하는가 ?

어떤 동작은 읽고 변경하는 일을 동시에 한다. 이런 동작은 값을 변경하고 리턴한다. .shift() 메서드가 좋은 예제인데 한 번 살펴보자.

 

var a = [1, 2, 3, 4];

var b = a.shift();

console.log(b);   // 1 → 값을 리턴

console.log(a);   // [2, 3, 4] → 값이 바뀜

 

.shift() 메서드는 값을 바꾸는 동시에 배열에 첫 번째 항목을 리턴한다. 변경하면서 읽는 동작이다.

 

이 동작을 카피-온-라이트로 어떻게 바꿀 수 있는가 ? 

 

카피-온-라이트에서 쓰기를 읽기로 바꾸었다. 읽기라는 말은 값을 리턴한다는 의미이다.

하지만 .shift() 메서드는 이미 읽기이다. 값을 리턴하고 있기 때문이다.

어떻게 해야 하는가 ? 두 가지 접근 방법이 있다.

 

1. 읽기와 쓰기 함수로 각각 분리한다.

2. 함수에서 값을 두 개 리턴한다.

 

선택할 수 있다면 첫 번째 접근 방법이 더 좋은 방법이다. 책임이 확실히 분리되기 때문이다. 

설계 서로 엉켜있는 것을 분리하는 작업이다.

 

 

쓰면서 읽기도 하는 함수 분리하기

쓰면서 읽기도 하는 함수를 분리하는 작업은 두 단계로 나눌 수 있다.

먼저 쓰기에서 읽기를 분리한다. 다음으로 쓰기에 카피-온-라이트를 적용해 읽기로 바꾼다. 

이 작업은 일반적인 카피-온-라이트를 적용하는 것과 같다.

 

읽기와 쓰기 동작으로 분리하기

.shift() 메서드의 읽기 동작은 값을 단순히 리턴하는 동작이다. .shift() 메서드가 리턴하는 값은 배열에 첫 번째 항목이다. 따라서 배열에 첫 번째 항목을 리턴하는 계산 함수를 만들면 된다. 이렇게 만든 함수는 읽기 동작만 한다. 또한, 숨겨진 압력이나 출력이 없기 때문에 계산이다. 

 

function first_element(array) {

   return array[0];

}

 

first_element() 함수는 배열을 바꾸지 않는 읽기 함수이기 때문에 카피-온-라이트를 적용할 필요가 없다.

 

.shift() 메서드의 쓰기 동작은 새로 만들 필요가 없다. .shift() 메서드가 하는 일을 그대로 감싸기만 하면 된다. 

 

function drop_first(array) {

   array.shift();

}

 

쓰기 동작을 카피-온-라이트로 바꾸기

쓰기와 읽기를 잘 분리했다. 하지만 drop_first() 함수는 인자로 들어온 값을 변경하는 쓰기이다. 이제 이 함수를 카피-온-라이트로 만들어 보자.

 

값을 변경 카피-온-라이트를 적용한 코드
function drop_first(array) {
   array.shift();
}
function drop_first(array) {
   var array_copy = array.slice();
   array_copy.shift();
   return array_copy;
}

 

읽기와 쓰기를 분리하는 접근 방법은 분리된 함수를 따로 쓸 수 있기 때문에 더 좋은 접근 방법이다.

 

 

값을 두 개 리턴하는 함수로 만들기

먼저 .shift() 메서드를 바꿀 수 있도록 새로운 함수로 감싼다. 다음으로 읽기와 쓰기를 함꼐 하는 함수를 읽기만 하는 함수로 바꾼다.

 

동작을 감싸기

첫 번째 단계는 .shift() 메서드를 바꿀 수 있도록 새로운 함수로 감싸는 것이다. 여기서 함수 리턴값을 무시하면 안된다. 

 

function shift(array) {

   return array.shift();

}

 

읽으면서 쓰기도 하는 함수를 읽기 함수로 바꾸기

인자를 복사한 후에 복사한 값의 첫 번째 항목을 지우고, 지운 첫 번째 항목과 변경된 배열을 함께 리턴하도록 바꾼다.

 

값을 변경 카피-온-라이트
function shift(array) {
   return array.shift();
}
function shift(array) {
   var array_copy = array.slice();
   var first = array_copy.shift();
   return {
      first : first,
      array : array_copy
   }
}

 

다른 방법

첫 번째 접근 방식을 사용해 두 값을 객체로 조합하는 방법이다.

 

function shift(array) {

   return {

      first : first_element(array),

      array : drop_first(array)

   }

}

 

첫 번째 접근 방법으로 만든 두 함수는 모두 계산이기 때문에 쉽게 조합할 수 있다. 조합해도 이 함수는 계산이다. 

 

연습문제) .pop() 메서드를 카피-온-라이트 버전으로 바꿔 보자. (추가 예정)

 

 

불변 데이터 구조를 읽는 것은 계산입니다

변경 가능한 데이터를 읽는 것은 계산이다.

쓰기는 데이터를 변경 가능한 구조르 만든다.

어떤 데이터에 쓰기가 없다면 데이터는 변경 불가능한 데이터이다.

불변 데이터 구조를 읽는 것은 계산이다.

쓰기를 읽기로 바꾸면 코드에 계산이 많아진다.

 

 

애플리케이션에는 시간에 따라 변하는 상태가 있다

데이터가 모두 불변형이면 애플리케이션에서 시간에 따라 변하는 상태를 어떻게 다뤄야 하는가 ? 

바꿀 수 있는 것이 없다면 사용자는 장바구니에 제품을 어떻게 넣을 수 있는가 ?

 

모든 값을 불변형으로 만들더라도 시간에 따라 바뀌는 값을 다룰 수 있어야 한다.

전역변수 shopping_cart 가 이에 해당한다. 장바구니가 바뀔 때 shopping_cart 전역변수에 새로운 값을 할당한다. 그래서 shopping_cart 는 항상 최신 장바구니를 나타낸다. 사실 장바구니 값은 새 값으로 교체(swapping) 된다고 할 수 있다.

 

shopping_cart = add_item(shopping_cart, shoes);

shopping_cart = remove_item_by_name(shopping_cart, "shirt");

 

shopping_cart 전역변수는 항상 최신값을 나타낸다. 필요할 때 새로운 값으로 교체한다. 

 

 

불변 데이터 구조는 충분히 빠르다

일반적으로 불변 데이터 구조는 변경 가능한 데이터 구조보다 메모리를 더 많이 쓰고 느리다. 

 

하지만 불변 데이터 구조를 사용하면서 대용량의 고성능 시스템을 구현하는 사례는 많이 있다. 일반 애플리케이션에 쓰기 충분히 빠르다는 증거이다. 이에 몇 가지 논점이 있다.

 

언제든 최적화할 수 있다

애플리케이션을 개발할 때 예상하기 힘든 병목 지점이 항상 있다. 그래서 성능 개선을 할 때는 미리 최적화하지 말라 한다.

불변 데이터 구조를 사용하고 속도가 느린 부분이 있다면 그때 최적화하라.

 

가비지 콜렉터는 매우 빠르다

어떤 가비지 콜렉터는 한두 개의 시스템 명령어로 메모리를 비울 수 있을 만큼 최적화되었다. 이걸 그냥 쓰면 된다.

 

생각보다 많이 복사하지 않는다

제품이 100개인 배열을 복사해도 참조만 복사된다. 데이터 구조의 최상위 단계만 복사하는 것을 얕은 복사(shallow copy)* 라 한다. 얕은 복사는 같은 메모리를 가리키는 참조에 대한 복사본을 만든다. 이것을 구조적 공유(structural sharing)** 라 한다.

 

함수형 프로그래밍 언어에는 빠른 구현체가 있다

클로저(Clojure) 에서 지원하는 불변 데이터 구조는 다른 언에서 참고할 만큼 효율적이다.

어떻게 효율적인가 ? 데이터 구조를 복사를 할 때 최대한 많은 구조를 공유한다. 그래서 더 적은 메모리를 사용하고 결국 가비지 콜렉터의 부담을 줄여준다. 구현은 우리가 한 것과 같은 카피-온-라이트를 기반으로 한다.

 

* 얕은 복사(shallow copy): 중첩된 데이터 구조에 최상위 데이터만 복사한다. 예를 들어, 객체가 들어 있는 배열이 있다면 얕은 배열만 복사하고 안에 있는 객체는 참조로 공유한다.

** 구조적 공유(structural sharing): 두 개의 중첩된 데이터 구조가 어떤 참조를 공유한다. 데이터가 바뀌지 않는 불변 데이터 구조라면 구조적 공유는 안전하다. 구조적 공유는 메모리를 적게 사용하고, 모든 것을 복사하는 것보다 빠르다.

 

 

객체에 대한 카피-온-라이트

배열에 대한 카피-온-라이트와 같은 단계로 구현할 수 있다.

 

1. 복사본 만들기

2. 복사본 변경하기

3. 복사본 리턴하기

 

객체는 Object.assign() 메서드로 복사본을 만들 수 있다. 

 

var object = {a : 1, b : 2};

var object_copy = Object.assign({}, object);

 

다음은 제품 가격을 설정하는 setPrice() 를 구현한 코드이다.

 

원래 코드 카피-온-라이트
function setPrice(item, new_price) {
   item.price = new_price;
}
function setPrice(item, new_price) {
   var item_copy = Object.assign({}, object);
   item_copy.price = new_price;
   return item_copy;
}

 

어떤 데이터 구조라도 위에 세 단계를 적용하면 된다.

 

 

자바스크립트 객체 훑어보기

자바스크립트 객체는 다른 언어의 해시 맵이나 연관 배열과 비슷하다. 객체는 키와 값으로 되어 있고, 키는 객체 안에서 유일하다. 키는 항상 문자열이지만 값은 아무 타입이나 될 수 있다. 다음은 객체에 대한 연산이다.

 

키로 값 찾기 [key]
키로 값을 찾는 동작이다. 만약 키가 없다면 undefined 가 나온다.

> var object = {a : 1, b : 2};
> object["a"];
1
키로 값 설정하기 .key 또는 [key]=
객체를 변경하는 동작이다. 두 가지 방법으로 객체에 값을 설정 할 수 있따. 키에 해당하는 값을 바꾼다. 만약 키가 있다면 값을 바꾸고, 없다면 값을 추가한다.

> var object = {a : 1, b : 2};
> object["a"] = 7;
7
> object
{a : 7, b : 2}
> object.c = 10;
10
> object
{a : 7, b : 2, c : 10}
키/값 쌍 지우기 delete
주어진 키로 키/값 쌍을 지운다. 이 동작도 객체를 변경하는 동작이다. 키로 값을 찾는 두 가지 문법 모두 값을 지우는 데 사용할 수 있다.

> var object = {a : 1, b : 2};
> delete object["a"];
true
> object
{b : 2}
객체 복사하기 Object.assign(a, b)
조금 복잡한 동작이다. Object.assign() 메서드는 b 객체에 모든 키 값을 a 객체로 복사한다. 빈 객체에 모든 키/값 쌍을 복사해서 b 의 복사본을 만들 수 있다.

> var object = {x : 1, y : 2};
> Object.assign({}, object);
{x : 1, y : 2}
키 목록 가져오기 Object.keys()
객체에 있는 키/값 쌍을 순회하고 싶다면, Object.keys() 함수를 사용해 모든 키를 가져와 순회할 수 있다. 객체에 있는 키를 배열로 리턴한다. 이 배열을 순회하면 된다.

> var object = {a : 1, b : 2};
> Object.keys(object);
["a", "b"]

 

연습문제) 카피-온-라이트 방식으로 객체에 값을 설정하는 objectSet() 함수를 만들어라.

 

o["price"] = 37;

 

function objectSet(object, key, value) {

   var copy = Object.assign({}, object);

   copy[key] = value;

   return copy;

}

 

연습문제) 위의 objectSet() 함수를 이용해 setPrice() 를 리팩터링하라.

 

function setPrice(item, new_price) {

   return objectSet(item, "price", new_price);

}

 

 

중첩된 쓰기를 읽기로 바꾸기

원래 코드 카피-온-라이트
function setPriceByName(cart, name, price) {
   for (var i = 0; i < cart.length; i++) {
      if (cart[i].name === name) {
         cart[i].price = price;
      }
   }
}
function setPriceByName(cart, name, price) {
   var cart_copy = cart.slice();
   for (var i = 0; i < cart_copy.length; i++) {
      if (cart_copy[i].name === name) {
        cart_copy[i] = setPrice(cart_copy[i], price);
      }
   }
   return cart_copy;
}

 

중첩된 쓰기도 중첩되지 않은 쓰기와 같은 패턴을 사용한다. 복사본을 만들고 변경한 다음 복사본을 리턴한다. 중첩된 항목에 또 다른 카피-온-라이트를 사용하는 부분만 다르다. 

 

최하위부터 최상위까지 중첩된 데이터 구조의 모든 부분이 불변형이어야 한다. 중첩된 데이터의 일부를 바꾸려면 변경하려는 값과 상위의 모든 값을 복사해야 한다. 

 

 

어떤 복사본이 생겼는가 ?

장바구니 안에 제품 세 개가 있다고 하자. 티셔츠와 신발, 양말이다. 

이제 티셔츠 가격을 13달러로 바꾸자. 중첩된 구조이므로 값을 바꾸려면 setPriceByName() 함수를 사용한다.

 

shopping_cart = setPriceByName(shopping_cart, "t-shirt", 13);

 

코드에서 복사본이 몇 개 생기는지 보자.

 

function setPriceByName(cart, name, price) {
   var cart_copy = cart.slice();   // 배열 복사
   for (var i = 0; i < cart_copy.length; i++) {
      if (cart_copy[i].name === name) {
         cart_copy[i] = setPrice(cart_copy[i], price);
      }
   }
   return cart_copy;
}

 

function setPrice(item, new_price) {
   var item_copy = Object.assign({}, object);   // 객체 복사
   item_copy.price = new_price;
   return item_copy;
}

 

배열 하나와 객체 세 개가 있었다. 어떤 복사본이 생겼는가 ?

복사본은 배열 하나(장바구니) 와 객체 하나(티셔츠) 이다.

나머지 객체 두 개는 복사하지 않았다. 

 

이유는 중첩된 데이터에 얕은 복사를 했기 때문이다. 그 결과로 구조적 공유가 되었다. 짧은 문장에 많은 용어가 있다.

 

 

얕은 복사와 구조적 공유를 그림으로 알아보기

네 개(배열 하나, 객체 세 개) 데이터 중 두 개(배열 하나, 객체 하나) 를 복사했다. 그리고 나머지 객체는 변경하지도 복사하지도 않았다. 그리고 원래 배열과 복사한 배열 모두 바뀌지 않은 객체를 가리킨다. 이것이 앞에서 말한 구조적 공유이다. 구조적 공유에서 공유된 복사본이 변경되지 않는 한 안전하다. 즉, 값을 바꿀 때는 복사본을 만들기 때문에 공유된 값은 변경되지 않는다. 

 

 

출처 : 쏙쏙 들어오는 함수형 코딩(Grokking Simplicity: Taming complex software with functional thinking)