키보드로도 계산해보세요. 🎉
# 계산기 구현에 관련된 이야기
- KeyboardEvent API
- 프레임워크 없이, 라이브러리 없이 작업
- 컴포넌트화도 하기
# 계산기 구현시 필요한 것들! 생각하는 시간을 가져봅시다.
- 필요한 버튼 :
온점(.)
,사칙연산( + - * /)
기호,계산하기(=)
기호,Clear
버튼 - 오류처리 구상
- 결과창 초기값 0.첫입력이 들어올 때, 0을 지우고선 값 표시하기 위한 처리
- 키보드입력을 받을시 숫자키와 필요한 키들만 입력받기
- null 체크
- eval 수행 실패시 에러처리
# KeyboardEvent 에 대하여
이미지상의 가장 상위 인터페이스인 Event 객체는 Object 객체를 상속받으며 Event, UIEvent, KeyboardEvent 모두 생성자 함수입니다. 즉 상위 객체의 기능 또한 모두 포함하고 있습니다. 이벤트 모델의 심도깊은 공부도 곁들이면 좋을 것 같습니다.
keyboardEvent 의 type 은 keydown
, keyup
, keypress
이 있는데 keydown 과 keypress 는 비슷해보이지만 약간의 차이가 있으며 keypress 는 권장사항이 아닙니다. (opens new window)
두 type의 차이는 keydown
type은 control, options, shift, tab, delete, 방향키, 문자, 숫자, 특수 문자키 등 즉 모든 키를 눌렀을 때 발생하는 이벤트타입이며 (keyup
type도 마찬가지)
keypress
type은 문자, 숫자, 특수 문자키를 눌렀을 때만 발생합니다.
// 파라미터로 받은 event 객체를 뒤져보자
document.addEventListener('keydown', (event) => {
event.key === 'Enter' ? 엔터키 누름 : 엔터키 아님
// event.keyCode === 13 ? 엔터키 누름 : 엔터키 아님
// event.altKey ? alt키 누름 : 아님
// event.shiftKey ? shift 누름 : 아님
// event.ctrlKey .. so on
})
# KeyboardEvent 객체에는 어떤 정보가 ?
수많은 정보를 제공하지만 alt, ctrl, shift
키가 같이 눌렸는지 알수 있고
metaKey
속성으로 OS별 ⌘ Command
나 ⊞
키 활성화 여부도 알 수 있습니다.
예전에는 key
속성 말고도 keyCode
속성도 구분자로 사용했지만 이 속성도 이제는 deprecated되어 key 속성을 대신 사용하라고 MDN 에서 권장합니다.
keyCode는 무려 249개나 되고 위 이미지는 일부만 발췌한 건데도 상당합니다.
엔터키는 13, ESC는 27....바로바로 알아먹기 어렵습니다. 직관적인 key 속성 사용 권장에 동감이 되네요.
하지만 레거시 프로젝트들은 여전히 사용 중 일테니 알아두긴 해야 할 것 같습니다.
입력한 키와 keyCode 대응 관계는 여기 (opens new window) 에서 편리하게 확인할 수 있네요.
다시 key로 돌아와서
key의 대응관계는 'Control', 'Enter','Shift','Enter','Ctrl','Enter'
등 다양하지만
자주 사용하는 key값들은 익혀두되 나머지는 공식문서 (opens new window) 를 참고하는 것이 가장 믿음직스러울 것 같습니다.
계산기를 구현하면서 사용해본 key 값들은 몇 개 안됩니다.
엔터키, 뒤로가기, esc 하고 몇개의 숫자키 들
document.addEventListener('keydown', function(keyEvent){
// 계산기 모양과 동일하게 자료 구성
// 엔터, esc, 뒤로가기 버튼들만 대응하고 이외의 key 들은 팅!
let numbers = [
'1', '2', '3', '/',
'4', '5', '6', '-',
'7', '8', '9', '+',
'.', '0', '*', '=',
'Enter', 'Escape', 'Backspace'
]
if (numbers.includes(keyEvent.key)) {
// 다 좋은데, 9항 연산. 실무에선 이렇게 하지 맙시다
keyEvent.key === 'Enter' ? calculate() : // 엔터면 계산 시작
keyEvent.key === '=' ? calculate() : // = 여도 계산 시작
keyEvent.key === 'Escape' ? clearScreen() : // esc 면 다 지워버려
keyEvent.key === 'Backspace' ? removeTail() : // 뒤로가기면 한 자씩 지우자
display(keyEvent.key) // 다 아니라면 입력받은 숫자 표시!
}
})
# 바로 완성본 입니다.
# html, css, js 버전
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>simple calculator</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<table class="calc">
<tr>
<td colspan="3" class="outputtext">
<!--입력이 안되도록 disabled 처리-->
<label for="result"><input type="text" class="display-box" id="result" value="0" disabled></label>
</td>
<td>
<!--js를 속성방식으로 바인딩! 스크립트에서 input[type="button"] 들 데려다가 하나씩 루프돌며 이벤트 핸들러를 부착해도 됨-->
<input type="button" class="button clear_button" value="C" onclick="clearScreen()">
</td>
</tr>
<tr>
<td>
<input type="button" class="button" value="1" onclick="display('1')">
</td>
<td>
<input type="button" class="button" value="2" onclick="display('2')">
</td>
<td>
<input type="button" class="button" value="3" onclick="display('3')">
</td>
<td>
<input type="button" class="button" value="/" onclick="display('/')">
</td>
</tr>
<tr>
<td>
<input type="button" class="button" value="4" onclick="display('4')">
</td>
<td>
<input type="button" class="button" value="5" onclick="display('5')">
</td>
<td>
<input type="button" class="button" value="6" onclick="display('6')">
</td>
<td>
<input type="button" class="button" value="-" onclick="display('-')">
</td>
</tr>
<tr>
<td>
<input type="button" class="button" value="7" onclick="display('7')">
</td>
<td>
<input type="button" class="button" value="8" onclick="display('8')">
</td>
<td>
<input type="button" class="button" value="9" onclick="display('9')">
</td>
<td>
<input type="button" class="button" value="+" onclick="display('+')">
</td>
</tr>
<tr>
<td>
<input type="button" class="button" value="." onclick="display('.')">
</td>
<td>
<input type="button" class="button" value="0" onclick="display('0')">
</td>
<td>
<input type="button" class="button" value="*" onclick="display('*')">
</td>
<td>
<input type="button" class="button calculate_button" value="=" onclick="calculate()">
</td>
</tr>
</table>
<script type="text/javascript" src="script.js"></script>
</body>
</html>
* {
margin: 0;
}
body {
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow-x: hidden;
flex-direction: column;
background-color: #219123;
/* gradient 가 좋다*/
background: linear-gradient(62deg, #b6fb7e 0%, #1f7a06 100%);
}
.calc {
padding: 20px;
border-radius: 1em;
height: 500px;
width: 400px;
margin: auto;
background-color: #ffffff;
box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.637);
}
.display-box {
font-family: 'Orbitron', sans-serif;
background-color: white;
border: solid black 1px;
color: black;
border-radius: 5px;
width: 95%;
height: 55%;
outline: none;
font-size: 22px;
}
.button {
font-family: 'Orbitron', sans-serif;
background-color: black;
color: white;
border: solid black 1px;
width: 80px;
border-radius: 5px;
height: 60%;
outline: none;
font-size: 30px;
cursor: pointer;
}
.button:active {
background: rgba(0, 0, 0, 0.568);
-webkit-box-shadow: inset 0 0 5px #e5e5e5;
-moz-box-shadow: inset 0 0 0 5px #219123;
box-shadow: inset 0 0 5px #ffffff;
}
.clear_button {
background-color: #219123;
}
.calculate_button {
background-color: #219123;
}
// 결과창 내용 지우기
const clearScreen = () => {
document.getElementById("result").value = 0
}
// 결과창 내용 표시하기
const display = (val) => {
// default 로 display 되어있는 0 을 지워주자
if(document.getElementById("result").value === '0'){
document.getElementById("result").value = '';
}
document.getElementById("result").value += val
}
// 계산
const calculate = () => {
let p = document.getElementById("result").value
if (p) {
try{
document.getElementById("result").value = eval(p)
}catch(e){
// eval 수행시 오류 발생 에러 캐치
document.getElementById("result").value = '입력오류';
}
} else {
// null 체크
alert('계산할 수치를 입력해주세요')
}
}
// 키보드 backspace 이벤트 대응하여 한 자씩 지우기
const removeTail = () => {
let temp = document.getElementById("result").value
let poppedArray = temp && [...temp]
poppedArray.pop()
document.getElementById("result").value = poppedArray.join('')
}
// 키보드 이벤트 처리
document.addEventListener('keydown', function(keyEvent){
// 계산기 모양과 동일하게 자료 구성
// 이외의 key 들은 팅
let numbers = [
'1', '2', '3', '/',
'4', '5', '6', '-',
'7', '8', '9', '+',
'.', '0', '*', '=',
'Enter', 'Escape', 'Backspace'
]
if (numbers.includes(keyEvent.key)) {
keyEvent.key === 'Enter' ? calculate() :
keyEvent.key === '=' ? calculate() :
keyEvent.key === 'Escape' ? clearScreen() :
keyEvent.key === 'Backspace' ? removeTail() :
display(keyEvent.key)
}
})
# vue2 컴포넌트
현재 페이지 상단에 구현된 계산기 코드 전체입니다.
모던 프레임워크로 만들면 DX 가 정말 좋아지는 것 같습니다.
노 프레임워크로 제작시 대략 1시간 가량 소요됬다면, vue 로 제작하는데 (js 버전의 산출물을 참고했다곤 해도) 5분도 안 걸렸구요.
<template>
<div class="body">
<table class="calc">
<tr>
<td colspan="3" class="outputtext">
<label for="result"><input type="text" class="display-box" id="result" v-model="result" value="0" disabled></label>
</td>
<td>
<input type="button" class="button clear_button" value="C" @click="clearScreen()">
</td>
</tr>
<tr>
<td>
<input type="button" class="button" value="1" @click="display('1')">
</td>
<td>
<input type="button" class="button" value="2" @click="display('2')">
</td>
<td>
<input type="button" class="button" value="3" @click="display('3')">
</td>
<td>
<input type="button" class="button" value="/" @click="display('/')">
</td>
</tr>
<tr>
<td>
<input type="button" class="button" value="4" @click="display('4')">
</td>
<td>
<input type="button" class="button" value="5" @click="display('5')">
</td>
<td>
<input type="button" class="button" value="6" @click="display('6')">
</td>
<td>
<input type="button" class="button" value="-" @click="display('-')">
</td>
</tr>
<tr>
<td>
<input type="button" class="button" value="7" @click="display('7')">
</td>
<td>
<input type="button" class="button" value="8" @click="display('8')">
</td>
<td>
<input type="button" class="button" value="9" @click="display('9')">
</td>
<td>
<input type="button" class="button" value="+" @click="display('+')">
</td>
</tr>
<tr>
<td>
<input type="button" class="button" value="." @click="display('.')">
</td>
<td>
<input type="button" class="button" value="0" @click="display('0')">
</td>
<td>
<input type="button" class="button" value="*" @click="display('*')">
</td>
<td>
<input type="button" class="button calculate_button" value="=" @click="calculate()">
</td>
</tr>
</table>
</div>
</template>
<script>
export default {
data() {
return {
result: ''
}
},
mounted() {
document.addEventListener('keydown', this.getKeyboardInput)
},
destroyed() {
// 위 addEventListener에 전달한 인수와 인수가 같아야 이벤트 핸들러가 제거된다.
document.removeEventListener('keydown', this.getKeyboardInput)
},
methods: {
clearScreen() {
this.result = 0
},
display(val) {
if(this.result === 0){
this.result = ''
}
this.result += val
},
calculate() {
if (this.result) {
try{
this.result = eval(this.result)
}catch(e){
this.result = '입력오류'
}
} else {
alert('계산할 수치를 입력해주세요')
}
},
removeTail() {
let poppedArray = this.result && [...this.result]
poppedArray.pop()
this.result = poppedArray.join('')
},
getKeyboardInput(keyEvent){
let numbers = [
'1', '2', '3', '/',
'4', '5', '6', '-',
'7', '8', '9', '+',
'.', '0', '*', '=',
'Enter', 'Escape', 'Backspace'
]
if (numbers.includes(keyEvent.key)) {
keyEvent.key === 'Enter' ? this.calculate() :
keyEvent.key === '=' ? this.calculate() :
keyEvent.key === 'Escape' ? this.clearScreen() :
keyEvent.key === 'Backspace' ? this.removeTail() :
this.display(keyEvent.key)
}
}
}
}
</script>
<style scoped>
table {
display: inline-block;
border-collapse: inherit;
}
td {
border: 0
}
/* 해당 블로그에 맞추느라 높이등의 스타일링이 위 버전과 조금 차이가 있음*/
.body {
height: 50vh;
border-radius: 1em;
display: flex;
align-items: center;
justify-content: center;
overflow-x: hidden;
flex-direction: column;
background-color: #219123;
background-image: linear-gradient(62deg, #b6fb7e 0%, #1f7a06 100%);
}
.calc {
padding: 20px;
border-radius: 1em;
width: auto;
margin: auto;
background-color: #ffffff;
box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.637);
}
.display-box {
font-family: 'Orbitron', sans-serif;
background-color: white;
border: solid black 1px;
color: black;
border-radius: 5px;
width: 95%;
height: 55%;
outline: none;
font-size: 22px;
}
.button {
font-family: 'Orbitron', sans-serif;
background-color: black;
color: white;
border: solid black 1px;
width: 80px;
border-radius: 5px;
height: 60%;
outline: none;
font-size: 30px;
cursor: pointer;
}
.button:active {
background: rgba(0, 0, 0, 0.568);
-webkit-box-shadow: inset 0 0 5px #e5e5e5;
-moz-box-shadow: inset 0 0 0 5px #219123;
box-shadow: inset 0 0 5px #ffffff;
}
.clear_button {
background-color: #219123;
}
.calculate_button {
background-color: #219123;
}
</style>