무엇을 만들어볼까 하다가, 자주 쓰는 계산기를 만들어 보았다.
다른 글들을 참고해보니 eval() 함수를 사용해 문자열 자체를 코드로 변환하여 연산하는 방식도 있었는데, 외부에서 입력된 코드를 그대로 실행시킨다는 점이 자칫 치명적인 단점이 될 수 있다는 우려가 있다. 따라서 eval()을 사용하지 않고 입력값을 변환하여 내부적으로 계산하는 방식으로 만들게 되었다. 참고로 ? << 물음표 버튼은 아직 기능이 없다.
기능 구현 목표는
1. 사칙연산
2. AC와 delete 기능
See the Pen Calculator by hyejj19 (@hyejj19) on CodePen.
CSS
main {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: 0.8fr 0.9fr repeat(5, 1fr);
grid-gap: 1.2rem;
background-color: #ececee;
border-radius: 0.7rem;
height: 43rem;
width: 25rem;
padding: 2rem;
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, rgba(0, 0, 0, 0.07) 0px 2px 4px,
rgba(0, 0, 0, 0.07) 0px 4px 8px, rgba(0, 0, 0, 0.07) 0px 8px 16px,
rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
}
.logo {
grid-column: 1 / span 4;
grid-row: 1 / 2;
display: flex;
align-items: flex-end;
font-size: 1.3rem;
margin-bottom: 0.8rem;
}
.input--wrapper {
grid-column: 1 / span 4;
grid-row: 2 / 3;
display: flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
margin-bottom: 1.5rem;
background-color: #b8b7bd;
box-shadow: rgba(0, 0, 0, 0.15) 1px 1px 1px 1px inset;
}
.input {
display: flex;
align-items: center;
justify-content: flex-end;
background-color: #878875;
width: 100%;
height: 100%;
padding: 0.2rem 0.8rem;
font-size: 2.6rem;
font-stretch: condensed;
box-shadow: rgba(0, 0, 0, 0.08) 2px 3px 1px 1px inset;
}
.keypads {
display: flex;
justify-content: center;
align-items: center;
background-color: white;
font-size: 1.3rem;
border-radius: 50%;
color: #0a0908;
box-shadow: rgba(0, 0, 0, 0.02) 1px 5px 3px 1px,
rgba(27, 31, 35, 0.15) 0px 2px 1px 1px;
transition: all 0.1s;
cursor: pointer;
}
.keypads:hover {
background-color: #e6e6e6;
}
/* 키 예외 */
.key--clear {
background-color: #d1281b;
color: black;
}
.key--clear:hover {
background-color: #f91504;
}
.key--equals {
background-color: #f2b90c;
color: black;
}
.key--equals:hover {
background-color: #ffd500;
}
.key--function {
color: #477934;
}
.key--function:hover {
background-color: #477934;
color: white;
}
/* 예외 끝 */
CSS는 Grid를 사용하여 레이아웃을 구성했다. flex만 써 보고 grid는 처음 사용해 보았는데, 요소를 원하는 위치에 배열하기가 간편하고, 간격과 크기도 원하는대로 조절할 수 있다는 점이 편리했다.
또 요소의 단위를 px 말고 rem으로 사용해 보았는데, 기준을 두고 상대적인 크기로 값을 정할 수 있어서 편리했다.
JS
- 먼저 코드 작성에 필요한 DOM 요소를 선택해주었다.
// ELEMENTS
const labelInput = document.querySelector('.input');
const keyClear = document.querySelector('.key--clear');
const keyDelete = document.querySelector('.key--delete');
const keyFunctions = document.querySelectorAll('.key--function');
const keyEquals = document.querySelector('.key--equals');
const keypads = document.querySelectorAll('.keypads');
const keyDot = document.querySelector('.key--dot');
- value값과 input 값이 표시될 labelInput과 clear키, delete키, 연산결과키, 소수점키를 각각 따로 가져왔고, 사칙연산키와 숫자키는 각각 keyFunctions, keypads 로 모두 가져와 주었다.
// 계산에 필요한 데이터
let value = 0;
let inputValue = '';
let operatorPre = '';
let operator = '';
- 연산을 수행하는데 필요한 데이터는 위와 같다. 이전 연산 값을 가진 value, 현재 입력된 값인 inputValue, 이전에 눌렀던 연산자 값인 operatorPre, 그리고 현재 입력한 연산자 operator.
이벤트 리스너 등록
- 다음으로 각 키에 이벤트 리스너를 달아, 기능에 맞는 값을 넣어 함수를 호출하도록 코드를 짰다.
// 이벤트 리스너
// 숫자 클릭이벤트 감지
keypads.forEach((key) => {
key.addEventListener('click', (e) => {
let num = e.target.textContent.trim();
setNumber(num);
});
});
// 연산자 클릭이벤트 감지
keyFunctions.forEach((key) =>
key.addEventListener('click', (e) => {
let key = e.target.textContent.trim();
setOperator(key);
})
);
// equals, clear, deleteOne, dot 클릭감지
keyEquals.addEventListener('click', () => {
equals();
});
keyClear.addEventListener('click', () => {
clear();
});
keyDelete.addEventListener('click', () => {
deleteOne();
});
keyDot.addEventListener('click', () => {
addDot();
});
// 키보드 이벤트 감지
window.addEventListener('keydown', function (e) {
let key = e.key;
if (!isNaN(key)) setNumber(key);
else if (key === '/' || key === '*' || key === '-' || key === '+')
setOperator(key);
else if (key === 'Enter') equals();
else if (key === 'Backspace') deleteOne();
else if (key === 'Escape') clear();
else if (key === '.') addDot();
});
- 키보드 이벤트도 해당 키보드의 key 값을 체크해서 조건에 맞는 함수가 실행되도록 하였다.
함수 작성
// 숫자 입력값 생성
const setNumber = function (num) {
if (!isNaN(num) && inputValue.length < 12) {
inputValue += num;
labelInput.textContent = inputValue;
}
};
- setNumber 함수는 keypads를 눌렀을 때 동작하는 함수인데, keypads 클래스가 모든 버튼에 적용되어 있으므로 해당 값이 숫자값인지 체크하고, 입력창에 한계가 있으므로 길이를 제한하여 화면에 출력하였다.
// 연산자 입력값 생성
const setOperator = function (key) {
operatorPre = operator;
operator = key;
if (value && inputValue) {
value = labelInput.textContent = operate(operatorPre);
}
value = value || inputValue;
inputValue = '';
};
- setOperator 함수는 사칙연산 키를 눌렀을 때 동작하는 함수이다.
먼저 이전 연산자 값을 operatorPre 에 저장하고, 새롭게 입력된 key 값을 operator에 재할당한다. 이 값을 가지고 이전에 이미 입력된 value값과 현재 inputValue가 존재할 때 이전 연산자를 가지고 연산하여 해당 결과값을 출력한 뒤 연산을 이어갈 수 있다.
다음 계산을 할 value는 value가 0일 때는 현재 입력된 값으로 할당하고, 이미 존재하면 존재하는 값이 된다.
// 계산 실행 함수
const operate = function (operator) {
value = Number(value);
inputValue = Number(inputValue);
switch (operator) {
case '/':
value /= inputValue;
break;
case '*':
value *= inputValue;
break;
case '-':
value -= inputValue;
break;
case '+':
value += inputValue;
break;
}
// 출력값의 자릿수 제한
if (value.toString().length > 12) {
value = value.toString().slice(0, 13);
}
return value.toLocaleString();
};
- 연산자를 받아 연산을 실행하는 함수이다. 함수 실행시 받아온 연산자로 switch문을 돌려 결과값을 반환한다.
연산 결과의 출력값이 입력 창의 범위를 넘어가면, 넘어가지 않는 범위만 출력하도록 한다.
3자리마다 자리수 구분을 하기 위해 str.toLocaleString(); 함수를 사용했다.
// equals, clear, deleteOne, addDot
const equals = function () {
value = labelInput.textContent = operate(operator);
operator = '';
};
const clear = function () {
value = 0;
inputValue = '';
operator = '';
labelInput.textContent = value;
};
const deleteOne = function () {
inputValue = inputValue.slice(0, -1);
labelInput.textContent = inputValue || 0;
};
const addDot = function () {
inputValue += '.';
labelInput.textContent = inputValue;
};
- 나머지 기능을 하는 함수는 위와 같다.
느낀 점
간단하게 만들 수 있다고 생각해서 코드 구조나 사용 흐름을 잘 고려하지 않고 만들기 시작했다.
만들면서 하나씩 문제점이 생겼는데, 먼저 연산키와 입력 값을 연속적으로 누를 때 새로 바뀐 연산자를 고려하지 않고 이전의 연산을 그대로 반복하는 오류였다. 고민을 하다 이 부분은 이전의 연산자 값을 저장하고, 특정 조건에서 이전의 값을 반영하여 연산하도록 작성해서 해결할 수 있었다.
그리고 키보드 이벤트를 감지할 때 해당 기능을 수행하는 함수를 클릭 이벤트리스너의 콜백 함수로 모조리 작성해버려서 다시 별도의 함수로 분리한 뒤 호출하는 방식으로 변경했다.
이렇게 중간중간 바뀌는 부분이 많아서 조금 더 깔끔하게 작성하지 못한 것 같은데, 다음에는 코드를 짜기 전에 어떤 구조로 만들지, 어떤 기능을 만들지 보다 명확하게 결정하고 만들어야겠다.
'JavaScript > JavaScript' 카테고리의 다른 글
[JavaScript] 문자열 메서드 정리 (0) | 2022.06.27 |
---|---|
[JavaScript] 데이터 타입 (0) | 2022.06.24 |
[JavaScript] arr.sort() 메서드 (배열의 정렬) (0) | 2022.05.19 |
[JavaScript] 원시타입 vs 참조타입 (0) | 2022.04.24 |
[JavaScript] this keyword (0) | 2022.04.18 |