Skip to main content

10장 일급 함수 1

written by
Jtree03
Jtree03 🏆Software Engineer

이번 장에서 살펴볼 내용

  • 왜 일급 값이 좋은지 알아봅니다.
  • 문법을 일급 함수로 만드는 방법에 대해 알아봅니다.
  • 고차 함수로 문법을 감싸는 방법을 알아봅니다.
  • 일급 함수와 고차 함수를 사용한 리팩터링 두 개를 살펴봅니다.

마케팅팀은 여전히 개발팀과 협의해야 합니다.

추상화 벽은 마케팅티미 사용하기 좋은 API 였습니다. 하지만 예상만큼 잘 안 되었습니다. 대부분은 개발팀과 협의 없이 일할 수 있었지만, 주어진 API 로는 할 수 없는 일이 있어서 새로운 것은 개발팀에 요청해야 합니다.

요구 사항은 다음과 같습니다: 장바구니에 있는 제품 값을 설정하는 기능, 장바구니에 있는 제품 개수를 설정하는 기능, 장바구니에 있는 제품에 배송을 설정하는 기능

코드의 냄새: 함수 이름에 있는 암묵적 인자

interface setPriceByName {
(cart: Cart, name: string, price: number): Cart;
}
interface setQuantityByName {
(cart: Cart, name: string, quantity: number): Cart;
}
interface setShippingByName {
(cart: Cart, name: string, shipping: number): Cart;
}
interface setTaxByName {
(cart: Cart, name: string, tax: number): Cart;
}

자세히 보면 함수 이름에 따라 비슷한 일을 하는 것으로 보입니다.

냄새를 맡는 법

함수 이름에 있는 암묵적 인자(implicit argument in function name) 냄새는 두 가지 특징을 보입니다.

  1. 함수 구현이 거의 똑같습니다.
  2. 함수 이름이 구현의 차이를 만듭니다.

함수 이름에서 서로 다른 부분이 암묵적 인자입니다.

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

  1. 함수 이름에 있는 암묵적 인자를 확인합니다.
  2. 명시적인 인자를 추가합니다.
  3. 함수 본문에 하드 코딩된 값을 새로운 인자로 바꿉니다.
  4. 함수를 부르는 곳을 고칩니다.

리팩터링 전

interface SetPriceByName {
(cart: Cart, name: string, price: number): Cart;
}
interface SetQuantityByName {
(cart: Cart, name: string, quantity: number): Cart;
}
interface SetShippingByName {
(cart: Cart, name: string, shipping: number): Cart;
}
interface SetTaxByName {
(cart: Cart, name: string, tax: number): Cart;
}

리팩터링 후

interface SetFieldByName {
(cart: Cart, name: string, field: string, value: string | number): Cart;
}

일급인 것과 일급이 아닌 것을 구별하기

자바스크립트에서 일급이 아닌 것

  • 수식 연산자
  • 반복문
  • 조건문
  • try/catch 블록

일급으로 할 수 있는 것

  • 변수에 할당
  • 함수의 인자로 넘기기
  • 함수의 리턴값으로 받기
  • 배열이나 객체에 담기

필드명을 문자열로 사용하면 버그가 생기지 않을까요?

타입스크립트를 이용하면 해결 완료!

일급 필드를 사용하면 API를 바꾸기 더 어렵나요?

기존의 API 를 삭제하지 않아도 맵핑을 이용하여 추가하면 해결이 됩니다. (개방 폐쇄 원칙)

const validItemFields = ['price', 'quantity', 'shipping', 'tax', 'number'];
const translations = { quantity: 'number' };

const setFieldByName: SetFieldByName = (cart, name, field, value) => {
if (!validItemFields.includes(field))
throw new Error(`Not a valid item field: '${field}'.`);
if (translations.hasOwnProperty(field)) field = translations[field];
const item = cart[name];
const newItem = objectSet(item, field, value);
const newCart = objectSet(cart, name, newItem);

return newCart;
};

객체와 배열을 너무 많이 쓰게 됩니다.

장바구니와 제품처럼 일반적인 엔티티는 객체와 배열처럼 일반적인 데이터 구조를 사용해야 합니다.

정적 타입 vs 동적 타입

...
예를 들어 어떤 연구에서는 정적 타입 언어와 동적 타입 언어를 구분하는 것보다
소프트웨어 품질을 위해 숙면을 하는 것이 더 중요하다고 합니다.
...

모두 문자열로 통신합니다.

...
그럼 정적 타입 언어는 쓰지 않아야 하나요? 그것은 아닙니다. 그럼 써야 하나요? 그것도 아닙니다.
다만 동적 타입 언어가 이런 문제를 만드는 것이 아니고 정적 타입 언어가 없어져야할 대상이 아니라는 것을 잘 알아야 합니다.
그리고 데이터의 단점 하나를 발견할 수 있었습니다.
그것은 바로 데이터는 항상 해석이 필요하다는 것입니다. (JSON.parse?)

어떤 문법이든 일급 함수로 바꿀 수 있습니다.

function plus(a: number, b: number) {
return a + b;
}
interface ARGS {
tryFunction: VoidFunction;
catchFunction?: VoidFunction;
finallyFunction?: VoidFunction;
}

function tryCatchFinally({
tryFunction,
catchFunction,
finallyFunction,
}: ARGS) {
try {
tryFunction();
} catch {
catchFunction?.();
} finally {
finallyFunction?.();
}
}

반복문 예제: 먹고 치우기

for (let i = 0; i < foods.lenghth; i++) {
const food = foods[i];
const cookedFood = cook(food);
eat(cookedFood);
}

for (let i = 0; i < dishes.lenghth; i++) {
const dish = dishes[i];
const washedDish = wash(dish);
const driedDish = dry(washedDish);
putAway(driedDish);
}

위의 배열 동작을 추상화할 수 있는데 그것을 forEach 라고 부릅니다.

function cookAndEat(food: Food) {
const cookedFood = cook(food);
const dish = eat(cookedFood);

return dish;
}

function clean(dish: Dish) {
const washedDish = wash(dish);
const driedDish = dry(dish);
putAway(driedDish);
}

const foods = await getFoods();
const dishes = foods.map(cookAndEat);
dishes.forEach(clean);

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

try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
function withLogging(callback: voidFunction) {
try {
callback();
} catch (error) {
logToSnapErrors(error);
}
}
withLogging(() => saveUserData(user));

이것은 무슨 문법인가요?

  1. 전역으로 정의하기
function saveCurrentUserData() {
saveUserData(user);
}
withLogging(saveCurrentUserData);
  1. 지역적으로 정의하기
function someFunction() {
const saveCurrentUserData = function () {
saveUserData(user);
};
withLogging(saveCurrentUserData);
}
  1. 인라인으로 정의하기
withLogging(function () {
saveUserData(user);
});

왜 본문을 함수로 감싸서 넘기나요?

감싼 함수를 호출하기 전까지 실행되지 않습니다. (지연 평가, Lazy evaluation)

이름 붙이기

const saveCurrentUserData = () => saveUserData(user);

컬렉션에 저장하기

array.push(() => saveUserData(user));

그냥 넘기기

withLogging(() => saveUserData(user));

선택적으로 호출하기

function callOnThursday(voidFunction: VoidFunction) {
if (today === 'Thursday') voidFunction();
}

나중에 호출하기

function callTomorrow(voidFunction: VoidFunction) {
sleep(oneDay);
voidFunction();
}

새로운 문맥 안에서 호출하기

function withLogging(voidFunction: VoidFunction) {
try {
voidFunction();
} catch (error) {
logToSnapErrors(error);
}
}

요점 정리

  • 일급 값은 변수에 저장할 수 있고 인자로 전달하거나 함수의 리턴값으로 사용할 수 있습니다.
  • 언어에는 일급이 아닌 기능(if, for, +, - 등)이 있는데 함수로 감싸 일급으로 만들 수 있습니다.
  • 어떤 언어는 함수를 일급 값처럼 쓸 수 있는 일급 함수가 있습니다.
  • 이는 어떤 단계 이상의 함수형 프로그래밍을 하는데 필요합니다.
  • 고차 함수로 다양한 동작을 추상화할 수 있습니다.
  • 함수 이름에 있는 암묵적 인자는 함수의 이름으로 구분하는 코드의 냄새입니다.
  • 동작을 추상화하기 위해 본문을 콜백으로 바꾸기 리팩터링을 사용할 수 있습니다.