[React] 리액트로 계산기 만들기

glow_soon·2022년 8월 6일
17

React

목록 보기
50/52
post-thumbnail

CSS를 사용할때 주로 sass를 썼었다, styled-component도 연습하면서 css grid에 대해서도 공부해보고 싶었다.
뭐가 좋을지 고민하다가 계산기 UI가 떠올랐고 내친김에 구현까지 해보았다.

레이아웃

우선 계산기UI 가운데 정렬을 위한 메인 컨테이너 스타일 컴포넌트를 만든다.

const MainContainer = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100vh;
`;

css grid의 사용법은 flex사용법과 유사하다. grid 메인컨테이너를 만들고 그안에 아이템을 넣어놓으면 된다.

const ButtonContainer = styled.div`
  display: grid;
`;
const Button = styled.button``;
const CalButton = styled(Button)``;
const ZeroButton = styled(Button)``;

안에 담길 button 컴포넌트들도 만들어준다.
styled(상속받을 컴포넌트 이름)으로 만들면 해당 컴포넌트의 속성을 그대로 가져온다.
연산자 버튼과, 0버튼은 스타일을 약간 다르게 해야했기에 따로 만들어 주었다.

스타일 컴포넌트의 사용법은 크게 어려운것이 없는거 같고 편리한거같다???? 자주 사용할것 같다

return (
    <MainContainer>
      <ButtonContainer>
        <Button>AC</Button>
        <Button>DEL</Button>
        <CalButton value="%">%</CalButton>
        <CalButton value="÷">÷</CalButton>
        <Button value={7}>7</Button>
        <Button value={8}>8</Button>
        <Button value={9}>9</Button>
        <CalButton value="×">×</CalButton>
        <Button value={4}>4</Button>
        <Button value={5}>5</Button>
        <Button value={6}>6</Button>
        <CalButton value="-">-</CalButton>
        <Button value={1}>1</Button>
        <Button value={2}>2</Button>
        <Button value={3}>3</Button>
        <CalButton value="+">+</CalButton>
        <ZeroButton value={0}>0</ZeroButton>
        <Button value=".">.</Button>
        <CalButton>=</CalButton>
      </ButtonContainer>
    </MainContainer>
  );

코드가 지저분해보인다면 기분탓이다.


아직 grid 속성을 준것이 없기 때문에 작고 깜찍하다.

grid-template-columns를 통해 아이템들의 비율을 정해줄 수 있다.

const ButtonContainer = styled.div`
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr;
`;

fr이란 단위는 숫자 비율대로 아이템들의 크기를 나눈다. 1:1:1:1 비율인 4개의 아이템이 들어있는 column을 만들겠다는 의미이다.

const ButtonContainer = styled.div`
  display: grid;
  grid-template-columns: repeat(4, 1fr);
`;

repeat 함수로 반복되는 값도 처리 가능, 위의 코드와 동일하다.


버튼들이 이쁘게 잘 정렬되었다

const ButtonContainer = styled.div`
  display: grid;
  width: 40%;
  max-width: 450px;
  height: 50%;
  grid-template-columns: repeat(4, 1fr);
`;

const Button = styled.button`
  background-color: #f2f3f5;
  border: none;
  color: black;
  font-size: 1.5rem;
  border-radius: 35px;
  cursor: pointer;
  box-shadow: 3px 3px 3px lightgray;

  &:active {
    margin-left: 2px;
    margin-top: 2px;
    box-shadow: none;
  }
`;

const CalButton = styled(Button)`
  font-size: 2rem;
  color: white;
  background-color: #4b89dc;
`;

const ZeroButton = styled(Button)``;

버튼들을 이쁘게 스타일링해주고, box-shadow와 active속성으로 누르는거 같은 효과도 주었다.
CalButtonButton 컴포넌트의 스타일을 상속받았고, 덮어쓸 속성만 다시 작성해주면 된다.

ZeroButton 컴포넌트는 비율을 다르게 줄것이기에 일단 남겨두었다.

버튼들이 뭔가 붙어있어서 답답한데, grid-column-gapgrid-row-gap 속성을 통해 grid 아이템 사이의 간격을 조정할수 있다.

const ButtonContainer = styled.div`
  display: grid;
  width: 40%;
  max-width: 450px;
  height: 50%;
  grid-template-columns: repeat(4, 1fr);
  grid-column-gap: 10px;
  grid-row-gap: 10px;
`;

0버튼의 영역을 더 크게 지정해줘야했기에 grid-column 속성을 사용했다.

위 그림과 같이 grid-column영역에는 번호가 매겨져있다 (grid-row도 마찬가지)

const ZeroButton = styled(Button)`
  grid-column: 1/3;
`;

1/3의 의미는 해당 컴포넌트의 영역을 1부터 3까지 차지하게 하겠다는것을 의미한다

잘 적용이 되었다

const InputBar = styled.input`
  width: 40%;
  max-width: 450px;
  height: 65px;
  margin-bottom: 10px;
  border-radius: 10px;
  font-size: 30px;
  border: 2px solid #4b89dc;
  text-align: right;
  padding-right: 20px;
  &:focus {
    outline: none;
  }
`;

계산결과를 보여줄 input창도 만들어준다.


내 작고 귀여운 계산기 스타일링이 완료되었다!!!!!!


기능 구현

const [calc, setCalc] = useState("");

계산결과를 담아줄 state이다.

const getNum = (e) => {
    setCalc((prev) => prev + e.target.value);
  };

  const getOper = (e) => {
    setCalc((prev) => prev + e.target.value);
  };

숫자와 연산기호를 담아줄 함수이다. calc state에 계속 붙어나갈것이다

const getPoint = (e) => {
    if (calc.length === 0) {
      return;
    }

    setCalc((prev) => prev + e.target.value);
  };

소숫점을 받을 함수도 만들었다. 맨처음부터 소수점을 받으면 입력이 안되도록 예외처리를 해주었다.

  const getResult = () => {
    
    let replace_str = calc.replace(/×/gi, "*").replace(/÷/gi, "/");

    if (isNaN(eval(replace_str))) {
      setCalc("");
    } else if (eval(replace_str) == Infinity) {
      alert("0으로 나눌수 없습니다.");
      setCalc("");
      return false;
    } else {
      setCalc((prev) => eval(replace_str));
    }
  };

계산결과를 받을 함수도 만든다. 곱셈 기호와 나눗셈 기호는 읽을수 없기 때문에 자바스크립트가 읽을 수 있는 *와 /로 replace함수와 정규표현식을 통해 치환해준다.

eval()은 문자열로 들어온 수식을 자동으로 계산해주는 함수이다.

무한대나 유효하지 않은 식은 예외처리를 해주었다.

return (
    <MainContainer>
      <InputBar readOnly value={calc} />
      <ButtonContainer>
        <Button>AC</Button>
        <Button>DEL</Button>
        <CalButton value="%" onClick={getOper}>
          %
        </CalButton>
        <CalButton value="÷" onClick={getOper}>
          ÷
        </CalButton>
        <Button value={7} onClick={getNum}>
          7
        </Button>
        <Button value={8} onClick={getNum}>
          8
        </Button>
        <Button value={9} onClick={getNum}>
          9
        </Button>
        <CalButton value="×" onClick={getOper}>
          ×
        </CalButton>
        <Button value={4} onClick={getNum}>
          4
        </Button>
        <Button value={5} onClick={getNum}>
          5
        </Button>
        <Button value={6} onClick={getNum}>
          6
        </Button>
        <CalButton value="-" onClick={getOper}>
          -
        </CalButton>
        <Button value={1} onClick={getNum}>
          1
        </Button>
        <Button value={2} onClick={getNum}>
          2
        </Button>
        <Button value={3} onClick={getNum}>
          3
        </Button>
        <CalButton value="+" onClick={getOper}>
          +
        </CalButton>
        <ZeroButton value={0} onClick={getNum}>
          0
        </ZeroButton>
        <Button value="." onClick={getPoint}>
          .
        </Button>
        <CalButton onClick={getResult}>=</CalButton>
      </ButtonContainer>
    </MainContainer>
  );

각 함수들을 적당한 곳에 onClick에 넣어준다. 코드가 지저분하다면 기분탓이다ㅎㅎ



소수같은 경우도 계산이 잘된다.

하지만 이런경우를 그냥 넘어갈수 없다 간단하게 예외처리를 해준다

const [operCheck, setOperCheck] = useState(true);
const [pointCheck, setPointCheck] = useState(true);

연산기호와 소숫점 관리를 위한 state를 선언한다. 일종의 자물쇠라고 생각하면 된다. true값이므로 자물쇠가 풀려있는 상태!!!

const getNum = (e) => {
    setCalc((prev) => prev + e.target.value);
    setOperCheck(true);
  };

  const getOper = (e) => {
    if (operCheck) {
      setCalc((prev) => prev + e.target.value);
      setOperCheck(false);
      setPointCheck(true); // 소숫점 다시 사용위해
    }
  };

  const getPoint = (e) => {
    if (calc.length === 0) {
      return;
    }
    if (pointCheck) {
      setCalc((prev) => prev + e.target.value);
      setPointCheck(false);
    }
  };

연산자 기호나, 소숫점이 입력으로 들어왔을때 state를 false로 바꿔버린다. if문의 조건에 의해 두번 눌렀을때 입력이 되지 않을것이다.
일단 위와 같은 경우는 잘 막아진다.

이제 AC버튼과 DEL버튼 구현만 하면 된다 (매우쉬움)

 const delCalc = () => {
    setPointCheck(true);
    setOperCheck(true);
    let str = String(calc).slice(0, -1);
    setCalc((prev) => str);
  };

  const allClear = () => {
    setPointCheck(true);
    setCalc((prev) => "");
  };

DEL버튼은 slice함수를 이용해 문자열 맨뒤 요소만 지운다.
AC버튼은 그냥 state 빈 문자열로 바까주기

전체 코드

import React from "react";
import { useState } from "react";
import styled from "styled-components";

function Calculator() {
  const [calc, setCalc] = useState("");
  const [operCheck, setOperCheck] = useState(true);
  const [pointCheck, setPointCheck] = useState(true);

  const getNum = (e) => {
    setCalc((prev) => prev + e.target.value);
    setOperCheck(true);
  };

  const getOper = (e) => {
    if (operCheck) {
      setCalc((prev) => prev + e.target.value);
      setOperCheck(false);
    }
  };

  const getPoint = (e) => {
    if (calc.length === 0) {
      return;
    }
    if (pointCheck) {
      setCalc((prev) => prev + e.target.value);
      setPointCheck(false);
    }
  };

  const getResult = () => {
    let replace_str = calc.replace(/×/gi, "*").replace(/÷/gi, "/");

    if (isNaN(eval(replace_str))) {
      setCalc("");
    } else if (eval(replace_str) == Infinity) {
      alert("0으로 나눌수 없습니다.");
      setCalc("");
      return false;
    } else {
      setCalc((prev) => eval(replace_str));
    }
  };

  const delCalc = () => {
    setPointCheck(true);
    setOperCheck(true);
    let str = String(calc).slice(0, -1);
    setCalc((prev) => str);
  };

  const allClear = () => {
    setPointCheck(true);
    setCalc((prev) => "");
  };

  return (
    <MainContainer>
      <InputBar readOnly value={calc} />
      <ButtonContainer>
        <Button onClick={allClear}>AC</Button>
        <Button onClick={delCalc}>DEL</Button>
        <CalButton value="%" onClick={getOper}>
          %
        </CalButton>
        <CalButton value="÷" onClick={getOper}>
          ÷
        </CalButton>
        <Button value={7} onClick={getNum}>
          7
        </Button>
        <Button value={8} onClick={getNum}>
          8
        </Button>
        <Button value={9} onClick={getNum}>
          9
        </Button>
        <CalButton value="×" onClick={getOper}>
          ×
        </CalButton>
        <Button value={4} onClick={getNum}>
          4
        </Button>
        <Button value={5} onClick={getNum}>
          5
        </Button>
        <Button value={6} onClick={getNum}>
          6
        </Button>
        <CalButton value="-" onClick={getOper}>
          -
        </CalButton>
        <Button value={1} onClick={getNum}>
          1
        </Button>
        <Button value={2} onClick={getNum}>
          2
        </Button>
        <Button value={3} onClick={getNum}>
          3
        </Button>
        <CalButton value="+" onClick={getOper}>
          +
        </CalButton>
        <ZeroButton value={0} onClick={getNum}>
          0
        </ZeroButton>
        <Button value="." onClick={getPoint}>
          .
        </Button>
        <CalButton onClick={getResult}>=</CalButton>
      </ButtonContainer>
    </MainContainer>
  );
}

const MainContainer = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100vh;
`;
const ButtonContainer = styled.div`
  display: grid;
  width: 40%;
  max-width: 450px;
  height: 50%;
  grid-template-columns: repeat(4, 1fr);
  grid-column-gap: 10px;
  grid-row-gap: 10px;
`;

const Button = styled.button`
  background-color: #f2f3f5;
  border: none;
  color: black;
  font-size: 1.5rem;
  border-radius: 35px;
  cursor: pointer;
  box-shadow: 3px 3px 3px lightgray;

  &:active {
    margin-left: 2px;
    margin-top: 2px;
    box-shadow: none;
  }
`;

const CalButton = styled(Button)`
  font-size: 2rem;
  color: white;
  background-color: #4b89dc;
`;

const ZeroButton = styled(Button)`
  grid-column: 1/3;
`;

const InputBar = styled.input`
  width: 40%;
  max-width: 450px;
  height: 65px;
  margin-bottom: 10px;
  border-radius: 10px;
  font-size: 30px;
  border: 2px solid #4b89dc;
  text-align: right;
  padding-right: 20px;
  &:focus {
    outline: none;
  }
`;
export default Calculator;

기본적인 기능은 잘 작동이 할것이다 (아마도?????????)

profile
나는야 코린이

0개의 댓글