반응형
안녕하세요! 오늘은 React와 shadcn/ui를 사용하여 완전한 기능을 갖춘 공학용 계산기를 만들어보겠습니다. 이 튜토리얼을 통해 React의 상태 관리, 이벤트 처리, 그리고 UI 컴포넌트 구성 방법을 배울 수 있습니다.
프로젝트 설정
먼저 필요한 의존성을 설치합니다:
# shadcn/ui 컴포넌트 설치
npx create-next-app@latest my-calculator --typescript --tailwind --eslint
cd my-calculator
npx shadcn-ui@latest init
컴포넌트 구조
우리의 계산기는 단일 React 컴포넌트로 구현됩니다. 주요 기능은 다음과 같습니다:
- 기본 산술 연산 (덧셈, 뺄셈, 곱셈, 나눗셈)
- 공학 계산 기능 (삼각함수, 로그, 제곱근)
- 상수 값 (π, e)
- 메모리 기능
상태 관리
계산기에는 4개의 주요 상태가 필요합니다:
const [display, setDisplay] = useState('0'); // 현재 화면에 표시되는 값
const [memory, setMemory] = useState(null); // 이전 계산 값 저장
const [waitingForOperand, setWaitingForOperand] = useState(true); // 새로운 숫자 입력 대기 상태
const [pendingOperator, setPendingOperator] = useState(null); // 대기 중인 연산자
계산 로직 구현
1. 숫자 입력 처리
const inputDigit = (digit) => {
if (waitingForOperand) {
setDisplay(String(digit));
setWaitingForOperand(false);
} else {
setDisplay(display === '0' ? String(digit) : display + digit);
}
};
2. 기본 연산 처리
const performOperation = (nextOperator) => {
const nextValue = parseFloat(display);
const operations = {
'+': (prevValue, nextValue) => prevValue + nextValue,
'-': (prevValue, nextValue) => prevValue - nextValue,
'×': (prevValue, nextValue) => prevValue * nextValue,
'÷': (prevValue, nextValue) => prevValue / nextValue,
};
if (memory === null) {
setMemory(nextValue);
} else if (pendingOperator) {
const currentValue = memory || 0;
const computedValue = operations[pendingOperator](currentValue, nextValue);
setMemory(computedValue);
setDisplay(String(computedValue));
}
setWaitingForOperand(true);
setPendingOperator(nextOperator);
};
3. 특수 함수 처리
const performSpecialOperation = (operation) => {
const currentValue = parseFloat(display);
let result;
switch (operation) {
case 'sin':
result = Math.sin(currentValue * Math.PI / 180);
break;
case 'cos':
result = Math.cos(currentValue * Math.PI / 180);
break;
case 'tan':
result = Math.tan(currentValue * Math.PI / 180);
break;
// ... 기타 특수 함수들
}
setDisplay(String(result));
setWaitingForOperand(true);
};
UI 구현
UI는 shadcn/ui의 Card와 Button 컴포넌트를 사용하여 구현합니다. 계산기는 그리드 레이아웃을 사용하여 버튼들을 배치합니다.
return (
<Card className="w-96">
<CardContent className="p-4">
{/* 디스플레이 */}
<div className="mb-4">
<input
type="text"
value={display}
readOnly
className="w-full text-right p-2 text-2xl bg-gray-100 rounded"
/>
</div>
{/* 버튼 그리드 */}
<div className="grid grid-cols-4 gap-2">
{/* 과학 기능 버튼 */}
<Button onClick={() => performSpecialOperation('sin')} variant="outline">sin</Button>
<Button onClick={() => performSpecialOperation('cos')} variant="outline">cos</Button>
{/* ... 기타 버튼들 */}
</div>
</CardContent>
</Card>
);
주요 기능 설명
- 숫자 입력
- 사용자가 숫자 버튼을 클릭하면
inputDigit
함수가 호출됩니다. - 현재 상태가 새로운 숫자 입력을 기다리는 중이라면 디스플레이를 새 숫자로 교체합니다.
- 그렇지 않다면 현재 디스플레이에 새 숫자를 추가합니다.
- 사용자가 숫자 버튼을 클릭하면
- 연산자 처리
- 연산자 버튼을 클릭하면
performOperation
함수가 호출됩니다. - 이전 계산 결과가 있다면 현재 입력된 숫자와 연산을 수행합니다.
- 결과를 메모리에 저장하고 디스플레이를 업데이트합니다.
- 연산자 버튼을 클릭하면
- 특수 함수
- sin, cos, tan 등의 특수 함수는
performSpecialOperation
함수에서 처리됩니다. - 현재 디스플레이 값을 입력으로 사용하여 계산을 수행합니다.
- 각도는 도(degree) 단위로 입력받아 라디안으로 변환하여 계산합니다.
- sin, cos, tan 등의 특수 함수는
사용자 경험 개선
- 시각적 피드백
- 버튼 클릭 시 시각적 피드백을 제공하기 위해 shadcn/ui의 버튼 스타일을 활용
- 연산자 버튼은 다른 색상을 사용하여 구분
- 에러 처리
- 0으로 나누기 등의 에러 상황 처리
- 잘못된 입력에 대한 피드백 제공
확장 가능성
이 계산기는 다음과 같은 방향으로 확장할 수 있습니다:
- 추가 공학 함수 구현 (역삼각함수, 지수함수 등)
- 단위 변환 기능
- 기록 기능 추가
- 테마 지원 (다크 모드/라이트 모드)
전체 소스 코드
전체 소스 코드는 아래와 같습니다:
import React, { useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
const ScientificCalculator = () => {
const [display, setDisplay] = useState('0');
const [memory, setMemory] = useState(null);
const [waitingForOperand, setWaitingForOperand] = useState(true);
const [pendingOperator, setPendingOperator] = useState(null);
const clearAll = () => {
setDisplay('0');
setMemory(null);
setWaitingForOperand(true);
setPendingOperator(null);
};
const inputDigit = (digit) => {
if (waitingForOperand) {
setDisplay(String(digit));
setWaitingForOperand(false);
} else {
setDisplay(display === '0' ? String(digit) : display + digit);
}
};
const inputDecimal = () => {
if (waitingForOperand) {
setDisplay('0.');
setWaitingForOperand(false);
} else if (display.indexOf('.') === -1) {
setDisplay(display + '.');
}
};
const performOperation = (nextOperator) => {
const nextValue = parseFloat(display);
const operations = {
'+': (prevValue, nextValue) => prevValue + nextValue,
'-': (prevValue, nextValue) => prevValue - nextValue,
'×': (prevValue, nextValue) => prevValue * nextValue,
'÷': (prevValue, nextValue) => prevValue / nextValue,
};
if (memory === null) {
setMemory(nextValue);
} else if (pendingOperator) {
const currentValue = memory || 0;
const computedValue = operations[pendingOperator](currentValue, nextValue);
setMemory(computedValue);
setDisplay(String(computedValue));
}
setWaitingForOperand(true);
setPendingOperator(nextOperator);
};
const performSpecialOperation = (operation) => {
const currentValue = parseFloat(display);
let result;
switch (operation) {
case 'sin':
result = Math.sin(currentValue * Math.PI / 180);
break;
case 'cos':
result = Math.cos(currentValue * Math.PI / 180);
break;
case 'tan':
result = Math.tan(currentValue * Math.PI / 180);
break;
case 'sqrt':
result = Math.sqrt(currentValue);
break;
case 'log':
result = Math.log10(currentValue);
break;
case 'ln':
result = Math.log(currentValue);
break;
default:
return;
}
setDisplay(String(result));
setWaitingForOperand(true);
};
return (
<Card className="w-96">
<CardContent className="p-4">
<div className="mb-4">
<input
type="text"
value={display}
readOnly
className="w-full text-right p-2 text-2xl bg-gray-100 rounded"
/>
</div>
<div className="grid grid-cols-4 gap-2">
<Button onClick={() => performSpecialOperation('sin')} variant="outline" className="text-sm">sin</Button>
<Button onClick={() => performSpecialOperation('cos')} variant="outline" className="text-sm">cos</Button>
<Button onClick={() => performSpecialOperation('tan')} variant="outline" className="text-sm">tan</Button>
<Button onClick={() => performSpecialOperation('sqrt')} variant="outline" className="text-sm">√</Button>
<Button onClick={() => performSpecialOperation('log')} variant="outline" className="text-sm">log</Button>
<Button onClick={() => performSpecialOperation('ln')} variant="outline" className="text-sm">ln</Button>
<Button onClick={() => setDisplay(String(Math.PI))} variant="outline" className="text-sm">π</Button>
<Button onClick={() => setDisplay(String(Math.E))} variant="outline" className="text-sm">e</Button>
<Button onClick={() => inputDigit(7)}>7</Button>
<Button onClick={() => inputDigit(8)}>8</Button>
<Button onClick={() => inputDigit(9)}>9</Button>
<Button onClick={() => performOperation('÷')} variant="secondary">÷</Button>
<Button onClick={() => inputDigit(4)}>4</Button>
<Button onClick={() => inputDigit(5)}>5</Button>
<Button onClick={() => inputDigit(6)}>6</Button>
<Button onClick={() => performOperation('×')} variant="secondary">×</Button>
<Button onClick={() => inputDigit(1)}>1</Button>
<Button onClick={() => inputDigit(2)}>2</Button>
<Button onClick={() => inputDigit(3)}>3</Button>
<Button onClick={() => performOperation('-')} variant="secondary">-</Button>
<Button onClick={() => inputDigit(0)}>0</Button>
<Button onClick={inputDecimal}>.</Button>
<Button onClick={() => performOperation('=')} variant="default">=</Button>
<Button onClick={() => performOperation('+')} variant="secondary">+</Button>
<Button onClick={clearAll} variant="destructive" className="col-span-4">Clear</Button>
</div>
</CardContent>
</Card>
);
};
export default ScientificCalculator;
반응형