본문 바로가기
Programming/JavaScript

'계산기 만들기' 토이 프로젝트(2) - [Javascript 입문 _14]

by Muko 2020. 7. 25.
728x90

저번 포스팅에서 계산기를 브라우저 화면에 띄우는 것 까지 완성했습니다. 그렇지만 아직 동작하지 않기에, 우리는 자바스크립트로 계산기가 동작하게끔 구현해야겠죠?

  • 숫자 입력
  • 소수점 입력
  • 사칙연산
  • 나머지 연산
  • 부호 반전
  • 백분율 계산
  • 계산 초기화
  • 결과 계산 후 출력

총 7가지 기능을 구현해야 하네요. 하나씩 차근차근 구현해보도록 하겠습니다.

입력

현재 사용자로부터 넘어오는 입력을 다루는 함수는 calculator.js에서 handleCalculator가 담당하고 있습니다. 여기서 handleCalculator은 event를 입력으로 받는데요, 우리는 이 event가 일어난 곳이 어디인지 알아야 적절한 반응을 보여줄 수 있도록 코드를 작성할 수 있습니다.

이벤트가 일어난 곳을 알고 싶으면 event.target을 사용하면 됩니다. target은 클릭이 일어난 element 자체를 반환하게 되고, 우리는 이 안에 적혀있는 텍스트를 가져옴으로써 현재 어떤 버튼에서 사용자가 클릭을 눌렀는지 파악할 수 있는 것이죠. 그래서 switch(event.target.innerText)를 분기 조건으로 사용했습니다.

그런데 event.target.innerText를 사용하고 싶을 때 마다 이렇게 긴 코드를 매 번 작성하는 것은 매우 비효율적입니다. 우리에게는 변수라는 아주 강력한 프로그래밍 무기가 있으니 사용해봅시다.

const handleCalculator = (event) => {
  const input = event.target.innerText;

  switch (input) {
    case '0':
    case '1':
    // ...
    // 이하 생략
  }
};

이제 input이라는 변수를 사용할 수 있게 되었습니다. 이 변수를 사용해서 지금부터 각 종류별 입력을 처리해보도록 하겠습니다.

숫자 입력

계산기에서 입력을 다루는 종류는 크게 세 가지 입니다.

"숫자, 연산자, 기타 도구"

그렇기에 입력이 들어올 때마다 우리는 이 세 가지 종류를 구분해서 처리할 수 있어야 합니다. 먼저 숫자를 분류하는 코드를 작성해보도록 하겠습니다.

const resultElem = document.querySelector('.result');
const isNumber = input => Number.isInteger(parseInt(input)) || input === '.';

const handleCalculator = event => {
  const input = event.target.innerText;

  if (isNumber(input)) {
    // 숫자 처리 함수
    clickNumbers(input);
  }
};

숫자임을 체크하는 함수인 isNumber 함수가 추가되었습니다.
입력으로 들어오는 input은 innerText로 가져온 변수이기 때문에 기본적으로 String 타입입니다. 그래서 isNumber 함수에서는 parseInt(input)을 통해서 숫자로 바꾸려는 시도를 합니다.

parseInt는 입력으로 문자열을 받고, 숫자일 경우 int형으로 바꿔서 반환합니다. 그런데 '-', '+' 등과 같이 숫자가 아닌 값이 들어올 경우에 parseInt는 NaN를 반환하게 됩니다. NaNNot a Number라는 뜻으로 Number.isInteger 메서드를 통과할 경우 false가 반환됩니다. 따라서 숫자로 이루어진 문자열일 경우 Number.isInteger를 통해 true가, 아닐 경우에는 false가 반환되게 됩니다.

그런데 함수를 따로 만들 필요 없이 그냥 이 메서드를 사용하면 되지 않나? 라는 질문이 떠오를 수 있습니다.
왜 이 함수를 따로 만들어서 사용하는지는 이 부분 뒤의 || input === '.' 과 연관이 있습니다. 계산기에서는 정수 뿐만 아니라 소수점도 입력할 수 있는데요, 기존의 메서드를 이용할 경우 이러한 소수점과 관련된 입력을 처리할 수 없게 됩니다. 따라서 숫자 또는 소수점 모두 프로그램에서는 숫자라고 인식할 수 있도록 만들기 위해서 따로 함수를 작성한 것입니다.

이제 입력으로 들어오는 값이 숫자인지 아닌지 판별할 수 있게 되었습니다.
그러면 숫자일 경우 처리할 수 있는 로직이 필요하겠죠?

clickNumbers함수를 작성해보도록 하겠습니다.

const clickNumbers = number => {
  if (number !== '.' && resultElem.innerText === '0') {
    resultElem.innerText = number;
  } else if (number !== '.' || resultElem.innerText.indexOf('.') === -1) {
    resultElem.inerText += number;
  }
};

앞에서 isNumber함수는 '온점(.)과 숫자' 모두 숫자로 처리하게끔 구현했습니다. 그래서 숫자 입력을 처리하는 로직을 작성할 때도 온점이 들어올 수 있다는 것을 고려하고 코드를 적어나아가야 합니다.

그래서 첫 번째 조건문에서 입력으로 들어온 숫자가 온점이 아니면서 동시에 0일 경우 입력으로 들어온 숫자로 대체한다라고 작성했습니다. 그리고 이 조건문에는 들어오지 않으면서, 입력으로 들어온 숫자가 마찬가지로 온점이 아니거나 현재 숫자 입력 결과 창에 온점(소수점)이 없을 경우 입력으로 들어온 숫자를 숫자 입력 결과에 적혀있는 숫자 뒤에 덧붙이게끔 구현했습니다.

만약 입력으로 5가 들어왔을 경우

// 입력 5가 들어올 경우
0 -> 5
1 -> 15
1. -> 1.5

와 같이 결과가 반영되게 됩니다.


연산자 입력

이번에는 연산자 입력을 다루어 보도록 하겠습니다.

const handleCalculator = event => {
  const input = event.target.innerText;

  if (isNumber(input)) {
    // 숫자 처리 함수
  } else {
    switch (input) {
      case '÷':
      case '×':
      case '-':
      case '+':
        // 연산자 처리 로직 작성
        break;
      case '=':
        // 결과 처리 로직 작성
        break;
    }
  }
};

PC나 핸드폰에서 계산기 프로그램을 실행시켜서 동작시켜보면 연산자를 여러번 눌렀을 때도 계산 결과가 업데이트 되고, 숫자와 연산자가 한 번만 들어왔을 경우에도 계산이 처리되는 것을 확인할 수 있습니다. 이와 동일하게 동작하게끔 구현하기 위해서 우리는 연산자, 피연산자 2개를 저장하고 있는 객체를 하나 만들겠습니다.

const calcObj = {
  operator: undefined,        // 연산자
  operandX: undefined,        // 피연산자 1
  operandY: undefined,        // 피연산자 2
  isDisplayClear: false,    // 숫자표기창 초기화 유무
  isResultClear: false,        // 결과 출력 후 숫자표기창 초기화 유무
};

const handleCalculator = event => {
  const input = event.target.innerText;

  if (isNumber(input)) {
    // 숫자 처리 함수
  } else {
    switch (input) {
      case '÷':
      case '×':
      case '-':
      case '+':
        // 연산자 처리 로직 작성 ... 1
        break;
      case '=':
        // 결과 처리 로직 작성 ... 2
        break;
    }
  }
};

이제 위의 코드에서 주석 1번2번에 들어갈 코드를 작성해보도록 하겠습니다.

const calculate = () => {
  switch(calcObj.operator){
    case '÷':
      divide(calcObj.operandX, calcObj.operandY);
      break;
    case '×':
      multiply(calcObj.operandX, calcObj.operandY);
      break;
    case '-':
      subtract(calcObj.operandX, calcObj.operandY);
      break;
    case '+':
      add(calcObj.operandX, calcObj.operandY);
      break;
  }
};

const handleCalculator = event => {
  const input = event.target.innerText;

  if (isNumber(input)) {
    // 숫자 처리 함수
  } else {
    switch (input) {
      case '÷':
      case '×':
      case '-':
      case '+':
        // 연산자 처리 로직 작성 ... 1
        calcObj.operandX = parseFloat(resultElem.innerText);
        calcObj.operator = input;
        break;
      case '=':
        // 결과 처리 로직 작성 ... 2
        calculate();
        break;
    }
  }
};

사칙연산

이렇게 작성할 경우, 실행시키면 에러가 나게 됩니다. 왜냐하면 calculate() 함수를 실행시켰을 때 동작할 함수들을 정의하지 않았거든요. 사칙연산과 관련된 함수들을 작성해보도록 하겠습니다.

const add = (x, y) => {
  resultElem.innerText = parseFloat(x) + parseFloat(y);
};

const subtract = (x, y) => {
  resultElem.innerText = x - y;
};

const multiply = (x, y) => {
  resultElem.innerText = x * y;
};

const divide = (x, y) => {
  const value = x / parseFloat(y);
  resultElem.innerText = Math.round(value * 1e12) / 1e12;
};

나머지 연산, 부호 반전, 백분율 계산, 계산 초기화

소제목으로 나열한 기능들도 switch - case문에서 분기를 타고 선택될 수 있도록 해야겠죠?

const handleCalculator = event => {
  const input = event.target.innerText;

  if (isNumber(input)) {
    // 숫자 처리 함수
  } else {
    switch (input) {
      case 'C':
        reset();
        break;
      case '±':
        reverse();
        break;
      case '%':
        getPercent();
        break;
      case '÷':
      case '×':
      case '-':
      case '+':
        // 연산자 처리 로직 작성 ... 1
        calcObj.operandX = parseFloat(resultElem.innerText);
        calcObj.operator = input;
        break;
      case '=':
        // 결과 처리 로직 작성 ... 2
        calculate();
        break;
    }
  }
};

이렇게 해당 기능에 해당하는 문자가 들어왔을 경우에 사용자가 생각한 기능이 동작하도록 switch case문을 조정해놓고나서, 해당 기능을 정의 해주어야 합니다.

const reset = () => {
  resultElem.innerText = 0;
  calcObj.operandX = undefined;
  calcObj.operandY = undefined;
  calcObj.operator = undefined;
  calcObj.isDisplayClear = false;
  calcObj.isResultClear = false;
}
const reverse = () => resultElem.innerText *= -1;
const getPercent = () => divide(resultElem.innerText, 100);

코드를 보다보면 아까부터 calcObj에 있는 isDisplayClear, isResultClear가 신경쓰인 분이 계실겁니다. 결과 출력과 관련된 부분에 이 값이 어떻게 사용되는지 설명하겠습니다.

결과 계산 후 출력

사실 지금도 숫자 하나, 연산자 하나, 숫자 하나 클릭 후 '=' 버튼을 누르면 결과가 출력되지 않는 것을 확인할 수 있습니다. 뭔가 원하는 것처럼 동작하지 않을거에요. 연산자를 입력하고 숫자를 다시 입력할 때 숫자가 사라지지 않았다던가 같은 현상이 일어날겁니다.

이왕 토이프로젝트를 시작한 김에 실제 계산기 프로그램이랑 동일하게 동작하게 만들면 더욱 좋겠죠? 우리가 추가적으로 구현해야하는 부분은 다음과 같습니다.

  • 연산자를 고른 뒤 숫자를 클릭하면 결과창에 존재하던 숫자 삭제 후, 새로 입력한 숫자가 출력
  • 숫자 하나, 연산자 하나 선택 후 '=' 클릭 했을 때 결과 반영되어 출력
  • 결과값 출력 후 숫자 입력하면 그 전의 결과값 반영될 수 있어야 함

위의 세 가지를 구현하기 위해서 필요한 것이 바로 isDisplayClearisResultClear 값 입니다.
여기서 부터 작성한 코드는 따로 설명을 적어놓지 않겠습니다. 코드 전체를 작성해본 뒤, 이 코드가 어떻게 이러한 결과를 만들었는 것인지 해석해보면서 공부해보면 더 많은 도움이 될 겁니다!

자, 전체 코드 들어갑니다!

const resultElem = document.querySelector('.result');
const calcObj = {
  operator: undefined,
  operandX: undefined,
  operandY: undefined,
  isDisplayClear: false,
  isResultClear: false,
};

const isNumber = input => {
  return Number.isInteger(parseInt(input)) || input === '.';
};

const reset = () => {
  resultElem.innerText = 0;
  calcObj.operandX = undefined;
  calcObj.operandY = undefined;
  calcObj.operator = undefined;
  calcObj.isDisplayClear = false;
  calcObj.isResultClear = false;
};

const reverse = () => {
  resultElem.innerText *= -1;
};

const clickNumbers = number => {
  if (calcObj.isDisplayClear) {
    resultElem.innerText = number !== '.' ? '' : '0.';
    calcObj.operandY = undefined;
    calcObj.isDisplayClear = false;
  } else if (calcObj.isResultClear) {
    resultElem.innerText = number !== '.' ? '' : '0.';
    calcObj.operandY = undefined;
    calcObj.operator = undefined;
    calcObj.isResultClear = false;
  }

  if (number !== '.' && resultElem.innerText === '0') {
    resultElem.innerText = number;
  } else if (!(number === '.' && resultElem.innerText.indexOf('.') !== -1)) {
    resultElem.innerText += number;
  }
};

const add = (x, y) => {
  resultElem.innerText = parseFloat(x) + parseFloat(y);
};

const subtract = (x, y) => {
  resultElem.innerText = x - y;
};

const multiply = (x, y) => {
  resultElem.innerText = x * y;
};

const divide = (x, y) => {
  const value = x / parseFloat(y);
  resultElem.innerText = Math.round(value * 1e12) / 1e12;
};

const getPercent = () => {
  divide(resultElem.innerText, 100);
};

const calculate = () => {
  switch(calcObj.operator){
    case '÷':
      divide(calcObj.operandX, calcObj.operandY);
      break;
    case '×':
      multiply(calcObj.operandX, calcObj.operandY);
      break;
    case '-':
      subtract(calcObj.operandX, calcObj.operandY);
      break;
    case '+':
      add(calcObj.operandX, calcObj.operandY);
      break;
  }
};

const handleCalculator = (event) => {
  const input = event.target.innerText;
  if (isNumber(input)) {
    clickNumbers(input);
  } else {
    switch (input) {
      case 'C':
        reset();
        break;
      case '±':
        reverse();
        break;
      case '%':
        getPercent();
        break;
      case '÷':
      case '×':
      case '-':
      case '+':
        if(!calcObj.isResultClear && !calcObj.isDisplayClear && calcObj.operandX !== undefined && calcObj.operandY !== undefined){
          calculate();
        }
        calcObj.operandX = parseFloat(resultElem.innerText);
        calcObj.operator = input;
        calcObj.isDisplayClear = true;
        calcObj.isResultClear = false;
        break;
      case '=':
        if (calcObj.operator === undefined) {
          calcObj.operandX = undefined;
        } else if (calcObj.operandY === undefined) {
          calcObj.operandY = parseFloat(resultElem.innerText);
        }
        if (calcObj.operandX !== undefined && calcObj.operator !== undefined) {
          calculate();
          calcObj.isResultClear = true;
          calcObj.operandX = parseFloat(resultElem.innerText);
        }
        break;
    }
  }
};

728x90

댓글