JavaScript - 계산기 만들기

Kim-DaHam·2023년 2월 23일
0

JavaScript

목록 보기
3/18
post-thumbnail

nightmare까지 다 완성하긴 했는데 reference랑 비교하니까 바보 같이 기뻐할 때가 아니란 걸 깨달았다...



🟩 계산기 만들기 과제

🟣 bare minimum test - 한 자리수 계산기

⬜ 전역변수

const calculator = document.querySelector('.calculator');
const buttons = calculator.querySelector('.calculator__buttons');
const firstOperend = document.querySelector('.calculator__operend--left');
const operator = document.querySelector('.calculator__operator');
const secondOperend = document.querySelector('.calculator__operend--right');
const calculatedResult = document.querySelector('.calculator__result');

<비슷하지만 다른 DOM 접근 방법>

  • document.getElementById(id) : id와 일치하는 dom 요소(Element 객체)를 반환. 일치하는 요소가 없으면 null.
  • document.getElementsByClassName(names)
    • 예시)
    • document.getElementsByClassName("test");
    • document.getElementsByClassName("red test"); : red와 test 클래스를 모두 가진 요소
    • document.getElementById("main").getElementsByClassName("test"); : main 요소 중 test 클래스를 가진 요소
  • document.getElementsByClassName("test")[0]; : test 클래스를 가진 요소 중 가장 첫 번째 요소
  • document.getElementsByName(name) : name 속성과 일치하는 dom 요소 반환
  • document.getElementsByTagName(name) : 해당 tag인 dom 요소 반환

🌠 checkpoint # querySelector 와 getElementById 차이


🔵 getElementById

element = document.getElementById(id);
  • id명과 일치하는 엘리먼트 객체를 반환한다.

🔴 querySelector

element = document.querySelector(selectors);
  • 선택자와 일치하는 첫 번째 엘리먼트 객체를 반환한다.

🔵 getElementByClassName

elements = element.getElmentsByClassName(names);
  • class명과 일치하는 엘리먼트들의 HTMLCollection을 반환한다.

🔴 querySelectorAll

elementList = parentNode.querySelectorAll(selectors);
  • 선택자와 일치하는 엘리먼트들의 NodeList을 반환한다.

<form id="productForm">
  <input id="productOne" class="product" type="text" value="Product 1" />
  <input id="productTwo" class="product" type="text" value="Product 2" />
  <input id="productThree" class="product" type="text" value="Product 3" />
</form>
  // getElementByClassName 🔵
  const products = document.getElementsByClassName("product");
  // querySelectorAll 🔴
  const products = document.querySelectorAll("#productForm .product");

🔵 HTMLCollection

  • 동적이다. DOM에 새로운 요소(Element)가 추가되면 HTMLCollection은 새로운 요소를 가져온다.
  • div span 등과 같은 요소 노드만 포함. 텍스트 노드는 포함하지 않음.
  • document.getElementById("parent").children; 자식 요소 접근 가능.
    • 자식 요소 노드 접근)
      1. parentDivp[2] : 인덱스로 접근
      1. parentDiv.namedItem("id"); : 아이디로 접근
      1. parentDiv.namedItem("name"); : 이름으로 접근
  • 유사 배열 객체. 반복문으로 요소 접근할 때 for...of 문을 사용. forEach() 사용 불가.

🔴 NodeList

  • 정적이다. 새로운 요소가 추가되어도 새로운 요소를 가져오지 않는다.
  • 요소 노드, 속성 노드, 텍스트 노드 등을 포함.
  • namedItem() 메서드를 가지고 있지 않아 자식 요소 노드로 접근 불가능.
  • 유사 배열 객체. forEach() 메서드 존재.

🎁 HTMLCollection/NodeList 차이점
🎁 getElementById/querySelector 차이점


⬜ 버튼 이벤트

buttons.addEventListener('click', function(event) {
	cosnt target = event.target;
    const action = target.classList[0]
    const buttonContent = target.textContent;
    ...
}
  • addEventListener(event, func, option); : 이벤트를 등록하는 가장 권장되는 방식(이벤트 리스너).

  • 화살표 함수 버전
  a.addEventListener('click', ()=> { });
  • dom.removeEventListener(event, func) : 이벤트 리스너 삭제. 더 이상 이벤트가 필요 없을 경우 메모리 누수를 방지해 반드시 삭제하자.
  • element.classList : element의 class 속성을 보여준다. css class의 현재 값을 반환한다.
    • elment.classList.toggle(class) : 0과 1이 반복. 클래스가 존재한다면 클래스를 제거하고, 클래스가 존재하지 않는다면 클래스를 추가함.
    • element.classList.add(class) : 명시된 클래스를 추가
    • element.classList.remove(class) : 명시된 클래스를 제거
  • element.textContent : 해당 요소의 텍스트 노드

🌠 자주 쓰는 이벤트

  • 포커스 이벤트(focus, blur)
  • 폼 이벤트(reset, submit)
  • 뷰 이벤트(scroll, resize)
  • 키보드 이벤트(keydown, keyup)
  • 마우스 이벤트(mouseenter, mouseover, click, dbclick, mouseleave)
  • 드래그 앤 드롭 이벤트(dragstart, drag, dragleave, drop)

🌠 textContent / innerHTML의 차이

  • textContent : 더 먼저 만들어졌으며 브라우저 호환성이 더 높다. 미세하게 더 가볍기도. 스페이스 공백을 앞,뒤,사이 할 것 없이 다 가져온다.(ie에서. 크롬에선 innerHTML이랑 똑같음)
  • innerHTML : 앞뒤 공백을 제거하고 가져옴.

⬜ 숫자 입력하기

if (target.matches('button')) { // 클릭 된 요소가 버튼 요소가 맞는지 확인
    if (action === 'number') {
      ...
    }
  }

target : 클릭 이벤트가 발생한 버튼 요소
dom.matches() : 요소에 해당 선택자가 있는지 확인하고 true/false 반환
action : target의 클래스 리스트 중 0번째 클래스


<button class="number">
<button class="operator">
<button class="clear">
<button class="decimal">

html 파일을 보면 모든 버튼 요소들이 종류 별로 클래스가 정해져 있다. action 이 'number'이면 숫자 버튼, 'operator'면 연산 버튼이다. 버튼을 저렇게 정리해놓으니까 깔끔하고 보기 좋아서 앞으로 참고 해야겠다.

그럼 이제 숫자 버튼이 클릭 되었을 때 계산기의 ■ + ■ = □ 안에 채워지게 만들어야 한다.


if (action === 'number') {
      if(firstOperend.innerText === '0') {
        firstOperend.innerText = buttonContent;
      } else
        secondOperend.innerText = buttonContent;
    }

firstOerend(첫 번째 숫자 칸)가 '0'(기본값)이면, 아직 첫 번째 숫자가 입력되지 않은 것이므로 클릭한 버튼의 숫자를 firstOperend.innerText 에 넣는다.

'0'이 아닌 경우, 첫 번째 숫자 칸에는 숫자가 입력 되었으므로 secondOperend.innerText 에 클릭한 버튼의 숫자인 buttonContent 를 할당한다.


⬜ 연산자 입력하기

if (action === 'operator') {
      operator.innerHTML = buttonContent;
      console.log('연산자 ' + buttonContent + ' 버튼');
    }

클릭 한 연산자 버튼의 텍스트 노드를 □ + □ = □ 의 연산자 요소인 operator 의 inner.HTML에 할당한다.


⬜ 계산하기 & 답 출력하기

if (action === 'calculate') {
      let result = calculate(firstOperend.innerText, operator.innerText, secondOperend.innerText);
      calculatedResult.innerText = result;
      console.log('계산 버튼');
    }

firstNumber secondNumber 변수에 각각 Number 형변환을 한 첫 번째 값/두 번째 값을 저장한다. 계산은 calculate 함수에서 실행한다.

아래는 calculate 함수이다.

function calculate(n1, operator, n2) {
  let result = 0;
      if(operator === '+')
        result = n1+n2;
      else if(operator === '-')
      result = n1-n2;
      else if(operator === '*')
      result = n1*n2;
      else if(operator === '/')
      result = n1/n2;
  return String(result);
}



🟣 Advanced Challenge test

이미 nightmare test 와 어느정도 뒤섞인 코드다. 지금 보니까 계산만 정상 작동할 뿐 어설픈 부분이 중간중간 보인다. 레퍼런스랑 비교하면서 정리해보고자 한다.

⬜ 전역변수

let firstNum=null, operatorForAdvanced, previousKey, previousNum; // 기본 (null 초기화는 내가 함)

// 아래는 내가 추가
let secondNum=null, clickedOperator;
let isOperatorIsOn = false;
let isFirstInput = true;
let isSecondInput = false;

firstNum : 첫 번째 입력 값
operatorForAdvanced : 클릭한 연산자 종류
previousKey : 바로 이전에 클릭한 버튼
previousNum : 이전 결과값

secondNum : 두 번째 입력 값
isOperatorIsOn : operator가 이미 클릭 된 상황인지 판단
isFirstInput : 첫 번째 값을 입력하는 중인지
isSecondInput : 두 번째 값을 입력하는 중인지

지금 보니까 내가 추가한 변수들은 전부 없어도 잘만 구현할 수 있었다..


🔵 reference : 버튼 이벤트 리스너 안에 const buttonContainerArray = buttons.children; 문을 추가하였다. 용도는 천천히 코드를 내려가면서 확인하자.


⬜ 첫 번째 숫자 입력하기

🔴 me

if (action === 'number') {
      let result = ''; // 최종 입력 숫자
      if(previousKey === 'operator' || isSecondInput){ // 두 번째 값을 입력 할 때라면
        isFirstInput = false; // 첫 번째 값 입력은 x
        if(secondNum === null && display.textContent !== '.') // 두 번째 값을 아직 아무것도 입력 안 했고 '.' 도 입력 안 했다면
        	display.textContent = '0'; // display 초기화
      } else { // 첫 번째 값을 입력 할 때라면
        isFirstInput = true; // 첫 번째 값 입력 허용
        firstNum = null; secondNum = null; // 계산의 첫 시작이라는 것이므로, 첫 번째 값/두 번째 값 초기화
      }

	  // 숫자가 아무 것도 입력 안 됐거나 || 이전에 누른 버튼이 Enter 거나 || 이전에 누른 버튼이 AC면
      if(display.textContent === '0' || previousKey === 'calculate' || previousKey === 'clear'){
        display.textContent = buttonContent; // 화면에 있는 숫자를 모두 지우고 다시 씀
      }else // 첫 번째 입력이 아니면('5'3 + '2'1 에서'5'와 '2'의 입력이 아니면)
        display.textContent = display.textContent.concat(buttonContent); // 이어 붙이기

      previousKey = 'number'; // 이전 키를 number로 변경
      result = Number(display.textContent); // 만들어진 숫자를 타입 변환 하여 result에 저장

      if(isFirstInput) // 첫 번째 값이라면
        firstNum = result; // firstNum에 저장
      else secondNum = result; // 두 번째 값이라면 secondNum에 저장
    }

아 복잡하다. 출국 전날 급하게 막무가내로 욱여넣은 가방을 펼쳐 보는 기분이다.


🔵 reference

if (action === 'number') {
	  // 아직 아무것도 입력 안 했거나 || 이전에 누른 버튼이 연산 버튼이거나 || 이전에 누른 버튼이 Enter 이면
      if (display.textContent === '0' || previousKey === 'operator' || previousKey === 'calculate') {
        display.textContent = buttonContent; // 화면에 완전히 새로 쓴다
      } else { // 그게 아니면
        display.textContent = display.textContent + buttonContent; // 이어붙인다.
      }
      previousKey = 'number'; // 이전 키 값을 number로 변경한다.
}

(이렇게 간단할 일인가?)


🟧 연산자 입력하기 & 삼 항 이상 다항식 연속 계산

🔴 me

if (action === 'operator') {
	  // 연산자 버튼이 이미 한 번 눌렸었고 && 이전에 누른 버튼이 숫자라면 (다항 연산이라면)
      if(isOperatorIsOn && previousKey==='number'){
        // 앞서 입력한 두 항을 먼저 계산한 다음 firstNum으로 교체시켜 준다. (2+3)+4 = 5+4
        firstNum = Number(calculate(firstNum, operatorForAdvanced, secondNum));
        secondNum = null; // 계산 된 두 번째 값은 초기
      }
      isOperatorIsOn = true; // 연산자 한 번 이상 눌렸음을 체크
      target.style.backgroundColor = '#00da75'; // 클릭한 연산자 버튼 색상 변경
      operatorForAdvanced = buttonContent; // 클릭한 연산자 종류 저장
      previousKey = 'operator'; // 이전 키 값 operator로 변경
      clickedOperator = target; // 클릭 한 연산자 버튼 요소를 저장
      isSecondInput = true; // 이제 두 번째 값을 입력할 차례임을 설정
    }

<문제점>

  • 다항 연산을 할 경우 버튼 색상을 변경하지 않았다. 동시에 여러 연산자 버튼의 색상이 다 변하게 되며, Enter 버튼을 눌렀을 때 마지막으로 클릭한 버튼 색상만 검은색으로 돌아온다.

  • 다항 연산을 할 때, 2+2+ 까지 눌렀을 때 앞의 2+2가 계산 된 4가 출력되지 않고, 그냥 계속 입력한 값만 나오게 만들어 버렸다. UI 센스가 없는 건지 수포자라 계산기 안 두드린 티가 나는 건지...


🔵 reference

if (action === 'operator') {
      target.classList.add('isPressed'); // 클릭한 연산자 버튼에 isPressed 라는 클래스 추가
      // 첫 번째 입력 값이 존재하고 && 연산 버튼도 클릭했고 && 이전에 누른 버튼이 operator가 아니고 && 이전에 누른 버튼이 Enter 가 아니면
      if (firstNum && operatorForAdvanced && previousKey !== 'operator' && previousKey !== 'calculate') {
        // 다항식에서 앞 두 숫자를 먼저 연산 후 출력 (4+4+ 까지 입력했을 경우 화면에 8 출력)
        display.textContent = calculate(firstNum, operatorForAdvanced, display.textContent);
      }
      firstNum = display.textContent; // 4+ 까지만 입력 하면 firstNum은 그대로인 셈. 다항 연산 했을 경우 변경 된 firstNum을 저장하게 된다.
      operatorForAdvanced = buttonContent; // 클릭한 연산자 저장
      previousKey = 'operator'; // 이전 키 값 변경
    }

firstNum 을 변경하는 부분에서 충격을 받았다. 나는 왜 저걸 굳이 if 조건문 안에 넣으려고 했을까? 화면을 계산 값으로 바꾸면 될 걸.


🟧 계산하기 & 답 출력하기

  • 첫번째 값 + 연산자 + Enter = 자기 자신 연산

  • 결과값 + 중복 Enter = 다중 계산

🔴 me

if (action === 'calculate') {
      let result = 0; // 최종 계산 값
      isOperatorIsOn = false; // Enter 버튼을 눌렀으니까 연산자 클릭했다는 flag는 초기화
      
	  // 3항 이상 다항 연산은 생각도 안 하고 적은 '연산자 버튼 색 검은색으로 바꾸기'
      if(clickedOperator) clickedOperator.style.backgroundColor = '#313132';

      if(previousKey === 'operator') // 두 번째 값을 입력하지 않고 곧바로 Enter를 눌러버렸을 때
        secondNum = firstNum; // 자기 자신에 대해 연산해야 하므로 두 번째 값을 첫 번째 값으로
      if(previousKey === 'calculate') // Enter를 여러 번 누르면
        // 다중 계산 한다. 1, -, 1, Enter, Enter, Enter 의 결과값은 -2
        result = calculate(previousNum, operatorForAdvanced, secondNum);
      else // Enter 한 번만 눌렀으면 정상 계산
        result = calculate(firstNum, operatorForAdvanced, secondNum);

      display.textContent = result; // 결과를 화면에 출력
      previousNum = Number(result); // 이전 값에 결과값을 저장해둔다
      previousKey = 'calculate'; // 이전 키 값 변경
      isSecondInput = false; // 이제 첫 번째 값을 입력해야 하므로 두 번째 값 입력 취소
    }

🔵 reference

if (action === 'calculate') {
      if (firstNum) { // 첫 번째 값 존재하고
        if (previousKey === 'calculate') { // Enter 연속으로 눌렀으면
          // 다중 계산한다.
          display.textContent = calculate(display.textContent, operatorForAdvanced, previousNum);
        } else { // Enter 한 번만 눌렀으면
          previousNum = display.textContent; 화면에 출력 되어있는 이전 값을 저장한다
          // 정상 계산.
          display.textContent = calculate(firstNum, operatorForAdvanced, display.textContent);
        }
      }
      previousKey = 'calculate';
    }

display.texContent 값을 calculator() 함수에 넣는 점에서 secondNumber 변수를 따로 지정한 나와 다르다.

(나는 아마 .5만 입력해도 0.5가 되도록 하라. 라는 말을 다르게 오해해서 이상한 변수를 만들었던 것 같다. 아무 숫자도 입력하지 않았을 때('0'일 때) . 버튼을 클릭하면 0이 사라지고 진짜 .만 출력 되게 만드느라..이것저것 조건문의 요건을 충족하려다 보니 쓸모없는 변수가 엄청 많아졌다.)

Enter를 누르기 직전 화면에 출력 되어있는 마지막 값이 곧 두 번째 값이고, 두 번째 값이 입력 되지 않았다더라도(1+Enter) 자기 자신에 의해 연산될 수 있다는 점이 간단하고 마음에 든다.


⬜ AC로 초기화 하기

🔴 me

if (action === 'clear') {
	  // 연산자 버튼 색 초기화(그래봤자 삼 항 이상 다항연산은 적용도 안 됨)
      if(clickedOperator) clickedOperator.style.backgroundColor = '#313132';
      firstNum = null; // 첫 번째 숫자 초기화
      secondNum = null; // 두 번째 숫자 초기화 (내가 멋대로 초기값 null로 했었음)
      operatorForAdvanced = '+'; // 아 이건 bare 난이도 초기화랑 헷갈렸나보다. 거기선 +가 기본상태라...
      previousKey = 'clear'; // 이전 키 변경
      display.textContent = 0; // 화면 초기화
      isSecondInput = false; // 두 번째 값 입력 flag 내림
    }

🔵 reference

if (action === 'clear') {
      firstNum = undefined; // let firstNum; 이라고 선언했었기 때문에 초기값은 undefined여야 한다.
      operatorForAdvanced = undefined; // 위와 마찬가지
      previousNum = undefined; // 또찬가지
      previousKey = 'clear'; // 이전 키 변경
      display.textContent = '0'; // 화면 초기화
    }



🟣 Nightmare test

🟧 실수 연산하기 - 소수점 입력하기

🔴 me

if (action === 'decimal') {
      if(previousKey !== 'decimal'){ // .을 다중 클릭해도 한 개만 출력 되도록
        if(previousKey === 'number') // 이전에 숫자 버튼을 눌렀다면
          display.textContent = display.textContent.concat('.'); // 이어붙이기
        else display.textContent = '.'; // 아니면 기본값 '0' 지우고 . 찍기 (오해의 시작)
      }
      previousKey = 'decimal'; // 이전 키 값 바꾸기
      }

🔵 reference

if (action === 'decimal') {
	  // 화면에 . 이 없고 && 이전에 클릭한 버튼이 연산자가 아닐 때 (1 + .5 이런 식은 아예 안 되게 막아버리는 구나...)
      if (!display.textContent.includes('.') && previousKey !== 'operator') {
        display.textContent = display.textContent + '.'; // . 이어붙이기
      } else if (previousKey === 'operator') { // 이전에 연산자 버튼 클릭했으면
        display.textContent = '0.'; // .5 가 아니라 0.5라고 출력 되도록 0. 붙임
      }
      previousKey = 'decimal'; // 이전 키 값 수정
    }

나는 왜 .5 도 계산 되도록 하라는 말을 .5로 출력하라는 걸로 이해했을까ㅜㅜ
저 부분에서 조건문 맞추느라 이상한 변수 많이 만들었는데...
(사실 조건 맞춘다 한들 이상한 변수 안 만들고도 가능했을지 모르지만)




📔 오늘의 후기

  • 코드를 혼자 0부터 작성하지 않고, 제공 되는 기본 토대 안에 제어문과 함수를 집어넣는 방식의 과제였다. 늘 그렇듯 다른 사람의 코드(기본 토대라도) 내가 이해하는 과정을 한 번 거치고, 그 안에 기능을 구현하는 건 어렵게 느껴지고 손에 땀이 난다.
    아직 자바스크립트 기초라 다행히 금방 이해를 마치고 프로그래밍을 할 수 있었지만, 이후 마주할 과제들이 조금 두렵다^^..
    이러한 방식의 과제를 했을 때 장점은 '구조가 이게 맞아?' 라는 고민을 코딩 내내 하지 않아도 된다는 점이다. 내가 직접 작성한 코드는 늘 복잡하고 정리정돈이 안 된 기분이라 다른사람이 봤을 때를 고려하면서 늘 망설임의 망설임의 망설임의 연속이었다. 그런데 이렇게 기본 틀을 제공하면 아, 시작하기 전에 이렇게 깔끔하게 구조를 짜놓으면 되는구나. 하고 깨닫고 간다.
  • 어찌저찌 나이트메어 테스트 끝까지 성공했다! 기분이 좋았다. 이럴 때가 제일 재밌다. 코딩 공부 자체는 재미 없다고 할 수도 없고 있다고 할 수도 없는 그저 그런 녀석인데. 이렇게 문제를 직접 풀면 그냥 게임처럼 재밌다. 하지만 문제는 나 혼자 작성하고 나 혼자 재밌으면 안 된다는 거다..

  • 페어 분께서 소수점 파트 코드 리뷰를 부탁하셨는데, 말문이 막혔다. 내 눈으로 내 머리로는 이해가 가는데 그걸 말로 서술하려니 뇌가 정지했다. 여러 조건문들이 서로 얽혀있어서 '소수점 파트'만 딱 꼬집어 설명하기 어려웠다. 이건 내가 코드를 예쁘게 작성하지 않은 탓이겠지.
    CleanCode 왈, 그냥 위에서 쭉 내려오면서 읽어도 하나의 한글 문서를 읽는 것마냥 자연스럽게 이해가 되어야 한다고 했는데, 내 코드는... 한참 멀었다. 변수 이름도 목적이 모호하게 작성한 것 같다. 다음부터는 꼭 별도의 정리 없이도 코드를 줄줄 리뷰할 수 있을 만큼 자연스러운 코드 흐름을 짜도록 노력해야겠다. 그러기 위해선 새로운 문제를 계속 푸는 것도 좋지만 이미 작성한 코드를 거듭 반복하여 더 나은 코드로 만드는 훈련도 필요한 것 같다. 내 첫 번째 목표는 계산기 코드를 그렇게 수정하는 거다! 오늘 계속 고민해본 다음 아마도 내일이나 주말에 정리하게 될 거 같다.

profile
다 하자

0개의 댓글