들어가며
개인 사이드 프로젝트로 재무관리 앱을 만들고 있는데 기능 중 하나로 계산기를 추가하고 싶었다. 언뜻 보기에는 계산기 그까이꺼 사칙연산만 대충 구현하면 되는 거 아닌가?!?! 싶어서 후딱 끝낼 줄 알았는데 주말 이틀을 꼬박 매달리고서야 완성했다.
처음부터 타입스크립트를 적용하려 하니 아직 사용법에 익숙치 않아서 이곳저곳 뻥뻥 터지는 에러들을 잡아내기 바빠 작업이 전혀 진전되지 않았다. 이대로는 답이 없을 것 같아 우선 순수 리액트로 작업을 마친 다음 복붙을 하여 오류가 뜨는 부분만을 잡아나가는 식으로 작업 방식의 가닥을 잡았다.
기본 구상
계산기의 전체적인 스타일링은 styled-components를 활용하였다. styled-components로 스타일을 적용하니 자주 쓰이는 style-set을 변수에 담아 필요할 때마다 활용할 수 있었고 style을 컴포넌트화하여 관리하기 때문에 코드의 재사용성도 뛰어났다.
구현할 기능 및 디자인은 갤럭시폰의 계산기와 윈도우 기본 계산기를 참고하였다. 또한 기능 구현에 필요한 함수들은 막히는 부분이 있으면 유튜브를 검색하여 내 방식대로 재해석해 곳곳에 녹여냈다.
기능 시현
전체 코드
GeneralCalculator.tsx
1. interface 및 state 선언부
import React, { useState } from 'react'; import * as S from './style'; import { MdOutlineTimer } from 'react-icons/md'; import { BsBackspace } from 'react-icons/bs'; import { messages } from '../../../messages'; interface CalHistoriesProps { id: number; prevNum: string | number | undefined; operation: string; midNum: string; totalNum: string | number | undefined; } const GeneralCalculator = () => { const [previousNumber, setPreviousNumber] = useState< string | number | undefined >(''); const [middleNumber, setMiddleNumber] = useState(''); const [inputNumber, setInputNumber] = useState(''); const [totalNumber, setTotalNumber] = useState<string | number | undefined>( '' ); const [operation, setOperation] = useState(''); const [isTotal, setIsTotal] = useState(false); const [notComplete, setNotComplete] = useState(false); const [calHistoryToggle, setCalHistoryToggle] = useState(false); const [calHistories, setCalHistories] = useState<CalHistoriesProps[]>([ { id: 0, prevNum: '', operation: '', midNum: '', totalNum: 0, }, ]); ... }
2. 기능 구현용 함수 선언부const inputValChecker = (val: number | string, inputVal: string) => { if (val.toString().includes('.') && inputVal === '.') return; if (!inputNumber && inputVal === '0') return false; if (val.toString().length > 14) return false; return true; }; const handleInputVal = (e: React.MouseEvent<HTMLElement>) => { const val = e.currentTarget.innerText; if (inputValChecker(inputNumber, val)) { setInputNumber(inputNumber + val); } if (middleNumber && totalNumber) { setPreviousNumber(totalNumber); setMiddleNumber(''); setTotalNumber(''); } setIsTotal(false); }; const generatedCalculation = ( oper: string, prevNum: string | number | undefined, inNum: string ) => { const prevNumber = prevNum ? prevNum : 0; const inNumber = inNum ? inNum : 0; const addition = +prevNumber + +inNumber; const subtraction = +prevNumber - +inNumber; let multiplication; let division; let calculated = 0; if ( (operation !== '×' && oper === '×') || (operation !== '÷' && oper === '÷') ) { multiplication = +prevNumber * 1; division = +prevNumber / 1; } else { multiplication = +prevNumber * +inNumber; division = +prevNumber / +inNumber; } switch (oper) { case '+': calculated = addition; break; case '-': calculated = subtraction; break; case '×': calculated = multiplication; break; case '÷': calculated = division; break; default: return; } handleSetCalHistories(oper, calculated); return calculated; }; const handleSetCalHistories = (_oper: string, _calculated: number) => { if (calHistories.length < 2) { setCalHistories((prevHis) => [ ...prevHis, { id: prevHis[prevHis.length - 1].id + 1, prevNum: previousNumber, operation: _oper, midNum: inputNumber, totalNum: _calculated, }, ]); } else { const arr = calHistories; setCalHistories(arr.slice(1, arr.length)); setCalHistories((prevHis) => [ ...prevHis, { id: prevHis[prevHis.length - 1].id + 1, prevNum: previousNumber, operation: _oper, midNum: inputNumber, totalNum: _calculated, }, ]); } }; const handleOperate = (e: React.MouseEvent<HTMLElement>) => { const oper = e.currentTarget.innerText; setOperation(oper); if (!previousNumber) { setPreviousNumber(inputNumber); } else if (previousNumber && !totalNumber) { setPreviousNumber( generatedCalculation(oper, previousNumber, inputNumber) ); } else if (middleNumber && totalNumber) { setPreviousNumber(totalNumber); } setMiddleNumber(''); setInputNumber(''); setTotalNumber(''); setIsTotal(false); setNotComplete(false); }; const handleEvaluate = () => { if (!inputNumber) { setNotComplete(true); return; } setPreviousNumber(previousNumber); setMiddleNumber(inputNumber); setInputNumber(''); setTotalNumber( generatedCalculation(operation, previousNumber, inputNumber) ); setOperation(operation); setIsTotal(true); setNotComplete(false); }; const handleDelete = () => { setInputNumber(inputNumber.toString().slice(0, -1)); }; const handleReset = () => { setPreviousNumber(''); setMiddleNumber(''); setInputNumber(''); setTotalNumber(''); setOperation(''); setIsTotal(false); setNotComplete(false); setCalHistories([ { id: 0, prevNum: '', operation: '', midNum: '', totalNum: 0, }, ]); }; const handleChangePercent = () => { if (inputNumber) { setInputNumber((+inputNumber * 0.01).toString()); } }; const handleChangeSign = () => { if (inputNumber) { if (+inputNumber > 0) { setInputNumber((+inputNumber * -1).toString()); } else { setInputNumber((+inputNumber * -1).toString()); } } }; const handleCalHistoryToggle = () => { setCalHistoryToggle(!calHistoryToggle); };
3. 컴포넌트 선언부return ( <S.CalBody> <S.CalInputArea> <S.PreviousNumberDiv> {previousNumber && parseFloat(previousNumber.toString()).toLocaleString()} {operation} {middleNumber && `${parseFloat(middleNumber).toLocaleString()} =`} </S.PreviousNumberDiv> <S.CurrentNumberDiv> {inputNumber && parseFloat(inputNumber).toLocaleString()} </S.CurrentNumberDiv> <S.TotalNumberDiv> {isTotal && totalNumber && parseFloat(totalNumber.toString()).toLocaleString()} </S.TotalNumberDiv> {calHistoryToggle && ( <S.CalHistoryDiv> <ul> {calHistories.map((his) => ( <li key={his.id}> {his.prevNum && ( <> <div> <span> {parseFloat(his.prevNum.toString()).toLocaleString()}{' '} </span> <span>{his.operation} </span> <span>{parseFloat(his.midNum).toLocaleString()} </span> <span>= </span> </div> <div> <span> {his.totalNum && parseFloat( his.totalNum.toString() ).toLocaleString()} </span> </div> </> )} </li> ))} </ul> </S.CalHistoryDiv> )} </S.CalInputArea> <S.CalControlButtonArea> <S.CalControlButton onClick={handleCalHistoryToggle}> <MdOutlineTimer /> </S.CalControlButton> <S.CalControlButton onClick={handleDelete}> <BsBackspace /> </S.CalControlButton> </S.CalControlButtonArea> <S.CalButtonArea> <S.CalButton style={S.clearButtonStyle} onClick={handleReset} disabled={calHistoryToggle} > C </S.CalButton> <S.CalButton style={S.operationButtonStyle} onClick={handleChangePercent} disabled={calHistoryToggle} > % </S.CalButton> <S.CalButton style={S.operationButtonStyle} onClick={handleOperate} disabled={calHistoryToggle} > ÷ </S.CalButton> <S.CalButton onClick={handleInputVal} disabled={calHistoryToggle}> 7 </S.CalButton> <S.CalButton onClick={handleInputVal} disabled={calHistoryToggle}> 8 </S.CalButton> <S.CalButton onClick={handleInputVal} disabled={calHistoryToggle}> 9 </S.CalButton> <S.CalButton style={S.operationButtonStyle} onClick={handleOperate} disabled={calHistoryToggle} > × </S.CalButton> <S.CalButton onClick={handleInputVal} disabled={calHistoryToggle}> 4 </S.CalButton> <S.CalButton onClick={handleInputVal} disabled={calHistoryToggle}> 5 </S.CalButton> <S.CalButton onClick={handleInputVal} disabled={calHistoryToggle}> 6 </S.CalButton> <S.CalButton style={S.operationButtonStyle} onClick={handleOperate} disabled={calHistoryToggle} > - </S.CalButton> <S.CalButton onClick={handleInputVal} disabled={calHistoryToggle}> 1 </S.CalButton> <S.CalButton onClick={handleInputVal} disabled={calHistoryToggle}> 2 </S.CalButton> <S.CalButton onClick={handleInputVal} disabled={calHistoryToggle}> 3 </S.CalButton> <S.CalButton style={S.operationButtonStyle} onClick={handleOperate} disabled={calHistoryToggle} > + </S.CalButton> <S.CalButton onClick={handleInputVal} disabled={calHistoryToggle}> 0 </S.CalButton> <S.CalButton onClick={handleInputVal} disabled={calHistoryToggle}> . </S.CalButton> <S.CalButton style={S.operationButtonStyle} onClick={handleChangeSign} disabled={calHistoryToggle} > +/- </S.CalButton> <S.CalButton style={S.operationButtonStyle} onClick={handleEvaluate} disabled={calHistoryToggle} > = </S.CalButton> </S.CalButtonArea> <S.CalMsgDiv> {inputNumber.toString().length > 14 && `${messages.generalCalMsg}`} {notComplete && `${messages.notCompleteMsg}`} </S.CalMsgDiv> </S.CalBody> ); }; export default GeneralCalculator;
style.ts
import styled from 'styled-components'; import * as S from '../../../styles'; /* styled-components */ export const CalBody = styled.div` ${S.alignments.center} flex-direction: column; margin: 50% auto; padding: 25px; width: 80%; border-radius: 20px; background-color: ${S.colors.black}; text-align: center; `; export const CalInputArea = styled.div` box-sizing: border-box; ${S.alignments.flexEnd} justify-content: space-evenly; flex-direction: column; margin-bottom: 15px; padding-right: 20px; width: 100%; height: 100px; border-radius: 10px; background-color: ${S.colors.lightBlack}; color: ${S.colors.white}; `; export const PreviousNumberDiv = styled.div` opacity: 0.7; `; export const CurrentNumberDiv = styled.div` font-size: 24px; `; export const TotalNumberDiv = styled.div` font-size: 24px; color: ${S.colors.beige}; `; export const CalHistoryDiv = styled.div` ${S.alignments.spaceBetween} justify-content: space-around; box-sizing: border-box; position: absolute; left: 10%; z-index: 999; padding: 10px 20px 0 20px; width: 312px; height: 110px; border-radius: 10px; background-color: ${S.colors.lightBlack}; ul { width: 100%; li { margin-right: 3px; opacity: 0.3; list-style: none; text-align: right; margin-bottom: 5px; div:first-child { display: flex; algign-items: center; justify-content: flex-end; } } li:last-child { opacity: 0.7; } } `; export const CalControlButtonArea = styled.div` ${S.alignments.center} justify-content: space-around; padding: 15px 0; width: 100%; border-top: 1px solid ${S.colors.blue}; border-bottom: 1px solid ${S.colors.blue}; background-color: ${S.colors.black}; `; export const CalControlButton = styled.button` border: none; background: none; color: ${S.colors.blue}; font-size: 24px; `; export const CalButtonArea = styled.div` display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: minmax(60px, auto); width: 100%; padding-top: 15px; `; export const CalButton = styled.button` margin: 4px; border: none; border-radius: 10px; background-color: ${S.colors.lightBlack}; color: ${S.colors.blue}; font-size: 20px; `; export const CalMsgDiv = styled.div` margin-top: 15px; color: ${S.colors.red}; `; /* styles */ export const controlButtonStyle = {}; export const clearButtonStyle = { gridColumn: '1/3', gridRow: '1', color: `${S.colors.red}`, }; export const evaluateButtonStyle = { gridColumn: '3/5', gridRow: '5', color: `${S.colors.beige}`, }; export const operationButtonStyle = { color: `${S.colors.beige}`, };
generalCalColors.ts
export const generalCalColors = { black: '#010101', lightBlack: '#171717', white: '#e6e6e6', red: '#cd3e38', beige: '#daaaa1', blue: '#adbae8', };
alignments.ts
export const alignments = { center: 'display: flex; align-items: center; justify-content: center;', flexEnd: 'display: flex; align-items: flex-end; justify-content: flex-end;', spaceBetween: 'display: flex; align-items: center; justify-content: space-between;', };
함께 보기
[My Projects] 계산기에 onKeyDown 이벤트 적용하기!
들어가며 얼마 전에 만든 계산기에 onKeyDown 이벤트를 추가했다. 기능 구현은 생각보다 간단했는데 미처 발견하지 못했던 자잘한 버그들이 발생했고 잡는 데 꽤 애를 먹었다. 리팩토링을 거치긴
gdk01.tistory.com
참고 자료
Build a CALCULATOR APP in REACT JS | A React JS Beginner Tutorial
The Perfect Beginner React Project
Build a Calculator with React JS
ReactJS Calculator App | Simple ReactJs Calculator
'👩💻 Programming > My Projects' 카테고리의 다른 글
[My Projects] 계산기에 onKeyDown 이벤트 적용하기! (0) | 2022.09.07 |
---|
댓글