바닐라 자바스크립트로 계산기 만들기 (feat. 코딩자율학습 제로초)
인프런에서 "코딩자율학습 제로초의 자바스크립트 입문"을 따라가고 있다.
자바스크립트 문법이나 동작원리에 대한 깊은 지식은 클래식한 도서나 코어 자바스크립트 문서, MDN 문서 등을 볼 예정이다. 제로초쌤의 해당 강의는 프로그래밍 사고력에 집중한 강의여서 좋다. 물론 기초문법 부분은 엄청 건너뛰며 봤지만, 확실히 순서도 그리는 연습이나 예외처리도 생각하면서, "코드 치는 행위"가 아닌 "프로그램을 만들고 있다"는 기분이 든다!
3월까지 완강하고, 4월에는 리액트 플젝이랑 JS심화 공부에 집중해야겠다.
이번주에는 섹션3과4를 수강했다. 섹션3에서는 끝말잇기 게임, 쿵쿵따 게임을 만들었고 섹션4에서는 계산기를 만들었다.
쿵쿵따 게임은 강의 제공 코드를 이미 본 상태였어서 제대로 공부가 안되었다.
섹션4에서 제작했던 계산기만 리뷰해보겠다!
순서도 그리기
정말 간단하게밖에 떠오르지 않았다. [숫자 - 연산자 - 숫자 - 계산]의 순서로 이루어져야 한다는 큰 구조만 생각했다.
어차피 해당 흐름은 어떤 케이스든 바뀌지 않을 것 같아서 사용자가 해당 흐름을 벗어날 수 있는 여러 케이스들을 고려하기로 했다.
생각보다 많았다. 어떤 버튼을 클릭하느냐에 따라서 어떤 조건을 고려해야하는지가 매우 달라졌다.
HTML 구조
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>계산기</title>
<style>
* {
box-sizing: border-box;
}
#result {
width: 100px;
height: 50px;
margin: 5px;
text-align: right;
}
#operator {
width: 50px;
height: 50px;
margin: 5px;
text-align: center;
}
button {
width: 50px;
height: 50px;
margin: 5px;
}
</style>
</head>
<body>
<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>
</body>
</html>
이러한 모습을 가진 계산기다. 맨위 input창은 읽기전용으로 설정되어있고, 왼쪽은 선택한 연산자, 오른쪽은 선택한 숫자를 보여주는 공간이다.
주요한 기능은,
1) 숫자버튼 클릭시
연산결과 때 활용할 변수에 저장 + 오른쪽 칸에 숫자 표시
2) 연산자버튼 클릭시
연산결과 때 활용할 변수에 저장 + 왼쪽 칸에 연산자 표시
3) 계산버튼 클릭시
이후 연산에 활용할 변수에 저장 + 오른쪽 칸에 숫자 표시
구현 과정
일단 모든 버튼 엘리먼트들을 저장한다. 다른 일반 변수들과의 구분을 위해 $를 변수명 앞에 붙였다.
숫자는 0부터 9까지 반복되므로, 반복문을 사용하여 저장하였고 이벤트리스너 추가를 초기화와 동시에 했다.
/* 버튼 엘리먼트 */
const numbers = new Array(10); // 숫자버튼
for (let i = 0; i < 10; i++) {
numbers[i] = document.querySelector(`#num-${i}`);
numbers[i].addEventListener("click", numClick);
}
//console.log(numbers);
const $plus = document.querySelector("#plus");
const $minus = document.querySelector("#minus");
const $multiply = document.querySelector("#multiply");
const $divide = document.querySelector("#divide");
const $calculate = document.querySelector("#calculate");
const $operator = document.querySelector("#operator");
const $result = document.querySelector("#result");
const $clear = document.querySelector("#clear");
이벤트 리스너를 모든 버튼에 추가했다. 크게 "숫자" "연산자" "계산" "초기화"에 따라 실행할 함수를 분류하였다.
추가적으로, 연산 과정에서 사용자가 선택한 숫자와 연산자를 저장할 변수들을 선언했다.
/* 기본 변수 */
let firstNum; // 첫번째 피연산자
let secondNum; // 두번째 피연산자
let operator; // 연산자
let globalResult; // 계산 결과 저장 변수
숫자 버튼 클릭 실행함수 numclick()
조금 복잡해졌다. 숫자버튼은 사실 [숫자 - 연산자 - 숫자 - 계산]의 큰 흐름 속에서 첫 번째 숫자나 두 번째 숫자 선택 시 사용된다.
그러나 사용자가 항상 그렇게 한다는 법은 없기 때문에 ...
위의 예외사항을 고려했던 것처럼, 아래와 같이 케이스를 나누어 생각했다.
firstNum | operator | secondNum | 처리 |
X | X | X | firstNum에 대입 |
X | X | O | 발생 불가능 |
X | O | X | 발생 불가능 |
O | X | X | 연산자 선택 유도 |
X | O | O | 발생 불가능 |
O | X | O | 발생 불가능 (firstNum, operator 존재조건) |
O | O | X | secondNum에 대입 |
O | O | O | 연산결과 있으면 연속계산 / 없으면 secondNum 업데이트 |
/* 숫자버튼 클릭 */
const numClick = (event) => {
clickedNum = Number(event.target.textContent);
if (!firstNum) {
firstNum = clickedNum;
$result.value = firstNum;
} else if (firstNum && operator && !secondNum) {
secondNum = clickedNum;
$result.value = secondNum;
console.log("계산결과: ", getResult(firstNum, secondNum, operator));
} else if (firstNum && operator && secondNum) {
if (globalResult) {
// 한 번 연산했을 때만, 이후로 연속 계산
firstNum = globalResult;
}
// 그러지 않았으면 그냥 두번째 피연산자 변경
secondNum = clickedNum;
$result.value = secondNum;
} else {
alert("연산자를 선택해주세요");
}
};
특히 사용자가 [숫자 - 연산자 - 숫자 - 숫자]를 입력했을 때와, [숫자-연산자-숫자-연산자]를 입력했을 때의 처리가 어려웠다.
둘다 firstNum, operator, secondNum 변수가 모두 값이 채워져있는 케이스기 때문이다.
[숫자-연산자-숫자-숫자]의 경우, secondNum을 그냥 변경하여 사용자가 두 번째 피연산자를 수정하는 것으로 처리했다.
(* 대신 이렇게하면, 첫 번째 피연산자도 수정할 수 있도록 이후 바꿔야될 것 같다)
[숫자-연산자-숫자-연산자]의 경우, 연산 결과를 이전에 도출했을 때만 연속적인 연산이 이어지도록 처리했다.최신 연산결과를 저장하는 globalResult변수의 값을 첫 번째 피연산자 값으로 업데이트하는 방식이다.
연산자버튼 클릭 실행함수 operClick()
연산자버튼은 첫 번째 피연산자가 있을 때만 담기도록 하였다.
/* 연산자버튼 클릭 */
const operClick = (event) => {
if (firstNum) {
operator = event.target.textContent;
$operator.value = operator;
}
};
계산버튼 클릭 실행함수 resultClick()
/* =버튼 클릭 */
const resultClick = () => {
if (firstNum && operator && secondNum) {
let final = getResult(firstNum, secondNum, operator);
$result.value = final;
globalResult = final;
} else {
//console.log("*오류파악* ", firstNum, secondNum, operator);
alert("입력 과정에 오류가 있습니다.");
}
};
계산 버튼 클릭 시, 오른쪽 input창에 결과값을 페인팅해주고, 최신 연산결과를 저장하는 globalResult에도 대입해준다.
물론, 두 개의 피연산자와 연산자가 제대로 존재할 때만 실행되도록 제한한다.
클리어버튼 클릭 실행함수 clearClick()
/* Clear버튼 클릭 */
const clearClick = () => {
firstNum = null;
secondNum = null;
operator = null;
$result.value = "";
$operator.value = "";
alert("연산 과정이 초기화되었습니다.");
};
모든 값을 초기화해주고, 입력창에 보이는 값도 지워준다.
계산함수 getResult()
연산자 버튼클릭 시 얻는 값은 연산 '기호'이므로, 실제 연산을 실행하는 함수를 따로 정의하였다.
(* 물론 곱셈 빼고는, 다 동일해서 더 효율적으로 하는 방법도 있을 것 같은데 ... 수정 시 고민해봐야겠다. 문자열 형태의 값을 연산자로 활용하는 방법을 찾지 못해서 해당 방식을 사용했다.)
/* 연산 함수 */
const getResult = (n1, n2, oper) => {
let result;
switch (oper) {
case "+":
result = n1 + n2;
break;
case "-":
result = n1 - n2;
break;
case "x":
result = n1 * n2;
break;
case "/":
result = n1 / n2;
break;
default:
console.log("연산자 에러");
break;
}
return result;
};
리뷰1
🍊 태도
아직 순서도를 그리는 단계에서부터 다양한 케이스나 필수적으로 고려해야하는 조건들이 잘 떠오르지 않는다.
강의를 들으면서 무작정 코드를 치는 것보다, 구조적으로 접근하는 방식으로 바뀔 수 있다는 가능성은 보인다!
아직 쉬운 예제이긴 하지만, 이런 습관을 조금씩 들이다보면 나중에는 무의식적으로도 해당 사고방식이 나올 수 있다고 믿는다...
🍊 지식
함수를 정의하는 위치가 호출하는 위치보다 뒤에 있으면 오류가 났다. 반면, 엘리먼트를 저장하는 변수의 선언 위치는 뒤에 있어도 상관이 없었는데 해당 부분에 대한 지식을 까맣게 잊었다... 보충이 필요한 부분 (TBD)
구현 과정2 - 여러자릿수 계산 가능하도록 수정
두 자릿수 이상부터는 연산이 안되도록 해놔서, 해당 부분을 수정했다. 사실 자릿수가 늘어나면 로직이 매우 복잡해지겠다고 생각했는데 강의를 참고하니 문자열로 처리했다가, 연산할 때만 정수로 바꿔주면 됐다.
1) 일단 피연산자와 임시 연산값을 저장하는 변수를 모두 빈문자열로 초기화한다.
/* 기본 변수 */
let firstNum = ""; // 첫번째 피연산자
let secondNum = ""; // 두번째 피연산자
let operator; // 연산자
let globalResult = ""; // 계산 결과 저장 변수
2) 연산을 담당하는 함수(getResult)에서 매개변수로 받은 첫 번째, 두 번째 피연산자들을 정수로 변환하여 값을 도출한다.
/* 연산 함수 */
const getResult = (n1, n2, oper) => {
let result;
n1 = Number(n1);
n2 = Number(n2);
switch (oper) {
case "+":
result = n1 + n2;
break;
case "-":
result = n1 - n2;
break;
case "x":
result = n1 * n2;
break;
case "/":
result = n1 / n2;
break;
default:
console.log("연산자 에러");
break;
}
return result;
};
3) 숫자버튼 클릭 시, 클릭한 숫자 값이 문자열 형태로 더해지도록 한다.
(사실 수정할 때 생각 먼저 안하고 코드부터 치니.. 헤맸다. 다음에는 그러지 말자... 심지어 이 글을 쓰는 와중에도 오류를 고쳤다)
어차피 operator의 존재여부를 기준으로, 없으면 firstNum 있으면 secondNum에 대한 처리기 때문에 해당값을 기준으로 분기처리한다. 중요한 건, secondNum을 바꿔주기 전에 globalResult를 먼저 firstNum에 넣어주어야 한다. 안 그러면, 연속연산시 입력 창에 숫자가 의도한 대로 나오지 않는다.
/* 숫자버튼 클릭 */
const numClick = (event) => {
clickedNum = event.target.textContent;
if (operator) {
secondNum += clickedNum;
$result.value = secondNum;
} else {
firstNum += clickedNum;
$result.value = firstNum;
}
};
4) = 버튼을 클릭하여 연산을 진행할 때, secondNum을 초기화해주고 기존 연산값도 저장해준다.
이 부분을 빠뜨리면 연속 연산을 할 때 숫자를 입력하면 이상한 형태로 화면에 그려진다 ^^...
지금은 연속 연산 시, 숫자를 클릭하면 이미 입력창에 나와있는 숫자 뒤에 연이어 여러 자릿수 숫자를 만들 수 있도록 했다.
연산자를 클릭하면 두 번째 숫자 클릭 후 연속 연산을 수행할 수 있다.
/* =버튼 클릭 */
const resultClick = () => {
if (firstNum && operator && secondNum) {
console.log(firstNum, secondNum, operator);
let final = getResult(firstNum, secondNum, operator);
$result.value = final;
globalResult = String(final);
firstNum = globalResult;
operator = null;
secondNum = "";
} else {
//console.log("*오류파악* ", firstNum, secondNum, operator);
alert("입력 과정에 오류가 있습니다.");
}
};
5) Clear시에는 첫 번째, 두 번째 숫자, globalResult를 모두 빈문자열로, 연산자는 null값으로 초기화해준다.
처음에는 firstNum, secondNum도 null값으로 초기화했었다. 그런데 혼자서 여러 가지 케이스를 테스트해보는 와중에 연속 계산을 하다가 중간에 초기화를 하면 입력값이 화면에 안 그려졌다. null + 문자열 연산을 하면 연결된다는 걸 몰랐다... null + "3" = "null3" 이런 식으로 되는 바람에! 이래서 자바스크립트는 왜 그모양일까라는 책이 있는 건지
문자열 변수는 초기화 시, 반드시 빈문자열로 ...
/* Clear버튼 클릭 */
const clearClick = () => {
firstNum = "";
secondNum = "";
operator = null;
globalResult = "";
$result.value = "";
$operator.value = "";
alert("연산 과정이 초기화되었습니다.");
};
리뷰2
🍊 태도
1. 무작정 코드부터 치는 습관은 정말 고쳐야 한다. 결론적으로는 비효율적이다.
2. 완성했다고 생각해도 내가 고려하지 못한 케이스와 예상치 못한 버그들은 참 다양하다. 열리고 겸손한 마음으로 반드시 테스트해보는 게 중요하다.
🍊 지식
null과 문자열을 더하면 null값이 아니라 그냥 연결된다. undefined와 NaN도 마찬가지다. 문자열 변수를 초기화해줄 땐, 대충 null넣는 게 아니라 빈문자열로 초기화해주자.
'프로그래밍 언어 > JavaScript' 카테고리의 다른 글
[프로그래머스 Lv.1] 개인정보 수집 유효기간 (자바스크립트) (1) | 2024.05.25 |
---|---|
[프로그래머스 Lv.0] 자바스크립트로 레벨0 모두 풀이하기 (0) | 2024.05.23 |
[자바스크립트 토픽 정리] 변수 선언위치에 대해 (1) | 2024.04.06 |
[자바스크립트 토픽 정리] 옵셔널 체이닝 ?. (0) | 2024.04.06 |
바닐라 자바스크립트로 숫자야구 만들기 (feat. 코딩자율학습 제로초) (0) | 2024.03.20 |