The journey to becoming a developer

My future is created by what I do today, not tomorrow.

Projects

자바스크립트로 계산기 만들기 2편 : JavaScript

Millie 2021. 9. 30. 12:13

자바스크립트로 계산기를 만들 때, 가장 먼저 생각해 볼 것은 입력된 숫자와 연산자들을 저장하는 방법이다. 이것을 쉽게 구현할 수 있도록 하는 것이 Class이다. 클래스 안의 다양한 함수들을 통해 계산기의 각 기능을 세분화하여 동작하도록 하였다.

1. 자바스크립트로 조작해 줄 HTML 요소들을 가져오기

const numberButtons = document.querySelectorAll('[data-number]');
const operationButtons = document.querySelectorAll('[data-operation]');
const equalsButton = document.querySelector('[data-equals]');
const deleteButton = document.querySelector('[data-delete]');
const allClearButton = document.querySelector('[data-all-clear]');
const previousOperandTextElement = document.querySelector(
  '[data-previous-operand'
);
const currentOperandTextElement = document.querySelector(
  '[data-current-operand'
);

여러 개인 숫자 버튼, 연산 버튼은 querySelectorAll로 가져 오고, 하나씩 있는 버튼들은 querySelector를 이용해서 가져와 변수에 할당해 준다. 또한 입력한 숫자들을 보여 줄 text element 역시 변수에 할당한다.

2. Calculator Class 만들기

constructor

class Calculator {
  constructor(previousOperandTextElement, currentOperandTextElement) {
    this.previousOperandTextElement = previousOperandTextElement;
    this.currentOperandTextElement = currentOperandTextElement;
    this.clear();
  }
 ...
  • class 키워드로 Claculator 클래스를 선언해 주고, 계산기를 위해서 모든 입력과 함수를 받아 주는 역할을 해 줄 생성자 함수를 만든다.
  • 생성자 함수는 previousOperandTextElement, currentOperandTextElement를 받는다. display text를 어디에 놓아야 할 지 알아야 하기 때문이다.
  • Calculator 클래스를 만들자 마자 clear 함수를 호출한다. 새로운 계산기를 만들면 모든 input들을 비우고, default value로 세팅하고 싶기 때문이다.

 

여기까지 했다면, 이제 계산기로서 작동하기 위한 기능들을 먼저 나열해 보고, 하나씩 함수로 구현해 보자.

 

  1. 모두 지우기 기능 (All Clear Button) : clear
  2. 한 숫자씩 지우기 기능 (Delete Button) : delete
  3. 숫자 클릭 시 하나씩 덧붙여서 Display 해 주는 기능 : appendNumber
  4. 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 선택하는 기능 : chooseOperation
  5. 연산 기능 : compute
  6. 숫자를 comma를 찍어서 결과창에 보여주는 기능 : getDisplayNumber
  7. Display를 업데이트 해 주는 기능 : updateDisplay

(1) clear

  clear() {
    this.currentOperand = '';
    this.previousOperand = '';
    this.operation = undefined;
  }
  • AC (All Clear) 버튼을 누르면, 모든 연산들이 지워져야 한다. 따라서 비어 있는 문자열을 할당한다.
  • operation의 경우, 모든 것을 지우게 되면 어떤 operation도 선택되지 않은 상태가 되어야 하기 때문에 undefined를 할당한다.

(2) delete

 delete() {
    this.currentOperand = this.currentOperand.toString().slice(0, -1);
  }
  • 가장 나중에 있는 값을 1개 잘라내는 함수이다. 우선 문자열로 변환한 후, slice 메서드를 사용하였다.
  • str.slice(beginIndex[, endIndex]) 메서드는 beginIndex부터 endIndex까지 텍스트를 추출하고 새로운 문자열을 반환한다. 단, endIndex를 포함하지 않고 추출한다. 여기서는 slice(0, -1)로 썼는데, 문자열의 처음부터 문자열의 마지막에서 두 번째 문자까지만 추출하는 것이다. 즉 만약 12345라는 숫자가 있으면 1234까지만 추출된다. 즉, 마지막 한 숫자를 delete해 주는 것이다.

(3) append number

  appendNumber(number) {
    if (number === '.' && this.currentOperand.includes('.')) return;
    this.currentOperand = this.currentOperand.toString() + number.toString();
  }
  • 숫자를 클릭할 때마다 해당하는 숫자가 덧붙여지도록 해야 한다. 우선 사용자가 선택한 숫자를 인자로 받아야 한다.
  • 소수점(.)의 경우 연속해서 찍히면 안 되고, 단 한 번만 찍을 수 있게 해야 하기 때문에 조건문을 덧붙였다. 이미 currentOperand에 소수점이 있다면, 리턴함으로써 소수점이 더 이상 붙여지지 않는다.
  • toString으로 숫자를 전부 문자열로 바꿔주는데, 이것은 코드에서 '+'을 쓸 때 숫자가 더해지지 않고, 연결되도록 하려고 의도하기 때문이다.

(4) choose operation

  chooseOperation(operation) {
    if (this.currentOperand === '') return;
    if (this.previousOperand !== '') {
      this.compute();
    }
    this.operation = operation;
    this.previousOperand = this.currentOperand;
    this.currentOperand = '';
  }

이 함수는 4가지의 연산들 중 하나를 클릭했을 때 발생한다. 따라서 사용자가 선택한 특정한 연산을 인자로 받아야 한다.

  • 가장 먼저 operation을 세팅해 준다. (this.operation = operation) 인자로 받아 온 operation을 할당해 준다. 그러면 계산기가 값을 연산할 때 어떤 연산자를 써야할 지를 알 수 있게 된다.
  • 그 후 this.previousOperand에 this.currentOperand를 할당해 주고, 현재 연산에는 비어 있는 스트링을 할당해 사용자가 다음 숫자를 입력할 수 있도록 비워 준다.
  • 조건문으로는 currentOperand가 비어 있다면 함수를 진행하지 않고, previousOperand가 비어 있지 않다면 compute() 함수를 실행하여 연산이 되도록 해 준다.

(5) compute

  compute() {
    let computation;
    const prev = parseFloat(this.previousOperand);
    const current = parseFloat(this.currentOperand);
    if (isNaN(prev) || isNaN(current)) return;
    switch (this.operation) {
      case '+':
        computation = prev + current;
        break;
      case '-':
        computation = prev - current;
        break;
      case '×':
        computation = prev * current;
        break;
      case '÷':
        computation = prev / current;
        break;
      default:
        return;
    }
    this.currentOperand = computation;
    this.operation = undefined;
    this.previousOperand = '';
  }
  • '=' 버튼을 누르게 되면 계산을 해 주는 함수이다. switch case 문으로 깔끔하게 작성하였다.
  • 가장 먼저 computation 변수를 선언하는데, 이것이 연산의 결과를 담을 것이다.
  • 변수 두 가지를 더 선언한다. prev, current : parseFloat 함수는 문자열을 분석해 부동소수점 실수로 반환해 준다. 이것을 사용하여 문자열을 숫자로 변경하자.
  • isNaN() 함수는 어떤 값이 NaN, 즉 숫자가 아닌지 판별한다. 이것은 숫자가 아니면 true, 숫자이면 false를 반환한다. 
    • 유저가 아무것도 입력하지 않고 '='을 클릭한다면 코드가 동작하지 않도록 하고 싶기 때문에 이러한 검사 과정을 추가하였다.
    • prev와 current 둘 중 하나가 true가 된다면, 즉 둘 중 하나라도 숫자가 아니라면 return을 하고 연산을 동작하지 않게 한다.
  • switch statement는 if statement가 체이닝되어 있는 것과 같다. case에 해당하는 연산자가 있다면 연산을 수행하고 break로 다른 연산이 되지 않도록 해 준다. default는 if statement에서는 else와도 같은 것이다. 만약 아무 case에도 매칭되지 않는다면 아무런 연산을 하지 않고 그냥 return을 해 준다.

(6) getDisplayNumber

  getDisplayNumber(number) {
    const stringNumber = number.toString();
    const integerDigits = parseFloat(stringNumber.split('.')[0]);
    const decimalDigits = stringNumber.split('.')[1];
    let integerDisplay;
    if (isNaN(integerDigits)) {
      integerDisplay = '';
    } else {
      integerDisplay = integerDigits.toLocaleString('en', {
        maximumFractionDigits: 0,
      });
    }
    if (decimalDigits != null) {
      return `${integerDisplay}.${decimalDigits}`;
    } else {
      return integerDisplay;
    }
  }
  • 숫자를 그냥 나열하는 게 아니라, 3개씩 콤마를 찍어서 보여주면 가독성이 더 좋아질 것이다. 예를 들어 5,555,555 이런 식으로. 이 함수는 그 역할을 해 준다.
  • {maximunFractionDigits:0}을 추가해서 이후에 decimal place가 없도록 해 준다.

(7) updateDisplay

  updateDisplay() {
    this.currentOperandTextElement.innerText = this.getDisplayNumber(
      this.currentOperand
    );
    if (this.operation != null) {
      this.previousOperandTextElement.innerText = `${this.getDisplayNumber(
        this.previousOperand
      )} ${this.operation}`;
    } else {
      this.previousOperandTextElement.innerText = '';
    }
  }
  • 말 그대로 버튼을 누를 때마다 Display를 업데이트해서 화면에 보일 수 있게 해 주는 역할을 하는 함수이다.
  • 위에서 만든 getDisplayNumber를 적용해서 숫자가 3개 단위로 끊어질 수 있게 해 준다.
  • else에 this.previousOperandTextElement.innerText = ''을 추가해서 연산자가 없고 =을 클릭했을 시, 즉 계산이 끝났을 때는 비워 주도록 한다.

3. 계산기 클래스로 객체 생성하기

const calculator = new Calculator(
  previousOperandTextElement,
  currentOperandTextElement
);

constructor로 값을 전달하기 위해서, previousOperandTextElement, currentOperandTextElement을 전달해 준다.

이제 실제로 calculator라는 object를 사용할 수 있게 되었다.

4. 버튼마다 Event Listener 등록하기

numberButtons.forEach(button => {
  button.addEventListener('click', () => {
    calculator.appendNumber(button.innerText);
    calculator.updateDisplay();
  });
});

operationButtons.forEach(button => {
  button.addEventListener('click', () => {
    calculator.chooseOperation(button.innerText);
    calculator.updateDisplay();
  });
});

equalsButton.addEventListener('click', () => {
  calculator.compute();
  calculator.updateDisplay();
});

allClearButton.addEventListener('click', () => {
  calculator.clear();
  calculator.updateDisplay();
});

deleteButton.addEventListener('click', () => {
  calculator.delete();
  calculator.updateDisplay();
});

각 버튼에 맞는 콜백 함수를 등록해주고, updateDisplay는 모두 해 줘야 하니 모든 버튼마다 등록해 준다.


후기

  • 계산기를 만들 수 있는 방법은 위와 같은 방법 말고도 많을 것이다. 그 중에서 나는 자바스크립트의 클래스 안에서 기능별로 함수를 구현하는 방법을 알아보았다. 다른 언어로 만들거나 혹은 프레임워크를 써서 계산기를 만드는 방법도 흥미로울 것 같다.
  • 개념을 깊게 파는 것도 도움이 되지만, 이렇게 작은 프로젝트에서라도 실제로 써먹어 가면서 알아가보니 더 재미있다고 느껴진다. 클래스와 생성자 함수를 쓰는 법에 대해서 좀더 알 수 있는 계기가 되었다.
  • 또한 기능을 세분화하고, 그 기능을 코드로 어떻게 만들어 갈 수 있는지 한 줄씩 생각해보며 하나의 함수를 만들어가야겠다고 생각했다.

 

Reference : https://youtu.be/j59qQ7YWLxw