중첩된 데이터에 함수형 도구 사용하기
앞선 장에서는 배열을 효과적으로 다루기 위한 함수형 도구를 살펴봤습니다.
이번 글에서는 객체를 다룰 수 있는 함수형 도구를 살펴봅니다.
스터디 회차: 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. 재귀 호출
});
}- 종료조건 설정하기
- 재귀 호출하기
- 종료 조건에 다가가기
중첩된 데이터 추상화의 벽 사용하기
예제: 중첩된 객체 데이터 변경하기
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);
});- 기억해야 할 것이 네 가지에서 세 가지로 줄었습니다.
- 동작의 이름이 있으므로 각각의 동작을 기억하기 쉽습니다.
정리하기
- 중첩 객체의 불변 업데이트는 "조회→변경→설정"이 단계마다 반복되고, 암묵적 인자가 함수 이름에 숨어 중복을 낳습니다.
- 개선하기
- 암묵적 인자 드러내기로 필드를 일반화합니다.
- 콜백 주입으로 동작을 일반화합니다.
- 재귀로 중첩을 일반화합니다.
- 추상화의 벽으로 도메인 지식을 캡슐화합니다.
