React/타입스크립트로 배우는 리액트(React.js) : 기초부터 최신 기술까지

섹션 9. (프로젝트) 계산기 만들기

favor 2026. 5. 2. 03:58
// App.tsx

import Calculator from "./components/Calculator";

export default function App () {
  return (
    <>
      <Calculator />
    </>
  );
}

 

 

// src/components/Calculator.tsx

import { useState } from "react";
import CalculatorButton from "./CalculatorButton";

const performCalculation = (
  prev: number,
  current: number,
  operation: string,
) => {
  switch (operation) {
    case "+":
      return prev + current;
    case "-":
      return prev - current;
    case "*":
      return prev * current;
    case "/":
      return prev / current;
    default:
      return current;
  }
};

const initialData: CalculatorState = {
  currentNumber: "0", // 현재 입력/표시되는 숫자
  previousNumber: "", // 이전에 입력된 숫자
  operation: null, // 현재 선택된 연산자 ("+", "-", "/", "*")
  isNewNumber: true, // 새로운 숫자 입력 여부
}

export default function Calculator() {
  const [calculatorState, setCalculatorState] = useState<CalculatorState>(initialData);

  // "C" 클릭됐을 때 실행되는 함수 (초기화 함수)
  const handleClear = () => {
    setCalculatorState(initialData);
  };

  // "+", "-", "*", "/", "-" 클릭됐을 때 실행되는 함수
  const handleOperator = (
    e: React.MouseEvent<HTMLInputElement, MouseEvent>,
  ) => {
    const operator = e.currentTarget.value;

    setCalculatorState((calculatorState) => {

      if (calculatorState.currentNumber === "" && operator) { // currentNumber(새로 입력된 숫자)가 없지만 연산자 기호를 누른 경우
        return calculatorState;
      } 

      const current = parseFloat(calculatorState.currentNumber); // 현재 입력한 값(문자열)을 숫자로 변환시켜 대입

      if (calculatorState.previousNumber && calculatorState.operation) { // 이전의 값이 있고, operation을 입력받았다면
        const prev = parseFloat(calculatorState.previousNumber);
        const result = performCalculation( // 연산 결과 대입 
          prev,
          current,
          calculatorState.operation,
        );
        return operator === "=" ? { // 등호 기호를 누른 경우
          currentNumber: result.toString(),
          previousNumber: "",
          operation: null,
          isNewNumber: true
        } : { // 등호 기호가 아닌 다른 기호를 누른 경우
          currentNumber: "",
          previousNumber: result.toString(),
          operation: operator,
          isNewNumber: true
        };
      } else if (operator === "=") { // 이전의 값이 없지만, operation이 "="이라면
        return { ...calculatorState, isNewNumber: true };
      } else {
        return {
          currentNumber: "",
          previousNumber: current.toString(), // 사용자가 연산자를 누르기 전에 입력한 값을 문자열로 변환시켜 대입
          operation: operator,
          isNewNumber: true,
        };
      }
    });
  };

  // 0~9 클릭됐을 때 실행되는 함수
  const handleNum = (e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
    console.log(e.currentTarget.value);

    const value = e.currentTarget.value; // 문자열
    setCalculatorState((calculatorState) => ({
      ...calculatorState,
      currentNumber: calculatorState.isNewNumber
        ? value // 새로운 숫자라면 그대로
        : calculatorState.currentNumber + value, // 새로운 숫자가 아니라면 이전에 것과 더함 (문자열끼리 연결)

      isNewNumber: false,
    }));
  };

  // . 클릭됐을 때 실행되는 함수
  const handleDot = () => {
    setCalculatorState((calculatorState) => {
      if (calculatorState.currentNumber.includes(".")) { // 현재 입력 값에 소수점이 포함되어 있는 경우
        return calculatorState;
      }

      // 그렇지 않다면
      return {
        ...calculatorState,
        currentNumber: calculatorState.currentNumber + ".",
        isNewNumber: false
      }
    })
  };

  const buttonConfigs: ButtonConfigs[] = [
    // 객체 형태이기 때문에 중괄호
    { value: "C", className: "calc-clear", onClick: handleClear },
    { value: "/", className: "calc-operator", onClick: handleOperator },
    { value: "1", className: "calc-num", onClick: handleNum },
    { value: "2", className: "calc-num", onClick: handleNum },
    { value: "3", className: "calc-num", onClick: handleNum },
    { value: "*", className: "calc-operator", onClick: handleOperator },
    { value: "4", className: "calc-num", onClick: handleNum },
    { value: "5", className: "calc-num", onClick: handleNum },
    { value: "6", className: "calc-num", onClick: handleNum },
    { value: "+", className: "calc-operator", onClick: handleOperator },
    { value: "7", className: "calc-num", onClick: handleNum },
    { value: "8", className: "calc-num", onClick: handleNum },
    { value: "9", className: "calc-num", onClick: handleNum },
    { value: "-", className: "calc-operator", onClick: handleOperator },
    { value: ".", className: "calc-dot", onClick: handleDot },
    { value: "0", className: "calc-num", onClick: handleNum },
    { value: "=", className: "calc-result", onClick: handleOperator },
  ];

  return (
    <>
      <div className="bg-[#1f1f1f] flex justify-center items-center h-screen">
        <article className="w-70.5 border border-[#333] bg-[#ccc] p-1">
          <form className=" grid grid-cols-[repeat(4, 65px)] auto-rows-[65px] gap-1">
            <input
              type="text"
              className="calc-input"
              readOnly
              value={calculatorState.currentNumber}
            />
            {buttonConfigs.map((button) => (
              <CalculatorButton key={button.value} {...button} />
            ))}
          </form>
        </article>
      </div>
    </>
  );
}

 

// src/components/CalculatorButton.tsx

export default function CalculatorButton({
  value,
  className,
  onClick,
}: ButtonConfigs) {
  return (
    <>
      <input type="button" className={className} value={value} onClick={onClick} />
    </>
  );
}

 

 

/* src/styles/index.css */

@import "./tailwind.css";
/* src/styles/tailwind.css */

@import "tailwindcss";

@utility calc-btn {
  @apply border-2 border-[#333] cursor-pointer text-lg hover:shadow-[1px_1px_2px_#333]
}

@layer components {
  .calc-input {
    @apply bg-white col-span-4 text-right calc-btn px-2
  }

  .calc-clear {
    @apply bg-[red] col-span-3 calc-btn
  }

  .calc-operator {
    @apply bg-[orange] calc-btn
  }

  .calc-num {
    @apply bg-white calc-btn
  }

  .calc-dot {
    @apply bg-[green] calc-btn
  }

  .calc-result {
    @apply col-span-2 calc-btn
  }
}

 

// src/types/props.d.ts 

type ButtonConfigs = {
  value: string;
  className: string;
  onClick: (e: React.MouseEvent<HTMLInputElement, MouseEvent>) => void;
};

type CalculatorState = {
  currentNumber: string;
  previousNumber: string;
  operation: null|string;
  isNewNumber: boolean;
};

 

 

 


화살표 함수에서의 객체 반환 문법

자바스크립트에서 중괄호의 용도 

1. 코드 블록

2. 객체 리터럴 

 

자바스크립트는 화살표 함수 바로 뒤에 나오는 중괄호를 객체가 아닌 함수의 실행 블록으로 해석 

--> 객체로 해석하게 하기 위해서는 중괄호를 소괄호로 감싸서 값임을 명시해야 함 

 

컴포넌트 외부 정의와 내부 정의 

  컴포넌트 외부 컴포넌트 내부
특징 컴포넌트가 리렌더링되어도 단 한 번만 생성 리렌더링될 때마다 매번 새로 생성
활용 데이터 컴포넌트의 state나 props를 참조하지 않는 경우 컴포넌트의 state나 props를 직접 참조하는 경우
주요 대상 변하지 않는 상수, 범용 유틸리티 함수, 정적 데이터 이벤트 핸들러, 상태 업데이트 로직, 파생 변수