계속 자바스크립트 알고리즘 문제만 풀다가, 처음으로 DOM을 조작하는 과제를 받았다. 이벤트 함수는 이미 입력되어 있어서 DOM은 맛보기 수준이긴 했지만, 드디어 내가 가장 좋아하는 CSS를 활용할 수 있어서 이틀간 즐겁게 코딩했다.
난이도는 총 세 난이도로 나누어져 있었는데,
처음에는 페어 분이나 나나 'Bare minimum만 잘 끝내보자!'는 마음으로 시작했다가 생각보다 금방 해결해서 남은 시간 동안 머리를 맞대니 Nightmare까지 클리어할 수 있었다! 🎉
이 과제를 하면서 계산기에 이렇게 많은 기능이 있다는 걸 처음 알게 돼서… 기능 구현은 힘들었지만 세상이 또 넓어졌다. 3*=만 눌러도 제곱 계산된다는 거 이번에 처음 알게 됨…
1일 차에는 계산기 디자인과 Bare minimum 클리어만 목표로 천천히 진행했다.
이미 틀이 거의 잡혀있어서 flex로 버튼 재배치해서 간격 맞춰주고, 버튼 사이즈, 모서리 각도 수정하고, 롤오버 효과 정도만 추가해줬다. 구글에서 맘에 드는 컬러 팔레트 찾는 게 제일 오래 걸렸음…
쓰면서 익혔던 코드 중에 기억할 만한 건, white-space랑 ellipsis.
.calculator__display--for-advanced {
background-color: #ffffff;
height: 70px;
width: 100%;
margin: 15px 0px 20px 0px;
border-radius: 15px;
font-size: 20px;
text-align: center;
padding: 25px 15px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
/* overflow-wrap: break-word; */
}
텍스트양이 정해진 구역을 넘어설 때 어떻게 처리할지를 지정해주는 CSS 코드인데, 디폴트 값은 break-word로 줄을 바꿔주는 거였지만 개인적으로 계산기에서 줄이 바뀌는 건 어색한 것 같아서 줄 바꿈 없이(nowrap) '…'으로 넘치는 숫자를 생략(ellipsis)해주는 코드를 썼다.
const display = document.querySelector('.calculator__display--for-advanced'); // calculator__display 엘리먼트와, 그 자식 엘리먼트의 정보를 모두 담고 있습니다.
let firstNum, operatorForAdvanced, previousKey, previousNum;
firstNum = "0";
operatorForAdvanced = "";
previousKey = "";
let secondNum = "";
let count = 0;
let decimalCount = 0;
let result = "";
let resultCount = 0;
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;
}
// TODO : n1과 n2를 operator에 따라 계산하는 함수를 만드세요.
// ex) 입력값이 n1 : '1', operator : '+', n2 : '2' 인 경우, 3이 리턴됩니다.
return String(result);
}
buttons.addEventListener('click', function (event) {
// 버튼을 눌렀을 때 작동하는 함수입니다.
const target = event.target; // 클릭된 HTML 엘리먼트의 정보가 저장되어 있습니다.
const action = target.classList[0]; // 클릭된 HTML 엘리먼트에 클레스 정보를 가져옵니다.
const buttonContent = target.textContent; // 클릭된 HTML 엘리먼트의 텍스트 정보를 가져옵니다.
// ! 위 코드는 수정하지 마세요.
// ! 여기서부터 Advanced Challenge & Nightmare 과제룰 풀어주세요.
if (target.matches('button')) {
if (action === 'number') {
if(count === 0){
if(firstNum[0] === "0"){
firstNum += buttonContent;
firstNum = firstNum.substring(1,firstNum.length)
} else {
firstNum += buttonContent ;
}
display.textContent = firstNum ;
} else {
if(secondNum[0] === "0"){
secondNum += buttonContent;
secondNum = secondNum.substring(1,secondNum.length)
} else {
secondNum += buttonContent ;
}
display.textContent = secondNum ;
}
}
if (action === 'operator') {
if(count === 0){
previousKey = buttonContent ;
}
operatorForAdvanced = buttonContent ;
if(secondNum !== ""){
previousNum = calculate(Number(firstNum),previousKey,Number(secondNum));
firstNum = previousNum;
secondNum = "";
previousKey = buttonContent ;
decimalCount = 0;
}
count += 1;
}
if (action === 'decimal') {
if(count === 0 && decimalCount === 0){
firstNum += "." ;
display.textContent = firstNum ;
decimalCount = 1;
} else if(count !== 0 && decimalCount !== 2){
secondNum += "." ;
display.textContent = secondNum ;
decimalCount = 2;
}
}
if (action === 'clear') {
display.textContent = 0;
firstNum = "0";
secondNum = "";
operatorForAdvanced = "";
result = "";
count = 0;
decimalCount = 0;
resultCount = 0;
}
if (action === 'calculate') {
if(operatorForAdvanced === ""){
result = firstNum ;
} else if(secondNum === ""){
secondNum = firstNum ;
result = calculate(Number(firstNum),operatorForAdvanced,Number(secondNum));
} else if(resultCount === 0){
result = calculate(Number(firstNum),operatorForAdvanced,Number(secondNum));
} else if(resultCount !== 0){
result = calculate(Number(result),operatorForAdvanced,Number(secondNum));
}
display.textContent = result ;
count = 0;
decimalCount = 0;
resultCount += 1;
}
}
});
Bare minimum으로 한 자릿수 계산 기능을 구현할 때, 숫자라는 같은 형식이 입력되는데 1) 첫 번째 피연산자와 2) 두 번째 피연산자를 어떻게 구분할 수 있을지를 가장 많이 고민했던 것 같다.
그래서 '연산자를 누른다'는 행위를 기준으로 두 피연산자를 구분할 수 있으니 연산자가 입력될 때 count라는 변수를 올라가게 해서 해결했었는데, Nightmare도 같은 방식으로 접근하다 보니 연산자용, 소수점용, 결과값용 카운트 변수를 다 따로 사용해 변수가 많아졌다…
Nightmare 클리어를 위한 가장 고난도의 조건은 다음과 같았다.
1,0,0,.,.,1,2,5,2,+,1,2,+,1,5,-,-,2,3,-,1,4,4,2,/,2,3,/,/,1,2,*,2,3,Enter를 연속으로 누르면 -111.48956666666668이(가) 화면에 표시되어야 합니다.
일의 자리가 아닌 숫자가 들어올 때, 연산자를 입력하기 전까지 숫자열을 덧붙여야 함. 단, 기본 입력값은 0이므로 숫자열의 맨 앞자리가 0이 되어서는 안 된다.
소수점은 숫자 값 안에서 가장 처음 누른 한 번만 사용할 수 있음.
Enter(=)를 연속으로 누를 때, 같은 피연산자를 같은 연산자로 반복 계산해야 함.
두 번째 피연산자를 입력하지 않은 채로 첫 번째 피연산자와 연산자만 입력받았을 때, 같은 숫자와 연산자로 반복 계산해야 함.
Enter(=)를 누르기 전까지, 숫자와 연산자만 눌렀을 때도 수식 순서대로 계산하고, 값을 저장해야 함.
대부분은 count 변수를 사용해서 해결할 수 있었는데, 변수가 많아지다 보니 코드의 배열순서와 어느 시점에서 카운트를 초기화해야 할 지를 생각해야 해서 어려웠던 것 같다.
일단 고민점들은 이렇게 접근해서 풀었다.
- 일의 자리가 아닌 숫자가 들어올 때, 연산자를 입력하기 전까지 숫자열을 덧붙여야 함. 단, 기본 입력값은 0이므로 숫자열의 맨 앞자리가 0이 되어서는 안 된다.
=> firstNum 문자열의 가장 앞 글자가 0일 경우, 입력값(문자열)을 더해준 후, 두 번째 문자열부터 문자열 끝까지를 firstNum으로 정의한다.
가장 앞 글자가 0이 아닐 경우, 입력값은 그대로 문자열 끝에 더해진다.
원래는 firstNum의 초기값을 공백("")으로 지정해서, 굳이 추가하지 않았던 조건이었지만 AC를 누른 후 Enter를 누를 경우, 값 출력화면이 공백이 되는 오류를 발견하여 초기값을 따로 "0"으로 지정해주었다.
if(firstNum[0] === "0"){
firstNum += buttonContent;
firstNum = firstNum.substring(1,firstNum.length)
} else {
firstNum += buttonContent ;
}
display.textContent = firstNum ;
- 소수점은 숫자 값 안에서 가장 처음 누른 한 번만 사용할 수 있음.
=> decimalCount 변수를 적용해주었다.
if (action === 'decimal') {
if(count === 0 && decimalCount === 0){
firstNum += "." ;
display.textContent = firstNum ;
decimalCount = 1;
} else if(count !== 0 && decimalCount !== 2){
secondNum += "." ;
display.textContent = secondNum ;
decimalCount = 2;
}
}
- Enter(=)를 연속으로 누를 때, 같은 피연산자를 같은 연산자로 반복 계산해야 함.
- 두 번째 피연산자를 입력하지 않은 채로 첫 번째 피연산자와 연산자만 입력받았을 때, 같은 숫자와 연산자로 반복 계산해야 함.
=> 이걸 해결하는 데 가장 시간을 오래 쓴 것 같다…
조건문의 여집합을 고려해서 코드의 순서를 배열하는 일이 가장 어려웠다.
일단 조건문을 순서대로 적으면,
1) 연산자가 입력되지 않았을 때, Enter를 누르면 화면에는 숫자가 그대로 출력되어야 함.
-> 원래는 결과값 변수를 따로 두지 않고 바로 calculate 함수를 호출했기 때문에 오류가 발생했었다. calculate 함수를 조건문 안으로 배열하고, result는 숫자로 남게 했다.
2) 두 번째 피연산자가 입력되지 않았을 때, 연산자는 존재한다면 두 번째 피연산자는 첫 번째 피연산자와 동일한 것으로 간주함.
3), 4) 첫 번째 피연산자, 연산자, 두 번째 피연산자가 모두 존재할 때 Enter를 처음 누른 상황이면, result는 calculate 함수의 결과값이 된다.
Enter만 연속으로 누르는 상황이라면 바로 전 계산 결과를 첫 번째 피연산자로 만들고, 이 값을 이전에 입력된 두 번째 피연산자와 calculate 함수를 이용하여 계산함.
-> resultCount는 AC를 누르기 전까지, 계속 올라가게 설정했다.
if (action === 'calculate') {
if(operatorForAdvanced === ""){
result = firstNum ;
} else if(secondNum === ""){
secondNum = firstNum ;
result = calculate(Number(firstNum),operatorForAdvanced,Number(secondNum));
} else if(resultCount === 0){
result = calculate(Number(firstNum),operatorForAdvanced,Number(secondNum));
} else if(resultCount !== 0){
result = calculate(Number(result),operatorForAdvanced,Number(secondNum));
}
display.textContent = result ;
count = 0;
decimalCount = 0;
resultCount += 1;
}
- Enter(=)를 누르기 전까지, 숫자와 연산자만 눌렀을 때도 수식 순서대로 계산하고, 값을 저장해야 함.
=> 이건 사실… 스스로 생각했다기보다는 기본 코드에 previousKey와 previousNum이라는 변수가 이미 선언되어 있어서 해결법을 떠올릴 수 있었다.
previousNum은 변수명에서 직관적으로 이해할 수 있었지만, previousKey를 어떻게 활용해야 할지 감이 안 잡혔는데 직접 코드를 실행해보면서 수정했다.
레퍼런스 코드에서는 previousKey에 입력된 모든 키를 저장한 후, 자료형을 검사하는 코드였는데 나는 연산자를 저장하는 데만 사용해서 코드가 좀 더 복잡하다…
if(secondNum !== ""){
previousNum = calculate(Number(firstNum),previousKey,Number(secondNum));
firstNum = previousNum;
secondNum = "";
decimalCount = 0;
}
처음 썼던 코드는 단순하게 "두 번째 피연산자가 존재할 때, 함수를 실행하고 결과값을 previousNum에 저장한 후 secondNum은 공백으로 만든다."였는데,
문제는 입력값이 [3 + 5 - 2]와 같은 순서로 들어올 수도 있다는 사실이었다…
그래서 "연산자가 처음 눌린 상황일 때, previousKey에 연산자를 일단 저장한다. AC나 Enter를 눌러 연산자 카운트가 초기화되지 않았다면, 두 번째 피연산자가 존재할 때, 바로 calculate 함수를 실행하고 현재 눌려있는 연산자를 previousKey로 재할당, 결과값을 previousNum에 저장한 후 secondNum는 초기화, 소수점 카운트는 0으로 만든다."
…는 복잡한 조건을 상정하여 해결할 수 있었다.
if (action === 'operator') {
if(count === 0){
previousKey = buttonContent ;
}
operatorForAdvanced = buttonContent ;
if(secondNum !== ""){
previousNum = calculate(Number(firstNum),previousKey,Number(secondNum));
firstNum = previousNum;
secondNum = "";
previousKey = buttonContent ;
decimalCount = 0;
}
count += 1;
}
Nightmare 케이스를 전부 해결한 후에도, 문제에서 제시한 케이스 외에 세 가지 오류 상황을 더 발견하여 추가로 코드를 더 수정했다.
1) 첫 번째 피연산자의 맨 앞에 0이 붙는 오류
ex. 0을 누른 후 자연수를 눌렀을 때, 01123 으로 숫자가 출력.
-> 접근법의 1번
2) 두 번째 이후로 입력하는 문자에 소수점이 붙지 않는 오류
-> decimalCount를 연산자에서 한 번, AC에서 한 번, Enter에서 한 번 초기화되게 설정해주었다.
3) AC를 누른 후, Enter를 눌렀을 때 계산기 출력화면이 공백으로 나오는 오류
실제 계산기에서는 값이 0으로 출력돼서, AC를 누른 후 [연산자, 숫자] 순으로 입력할 경우, 첫 번째 피연산자는 0으로 계산됐다.
ex.[AC, *, 3] = 0, [AC, + 6] = 6
-> firstNum의 초기값을 "0"으로 지정하여 해결
아직 발견하지 못한 오류 케이스가 더 있을 수도 있지만, 일단 내가 계산기를 두드려보며 비교해본 결과로는 계산기마다 차이를 보이는 값 빼고는 동일하게 출력된다.
어쨌든 모카테스트는 문제없이 통과!
항상 레퍼런스 코드 읽을 때마다 느끼는 거지만…
내 코드는 엄청 문과적이라는 생각이 든다……🥲
아직 코드 쓰는 일에 익숙하지 않아서 그런가…
if (action === 'decimal') {
if (!display.textContent.includes('.') && previousKey !== 'operator') {
display.textContent = display.textContent + '.';
} else if (previousKey === 'operator') {
display.textContent = '0.';
}
previousKey = 'decimal';
}
display.textContent를 어떻게 활용할지 몰라서 나는 거의 다 새 변수를 만들어줬는데, 엄청 간단한 코드로 해결되는 것들이 많았다. 특히 소수점 코드는 변수 할당 없이 display.textContent로 간단하게 처리돼서…
includes 메소드의 활용을 익혀야겠다.