Ch10-11 일급 함수

·

4 min read

코드 냄새와 중복을 없애 추상화를 잘할 수 있는 리팩터링 두 가지

암묵적 인자를 드러내기 express implicit argument

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

  • 함수 구현이 거의 똑같음

  • 함수 이름이 구현의 차이를 만듦

  • ex.

      cart = setPriceByName(cart, 'shoe', 13);
      cart = setQuantityByName(cart, 'shoe', 13);
      cart = setShippingByName(cart, 'shoe', 0);
      cart = setTaxByName(cart, 'shoe', 2.34);
    

리팩터링

  • 함수 이름에 있는 암묵적 인자를 확인

  • 명시적인 인자를 추가

    • 일급 값으로 만드는 것이 중요
  • 함수 본문에 하드 코딩된 값을 새로운 인자로 바꾸기

  • 함수 호출부 고치기

  • ex.

      cart = setFieldByName(cart, 'shoe', 'price', 13);
      cart = setFieldByName(cart, 'shoe', 'quantity', 13);
      cart = setFieldByName(cart, 'shoe', 'shipping', 0);
      cart = setFieldByName(cart, 'shoe', 'tax', 2.34);
    

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

  • JS에서 일급이 아닌 것: 수식 연산자, 반복문, 조건문, try/catch 블록

  • 일급으로 할 수 있는 것

    • 변수에 할당

    • 함수의 인자로 넘기기

    • 함수의 리턴값으로 받기

    • 배열이나 객체에 담기

  • 일급이 아닌 것을 찾아 일급으로 바꾸는 기술이 FP에서 중요

    • ex. + 연산자는 변수에 할당할 수 없지만 function plus(a, b)와 같은 함수를 만들 수 있음

일급 필드를 사용하면 API를 바꾸기 더 어려워지는 게 아닐까?

코드 전체를 바꾸지 않고, 추상화 벽 위에서 기존 필드명을 그대로 사용하고 싶다면, 내부에서 간단히 바꿔주면 된다

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

function setFieldByName(cart, name, field, value) {
  // 런타임에도 안전하게 문자열 검사
  if (!validItemFields.includes(field)) throw 'Not a Valid item 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;
}

데이터 지향 data orientaion

이벤트와 엔티티에 대한 사실을 표현하기 위한 일반 데이터 구조를 사용하는 프로그래밍 형식

  • 객체, 배열

  • 일반적인 엔티티는 일반적인 데이터 구조를 사용해야 한다

  • 데이터가 미래에 어떤 방법으로 해석될지 미리 알 수 없기 때문에 필요할 때 알맞은 방법으로 해석할 수 있어야

함수 본문을 콜백으로 바꾸기 replace body with callback

공통 본문을 고차 함수로 분리

리팩터링

  • 함수 본문에서 바꿀 부분의 앞부분-뒷부분 확인

  • 리팩터링 할 코드를 함수로 빼내기

  • 빼낸 함수의 인자로 넘길 부분을 또 다른 함수로 빼내기

ex.

원래 코드 함수 본문에서 바꿀 부분의 앞부분-뒷부분 확인

// 앞부분
try {
  // 본문
  saveUserData(user);
  // 뒷부분
} catch (error) {
  logToSnapErrors(error);
}

// 앞부분
try {
  // 본문
  fetchProduct(productId);
  // 뒷부분
} catch (error) {
  logToSnapErrors(error);
}

리팩터링 할 코드를 함수로 빼내기

function withLogging() {
  try {
    // 본문
    fetchProduct(productId);
    // 뒷부분
  } catch (error) {
    logToSnapErrors(error);
  }
}

withLogging();

빼낸 함수의 인자로 넘길 부분을 또 다른 함수로 빼내기

function withLogging(f) {
  try {
    f();
  } catch (error) {
    logToSnapErrors(error);
  }
}

// 인자로 넘길 본문을 익명 함수로 빼내기
withLogging(() => saveUserData(user));
  • 함수에 일반 데이터 값이 아닌 함수를 전달하는 이유

    • 함수 안에 있는 코드가 특정 문맥 안에서 실행돼야 하기 때문

    • 고차 함수를 쓰면 다른 곳에 정의된 문맥에서 코드를 실행할 수 있음

배열 copy-on-write 리팩토링

원래 코드

function arraySet(array, idx, value) {
  const copiedArray = array.slice();
  // 바뀌는 본문
  copiedArray[idx] = value;
  return copiedArray;
}

function arrayPush(array, element) {
  const copiedArray = array.slice();
  // 바뀌는 본문
  copiedArray.push(element);
  return copiedArray;
}

function arrayDropLast(array) {
  const copiedArray = array.slice();
  // 바뀌는 본문
  copiedArray.pop();
  return copiedArray;
}

function arrayDropFirst(array) {
  const copiedArray = array.slice();
  // 바뀌는 본문
  copiedArray.shift();
  return copiedArray;
}

함수 빼내기, 콜백 빼내기

function arraySet(array, idx, value) {
  return withArrayCopy(array, (copiedArray) => {
    // 바뀌는 본문을 익명 함수로 빼내기
    copiedArray[idx] = value;
  });
}

// 바뀌지 않는 부분을 함수로 빼내기
function withArrayCopy(array, callback) {
  const copiedArray = array.slice();
  callback(copiedArray);
  return copiedArray;
}

logging 함수 리팩토링

  • 로그를 남기지 않는 함수를 로그를 남기는 함수로 감싸기

원래 함수

try {
  saveUserData(user);
} catch (error) {
  logToSnapErrors(error);
}

try {
  fetchProduct(productId);
} catch (error) {
  logToSnapErrors(error);
}

이름을 명확하게 바꿈

try {
  saveUserDataNoLogging(user);
} catch (error) {
  logToSnapErrors(error);
}

try {
  fetchProductWithNoLogging(productId);
} catch (error) {
  logToSnapErrors(error);
}

중복 제거; 익명 함수로 만들고, 인자를 일반적인 이름으로 바꾸기

function (arg) {
  try {
    saveUserDataNoLogging(arg);
  } catch (error) {
    logToSnapErrors(error);
  }
}

함수 본문을 콜백으로 바꾸기

function wrapLogging(f) {
  try {
    f(arg);
  } catch (error) {
    logToSnapErrors(error);
  }
}

const saveUserDataWithLogging = wrapLogging(saveUserDataNoLogging);
const fetchProductaWithLogging = wrapLogging(fetchProductNoLogging);

함수를 리턴하는 함수는 함수 팩토리와 같다

전체 프로그램을 고차 함수로 만들어도 될까?

가능하겠지만, 더 중요한 질문은 "정말 그것이 필요한가"임

고차 함수는 강력하지만, 비용이 따름.

직관적인 방법에 비해, 코드가 더 읽기 쉬운가? 중복 코드를 없앨 수 있는가? 코드가 하는 일이 무엇인지 쉽게 알 수 있는가? 등 질문들을 놓치면 안된다