일급 함수 I
리팩터링 과정에서 함수 이름에 숨어 있는 암묵적 인자가 의외로 많은 중복을 만듭니다.
이를 해결하는 방법은 두 가지입니다.
- 암묵적 인자 드러내기: 함수 이름에 들어 있던 값을 인자로 끌어내기
- 본문을 콜백으로 바꾸기: 공통 제어 흐름을 함수로 묶고, 다른 동작은 인자로 전달하기
이 글에서는 함수형 코딩 관점에서 이 두 가지 리팩터링 패턴을 예제와 함께 정리합니다.
스터디 회차: 9회차 (2025년 9월 2일)
함수 이름에 있는 암묵적 인자
- 함수 본문에서 사용하는 어떤 값이 함수 이름에 나타난다면 함수 이름에 있는 암묵적 인자입니다.
- 이는 코드의 냄새가 됩니다. 이는 "암묵적 인자 드러내기" 나 "함수 본문을 콜백으로 바꾸기"를 통해 해결할 수 있습니다.
코드의 냄새
더 큰 문제를 가져올 수 있는 코드입니다.
특징
- 거의 똑같이 구현된 함수가 있습니다.
- 함수 이름이 구현에 있는 다른 부분을 가리킵니다.
함수 이름에 있는 암묵적 인자를 드러내기
- 암묵적 인자를 드러내기 리팩토링은 암묵적 인자가 일급 값이 되도록 함수에 인자를 추가합니다.
- 이렇게 하면 잠재적 중복을 없애고 코드의 목적을 더 잘 표현 할 수 있습니다.
예제: 장바구니 제품 속성 변경하기
tsx
function setPriceByName(cart, name, price) {
var item = cart[name];
var newItem = objectSet(item, "price", price);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
function setShippingByName(cart, name, ship) {
var item = cart[name];
var newItem = objectSet(item, "shipping", ship);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
문제점
- 함수의 구현이 거의 똑같습니다.
- 함수의 이름이 구현의 차이를 만듭니다.
마치 함수의 이름에 있는 일부가 인자처럼 동작하는 것 같습니다.
단계
- 함수 이름에 있는 암묵적 인자를 확인합니다.
- 명시적인 인자를 추가합니다.
- 함수 본문에 하드 코딩된 값을 새로운 인자로 바꿉니다.
- 함수를 호출하는 곳을 고칩니다.
개선하기
tsx
function setPriceByName(cart, name, price) {
var item = cart[name];
var newItem = objectSet(item, "price", price);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
- 함수 이름에 있는 암묵적 인자를 확인합니다.
- 함수 이름에 있는 Price가 암묵적 인자입니다.
tsx
function setFieldByName(cart, name, field, value) {
var item = cart[name];
var newItem = objectSet(item, field, value);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
- 명시적인 인자를 추가합니다.
- 함수 본문에 하드 코딩된 값을 새로운 인자로 바꿉니다.
tsx
// Before
cart = setPriceByName(cart, "shoe", 13);
cart = setQuantityByName(cart, "shoe", 3);
cart = setShippingByName(cart, "shoe", 0);
cart = setTaxByName(cart, "shoe", 2.34);
// After
cart = setFieldByName(cart, "shoe", "price", 13);
cart = setFieldByName(cart, "shoe", "quantity", 3);
cart = setFieldByName(cart, "shoe", "shipping", 0);
cart = setFieldByName(cart, "shoe", "tax", 2.34);
- 원래는 4개의 함수를 선언해야했지만, 개선하고나서는 하나의 함수로 해결이 가능해졌습니다.
필드 명을 문자열로 하면 버그가 생기지 않을까요?
- 이를 해결하기 위한 방법은 컴파일 타임에 검사하는 것과 런타임에 검사하는 것입니다.
- 자바스크립트는 정적 타입 언어가 아니기 때문에 런타임 검사 방법을 통해 필드명이 올바른지 확인할 수 있습니다.
tsx
var validItemFields = ["price", "quantity", "shipping", "tax"];
function setFieldByName(cart, name, field, value) {
if (!validItemFields.includes(field))
throw "Not a valid item field: " + "'" + field + "'.";
var item = cart[name];
var newItem = objectSet(item, field, value);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
API 문서에 필드명을 명시하면 영원히 필드명을 바꾸지 못하는 것이 아닌가요?
- 맞습니다. 필드명은 계속 유지해야합니다. 하지만 구현이 외부에 노출된 것은 아닙니다.
- 만약 내부에서 정의한 필드명이 바뀐다고 해도 원래 필드명을 사용하게 하면서, 내부에서 바꿔주면 됩니다.
tsx
var translations = { quantity: "number" };
function setFieldByName(cart, name, field, value) {
if (translations.hasOwnProperty(field)) field = translations[field];
var item = cart[name];
var newItem = objectSet(item, field, value);
var newCart = objectSet(cart, name, newItem);
return newCart;
}
함수 본문을 콜백으로 바꾸기
- 함수 본문을 콜백으로 바꾸기 리팩토링으로 함수 본문에 어떤 부분을 콜백으로 바꿉니다.
- 이렇게 하면 일급 함수로 어떤 함수에 동작을 전달할 수 있습니다.
예제: 로깅하는 코드 분리하기
tsx
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
try {
fetchProduct(productId);
} catch (error) {
logToSnapErrors(error);
}
문제점
- 두 개의 코드에서는 문법적으로 비슷한 부분이 많습니다.
- 비슷한 부분을 찾아 하나로 만들 수 있다면 코드의 중복을 없앨 수 있습니다.
단계
- 함수 본문에서 바꿀 부분의 앞부분과 뒷부분을 확인합니다.
- 리팩토링 할 코드를 함수로 빼냅니다.
- 빼낸 함수의 인자로 넘길 부분을 또 다른 함수로 빼냅니다.
개선하기
tsx
function withLogging() {
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
}
withLogging();
- 코드를 함수로 만들고 설명할 수 있는 이름을 붙입니다.
- 본문과 본문의 앞부분과 뒷부분을 구분합니다.
- saveUserData(user) 를 기준으로 앞부분, 뒷부분으로 구분할 수 있습니다.
- 본문 부분을 빼낸 함수의 인자로 전달한 함수로 바꿉니다.???
tsx
function withLogging(f) {
try {
f();
} catch (error) {
logToSnapErrors(error);
}
}
- f는 함수를 의미하고 원래 본문이 있던 곳에서 인자로 받은 함수를 호출합니다.
tsx
withLogging(function () {
saveUserData(user);
});
- 본문을 전달합니다.
왜 본문을 함수로 감싸서 넘기나요?
코드가 바로 실행되면 안되기 때문입니다. 함수로 감싸는 방법은 코드의 실행을 미루는 일반적인 방법입니다.
실행을 미루고 withLogging 함수에서 만든 try/catch 문안에서 실행하도록 만듭니다.
함수의 실행을 전달하면 어떻게 되나요?
tsx
function withLogging(data) {
try {
data;
} catch (error) {
logToSnapErrors(error);
}
}
withLogging(saveUserData(user));
함수 대신, 함수의 실행을 전달한다고 가정해봅시다.
함수 자체를 전달하지 않고 결과를 전달합니다.
이 함수는 try/catch 문 밖에서 부르기 때문에 에러가 발생할 경우, withLogging 함수를 부르기 전에 발생하여 try/catch 문이 제대로 동작하지 않습니다.
정리하기
- 함수 이름에 숨어 있는 암묵적 인자는 코드의 냄새이며, 반드시 명시적 인자로 드러내야 합니다.
- 이를 위해 함수에 새로운 인자를 추가하고, 하드코딩된 값을 치환해 중복을 줄일 수 있습니다.
- 또, 함수 본문을 콜백으로 바꾸면 원하는 동작을 일급 함수로 전달해 실행할 수 있습니다.
- 결국 암묵적 인자는 드러내고, 공통 제어 흐름은 콜백으로 추출함으로써 코드의 중복은 줄고 의도는 더욱 선명해집니다.