Skip to content

중첩된 데이터에 함수형 도구 사용하기

앞선 장에서는 배열을 효과적으로 다루기 위한 함수형 도구를 살펴봤습니다.

이번 글에서는 객체를 다룰 수 있는 함수형 도구를 살펴봅니다.

스터디 회차: 13회차 (2025년 9월 9일)

객체를 다루기 위한 고차함수 사용하기

예제: 장바구니 아이템 속성 변경하기

tsx
function incrementQuantity(item) {
  var quantity = item["quantity"];
  var newQuantity = quantity + 1;
  var newItem = objectSet(item, "quantity", newQuantity);
  return newItem;
}

function incrementSize(item) {
  var size = item["size"];
  var newSize = size + 1;
  var newItem = objectSet(item, "size", newSize);
  return newItem;
}

문제점

  • 함수 이름(incrementQuantity, incrementSize) 속에 무슨 필드를 변경하는지가 암묵적으로 숨겨져 있어, 함수 수가 필드 수만큼 늘어납니다.
  • "조회 → 변경 → 설정" 패턴이 반복됩니다.

개선하기: 암묵적 인자를 드러내기 리팩터링

tsx
function incrementField(item, field) {
  var value = item[field];
  var newValue = value + 1;
  var newItem = objectSet(item, field, newValue);
  return newItem;
}

function decrementField(item, field) {
  var value = item[field];
  var newValue = value - 1;
  var newItem = objectSet(item, field, newValue);
  return newItem;
}
  • 여전히 increment, decrement 와 같이 비슷한 동작들이 존재합니다.

개선하기: 함수 본문을 콜백으로 바꾸기 리팩터링

tsx
function updateField(item, field, modify) {
  var value = item[field];
  var newValue = modify(value);
  var newItem = objectSet(item, field, newValue);
  return newItem;
}

// updateField -> update
function update(object, key, modify) {
  var value = object[key];
  var newValue = modify(value);
  var newObject = objectSet(object, key, newValue);
  return newObject;
}
  • 이제 "무엇을 어떻게 바꿀지"를 콜백으로 외부에 맡깁니다.
  • 증가, 감소, 클램핑, 포맷 변경 등 모든 변형을 modify로 주입하여 해결할 수 있습니다.
  • 이름을 일반적으로 바꾸어 줍니다.

중첩된 객체의 속성 변경하기

예제: 사이즈 옵션 변경하기

tsx
var shirt = {
  name: "shirt",
  price: 13,
  options: {
    color: "blue",
    size: 3,
  },
};

function incrementSize(item) {
  var options = item.options; // 조회
  var size = options.size; // 조회
  var newSize = size + 1; // 변경
  var newOptions = objectSet(options, "size", newSize); // 설정
  var newItem = objectSet(item, "options", newOptions); // 설정
  return newItem;
}

문제점

  • 객체의 속성을 변경하기 때문에 update 를 적용할 수 있습니다.
  • 자세히 보면 중첩된 "조회 > 변경 > 설정" 패턴을 가지고 있습니다.

개선하기

tsx
function incrementSize(item) {
  var options = item.options; // 조회
  var newOptions = update(options, "size", increment); // 변경
  var newItem = objectSet(item, "options", newOptions); // 설정
  return newItem;
}
  • 안쪽에 update 를 적용하고 보니, 바깥쪽에 존재하는 조회 > 변경 > 설정이 드러났습니다.
  • 한번더 update를 적용할 수 있습니다.
tsx
function incrementSize(item) {
  return update(item, "options", function (options) {
    return update(options, "size", increment);
  });
}
  • 중첩된 객체에 중첩된 단계만큼 update를 사용할 수 있다는 것을 알게 되었습니다.
  • 그런데 안쪽에 update는 암묵적 인자를 사용하고 있습니다. 더 개선할 수 있습니다.
tsx
// 1. size를 인자로 받습니다.
function incrementOption(item, option) {
  return update(item, "options", function (options) {
    return update(options, option, increment);
  });
}

// 2. increment 부분도 인자로 받도록 변경합니다.
function updateOption(item, option, modify) {
  return update(item, "options", function (options) {
    return update(options, option, modify);
  });
}

// 3. Option에 대한 필드명도 인자로 변경합니다.
function update2(object, key1, key2, modify) {
  return update(object, key1, function (value1) {
    return update(value1, key2, modify);
  });
}

// 최종 코드
function incrementSize(item) {
  return update2(item, "options", "size", function (size) {
    return size + 1;
  });
}
  • 암묵적 인자를 명시적 인자로 모두 바꾸어 주고 이름을 일반적인 update2로 변경합니다.

세 번 중첩된 객체의 속성 변경하기

예제: 장바구니 안에 있는 제품의 옵션 변경하기

tsx
function incrementSizeByName(cart, name) {
  var item = cart[name]; // 조회
  var options = item.options; // 조회
  var size = options.size; // 조회
  var newSize = size + 1; // 변경
  var newOptions = objectSet(options, "size", newSize); // 설정
  var newItem = objectSet(item, "options", newOptions); // 설정
  var newCart = objectSet(cart, name, newItem); // 설정
  return newCart;
}

// 중첩된 update 적용하기
function incrementSizeByName(cart, name) {
  return update(cart, name, function (item) {
    return update(item, "options", function (options) {
      return update(options, "size", function (size) {
        return size + 1;
      });
    });
  });
}

// 암묵적 인자 제거하기
function update3(object, key1, key2, key3, modify) {
  return update(object, key1, function (object2) {
    return update2(object2, key2, key3, modify);
  });
}

문제점

  • 중첩된 조회 > 변경 > 설정을 update 함수를 적용하고 암묵적 인자를 제거하여 update3를 만들 수 있습니다.
  • 하지만 update3처럼 중첩 수만큼 함수를 계속 늘리는 건 확장성이 없습니다.

재귀로 개선하기

tsx
function update2(object, key1, key2, modify) {
  return update(object, key1, function (value1) {
    return update1(value1, key2, modify);
  });
}

function update1(object, key1, modify) {
  return update(object, key1, function (value1) {
    return update0(value1, modify);
  });
}

function update0(value, modify) {
  return modify(value);
}
  • updateX 를 만들려고한다면 update 안에 updateX-1 을 호출하면 됩니다.
  • 그리고 update0는 중첩되지 않은 객체를 의미하므로 조회나 설정을 하지 않고 변경을 합니다.

인자에 대한 정보 추가하기

tsx
function updateX(object, depth, key1, key2, key3, modify) {
  return update(object, key1, function (value1) {
    return updateX(value1, depth - 1, key2, key3, modify);
  });
}
  • depth를 전달하여 인자의 개수에 대한 정보를 추가합니다.
  • 하지만 depth와 인자의 개수가 달라진다면 버그가 발생할 수 있습니다.
    • 이는 배열 자료구조를 통해 해결할 수 있습니다.
    • key를 배열로 전달하고 depth를 배열의 길이로 나타냅니다.

배열을 사용하여 전달하기

tsx
function updateX(object, keys, modify) {
  var key1 = keys[0];
  var restOfKeys = drop_first(keys);
  return update(object, key1, function (value1) {
    return updateX(value1, restOfKeys, modify);
  });
}
  • 재귀 호출을 통해 keys의 값을 하나씩 읽어가며 중첩된 객체에 접근할 수 있습니다.
  • 하지만 udpate0의 경우를 고려하여 특별한 처리가 필요합니다.

0에 대한 처리 추가하기

tsx
function nestedUpdate(object, keys, modify) {
  if (keys.length === 0) return modify(object);
  var key1 = keys[0];
  var restOfKeys = drop_first(keys);
  return update(object, key1, function (value1) {
    return nestedUpdate(value1, restOfKeys, modify);
  });
}
  • 길이가 0인 경우 키가 없기 때문에 특별하게 처리하여 modify 를 실행합니다.
  • 이름을 일반적인 것으로 바꾸어 줍니다.

안전한 재귀 사용법

tsx
function nestedUpdate(object, keys, modify) {
  // 1. 종료 조건
  if (keys.length === 0) return modify(object);

  var key1 = keys[0];
  var restOfKeys = drop_first(keys); // 3. 종료 조건에 다가가기
  return update(object, key1, function (value1) {
    return nestedUpdate(value1, restOfKeys, modify); // 2. 재귀 호출
  });
}
  1. 종료조건 설정하기
  2. 재귀 호출하기
  3. 종료 조건에 다가가기

중첩된 데이터 추상화의 벽 사용하기

예제: 중첩된 객체 데이터 변경하기

tsx
function incrementSizeByName(cart, name) {
  return nestedUpdate(cart, [name, "options", "size"], function (size) {
    return size + 1;
  });
}

문제점

  • 경로에 따라 중첩된 각 단계에는 어떤 키가 있는지 기억해야합니다.
  • 같은 작업을 하면서 알아야할 데이터 구조를 추상화의 벽을 사용하여 줄일 수 있습니다.

개선하기

tsx
function updatePostById(category, id, modifyPost) {
  return nestedUpdate(category, ["posts", id], modifyPost);
}

function updateAuthor(post, modifyUser) {
  return update(post, "author", modifyUser);
}

function capitalizeName(user) {
  return update(user, "name", capitalize);
}

updatePostById(blogCategory, "12", function (post) {
  return updateAuthor(post, capitalizeUserName);
});
  • 기억해야 할 것이 네 가지에서 세 가지로 줄었습니다.
  • 동작의 이름이 있으므로 각각의 동작을 기억하기 쉽습니다.

정리하기

  • 중첩 객체의 불변 업데이트는 "조회→변경→설정"이 단계마다 반복되고, 암묵적 인자가 함수 이름에 숨어 중복을 낳습니다.
  • 개선하기
    • 암묵적 인자 드러내기로 필드를 일반화합니다.
    • 콜백 주입으로 동작을 일반화합니다.
    • 재귀로 중첩을 일반화합니다.
    • 추상화의 벽으로 도메인 지식을 캡슐화합니다.