JavaScript

함수 사용하기 - 계산기 (고차 함수 사용, if 문 중첩 제거)

sangchu 2023. 1. 21. 00:21

중복(반복)이 있으면 제거하자. 하지만 모든 반복을 제거할 수 없을 수 있으므로 최대한 줄이도록 해보자.

이번 시간에는 고차함수를 통해 중복을 제거하는 방법을 알아볼 것이다.

고차함수란?

const func = () => {
  return () => {
    console.log("hello");
  };
};
const innerFunc = func(); // func의 return 값
innerFunc(); // hello

여기서 innerFunc는 아래 코드와 같다.

func()의 리턴값이기 때문이다.

const innerFunc = () => {
    console.log("hello");
}

함수가 호출된 코드(함수 이름 뒤에 ()가 붙은 코드)가 있다면 그 부분을 실제 return 값으로 치환하면 이해하기 쉽다.

const innerFunc1 = func();
const innerFunc2 = func();
const innerFunc3 = func();

innerFunc1(); // hello
innerFunc2(); // hello
innerFunc3(); // hello

만약 hello라는 문자열을 다른 값으로 바꾸고 싶다면,

즉 반환하는 값을 바꾸고 싶다면 매개변수를 사용한다.

바꾸고 싶은 자리를 매개변수로 만들면 된다.

const func = (msg) => {
  return () => {
    console.log(msg);
  };
};
const innerFunc1 = func("hello"); 
const innerFunc2 = func("hi"); 
const innerFunc3 = func(); 

innerFunc1(); // hello
innerFunc2(); // hi
innerFunc3(); // undefined

이처럼 함수를 만드는 함수를 고차함수(high order function)라고 한다.

구글링할 땐 고차함수라고 치는 것보단 영어로 쳐야지 잘 나온다.

참고로 화살표 함수 문법에 따라 함수의 본문에서 바로 반환하는 값이 있으면 {와 return을 생략할 수 있다.

함수 안에 함수가 있다고 해석한다. 함수가 함수를 리턴한다.

const func = (msg) => () => {
    console.log(msg);
};

 

순서도

먼저 순서도를 그려보자

사용자의 입력 직전에는 대기가 걸린다.

순서도는 하나하나가 더 작은 단위로 쪼개질 수 있다.

애매할때, 추가적인 의문이 생길 때 → 원이 잘못 만들어진거임. 더 세세하게 쪼개줘야한다. EX) 숫자를 변수에 저장한다 → 어떤변수?.. 어떨 때?..

처음엔 순서도를 엄청 자세히 그리다가 익숙해지면 간단하게 그리게되고 그것도 익숙해지면 머릿속으로 그릴 수 있게 된다.

지금은 입문자니까 세세하게 하자.

 

프로그램 코드

HTML, CSS

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>계산기</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <!-- readonly : 글자를 쓸 수 없게(js로만 쓸 수 있다) -->
    <input readonly id="operator" />
    <input readonly type="number" id="result" />
    <div class="row">
      <button id="num-7">7</button>
      <button id="num-8">8</button>
      <button id="num-9">9</button>
      <button id="plus">+</button>
    </div>
    <div class="row">
      <button id="num-4">4</button>
      <button id="num-5">5</button>
      <button id="num-6">6</button>
      <button id="minus">-</button>
    </div>
    <div class="row">
      <button id="num-1">1</button>
      <button id="num-2">2</button>
      <button id="num-3">3</button>
      <button id="divide">/</button>
    </div>
    <div class="row">
      <button id="clear">C</button>
      <button id="num-0">0</button>
      <button id="calculate">=</button>
      <button id="multiply">x</button>
    </div>
    <script src="script.js"></script>
  </body>
</html>
* {
  box-sizing: border-box;
}
#result {
  width: 180px;
  height: 50px;
  margin: 5px;
  text-align: right;
}
#operator {
  width: 50px;
  height: 50px;
  margin: 5px;
  text-align: center;
}
button {
  width: 50px;
  height: 50px;
  margin: 5px;
}

 

고차함수로 중복 줄이기

let numOne = "";
let operator = "";
let numTwo = "";
const $operator = document.querySelector("#operator");
const $result = document.querySelector("#result");
document.querySelector("#num-0").addEventListener("click", () => {
  if (operator) {
    numTwo += "0";
  } else {
    numOne += "0";
  }
  $result.value += "0"; // js 변수 데이터를 바꾸면, 화면에도 데이터를 바꿔줘야한다.
});
document.querySelector("#num-1").addEventListener("click", () => {
  if (operator) {
    numTwo += "1";
  } else {
    numOne += "1";
  }
  $result.value += "1";
});

이렇게 하면 숫자 0부터 9까지 중복되는 부분이 생긴다.

중복되는 부분 중, 숫자만 다르니 이를 매개변수로 한 함수를 만든다.

아래 코드와 같이 onClickNumber 함수를 만들었다.

const onClickNumber = (number) => {
  if (operator) {
    numTwo += number;
  } else {
    numOne += number;
  }
  $result.value += number;
};
document.querySelector("#num-0").addEventListener("click", onClickNumber("0"));
document.querySelector("#num-1").addEventListener("click", onClickNumber("1"));

하지만 이렇게 사용하면 원하지 않는 결과가 나올것이다.

리스너 함수의 두번째 매개변수에는 함수 이름이 와야하며, 호출된 함수가 오면 안된다.

()가 붙여지는 순간 그 함수가 리스너와 상관없이 바로 호출 된다.

onClickNumber 함수는 따로 return 값을 지정하지 않아서 undefined가 반환된다.

즉, 우리는 리스너 함수 두번째 매개변수에 함수가 아닌 undefined를 넣은 셈이 된다.

그리고 클릭하기 전에 이미 함수가 호출됐으므로 $result.value += number; 로 인해 화면에 숫자가 출력된다.

우리는 그래서 이제 onClickNumber의 return값에 함수를 넣어줄 것이다.

실제 동작하는걸 return안에 넣는다.

const onClickNumber = (number) => {
  return () => {
    if (operator) {
      numTwo += number;
    } else {
      numOne += number;
    }
    $result.value += number;
  };
};
document.querySelector("#num-dEventListener("click", onClickNumber("0"));
document.querySelector("#num-1").addEventListener("click", onClickNumber("1"));

정리하자면, 리스너 두번째 매개변수인 함수자리에 함수를 리턴해주고,

클릭했을때 함수가 실행이 된다.

const onClickNumber = (number) => () => {
  if (operator) {
    numTwo += number;
  } else {
    numOne += number;
  }
  $result.value += number;
};

화살표함수는 중괄호와 return이 붙으면 생략할 수 있으므로 위와 같이 수정한다.

코드는 최대한 줄이고, 줄인 코드에 익숙해지자.

 

event 객체로 중복 제거하기

우리가 만들고자 하는 코드는 사실 고차함수 없이도 작성할 수 있다.

우리의 경우 event.target.textContent를 이용하면 되기 때문이다.

그저 고차함수를 연습하기 위해 위와같이 작성한 것이다.

고차함수 익숙하지 않으면 줄여쓰지 않고 위에처럼 return이렇게 만들며 연습하자.

중요한건 순서도를 짜는 것이다. 좀 더 깔끔하게 옮기는건 잔재주다. 잔재주도 많이 익혀두자.

const onClickNumber = (event) => {
  if (operator) {
    numTwo += event.target.textContent; // textContent는 문자열이다.
  } else {
    numOne += event.target.textContent;
  }
  $result.value += event.target.textContent;
};

document.querySelector("#num-0").addEventListener("click", onClickNumber);
document.querySelector("#num-1").addEventListener("click", onClickNumber);
document.querySelector("#num-2").addEventListener("click", onClickNumber);
document.querySelector("#num-3").addEventListener("click", onClickNumber);
document.querySelector("#num-4").addEventListener("click", onClickNumber);
document.querySelector("#num-5").addEventListener("click", onClickNumber);
document.querySelector("#num-6").addEventListener("click", onClickNumber);
document.querySelector("#num-7").addEventListener("click", onClickNumber);
document.querySelector("#num-8").addEventListener("click", onClickNumber);
document.querySelector("#num-9").addEventListener("click", onClickNumber);

연산자 같은 경우는 그렇게 활용 못하므로 고차함수로 작성한다.

const onClickOperator = (op) => () => {
  if (numOne) {
    operator = op;
    $operator.value = op;
  } else {
    alert("숫자를 먼저 입력하세요.");
  }
};
document.querySelector("#plus").addEventListener("click", onClickOperator("+"));
document.querySelector("#minus").addEventListener("click", onClickOperator("-"));
document.querySelector("#divide").addEventListener("click", onClickOperator("/"));
document.querySelector("#multiply").addEventListener("click", onClickOperator("*"));

 

중첩 if 문 줄이기

numTwo를 입력할때, numOne의 숫자는 지워줘야하므로 아래에 해당 코드를 넣었다.

const onClickNumber = (event) => {
  if (operator) {
		if (!numTwo) {
      $result.value = "";
    }
    numTwo += event.target.textContent;
  } else {
    numOne += event.target.textContent;
  }
  $result.value += event.target.textContent;
};

if문이 중첩되는 상황은 피하자. 미리 정리를 잘 해두자.

 

중첩 제거하는 테크닉

  1. if 문 다음에 나오는 공통된 절차를 각 분기점 내부에 넣는다.
  2. 분기점에서 짧은 절차부터 실행하게 if문을 작성한다.
  3. 짧은 절차가 끝나면 return(함수 내부의 경우)이나 break(for 문 내부의 경우)로 중단한다
  4. else를 제거한다(이때 중첩 하나가 제거된다.)
  5. 다음 중첩된 분기점이 나오면 1~4의 과정을 반복한다.

위 코드를 순서대로 해보면 다음과 같다.

 

1. if 문 다음에 나오는 공통된 절차를 각 분기점 내부에 넣는다.

const onClickNumber = (event) => {
  if (operator) {
		if (!numTwo) {
      $result.value = "";
    }
    numTwo += event.target.textContent;
	  $result.value += event.target.textContent;
	} else {
    numOne += event.target.textContent;
		$result.value += event.target.textContent;
  }
};

이전에는 위 코드처럼 중복이 되면 밖으로 빠져나오게 했는데, 그 과정을 반대로 한 것이다.

 

2.분기점에서 짧은 절차부터 실행하게 if문을 작성한다.

const onClickNumber = (event) => {
  if (!operator) {
    numOne += event.target.textContent;
		$result.value += event.target.textContent;
	} else {
	if (!numTwo) {
      $result.value = "";
    }
    numTwo += event.target.textContent;
	  $result.value += event.target.textContent;
  }
};

기존에 else문에 있는 코드가 더 짧았으므로 위로 올려줬다.

그럼에 따라 if문 조건을 !operator로 바꿔주었다.

 

3. 짧은 절차가 끝나면 return(함수 내부의 경우)이나 break(for 문 내부의 경우)로 중단한다

const onClickNumber = (event) => {
  if (!operator) {
    numOne += event.target.textContent;
		$result.value += event.target.textContent;
		return;
	} else {
	if (!numTwo) {
      $result.value = "";
    }
    numTwo += event.target.textContent;
	  $result.value += event.target.textContent;
  }
};

함수이므로 return을 넣어주었다.

 

4. else를 제거한다(이때 중첩 하나가 제거된다.)

const onClickNumber = (event) => {
  if (!operator) {
    numOne += event.target.textContent;
		$result.value += event.target.textContent;
		return;
	} 
	if (!numTwo) {
	    $result.value = "";
	  }
	  numTwo += event.target.textContent;
	  $result.value += event.target.textContent;
};

else문이 사라져서 읽기 어렵다고 생각할 수 있지만, 익숙해지면 이렇게 작성하는 것이 읽기 편하다고 한다.

 

결과 계산하기

document.querySelector("#calculate").addEventListener("click", () => {
  if (numTwo) {
    switch (operator) {
      case "+":
        $result.value = parseInt(numOne) + parseInt(numTwo);
        break;
      case "-":
        $result.value = numOne - numTwo;
        break;
      case "*":
        $result.value = numOne * numTwo;
        break;
      case "/":
        $result.value = numOne / numTwo;
        break;
      default:
        break;
    }
  } else {
    alert("숫자를 먼저 입력하세요.");
  }
});
document.querySelector("#clear").addEventListener("click", () => {
  numOne = "";
  operator = "";
  numTwo = "";
  $operator.value = "";
  $result.value = "";
}); 
// 초기상태가 기억이 안나면, 항상 코드를 짤 때 맨 위에 선언하면 된다.

 

JS 최종 코드

let numOne = "";
let operator = "";
let numTwo = "";
const $operator = document.querySelector("#operator");
const $result = document.querySelector("#result");

const onClickNumber = (event) => {
  if (operator) {
    if (!numTwo) {
      $result.value = "";
    }
    numTwo += event.target.textContent;
  } else {
    numOne += event.target.textContent;
  }
  $result.value += event.target.textContent;
};

document.querySelector("#num-0").addEventListener("click", onClickNumber);
document.querySelector("#num-1").addEventListener("click", onClickNumber);
document.querySelector("#num-2").addEventListener("click", onClickNumber);
document.querySelector("#num-3").addEventListener("click", onClickNumber);
document.querySelector("#num-4").addEventListener("click", onClickNumber);
document.querySelector("#num-5").addEventListener("click", onClickNumber);
document.querySelector("#num-6").addEventListener("click", onClickNumber);
document.querySelector("#num-7").addEventListener("click", onClickNumber);
document.querySelector("#num-8").addEventListener("click", onClickNumber);
document.querySelector("#num-9").addEventListener("click", onClickNumber);

const onClickOperator = (op) => () => {
  if (numOne) {
    operator = op;
    $operator.value = op;
  } else {
    alert("숫자를 먼저 입력하세요.");
  }
};
document.querySelector("#plus").addEventListener("click", onClickOperator("+"));
document
  .querySelector("#minus")
  .addEventListener("click", onClickOperator("-"));
document
  .querySelector("#divide")
  .addEventListener("click", onClickOperator("/"));
document
  .querySelector("#multiply")
  .addEventListener("click", onClickOperator("*"));
document.querySelector("#calculate").addEventListener("click", () => {
  if (numTwo) {
    switch (operator) {
      case "+":
        $result.value = parseInt(numOne) + parseInt(numTwo);
        break;
      case "-":
        $result.value = numOne - numTwo;
        break;
      case "*":
        $result.value = numOne * numTwo;
        break;
      case "/":
        $result.value = numOne / numTwo;
        break;
      default:
        break;
    }
  } else {
    alert("숫자를 먼저 입력하세요.");
  }
});
document.querySelector("#clear").addEventListener("click", () => {
  numOne = "";
  operator = "";
  numTwo = "";
  $operator.value = "";
  $result.value = "";
});

 

셀프 체크 - 연이어 계산하기

1+3-4*2+1 이렇게 연달아 계산할 수 있도록 하고, 음수로 변환 가능하게 만드는게 과제였다.

 

연달아 계산하기 위해, 연산자 버튼 클릭할때를 고려하면 되므로 다음과 같이 순서도를 수정하였다.

  
let numOne = "";
let operator = "";
let numTwo = "";
const $operator = document.querySelector("#operator");
const $result = document.querySelector("#result");

const onClickNumber = (event) => {
  if (operator) {
    if (!numTwo) {
      $result.value = "";
    }
    numTwo += event.target.textContent;
  } else {
    numOne += event.target.textContent;
  }
  $result.value += event.target.textContent;
};

document.querySelector("#num-0").addEventListener("click", onClickNumber);
document.querySelector("#num-1").addEventListener("click", onClickNumber);
document.querySelector("#num-2").addEventListener("click", onClickNumber);
document.querySelector("#num-3").addEventListener("click", onClickNumber);
document.querySelector("#num-4").addEventListener("click", onClickNumber);
document.querySelector("#num-5").addEventListener("click", onClickNumber);
document.querySelector("#num-6").addEventListener("click", onClickNumber);
document.querySelector("#num-7").addEventListener("click", onClickNumber);
document.querySelector("#num-8").addEventListener("click", onClickNumber);
document.querySelector("#num-9").addEventListener("click", onClickNumber);

const onClickOperator = (op) => () => {
  if (numOne) {
    if (numTwo) {
      switch (operator) {
        case "+":
          $result.value = parseInt(numOne) + parseInt(numTwo);
          break;
        case "-":
          $result.value = numOne - numTwo;
          break;
        case "*":
          $result.value = numOne * numTwo;
          break;
        case "/":
          $result.value = numOne / numTwo;
          break;
        default:
          break;
      }
      numOne = $result.value;
      numTwo = "";
    }
    operator = op;
    $operator.value = op;
  } else {
    alert("숫자를 먼저 입력하세요.");
  }
};
document.querySelector("#plus").addEventListener("click", onClickOperator("+"));
document
  .querySelector("#minus")
  .addEventListener("click", onClickOperator("-"));
document
  .querySelector("#divide")
  .addEventListener("click", onClickOperator("/"));
document
  .querySelector("#multiply")
  .addEventListener("click", onClickOperator("*"));
document.querySelector("#calculate").addEventListener("click", () => {
  if (numTwo) {
    switch (operator) {
      case "+":
        $result.value = parseInt(numOne) + parseInt(numTwo);
        break;
      case "-":
        $result.value = numOne - numTwo;
        break;
      case "*":
        $result.value = numOne * numTwo;
        break;
      case "/":
        $result.value = numOne / numTwo;
        break;
      default:
        break;
    }
  } else {
    alert("숫자를 먼저 입력하세요.");
  }
});
document.querySelector("#clear").addEventListener("click", () => {
  numOne = "";
  operator = "";
  numTwo = "";
  $operator.value = "";
  $result.value = "";
});

// 음수 전환
document.querySelector("#negative").addEventListener("click", () => {
  if ($result.value >= 0) {
    if (!numTwo) {
      numOne = -numOne;
    } else {
      numTwo = -numTwo;
    }
  }
  $result.value = "-" + $result.value;
});

음수변환같은 경우는 그냥 html 버튼 하나를 더 추가해서 해당 버튼을 클릭할 시 ' - '를 붙도록 했다.

만약 이미 - 부호가 붙은 경우에는 처리 안되도록 했다. 

 

 

 

이번 self-check는 초반에 어떻게 해야할 지 감이 안잡혔다. 그러다 순서도를 처음부터 혼자 힘으로 그려보는 과정에서 순서도를 완성시킬 수 있었고, 그것대로 코딩을 하니까 원하는대로 동작했다.

그냥 머릿속으로 순서도를 생각했으면 못풀었을 것이다. 순서도를 시각적으로 그리는 것의 중요성을 느꼈다.  

머릿속으로도 그릴 수 있는 그 순간까지.. 열심히 순서도를 그려봐야겠다.

그리고 어떻게 동작하는지 궁금한 부분은 다 console.log를 찍어보며 확인했다. 이것도 큰 도움이 된 것 같다.

 

 

 

 

참고: Let’s Get IT 자바스크립트 프로그래밍 책