Skip to content

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

앞 장에서는 배열을 다루는 함수형 도구를 살펴봤습니다.

이번 장에서는 객체를 다루는 함수형 도구에 대해 알아보고, 특히 중첩된 데이터를 재귀적으로 안전하게 접근하는 방법을 다룹니다.

객체 리팩토링하기

예를 들어, 객체의 quantity 필드를 1 증가시키는 함수는 다음과 같이 작성할 수 있습니다.

jsx
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;
}

문제는 함수 이름에 암묵적 인자(필드 이름)가 숨어 있다는 점입니다.

이 경우 암묵적 인자를 드러내기 리팩토링을 적용할 수 있습니다.

1차 리팩토링

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

이제 어떤 필드를 증가시킬지 명시적으로 드러납니다.

하지만 여전히 decrement, double 같은 변형이 생기면 중복이 발생합니다.

2차 리팩토링 (본문을 콜백으로 바꾸기)

jsx
function update(object, key, modify) {
  var value = object[key];
  var newValue = modify(value);
  var newObject = objectSet(object, key, newValue);
  return newObject;
}
  • update고차 함수로 변환되어 값의 변형 로직을 외부에서 주입합니다.
  • objectSet을 통해 원본을 훼손하지 않는 카피-온-라이트 원칙을 유지합니다.

중첩된 객체 리팩토링하기

객체 안에 또 다른 객체가 있을 때는 update만으로는 부족합니다.

jsx
const shirt = {
  options: {
    color: "blue",
  },
};

단순히 update를 여러 번 중첩 호출하면 코드가 장황해지고 유지보수가 어려워집니다.

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

따라서 재귀 함수를 활용해 일반화할 수 있습니다.

재귀를 통한 일반화

jsx
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); // 재귀 호출
  });
}
  • keys 배열에 깊이만큼의 키를 전달
  • keys.length === 0일 때 modify 실행
  • 그렇지 않으면 첫 번째 키로 접근 후 나머지 키에 대해 재귀 호출

안전한 재귀 작성 규칙

  1. 종료 조건: 더 이상 자신을 호출하지 않는 조건이 반드시 필요하다.
  2. 재귀 호출: 최소 한 번 이상은 자기 자신을 호출해야 한다.
  3. 종료 조건에 다가가기: 호출을 반복할수록 종료 조건에 가까워져야 한다.

깊이 중첩된 데이터와 추상화 벽

직접 경로를 다 쓰면 코드가 복잡해집니다.

jsx
nestedUpdate(blogCategory, ["posts", "12", "author", "name"], capitalize);

이 코드는 내부 구조를 너무 많이 알아야 하고, 기억하기도 어렵습니다.

따라서 추상화 벽(Abstraction Barrier)을 적용합니다.

추상화 벽 적용 후

jsx
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);
});
  1. 내부 구조를 몰라도 사용할 수 있습니다. → 기억 부담 감소
  2. 함수 이름이 동작을 드러내므로 이해하기 쉬워집니다. → 가독성 증가

정리하며

이번 장에서는 객체를 다루는 함수형 도구를 살펴보고, 특히 중첩된 데이터를 재귀적으로 안전하게 접근하는 방법을 다뤘습니다.

또한 깊은 경로를 직접 나열하는 대신 추상화 벽을 세우면 내부 구조를 알 필요 없이 함수를 사용할 수 있습니다. 그리고 함수 이름만으로 의도가 드러나 복잡성을 줄일 수 있습니다.