반디북
쏙쏙 들어오는 함수형 코딩
Ch6
우석

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

모든 동작은 읽기 / 쓰기 또는 둘 다로 분류할 수 있다.

/* 읽기 */
function find_item(cart, index) {
  return cart[index];
}
 
/* 쓰기 */
function add_item(cart, item) {
  return cart.push(item);
}
 
/* 둘 다 */
function drop_item_by_name(cart) {
  return cart.shift();
}
  • 모든 동작은 읽기 / 쓰기 또는 둘 다로 분류할 수 있다.
  • 읽기는 데이터가 바뀌지 않기 때문에 다루기가 쉽다.
  • 하지만 쓰기 작업이 추가될 경우 데이터를 바꾼다.
  • 쓰기 작업으로 바뀐 값은 어디서 사용될지 모르기 때문에 바뀌지 않도록 원칙이 필요하다.

카피-온-라이트로 쓰기를 읽기로 바꿀 수 있다.

  • 카피-온-라이트 3개의 작업으로 이루어진다.
    • 복사본 만들기
    • 복사본 변경하기(원하는 만큼)
    • 복사본 리턴하기
  • 카피-온-라이트를 적용하면 외부 데이터를 변경하지 않기 때문에 읽기 작업으로 분류할 수 있다.
  • 즉, 복사를 통해 데이터의 불변성이 보장되어 쓰기 작업의 복잡성이 사라진다.
/* 쓰기 */
function add_item(cart, item) {
  var new_cart = cart.slice()
  new_cart.push(item)
  return new_cart;
}
 
/* 둘 다 */
function drop_item_by_name(cart) {
  var new_cart = cart.slice()
  var first = new_cart.shift()
 
  return {
    first,
    cart: new_cart
  }
}
  • 위 코드처럼 카피-온-라이트를 적용하면 데이터의 불변성이 보장되어 쓰기 작업을 읽기로 바꿀 수 있다.

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

var array = [1, 2, 3, 4, 5];
var name = "hello";
 
var obj = {
  array,
  name
}
 
var new_obj = Object.assign({}, obj)
  • 위 코드는 obj를 복사해 new_obj를 생성하는 코드이다.
  • 하지만 Object.assign()를 활용해 객체를 복사해도 중첩 객체의 경우 얕은 복사가 되어 두 객체에서 공유하게 된다.
  • 위 코드에서는 obj, new_obj가 array 키에 대한 value로 동일한 배열 객체(array)를 가진다.

/* item 객체 형태 */
{
  name: "name",
  price: 1234,
}
 
function set_price_by_name(cart, name, price) {
  var new_cart = cart.slice()
  for (var i = 0; i < new_cart.length; i++) {
    if (new_cart[i].name === name) {
      var new_item = Object.assign({}, new_cart[i]);
      new_item.price = price;
      new_cart[i] = new_item;
    }
  }
}
  • 위 코드에서 깊은 복사가 일어나는 부분은 cart의 배열 객체와 Object.assign()에 의해 복사되는 item 객체 뿐이다.
  • 즉, price가 변경되지 않는 item의 경우 불변성이 보장되지 않고 구조적 공유를 하게 된다.
  • 하지만 이 객체들은 set_price_by_name() 메서드에서 변경하는 데이터가 아니기 때문에 문제가 되지 않는다.
  • 위 메서드에서 변경이 일어나는 객체의 경우 복사본을 만들기 때문에 공유된 값은 변경되지 않는다고 확신할 수 있다.
만혁

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

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

  • 읽기 : 데이터에서 정보를 가져온다. 데이터를 변경하지 않는다.
  • 쓰기 : 데이터를 변경한다.

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

  1. 복사본 만들기
  2. 복사본 변경하기
  3. 복사본 리턴하기
function add_elelment_last(array, elem) {
    const newArray = array.slice(); // 1. 복사본 만들기
    newArray.push(elem); // 2. 복사본 변경하기
    return newArray; // 3. 복사본 리턴하기
}

add_elelment_last 함수는 데이터를 변경하지 않고 정보를 리턴했기 때문에 읽기

카피-온-라이트는 쓰기를 읽기로 바꾼다.

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

function firstElement(array) {
    return array[0];
}
function dropFirst(array) {
    const copy = array.slice();
    copy.shift();
    return copy;
}

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

function shift(array) {
    const copy = array.slice();
    const first = copy.shift();
    return {
        first: first,
        array: copy
    };
}
 
function shift(array) {
    return {
        first: firstElement(array),
        array: dropFirst(array)
    };
}
연습 문제

카피-온-라이트로 변경

const mailingList = [];
function addContact(list, email) {
    const copy = list.slice();
    copy.push(email);
    return copy;
}
function submitFormHandler(event) {
    const from = event.target;
    const email = form.elements['email'].value;
    const added = addContact(mailingList, email)
    mailingList = added;
}

.pop() 메서드를 카피-온-라이트로 변경

const a = [1,2,3,4];
const b = a.pop();
console.log(b); // 4출력
console.log(a); // [1,2,3] 출력
function lastElement(array) {
    return array[array.length - 1]
}
 
function dropLast(array) {
    const copy = array.slice();
    copy.pop();
    return copy;
}
 
function pop(array) {
    return {
        last: lastElement(array),
        array: dropLast(array)
    };
}

.push() 메서드를 카피-온-라이트로 변경

function push(array, elem) {
    const copy = array.slice();
    copy.push(elem);
    return copy;
}

addContact() 리팩토링

function addContact(mailingList, email) {
    return push(mailingList, email);
}

arraySet() 함수 만들기

a[15] = 2;
 
function arraySet(array, idx, value) {
    const copy = array.slice();
    copy[idx] = value;
    return copy;
}

objectSet() 함수 만들기

o['price'] = 37;
 
function objectSet(object, key, value) {
    const copy = Object.assign({}, object);
    copy[key] = value;
    return copy;  
}

setPrice() 리팩토링

function setPrice(item, newPrice) {
    const edited = objectSet(item, 'price', newPrice);
    return edited;
}

setQuantity() 함수 만들기

function setQuantity(item, newQuantity) {
    const edited = objectSet(item, 'quantity', newQuantity);
    return edited;
}

delete 연산 만들기

const a = {x : 1};
deletea['x'];
a // {} 출력
 
function objectDelete(object, key) {
    const copy = Object.assign({}, object);
    delete copy[key]
    return copy;
}

다음 중첩된 동작을 카피-온-라이트로 변경

 
// as-is
function setQuantityByName(cart, name, quantity) {
    for(const i = 0; i < cart.length; i++) {
        if(cart[i].name === name) {
            cart[i].quantity = quantity;
        }
    }
}
 
// to-be
function setQuantityByName(cart, name, quantity) {
    const copy = cart.slice();
    for(const i = 0; i < copy.length; i++) {
        if(copy[i].name === name) {
            copy[i] = setQuantity(copy[i], quantity);
        }
    }    
    return copy;
}