Medium - Clean up your code by removing ‘if-else’ statements

원문 - bitfish

Trend 파악을 위한 Medium 기고문 포스팅 - if-else 구문을 없애서 여러분의 코드를 더욱 깔끔하게 만드세요; 자바스크립트를 더욱 멋있게 사용하는 팁을 알려드리죠

Photo by Mazhar Zandsalimi on Unsplash

JS 코드를 작성할 때 우리는 복잡한 로직을 구현해야 하는 상황에 종종 마주합니다. 일반적으로 여러분은 if/else, switch 문을 사용해서 다수의 조건문을 해결할 수 있지만 이것은 문제가 될 여지가 있습니다. 로직의 복잡도가 올라가면 if/else, switch 문은 점점 겉잡을 수 없이 커질 것입니다. 이 기사에서는 조건문 처리에 있어서 더욱 우아하게 처리할 수 있는 방법을 알려드릴 것입니다.

먼저 다음과 같은 코드를 살펴 봅시다.

// Button click event
// @param {number} status
// Activity status: 1 in progress, 2 in failure, 3 out of stock, 4 in success, 5 system cancelled

const onButtonClick = ( status ) => {
  if ( status == 1 ) {
    sendLog( 'processing' )
    jumpTo( 'IndexPage' )
  } else if ( status == 2 ) {
    sendLog( 'fail' )
    jumpTo( 'FailPage' )
  } else if ( status == 3 ) {
    sendLog( 'fail' )
    jumpTo( 'FailPage' )
  } else if ( status == 4 ) {
    sendLog( 'success' )
    jumpTo( 'SuccessPage' )
  } else if ( status == 5 ) {
    sendLog( 'cancel' )
    jumpTo( 'CancelPage ')
  } else {
    sendLog( 'other' )
    jumpTo( 'Index' )
  }
}

이 코드에서 버튼 클릭에 대한 로직을 볼 수 있으실 겁니다. 액티비티 상태에 따라서 2가지 일을 하거나 로그를 발생하고 일치하는 페이지로 이동하는 것으로 여러분은 쉽게 스위치 구문으로 재작성 하실 수 있을겁니다.

const onButtonClick = ( status ) => {
  switch ( status ) {
    case 1:
      sendLog( 'processing' )
      jumpTo( 'IndexPage' )
      break;
    case 2:
    case 3:
      sendLog( 'fail' )
      jumpTo( 'FailPage' )
      break;
    case 4:
      sendLog( 'success' )
      jumpTo( 'SuccessPage' )
      break;
    case 5:
      sendLog( 'cancel' )
      jumpTo( 'CancelPage' )
      break;
    default:
      sendLog( 'other' )
      jumpTo( 'Index' )
      break;
  }
}

if/else 구문보다는 확실히 깔끔하지만 자세히 보시면 트릭이 숨어있습니다. case2와 3이 동일한 로직이기 때문에 break를 생략해서 2번 케이스인 경우 3번 케이스의 로직을 수행하도록 한 것이죠. 그렇지만 더욱 간단한 방법이 있습니다.

const actions = {
  '1': ['processing', 'IndexPage'],
  '2': ['fail', 'FailPage'],
  '3': ['fail', 'FailPage'],
  '4': ['success', 'SuccessPage'],
  '5': ['cancel', 'CancelPage'],
  'default': ['other', 'Index'].
}

const onButtonClick = ( status ) => {
  let action = action[status] || action['default'],
    logName = action[0],
    pageName = action[1]
  sendLog(logName)
  jumpTo(pageName)  
}

위의 코드는 더욱 깔끔해 보입니다. 게다가 좋은 점은 이러한 접근은 조건문의 판단을 객체의 이름으로 해서 수행된 로직이 적절한 객체의 이름이 되도록 하는 것입니다. 버튼이 클릭되면 이 메소드는 이항 조건을 수행할 것이고 로직의 판단은 객체의 특성을 찾아보는 것이 됩니다. 이것도 괜찮지만 더 좋은 방법이 있을까요?

물론 입니다.

const actions = new Map([
  [1, ['processing', 'IndexPage']],
  [2, ['fail', 'FailPage']],
  [3, ['fail', 'FailPage']],
  [4, ['success', 'SuccessPage']],
  [5, ['cancel', 'CancelPage']],
  ['default', ['other', 'Index']]
])

const onButtonClick = ( status ) => {
  let action = actions.get( status ) || actions.get('default')
  sendLog(action[0])
  jumpTo(action[1])
}

객체 대신에 Map을 사용해서 얻을 수 있는 장점은 여러가지 입니다만 추후 소개해 드리고 우선 맵 객체와 일반 객체 사이에 차이점 부터 알아봅시다.

  • 객체가 자체적으로 프로퍼티를 가지고 있으면 객체는 항상 프로토타입 키를 가집니다.
  • 객체의 키는 항상 문자열이나 심볼이지만 맵의 키는 모든 것이 될 수 있습니다.
  • 객체의 키-값 쌍은 수동으로 결정되는 반면에 맵의 키-값 쌍이 몇개나 있는지 size 특성을 이용해서 쉽게 얻을 수 있습니다.

이제 좀 더 문제를 어렵게 만들어 봅시다. 버튼이 클릭되면 상태값 뿐만 아니라 사용자의 신원도 확인해야 합니다.

//Button click event
//@param {number} status
//Activity status: 1 in progress, 2 in failure, 3 out of stock, 4 in success, 5 system canceled
//
//@param {sting} identity: guest, master

const onButtonClick = ( status, identity ) => {
  if(identity == 'guest'){
    if(status == 1) {
      //do sth
    } else if ( status == 2 ) {
      // do sth
    } else if ( status == 3 ) {
      // do sth
    } else if ( status == 4 ) {
      // do sth
    } else if ( status == 5 ) {
      // do sth
    } else {
      // do sth
    }
  } else if ( identity == 'master' ) {
    if ( status == 1 ) {
      // do sth
    } else if ( status == 2 ) {
      // do sth
    } else if ( status == 3 ) {
      // do sth
    } else if ( status == 4 ) {
      // do sth
    } else if ( status == 5 ) {
      // do sth
    } else {
      // do sth
    }
  }
}

위의 예제에서 보다시피 평가해야할 로직이 두배가 되니까 판단도 두배가 되고 코드도 두배가 됩니다. 어떻게 코드를 더 깔끔하게 할 수 있을까요?

const actions = new Map([
  ['guest_1', ()=>{/*do sth*/}],
  ['guest_2', ()=>{/*do sth*/}],
  ['guest_3', ()=>{/*do sth*/}],
  ['guest_4', ()=>{/*do sth*/}],
  ['guest_5', ()=>{/*do sth*/}],
  ['master_1', ()=>{/*do sth*/}],
  ['master_2', ()=>{/*do sth*/}],
  ['master_3', ()=>{/*do sth*/}],
  ['master_4', ()=>{/*do sth*/}],
  ['master_5', ()=>{/*do sth*/}],
  ['default', ()=>{/*do sth*/}],
])

const onButtonClick = ( identity, status ) => {
  let action = actions.get(`${identity}_${status}`) || actions.get('default')
  action.call(this)
}

위 로직의 핵심은 이것입니다: 두개의 조건문을 문자열로 취급을 해서 맵의 키로 사용하고 나중에 일치하는 문자열을 찾아서 값을 가져오는 것입니다. 물론 맵을 객체로 바꿔도 됩니다.

const actions = {
  'guest_1': ()=>{/*do sth*/},
  'guest_2': ()=>{/*do sth*/},
  //...
}

const onButtonClick = ( identity, status ) => {
  let action = actions[`${identity}_${status}`] || actions['default']
  actions.call(this)
}

문자열을 사용해서 찾는게 조금 이상하다면 다른 방법도 있습니다. 맵 객체를 키로 사용하는 것이죠.

const actions = new Map([
  [{identity: 'guest', status:1 }, ()=>{/*do sth*/}],
  [{identity: 'guest', status:2 }, ()=>{/*do sth*/}],
  //...
])
const onButtonClick = ( identity, status ) => {
  let action = [...actions].filter(([key,value]) => (key.identity == identity && key.status == status))
  action.forEach(([key,value]) => value.call(this))
}

맵과 객체의 차이점을 보셨겠지만 맵은 모든 데이터를 키로 사용할 수 있습니다. 이제 조금만 더 어렵게 해봅시다. 만약 게스트인 경우 1-4 프로세스 로직이 모두 같다면 어떻게 할까요? 가장 형편없는 방식은 이것일 겁니다.

const actions = new Map([
  [{identity: 'guest', status: 1}, ()=> {/*function A*/}],
  [{identity: 'guest', status: 2}, ()=> {/*function A*/}],
  [{identity: 'guest', status: 3}, ()=> {/*function A*/}],
  [{identity: 'guest', status: 4}, ()=> {/*function A*/}],
  [{identity: 'guest', status: 5}, ()=> {/*function B*/}],
])

좀더 나은 접근 법은 로직 함수의 절차를 캐시하는 것일 겁니다.

const actions = () => {
  const functionA = () => {/*do sth*/}
  const functionB = () => {/*do sth*/}
  return new Map([
    [{identity: 'guest', status: 1}, functionA],
    [{identity: 'guest', status: 2}, functionA],
    [{identity: 'guest', status: 3}, functionA],
    [{identity: 'guest', status: 4}, functionA],
    [{identity: 'guest', status: 5}, functionB],
    //...
  ])
}

const onButtonClick = ( identity, status ) => {
  let action = [...actions()].filter(([key, value]) => (key.identity == identity && key.status == status ))
  action.forEach(([key,value]) => value.call(this))
}

이 정도면 아마 일반적으로 충분하실 겁니다만 솔직히 말해서 functionA가 4번이나 덮어쓰기 되는 것이 맘에 들지 않습니다. 만약 정말로 상황이 복잡해져서 identity가 3개의 상태를 가지고 있고 status가 10개의 상태를 가지고 있고 여러분이 30개의 프로세스 로직을 작성해야 하고 많은 경우가 동일하면 이것은 용납하기 어려울 것입니다.

그럴 때는 이렇게 하시면 됩니다.

const actions = () => {
  const functionA = () => {/*do sth*/}
  const functionB = () => {/*do sth*/}
  return new Map([
    [/^guest_[1-4]$/, functionA],
    [/^guest_5$/,functionB],
    //...
  ])
}

const onButtonClick = ( identity, status ) => {
  let action = [...actions()].filter(([key,value])=>(key.test(`${identity}_${status}`)))
  action.forEach(([key,value])=>value.call(this))
}

객체대신 맵을 사용했을 때 장점은 이렇게 정규표현을 키로 사용할 수 있기 때문입니다. 만약 요구사항이 모든 guest 들은 로그를 보내고 다른 상태에서는 분리된 로직 절차를 수행하도록 해야 한다면 우리는 다음과 같이 작성할 수 있습니다.

const actions = () => {
  const functionA = () => {/*do sth*/}
  const functionB = () => {/*do sth*/}
  const functionC = () => {/*send log*/}
  return new Map([
    [/^guest_[1-4]$,functionA],
    [/^guest_5$,functionB],
    [/^guest_.*/functionC],
    //...
  ])
}

const onButtonClick = (identity, status) => {
  let action = [...actions()].filter(([key,value]) => key.test(`${identity}_${status}`))
  action.forEach(([key,value])=>value.call(this))
}

정규 조건을 만족하는 모든 로직들이 실행될 것이기 때문에 공통 로직이나 개별 로직 모두 실행될 수 있습니다.

Summary

  • 이항 조건판단 : 객체에 저장
  • 이항 조건판단 : 맵에 저장
  • 조건이 여러개 : 문자열에 조건을 연결해서 담고 객체에 저장
  • 조건이 여러개 : 문자열에 조건을 연결해서 담고 맵에 저장
  • 조건이 여러개 : 객체에 조건을 저장하여 맵에 저장
  • 조건이 여러개 : 조건을 정규식으로 표현하여 맵에 저장

© 2019. All rights reserved.

Powered by Hydejack v8.1.1