비슷하게 생긴 함수들이 마케팅 팀 요청으로 인해 늘어났다는 이야기로 시작한다. 분명 실무에서 그때그때 필요한 코드를 짜다보면 발생 가능한 문제다. 비슷한 기능을 하는 함수들을 하나의 함수로 통합해 모듈로 사용하면 불필요한 용량을 줄일 수 있기 때문이다.
이를 위해 분명하게 보이지 않으나 비슷한 기능을 하는 함수들의 차이점은 필드를 결정하는 문자열이 함수 이름에 있다는 것으로, 그 일부가 인자처럼 동작한다는 해결방법을 제시하며, 이 코드의 냄새를 “함수 이름에 있는 암묵적 인자” 라고 부른다.
해결 예시
// 이전 function setPriceByName(cart, name, price) { var item = cart[name]; var newItem = objectSet(item, 'price', price); ... } cart = setPriceByName(cart, "shoe", 13); // 이후 function setFieldByName(cart, name, value, field) { var item = cart[name]; var newItem = objectSet(item, field, value); ... } cart = setFieldByName(cart, "shoe",'price', 13);
"암묵적 인자를 드러내기" 리팩토링을 통해 필드명을 일급 값으로 만든다. 리팩토링 전에는 필드명이 함수 이름에 암묵적으로 있었으며 API로도 제공되지 않았으나, 이제 암묵적인 이름은 인자로 넘기는 값이 되었다.
해당 문제를 해결하기 위한 두 가지 방법은 컴파일 타임에 검사하는 것과 런타임에 검사하는 것이다.
주로 정적 타입 시스템에서 사용하며 자바스크립트의 슈퍼셋인 타입스크립트로 문자열이 사용할 수 있는 필드인지 확인 가능하다.
함수를 실행할 때마다 동작하여 전달한 문자열이 올바른 문자열인지 확인하는 방식이다.
var validItemFields = ['price', 'quantity', 'shipping', 'tax'];
function setFieldByName(cart, name, value, field) {
if(!validItemFields.includes(field))
throw "Not a valid item field: " + "'" + field + "'.";
var item = cart[name];
var newItem = objectSet(item, field, value);
...
}
필드명을 일급으로 만들어 사용하면 세부 구현을 밖으로 노출하는 것인지에 대한 우려 발생한다.
내부에서 정의한 필드명이 바뀐다고 해도 사용하는 사람들이 원래 필드명을 그대로 사용하고 싶다면 내부에서 필드명을 바꿔주는 코드 추가하면 된다.
var translations = {'quantity': 'number'};
// 함수 내에 추가
if(translations.hasOwnProperty(field)) field = translations[field];
// 1번
const multyply = (x, y) => {return x * y};
// 2번
const incrementFieldByName = (cart, name, field) => {
var item = cart[name];
var value = item[field];
var newValue = value + 1;
var newItem = objectSet(item, field, newField);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
해시 맵이 속성과 값을 잘 표현하는데 자바스크립트에서는 객체로 동일한 효과를 볼 수 있다.
❗ 데이터를 사용할 때 인터페이스로 감싸주면 데이터를 정해진 방법으로만 쓸 수 있다. 그러나 보편적인 엔티티는 객체와 배열처럼 일반적인 데이터 구조 사용해야 함.
예시 코드
// f는 함수로, forEach 함수는 인자를 함수로 받을 수 있는 고차함수
function forEach(array, f) {
for(var i = 0; i < array.length; i++) {
var item = array[i];
f(item);
}
}
function cookAndEat(food) {
cook(food);
eat(food);
}
function clean(dish) {
wash(dish);
dry(dish);
putAway(dish);
}
// 수정
forEach(foods, function(food) {
cook(food);
eat(food);
});
forEach(dishes, function(dish) {
wash(dish);
dry(dish);
putAway(dish);
});
카피-온-라이트의 규칙은 복사본을 만들고 수정한 다음 복사본을 리턴하는 것이므로, 달라지는 부분은 수정하는 방법이고, 복사본을 만들고 리턴하는 동작은 항상 같다.
따라서 달라지는 부분을 본문으로, 달라지지 않는 앞뒤 사이에 오면 적용이 가능하다.
// 앞, 뒤 확인하기
function arraySet(arr, idx, val) {
let copy = arr.slice(); // 앞. 복사본을 만드는 과정으로 어떤 경우에도 변하지 않음
copy[idx] = val; // 본문
return copy; // 뒤. 복사본을 반환하는 과정으로 어떤 경우에도 변하지 않음
}
// 원본에서 함수 추출
function arraySet(arr, idx, val) {
return withArrayCopy(arr);
}
// 추출한 함수
function withArrayCopy(arr) {
let copy = arr.slice();
copy[idx] = val; // val 값이 없어 작동 x
return copy;
}
// 본문을 인자로 받음
function arraySet(arr, idx, val) {
return withArrayCopy(arr, function(copy) {
copy[idx] = val;
});
}
// 추출한 함수 → copy-on-write 원칙을 따르고 재사용할 수 있는 함수
function withArrayCopy(arr, modify) {
let copy = arr.slice();
modify(copy);
return copy;
}
arraySet()
함수 실행 단계arraySet()
함수 실행withArrayCopy()
실행withArrayCopy()
내에서 만든 변수 copy
가 콜백 함수 인자가 되어 modify
를 실행modify
가 익명 함수로 실행되어 copy[idx] = val
를 수행함copy
를 반환하며 withArrayCopy()
실행 종료arraySet()
실행 종료// 배열 메소드를 활용하는 함수에 withArrayCopy 적용하기
const push = (arr, el) => {
return withArrayCopy(arr, (copy) => {
copy.push(el);
});
}
const drop_last = (arr) => {
return withArrayCopy(arr, (copy) => {
copy.pop();
});
}
const drop_first = (arr) => {
return withArrayCopy(arr, (copy) => {
copy.shift();
});
}
// 객체에도 카피-온-라이트 적용하기
function objectSet(object, key, value) {
let copy = Object.assign({}, object);
copy[key] = value;
return copy;
}
function objectDelete(object, key) {
let copy = Object.assign({}, object);
delete copy[key]
return copy;
}
// withObjectCopy 함수 생성
function withObjectCopy(object, modify) {
let copy = Object.assign({}, object);
modify(copy);
return copy;
}
// 상기 함수 리팩토링
function objectSet(object, key, value) {
return withObjectCopy(object, function(copy) {
copy[key] = value;
});
}
function objectDelete(object, key) {
return withObjectCopy(object, function(copy) {
delete copy[key];
});
}
함수형 프로그래밍에서 함수를 통합하고 단순화하면서 인자와 함수의 역할이 뭔지를 명확하게 하는 작업이 얼마나 중요한 부분인지 이 두 장을 통해서 확실히 알았다.
이에 더해서 단순히 인자로 변수에 저장된 값만 전달하는 것이 아니라 함수를 넘겨주어 고차 함수로 만든다면 굳이 불필요한 중복 함수를 만들 필요가 없다는 점도 앞으로 프로그래밍을 할 때 적절하게 사용하는 연습을 해야겠다고 다짐했다.
이제 파트2로 넘어와서 조금 더 구체적이고 실제로 코딩에 쉽게 반영하기 어려운 내용들이 나오고 있는데 잊지 않고 머릿속에 잘 저장해뒀다가 반영하게 된다면 기쁠 것 같다!😄