Extract Function, 함수 추출하기에 대해 알아보자.

요약

코드

// before
function printOwing(invoice) {
  printBanner();
  let outstanding = calculateOutstanding();
 
  // 세부 사항 출력
  console.log(`고객명: ${invoice.customer}`);
  console.log(`채무액: ${outstanding}`);
}
// after
function printOwing(invoice) {
  printBanner();
  let outstanding = calculateOutstanding();
 
  // 세부 사항 출력
  printDetails(outstanding);
 
  function printDetails(outstanding) {
    console.log(`고객명: ${invoice.customer}`);
    console.log(`채무액: ${outstanding}`);
  }
}

배경

  • 코드 조각을 찾아 독립된 함수로 추출하고 이름을 붙인다.
  • 언제 독립된 함수로 묶어야 하는가?
  • 함수로 추출하는 것은 “목적과 구현을 분리”하는 행위다.
  • 즉, 구현을 대표하는 이름을 붙일 수 있을 때 추출한다.
  • 이러한 원칙을 적용하게 되면 함수를 아주 짧게 작성하는 습관이 들게 된다.
  • 다만 짧은 함수는 좋은 이름을 지었을 때 발휘된다.

절차

  1. 함수를 새로 만들고 목적을 드러내는 이름을 붙인다.
    • 어떻게에 집중하면 안된다. 무엇을 하는지가 드러나야 한다.
    • 목적이 더 잘 드러난다면 함수의 길이는 중요치 않다. 한줄도 상관없다.
  2. 추출할 코드를 복사하여 새 함수에 붙여넣는다.
  3. 추출한 코드 중 원본 함수의 지역 변수를 참조하거나 추출한 함수의 유효범위를 벗어나는 변수는 없는지 검사한다. 있다면 매개변수로 전달한다.
    • 중첩함수를 사용한다면 이런 문제가 없다.
    • 사용은 하지만 값이 바뀌지 않는 변수라면 함수의 인수로 다 받아버린다.
    • 추출한 코드 안에서만 사용하는 변수가 있는데, 선언을 밖에서 한다면 안으로 집어넣는다.
    • 추출한 코드 안에서 값이 바뀌는 변수 중에서 값으로 전달되는 것은 주의하자.
    • 너무 값을 수정하는 지역 변수가 많다면 추출을 멈추고 더 작은 범위에서 리팩토링을 시도해보아야 한다.
  4. 변수를 다 처리했다면 컴파일한다.
  5. 원본 함수에서 추출한 코드 부분을 새로 만든 함수를 호출하는 문장으로 바꾼다.
  6. 테스트한다.
  7. 다른 코드에 방금 추출한 것과 똑같거나 비슷한 코드가 없는지 살핀다. 있다면 방금 추출한 새 함수를 호출하도록 바꿀지 검토한다.
    • 비슷한 코드가 있다면 추출한 함수를 호출하도록 바꾸고 테스트한다.
    • 비슷한 코드가 없다면 추출한 새 함수를 인라인하거나 다른 함수로 추출한다.

예시

function printOwing(invoice) {
  let outstanding = 0;
 
  console.log('***********************');
  console.log('**** 고객 채무 ****');
  console.log('***********************');
 
  // 미해결 채무(outstanding)를 계산한다.
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
 
  // 마감일(dueDate)을 기록한다.
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
 
  // 세부 사항을 출력한다.
  console.log(`고객명: ${invoice.customer}`);
  console.log(`채무액: ${outstanding}`);
  console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`);
}

1. 유효범위를 벗어나는 변수가 없을 때

function printOwing(invoice) {
  let outstanding = 0;
 
  printBanner();
 
  // 미해결 채무(outstanding)를 계산한다.
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
 
  // 마감일(dueDate)을 기록한다.
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
 
  printDetails();
 
  function printBanner() {
    console.log('***********************');
    console.log('**** 고객 채무 ****');
    console.log('***********************');
  }
 
  function printDetails() {
    console.log(`고객명: ${invoice.customer}`);
    console.log(`채무액: ${outstanding}`);
    console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`);
  }
}

2. 지역 변수가 사용할 때

function printOwing(invoice) {
  let outstanding = 0;
 
  printBanner();
 
  // 미해결 채무(outstanding)를 계산한다.
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
 
  // 마감일(dueDate)을 기록한다.
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
 
  printDetails(invoice, outstanding);
 
  function printBanner() {
    console.log('***********************');
    console.log('**** 고객 채무 ****');
    console.log('***********************');
  }
}
 
function printDetails(invoice, outstanding) {
  console.log(`고객명: ${invoice.customer}`);
  console.log(`채무액: ${outstanding}`);
  console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`);
}
  • 중첩 함수에서 지역 함수로 빼버렸다.
  • 그 과정에서 함수의 파라미터로 받도록 변경했다.
function printOwing(invoice) {
  let outstanding = 0;
 
  printBanner();
 
  // 미해결 채무(outstanding)를 계산한다.
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
 
  recordDueDate(invoice);
  printDetails(invoice, outstanding);
 
  function printBanner() {
    console.log('***********************');
    console.log('**** 고객 채무 ****');
    console.log('***********************');
  }
}
 
function recordDueDate(invoice) {
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}
 
function printDetails(invoice, outstanding) {
  console.log(`고객명: ${invoice.customer}`);
  console.log(`채무액: ${outstanding}`);
  console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`);
}
  • 마찬가지로 마감일 설정 로직도 추출한다.

3. 지역 변수의 값을 변경할 때

  • 지역변수 값이 변경되면 까다로워진다.
  • 당장 어떻게 하는지 보고 복기해보자.
function printOwing(invoice) {
  let outstanding = 0;
 
  printBanner();
 
  // 미해결 채무(outstanding)를 계산한다.
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
 
  recordDueDate(invoice);
  printDetails(invoice, outstanding);
 
  function printBanner() {
    console.log('***********************');
    console.log('**** 고객 채무 ****');
    console.log('***********************');
  }
}
function printOwing(invoice) {
  printBanner();
 
  let outstanding = 0;
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
 
  recordDueDate(invoice);
  printDetails(invoice, outstanding);
 
  function printBanner() {
    console.log('***********************');
    console.log('**** 고객 채무 ****');
    console.log('***********************');
  }
}
  1. 일단 변수 선언문을 변수가 사용되는 코드 근처로 “슬라이드”한다.
function printOwing(invoice) {
  printBanner();
 
  let outstanding = 0;
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
 
  recordDueDate(invoice);
  printDetails(invoice, outstanding);
 
  function printBanner() {
    console.log('***********************');
    console.log('**** 고객 채무 ****');
    console.log('***********************');
  }
}
 
function calculateOutstanding(invoice) {
  let outstanding = 0;
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
  return outstanding;
}
  1. 일단 추출할 대상 코드를 뽑아서 함수로 분리한다.
function printOwing(invoice) {
  printBanner();
 
  let outstanding = calculateOutstanding(invoice);
 
  recordDueDate(invoice);
  printDetails(invoice, outstanding);
}
 
function printBanner() {
  console.log('***********************');
  console.log('**** 고객 채무 ****');
  console.log('***********************');
}
 
function calculateOutstanding(invoice) {
  let outstanding = 0;
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
  return outstanding;
}
  1. outstading 변수는 변경되는 곳이 제한적이다. 함수 호출의 결과를 받아서 변경한다.
function printOwing(invoice) {
  printBanner();
 
  const outstanding = calculateOutstanding(invoice);
 
  recordDueDate(invoice);
  printDetails(invoice, outstanding);
}
 
function printBanner() {
  console.log('***********************');
  console.log('**** 고객 채무 ****');
  console.log('***********************');
}
 
function calculateOutstanding(invoice) {
  let result = 0;
  for (const o of invoice.orders) {
    result += o.amount;
  }
  return result;
}
 
function recordDueDate(invoice) {
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}
 
function printDetails(invoice, outstanding) {
  console.log(`고객명: ${invoice.customer}`);
  console.log(`채무액: ${outstanding}`);
  console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`);
}
  1. 코딩 스타일을 변경한다.

Reference