Development/Javascript

Javascript 함수 리팩토링 과정

highcastlee 2021. 11. 24. 02:22

 

 

 

본 포스팅은 javascript 함수를 정의할 때, 리팩토링하는 과정을 예제로 다루고 있습니다.

 

 

 

0. 요구사항 분석

요구사항
특정 날짜의 Date 객체를 인수로 받아 새로운 포맷("yyyy-mm-dd")으로 변경한다.

함수 예시
formatDate(new Date('2021/01/04'))

반환
"2021-01-04"

1. 요구사항을 만족하는 것이 가장 우선이다.

  좋은 코드의 1순위는 요구사항을 모두 만족하는 코드입니다. '좋은 코드'라는 강박에 사로잡혀 기능 구현도 되지 않은 코드를 리팩토링하려는 것은 능숙한 개발자가 아닌 이상 개발 시간을 늦추기만 할 뿐입니다. 처음부터 잘 작성하려는 것보다는 일단 기능 구현을 완성한 후, 리팩토링을 진행하는 방향이 많은 개발자들로부터 권장되고 있습니다.

function formatDate (date) {
  let year = date.getFullYear()
  let month = date.getMonth() + 1
  let day = date.getDate()

  month = month < 10 ? '0' + month : month
  day = day < 10 ? '0' + day : day

  return [year, month, day].map(String).join('-')
}

console.log(formatDate(new Date('2021/01/04')))	// "2021-01-04"

2. 중복을 제거하라.

  함수를 사용하는 목적은 코드의 재사용을 위함입니다. 함수가 아니어도 동일한 코드가 서로 다른 곳에서 재사용된다면, 수정이 필요할 때 해당 코드를 각각 수정해야하기 때문에 유지 보수성이 떨어질 수 밖에 없습니다. 즉, 재사용 되는 코드를 찾아서 중복을 제거하는 것이 리팩토링의 주요 체크 포인트 중 하나입니다.

// 리팩토링 전
function formatDate (date) {
  let year = date.getFullYear()
  let month = date.getMonth() + 1
  let day = date.getDate()

  month = month < 10 ? '0' + month : month		// A
  day = day < 10 ? '0' + day : day			// B

  return [year, month, day].map(String).join('-')	// C
}


console.log(formatDate(new Date('2021/01/04')))	// "2021-01-04"

위는 1번에서 작성한 코드입니다. 위 예제에서 중복이 발생하는 위치는 (A,B) 그리고 C 입니다.

 

작업 1번
  A와B는 변수만 바꾸어 적용하면 동일한 형태로 동작하는 것을 확인할 수 있습니다. 따라서, 함수를 따로 만들어 적용하겠습니다.
작업 2번
  C는 year, month, day를 String으로 변환하고 join으로 합쳐서 반환합니다. year, month, day 라는 변수가 굳이 필요 없어도 작업이 가능할 듯합니다.

 

// 리팩토링 후
const format = num => (num < 10 ? '0' + num : num + '')

const formatDate = (date) => {
    return `${date.getFullYear()}-${format(date.getMonth() + 1)}-${format(date.getDate())}`
}

console.log(formatDate(new Date('2021/01/04')))	// "2021-01-04"

 


 

3. 전역에서 재사용되지 않는 함수는 내부로 숨긴다.

  함수는 재사용을 위해 정의되지만, 의도한 바가 아닌 다른 모든 위치에서 재사용 가능하도록 정의하는 것은 오히려 다른 개발자가 보기에는 해당 함수가 마치 다른 어딘가에서 재사용될 것이라는 생각을 하게 만듭니다. 즉, 함수를 개발할 때, 함수의 재사용 범위에 대한 의도를 명확히 하고, 적절한 위치에 정의하는 것이 중요하다는 뜻입니다. 이러한 관점에서 2번의 코드를 다시 한 번 보겠습니다 

// 리팩토링 전
const format = num => (num < 10 ? '0' + num : num + '')

const formatDate = (date) => {
    return `${date.getFullYear()}-${format(date.getMonth() + 1)}-${format(date.getDate())}`
}

console.log(formatDate(new Date('2021/01/04')))	// "2021-01-04"

위 코드에서 format 함수는 formatDate에서 재사용하기 위해 만들었지만, 다른 곳에서 재사용되는 것은 아직 의도에 없습니다. 만약, 개발 중 다른 곳에서도 해당 함수를 재사용하게 된다면, 사용할 수 있는 범위로 함수 정의 위치를 변경해야겠죠. 다만 위의 상황에서는 formatDate에서만 format을 사용하겠습니다.

// 리팩토링 후
const formatDate = (date) => {
    const format = num => (num < 10 ? '0' + num : num + '')
    return `${date.getFullYear()}-${format(date.getMonth() + 1)}-${format(date.getDate())}`
}

console.log(formatDate(new Date('2021/01/04')))	// "2021-01-04"
console.log(formatDate(new Date('2021/12/24')))	// "2021-12-24"

자, format 함수를 formatDate 내부에서 정의하도록 수정했습니다. 이제 format은 formatDate 안에서만 재사용할 수 있습니다.

 

 


 

4. 동일한 변수, 함수를 반복해서 정의하는 것을 피하라.

  이제 내부에서 재사용하는 함수의 범위까지 고려하여 함수 작성했습니다. 하지만, 함수는 재사용 가능하기 때문에 formatDate라는 함수를 아주 많이 사용하게 된다면, formatDate 함수가 실행될 때마다 내부의 동일한 format 함수가 계속 정의되는 새로운 문제가 생겼습니다. 함수 내부에서 format은 한 번만 정의하고 해당 format 함수를 사용하는 부분은 실행될 때마다 동작하도록 하는 방법으로는 무엇이 있을까요? 이런 상황에서, 사용되는 개념이 바로 클로저(Closure)입니다. (클로저에 대한 포스팅은 추후 추가하도록 하겠습니다.)

 

  간단하게 클로저의 원리는 설명하자면, 렉시컬 스코프(Lexical scope)라고 할 수 있으며, 이는 함수가 평가되어 정의되는 시점에 실행 중인 실행 컨텍스트가 해당 함수의 상위 스코프로 저장되는 것을 의미합니다. 외부 함수 내에서 중첩 함수가 정의될 때, 이 중첩 함수는 호출되는 위치와 상관 없이 외부 함수를 자신의 상위 스코프로 기억하고, 해당 스코프에서 사용할 수 있는 변수와 함수들을 참조할 수 있게 됩니다.

 

  따라서, formatDate를 클로저로 만들어 format을 한 번만 정의하고 실행할 수 있도록 만들어보겠습니다.

// 리팩토링 후
const formatDate = (() => {
  const format = num => (num < 10 ? '0' + num : num + '')
  return date =>{
    return `${date.getFullYear()}-${format(date.getMonth() + 1)}-${format(date.getDate())}`
  }
})()

console.log(formatDate(new Date('2021/01/04')))	// "2021-01-04"
console.log(formatDate(new Date('2021/12/24')))	// "2021-12-24"

 

formatDate는 즉시 실행 함수를 실행한 결과로 반환된 함수이지만, 해당 함수가 정의된 시점의 상위 스코프가 format 함수를 가지고 있기 때문에 즉시 실행 함수의 반환 함수도 해당 format을 참조할 수 있게 되었습니다. 즉, formatDate를 여러 번 사용해도 format 함수는 한 번만 정의되고, 동작도 정상적으로 동작하게 되었습니다.

 

 


 

마무리

  가장 기본적인 [중복 제거]를 중심으로 리팩토링 과정을 예제와 함께 소개했습니다. 꽤 간단한 함수이기 때문에 고려할 수 있는게 많지는 않았지만, 실제 애플리케이션 개발에서 복잡한 함수를 개발할 경우, 단순히 중복을 제거하는 것 외에도 다양한 리팩토링 고려사항들이 있을 수 있을 것입니다. 또한 내가 보기에 최선이라고 생각되지만, 다른 개발자들의 관점을 또 다를 수 있기 때문에 항상 열린 마음으로 보다 나은 코드에 대한 존재 가능성을 염두에 두는 것이 좋을 듯 합니다.

 

 + 리팩토링은 가독성과 유지보수 측면에서 보다 나은 코드를 작성하기 위한 노력입니다. 저는 변수 이름, 함수 이름을 결정하는 것에서 꽤 어려움을 느끼는데, 위 예제에서도 format이라는 함수 명이 해당 함수의 동작을 잘 설명하고 있지 않다는 생각이 듭니다. 10보다 작으면 0을 붙인 숫자를 문자열로 반환하고, 10 이상이면 원본 숫자를 문자열로 반환하는 두 가지 일을 하기 때문인지 formatStringFromNumber라고 하기도 애매하고 zeroFormat이나 addZero라고 하기에도 명확한 의미는 아니라는 생각이 들었습니다. 그래서 저는 일단 format으로 해놓고 다른 부분에서 리팩토링을 하자는 마음으로 진행했지만, 실제 웹 애플리케이션 개발에서 리팩토링을 진행할 때에는 수정할 수 있는 부분과 어떻게 수정해야할지 모르겠는 상황이 충분히 발생할 경우, 주변 동료들에게 조언을 구하는 등 다양한 방법으로 보다 나은 코드를 고민을 하는 것이 중요하다고 생각합니다. 물론, 크리티컬한 부분이 아닌 선에서 리팩토링을 내려놓고 다음 작업을 진행하는 것 또한 상황에 맞는 효율적인 개발이 될 수도 있습니다.