본문 바로가기
👩‍💻 Programming/My Projects

[My Projects] 리액트로 계산기 만들기! feat. TypeScript

by codingBear 2022. 9. 5.
728x90
반응형

들어가며

 개인 사이드 프로젝트로 재무관리 앱을 만들고 있는데 기능 중 하나로 계산기를 추가하고 싶었다. 언뜻 보기에는 계산기 그까이꺼 사칙연산만 대충 구현하면 되는 거 아닌가?!?! 싶어서 후딱 끝낼 줄 알았는데 주말 이틀을 꼬박 매달리고서야 완성했다. 

 처음부터 타입스크립트를 적용하려 하니 아직 사용법에 익숙치 않아서 이곳저곳 뻥뻥 터지는 에러들을 잡아내기 바빠 작업이 전혀 진전되지 않았다. 이대로는 답이 없을 것 같아 우선 순수 리액트로 작업을 마친 다음 복붙을 하여 오류가 뜨는 부분만을 잡아나가는 식으로 작업 방식의 가닥을 잡았다. 


기본 구상

  계산기의 전체적인 스타일링은 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;',
};

함께 보기

https://gdk01.tistory.com/158

 

[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

 
728x90
반응형

댓글