개발/JavaScript

[JavaScript] 계산기를 만들어 보았습니다.

xuwon 2024. 10. 11. 21:00

자바스크립트 강의를 마무리 하면서, 계산기를 만들어 보았습니다.

미리보기

기능 구현뿐만 아니라 보기 좋은 코드에 대한 고민을 가지고 코드를 작성했으며,
실제로 리팩토링도 여러 번 진행하였기에 기록하기 위해 블로그를 작성합니다.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <link href="styles.css" rel="stylesheet" />
    </head>
    <body>
        <div class="calculator-button">
            <div id="button" style="width: 100px; height: 100px; background-color:rgb(186, 186, 186)">
                <img src="images/calculator.png" style="width: 80px; height: 80px;">
            </div>
        </div>
        <div class="calculator-container">
            <div class="basic-buttons">
                <div id="red-button"></div>
                <div id="yellow-button"></div>
                <div id="green-button"></div>
            </div>
            <div id="display" style="color: white;"><p id="displayVariable">0</p></div>
            <div id="buttons" style="display: flex;">
                <div class="button function C"><p class="text">C</p></div>
                <div class="button function reverse"><p class="text">±</p></div>
                <div class="button function per"><p class="text">%</p></div>
                <div class="button operator"><p class="text" style="font-size: 40px">/</p></div>
                <div class="button number"><p class="text">7</p></div>
                <div class="button number"><p class="text">8</p></div>
                <div class="button number"><p class="text">9</p></div>
                <div class="button operator"><p class="text" style="padding-bottom: 16px;">x</p></div>
                <div class="button number"><p class="text">4</p></div>
                <div class="button number"><p class="text">5</p></div>
                <div class="button number"><p class="text">6</p></div>
                <div class="button operator"><p class="text">-</p></div>
                <div class="button number"><p class="text">1</p></div>
                <div class="button number"><p class="text">2</p></div>
                <div class="button number"><p class="text">3</p></div>
                <div class="button operator"><p class="text">+</p></div>
                <div class="button" style="background-color: #353535;"><p class="text">❤︎</p></div>
                <div class="button number zero"><p class="text">0</p></div>
                <div class="button dot"><p class="text">.</p></div>
                <div class="button equals"><p class="text" style="padding-bottom: 6px;">=</p></div>
            </div>
        </div>

        <script src="script.js"></script>
    </body>
</html>

우선 HTML 코드입니다.
버튼들을 하나하나 다 div 로 썼는데, 다른 방법이 있을지는 모르겠어요...

전체적인 구조는

calculator-button (계산기 앱 버튼),
calculator-container (계산기 컨테이너),
- display (계산기 화면),
- buttons (계산기 버튼들)

이렇게 구성이 되어 있습니다.

버튼의 종류는

1. function (C, Reverse, %)
2. operator
3. number (zero 포함)
4. dot
5. equals

이렇게 있습니다.

calculator-button 클래스는 계산기 앱 모양 버튼을 만들었습니다.

// calBtn 클릭 시 계산기 토글 기능!
calBtn.addEventListener('click', function() {
    container.classList.toggle('opacity');
})

classList.toggle() 메소드를 활용하여, opacity 클래스가 없으면 추가하고, 있으면 삭제하도록 작성하였습니다.

opacity 클래스에는 이러한 CSS를 적용시켰습니다.

투명하게만 만들면 계산기 컨테이너의 버튼들이 그대로 클릭되므로,
pointer-events: none; 을 적용하여 클릭 이벤트를 비활성화 시켰습니다.

.opacity {
    opacity: 0; /* 사라질 때 투명하게 만듦 */
    pointer-events: none; /* 클릭 비활성화 */
}

 

그리고 서서히 투명도가 줄어들게 하기 위해서,
calculator-container 의 CSS에 transition 을 설정하여 애니메이션을 만들어 주었습니다.

나머지 CSS는 애플 계산기 이미지를 참고하여 따라 만들었습니다 ^^;


 

이제 JavaScript 로 넘어가보겠습니다.

버튼 클릭 시 display에 보여주기

 

우선 querySelectorAll, querySelector, getElementById 등으로 필요한 요소들은 전부 받아온 상태입니다.

// 버튼 클릭 시 display에 출력하기
const buttonEventListener = function (btn) {
    if (btn.classList.contains('number') || btn.classList.contains('dot')) { // 화면에 출력해야하는 클래스들
        // document로 하면 맨 첫번째 .text만 선택함

        if (shouldClearDisplay) { // =이나 연산자 있음!
            displayVariable.textContent = ''; // 한 번만 초기화
            shouldClearDisplay = false; // 초기화 후 다시 false로 설정
        }

        displayVariable.textContent += btn.querySelector('.text').textContent;
    }

    console.log(btn.querySelector('.text').textContent);
}

먼저, 이 계산기는 number 버튼과 dot 버튼만 display에 출력하도록 되어 있습니다.
그렇기에 if문을 사용해서 numberdot만 이 함수가 적용될 수 있도록 작성하였습니다.

shouldClearDisplay 는 추후에 설명하겠지만, 이전에 = 버튼이나 연산자 버튼이 눌렸는지 확인을 위한 변수입니다.
(=, 연산자 버튼을 누르면 shouldClearDisplaytrue로 바뀜)

연산자 버튼이 눌린 후 다음 숫자를 기입할 때, displayVariable이 초기화 되어야 해서 그렇습니다.

 

  displayVariable.textContent += btn.querySelector('.text').textContent;  

display에 출력하는 건
diplayVariabletextContent에 눌리는 버튼들의 textContent를 따와서 추가해 주면 됩니다.
(displayVariable.textContent는 초기엔 0으로 세팅되어 있습니다.)

그런데 0으로 세팅된 계산기에서 버튼 '2'를 누른다고 해서 '02' 이렇게 출력되진 않잖아요.
그래서 버튼이 눌렸을 때 초기화를 하는 과정이 필요합니다.

  displayVariable.textContent = '';  

 이렇게 해서, 계산기에서 버튼을 눌렀을 때 정상적으로 화면에 출력하는 것까지 완성되었습니다.



그 다음으로는 계산기의 핵심 기능인 계산 기능을 구현해 보도록 하겠습니다.

 


 

연산자를 눌러 계산하기

이 함수는 연산자 버튼을 클릭했을 때 실행되는 이벤트 리스너 함수를 먼저 이해하는 것이 좋을 것 같습니다.

operBtns.forEach(function(button) { // + - * /
    button.addEventListener('click', function () {
        if (firstOperand === null) { // 맨 처음
            // 첫 번째 피연산자를 설정
            firstOperand = parseFloat(displayVariable.textContent);
        } else if (operator !== null) {
            // 이미 연산자가 있을 경우 계산을 수행
            // 연산자가 없으면 계산 X 
            calculate();
        }
        
        operator = button.querySelector('.text').textContent; // 새로운 연산자 설정
        
        shouldClearDisplay = true; // 다음 입력 시 화면을 지우기 위한 플래그
        
        const arr = {
            'first operand': firstOperand,
            operator: operator
        }

        console.log(arr);
    });
});

우선, querySelectorAlloperator 버튼들을 받아왔기에,
고차함수를 사용해서 각각 button에 이벤트 리스너를 설정해야 합니다.

저는 return 값이 없는 forEach를 사용했습니다.


예를 들어, 제가 2를 누르고 + 버튼을 누르면,
2firstOperand에 할당됩니다.
(firstOperand는 처음 연산을 진행할 때, 즉 null일 때만 할당이 됩니다.)

 

  operator = button.querySelector('.text').textContent;  

그리고 operator라는 변수를 만들어서 연산자를 넣어 주었는데요,
연산자는 계산 함수에서 계산이 끝나고 난 후 초기화가 됩니다. (전역 변수)

당연하겠지만 연산자가 있을 때만 계산이 가능합니다. (처음 연산자가 눌린 경우는 연산을 진행하지 않음.)

 

  shouldClearDisplay = true;  

앞서 설명한 바와 같이, 두번째 피연산자를 입력할 때 화면이 비워져야 하므로
shouldClrearDisplaytrue로 변경합니다.

 

 
const calculate = function () {
    secondOperand = parseFloat(displayVariable.textContent);

    if (operator === '+') {
        result = firstOperand + secondOperand;
    } else if (operator === '-') {
        result = firstOperand - secondOperand;
    } else if (operator === 'x') {
        result = firstOperand * secondOperand;
    } else if (operator === '/') {
        result = firstOperand / secondOperand;
    }

    displayVariable.textContent = result;

    firstOperand = result;
    
    secondOperand = null;
    operator = null; // 연산자 비우기

    shouldClearDisplay = true; // 연산자 선택됨!
    confirmReverse = false;
}

계산을 수행하는 계산 함수입니다.

secondOperand는 연산자 이후 작성되는 피연산자겠죵! 

아까 연산자 버튼에 적용된 이벤트 리스너에서 operator를 설정했잖아요?
operator에 따라 조건문을 작성했습니다.

  displayVariable.textContent = result;  

result에 연산 결과를 할당하여 displayVariabletextContent로 넣어줍니다.
이러면 display에 연산 결과가 잘 나오는 것을 볼 수 있습니다.

그리고 여기서 중요 ☆

  firstOperand = result;  

첫번째 피연산자를 연산에서 나온 결과로 바꾸어 줍니다.

이유는 연속되는 연산을 수행하기 위해서인데요. 예시는 이렇습니다.

더보기

1. 2 버튼 클릭

2. + 연산자 버튼 클릭
-> firstOperand = 2; operator = '+';

3. 3 버튼 클릭

4. - 버튼 클릭
-> secondOperand = 3; result = 5; -> firstOperand = 5; secondOperand = null; operator = null; -> operator = '-';

5. 6 버튼 클릭

6. = 버튼 클릭
-> secondOperand = 6; result = -1; -> firstOperand = -1; secondOperand = null; operator = null;

이해가 되실지 모르겠습니다....


아무튼 그리고는

  secondOperand = null;  
  operator = null;  

두번째 피연산자와 연산자 변수를 비워줍니다.

  shouldClearDisplay = true;  

그리고 연산이 되었다는 건 어쨌든 연산자가 선택이 되었다는 거니까, 다음 버튼을 클릭할 땐 display를 비웁니다.

(지금 생각해보면, 어차피 operator 이벤트 리스너에서 true로 바꿔주는데,
calculate 함수 말고 equals 이벤트 리스너에만 추가할 걸 그랬네요.)

  confirmReverse = false;  

해당 코드는 ± 버튼을 눌렀는지 안 눌렀는지 확인하는 변수입니다.

 


 

특정 Button 클릭 시 실행될 이벤트 리스너 함수

 

저는 기능에 따라 버튼들을 구분하여 함수를 작성하였구요,

1. C 버튼
2. dot 버튼
3. equals 버튼
4. operator 버튼 (앞에서 작성)
5. ± 버튼
6. % 버튼

이렇게 6가지로 작성했습니당.
처음에는 조건문 남발하며 만들었는데, 보기 안 좋아서 바꿨어요.


먼저 C 버튼입니다.
별 거 없고 그냥 초기화 해주시면 돼요.
숫자, 피연산자, 연산자, 반전 버튼 클릭, 연산자 버튼 클릭 등등 전부 초기화 합니다.

CBtn.addEventListener('click', function () {
    // C 버튼 클릭 시 0으로 초기화
    displayVariable.textContent = '0';
    firstOperand = null;
    secondOperand = null;
    operator = null;

    confirmReverse = false;
    shouldClearDisplay = false; // 연산자도 초기화
})

 

두번째는 dot 버튼입니다.
dot 버튼은 숫자와는 다른 것이, 초기에 0이 있을 때 숫자는 0을 지우고 숫자가 입력되는데
dot 0을 지우지 않고 그냥 화면에 입력됩니다.

dotBtn.addEventListener('click', function () {
    // dot 버튼 클릭 시
    // 1. 0인지 확인할 필요 X
    // 2. .이 이미 있는지 확인 필요
    // 3. 바로 앞에 연산자가 왔었는지 확인 필요
    if (!(displayVariable.textContent.indexOf('.') !== -1) && operator === null) { // 없다면
        buttonEventListener(dotBtn);
    }
})

그래서 초기값이 0인지 확인할 필요가 없습니다.

확인해야 할 것은 .이 이미 있는지 확인을 해야겠죠.

  displayVariable.textContent.indexOf('.'!== -1  


그리고 연산자 바로 뒤 피연산자로 입력될 경우, .은 맨 앞에 올 수 없습니다.

  operator === null  

그래서 그 두가지를 조건문으로 작성하였습니다.
두 조건을 만족하는 경우 displaydot을 표시합니다.

 

세번째로는 equals 버튼입니다.
가장 간단한게, 연산 함수만 호출해주면 됩니다.

equalsBtn.addEventListener('click', function () {
    calculate();
});

 

네번째로는 연산자 버튼인데, 위에서 설명했으니 넘어가고
다섯번째인 ± 버튼입니다.

현재 display에 보여지는 수를 반전시킵니다.

reverseBtn.addEventListener('click', function(){
    let original = parseFloat(displayVariable.textContent);
    original = -original;

    // ± 버튼을 누르면 firstOperand 반전 시킴
    // -> =을 누른 후에도 적용 가능 (result 반전)
    firstOperand = original;

    displayVariable.textContent = original + '';

    confirmReverse = !confirmReverse; // 누를 때마다 반전
})

displayVariable.textContent parseFloat 를 사용하여 실수형으로 변환합니다.
그리고 부호를 반전해주면 됩니다~!

그냥 이렇게만 하면 될 줄 알았는데, 문제가 있더라구요.
연산 함수를 보면 연산 결과인 resultfirstOperand에 할당하잖아요?!

그래서 그 뒤로는 부호를 반전해도 firstOperand에 적용이 안됩니다!

따라서

  firstOperand = original;  

이렇게 firstOperand에 직접! original을 할당해줍니다.

displayVariable.textContent = original + '';

그리고 original을 문자열로 변환하여 다시 displayVariable.textContent에 넣어주면 됩니다~

 

그리고 reverse 버튼이 눌렸다는 것을 확인하기 위한 confirmReverse 변수도 누를 때마다 불린 값을 반전시켜주면 됩니다.
(초기값은 false이기에 음수면 true, 양수면 false.)

지금 생각해보니... confirmReverse 변수는 쓸모가 없습니다.
어차피 피연산자에 부호까지 포함하여 값을 넣어주기 때문이에요. ㅠㅠ 이제 알았네...

원래는 confirmReverse 값에 따라 양수인지 음수인지 판단하려 했던 건데, 그럴 필요가 없네요.
지우겠습니다...

 

다음으로 per 버튼입니다.

perBtn.addEventListener('click', function() {
    displayVariable.textContent *= 0.01
})

간단하게 0.01 곱해서 할당해주면 됩니다.



 

숫자 버튼에 이벤트 리스너 적용

 

buttons.forEach(function (button) {
    // dot 버튼은 이미 별도로 처리하므로 무시
    if (!button.classList.contains('dot') && button.classList.contains('number')) { 
        button.addEventListener('click', function () {
            if (displayVariable.textContent === '0') {
                displayVariable.textContent = '';
            }
            buttonEventListener(button);
        });
    }
});

위에서 작성한 이벤트 리스너 함수를 버튼에 적용시킵니다.

 

  !button.classList.contains('dot'&& button.classList.contains('number')  

위에서 언급했듯이 dot 버튼은 초기값 0을 지우지 않으므로, dot 버튼일 때는 제외하고
number 클래스를 가진 button들에만 적용합니다.

classList.contains(className) 는 해당 요소의 클래스에 className이 포함되어 있는지를 불린값으로 반환합니다.

 

그리고 displayVariable.textContent 가 초기값인 0일 때,
다음 숫자 버튼이 입력되면 화면을 초기화 한 후 화면에 입력합니다.





이렇게 해서 계산기 완성입니다!!

제가 처음에 작성했던 코드는 기능이 전부 들어가지 않았음에도 200줄이 훌쩍 넘었는데요...

이유는 단일 책임 원칙을 따르겠다고 굳이 굳이 간단한 것들도 다 함수로 쪼개고 그래서
한 줄짜리 함수들도 막 나오고 하다 보니까.....ㅜㅠ

저희 팀원분께서 같은 동작을 하는 함수들을 짚어주시고 간단한 것들은 함수로 쪼개지 않아도 될 것 같다
피드백을 주셔서 고쳐보니 코드가 3분의 1로 줄어드는 매직을 보게 되었습니다.



과제 수행 기간은 정해져 있는데 그 기간동안 여행이 계획되어 있었어서... 급히 만드느라 아주 진땀 뺐습니다. ㅠㅠ
계산기 로직이 생각보다 어려워서 기가 많이 죽었는데 그래도 완성해내서 기뻐요...

블로그로 작성하니 제 코드도 다시 보게 되고, 또 고쳐야 할 부분들이 보이고 이러네요.
블로그의 순기능!!!

요즘 잘 못 썼는데 다시 열심히 써야겠어요 흐흐.


https://github.com/xuuwon/calculator

 

GitHub - xuuwon/calculator

Contribute to xuuwon/calculator development by creating an account on GitHub.

github.com