Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2단계 - 계산기] 호프(김문희) 미션 제출합니다. #46

Merged
merged 10 commits into from
Apr 26, 2022
14 changes: 0 additions & 14 deletions README.old.md

This file was deleted.

237 changes: 89 additions & 148 deletions src/components/Calculator.js
Original file line number Diff line number Diff line change
@@ -1,177 +1,118 @@
import { Component } from 'react';
import {
useLayoutEffect,
useEffect,
useState,
useRef,
useCallback,
} from 'react';
import '../styles/Calculator.css';
import { computeExpression, hasInput, computeNextOperand } from '../utils';
import Expression from './Expression';
import Digits from './Digits';
import Modifier from './Modifier';
import Operations from './Operations';

const computeExpression = ({ firstOperand, secondOperand, operation }) => {
if (operation === '/') {
return Math.floor(firstOperand / secondOperand);
}
if (operation === 'X') {
return firstOperand * secondOperand;
}
if (operation === '-') {
return firstOperand - secondOperand;
}
if (operation === '+') {
return firstOperand + secondOperand;
}
};

const hasInput = ({ firstOperand, secondOperand, operation }) =>
firstOperand !== '0' || secondOperand !== '' || operation !== null;
const Calculator = () => {
const [state, setState] = useState({
firstOperand: 0,
secondOperand: -1,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

초기값이 -1인 이유는 뭔가요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 뒷자리 숫자가 '없다'라는 것을 표현해주기 위해서 일단 -1로 초기화해주었는데요, 0 으로 바꾸는게 더 맞다고 판단됩니다. 지적해주셔서 감사합니다.

operation: null,
isError: false,
});
Copy link

@roy-jung roy-jung Apr 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하나의 state에 모든 정보를 객체형태로 넣는 것은 안티패턴입니다.
이상태로는 어떤 값 하나만 변경되더라도 변경되지 않은 다른 모든 하위컴포넌트가 다시 렌더될거에요.
변경된 부분만 렌더링하자! 라는 리액트의 컨셉에 정면으로 배치되는 셈이죠.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 일단 계산기 내에서 각각 상태값이 거의 함께 변경되어서 객체형태로 수정해주었는데요,
로이님 말씀을 듣고보니까 불필요한 하위 컴포넌트의 리렌더링을 일으킬 수 있겠군요! 지적해주셔서 감사합니다 :)


const computeNextOperand = (currentOperand, digit) => {
return currentOperand.length >= 3
? `${currentOperand.slice(0, -1)}${digit}`
: `${Number(currentOperand + digit)}`;
};
const ref = useRef(null);

class Calculator extends Component {
constructor() {
super();
useLayoutEffect(() => {
const memoizedState = JSON.parse(localStorage.getItem('prevState'));
this.state = memoizedState
? memoizedState
: {
firstOperand: '0',
secondOperand: '',
operation: null,
isError: false,
};
}
if (!memoizedState) return;
setState(memoizedState);
}, []);

componentDidMount() {
window.addEventListener('beforeunload', this.onBeforeUnload);
}
useEffect(() => {
ref.current = state;
}, [state]);

componentWillUnmount() {
window.removeEventListener('beforeunload', this.onBeforeUnload);
localStorage.setItem('prevState', JSON.stringify(this.state));
}
useEffect(() => {
window.addEventListener('beforeunload', handleWindowClose);
return () => window.removeEventListener('beforeunload', handleWindowClose);
}, []);

initState = () => {
this.setState({
firstOperand: '0',
secondOperand: '',
operation: null,
isError: false,
});
const handleWindowClose = (e) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 함수를 어떤 이유에서 이렇게 작성하셨는지는 이해하겠습니다. 다만 함수의 쓰임새가 useEffect 안에서만 이뤄지고 있으니, useEffect 안으로 옮기시는 편이 더 나을 것 같아요. 현재 상태로는 Calculator가 render될 때마다 새롭게 정의되는 거거든요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! 일단 함수를 다 분리한다는 목적으로 했는데, 렌더될때마다 함수가 새로 생성되어서 성능상 좋지 못하다는 것은 간과했네요 😭 지적해주셔서 감사합니다.

e.preventDefault();
if (!hasInput(ref.current)) return;
localStorage.setItem('prevState', JSON.stringify(ref.current));
e.returnValue = '';
};

onClickDigit = ({ target }) => {
const { textContent: digit } = target;
this.state.operation
? this.setState(({ secondOperand }) => ({
secondOperand: computeNextOperand(secondOperand, digit),
isError: false,
const onClickDigit = (digit) => () => {
state.operation
? setState((prevState) => ({
...prevState,
secondOperand: computeNextOperand(
prevState.secondOperand === -1 ? 0 : prevState.secondOperand,
digit,
),
}))
: this.setState(({ firstOperand }) => ({
firstOperand: computeNextOperand(firstOperand, digit),
isError: false,
: setState((prevState) => ({
...prevState,
firstOperand: computeNextOperand(prevState.firstOperand, digit),
}));
};

onClickOperation = ({ target }) => {
const { textContent: operation } = target;
const onClickOperation = (operation) => () => {
if (operation !== '=') {
this.setState({ operation });
setState((prevState) => ({
...prevState,
operation,
}));
return;
}

const result = computeExpression({
firstOperand: Number(this.state.firstOperand),
secondOperand: Number(this.state.secondOperand),
operation: this.state.operation,
firstOperand: Number(state.firstOperand),
secondOperand: Number(state.secondOperand),
operation: state.operation,
});

if (isFinite(result)) {
this.setState({
firstOperand: `${result}`,
secondOperand: '',
setState((prevState) => ({
...prevState,
firstOperand: result,
secondOperand: -1,
operation: null,
});
}));
return;
}

this.setState(() => ({
isError: true,
setState((prevState) => ({
...prevState,
isError: false,
}));
};

onBeforeUnload = (e) => {
e.preventDefault();
localStorage.setItem('prevState', JSON.stringify(this.state));
if (hasInput(this.state)) {
e.returnValue = '';
}
};
const onClickModifier = useCallback(() => {
setState({
firstOperand: 0,
secondOperand: -1,
operation: null,
isError: false,
});
}, []);

return (
<>
<div>숫자는 3자리 까지만 입력이 가능합니다.</div>
<div className="calculator">
<Expression
isError={state.isError}
firstOperand={state.firstOperand}
operation={state.operation}
secondOperand={state.secondOperand}
/>
<Digits onClick={onClickDigit} />
<Modifier onClick={onClickModifier} />
<Operations onClick={onClickOperation} />
</div>
</>
);
};

render() {
return (
<>
<div>숫자는 3자리 까지만 입력이 가능합니다.</div>
<div className="calculator">
<h1 id="total">
{this.state.isError
? '오류'
: `${this.state.firstOperand}
${this.state.operation ?? ''}
${this.state.secondOperand}`}
</h1>
<div className="digits flex">
<button className="digit" onClick={this.onClickDigit}>
9
</button>
<button className="digit" onClick={this.onClickDigit}>
8
</button>
<button className="digit" onClick={this.onClickDigit}>
7
</button>
<button className="digit" onClick={this.onClickDigit}>
6
</button>
<button className="digit" onClick={this.onClickDigit}>
5
</button>
<button className="digit" onClick={this.onClickDigit}>
4
</button>
<button className="digit" onClick={this.onClickDigit}>
3
</button>
<button className="digit" onClick={this.onClickDigit}>
2
</button>
<button className="digit" onClick={this.onClickDigit}>
1
</button>
<button className="digit" onClick={this.onClickDigit}>
0
</button>
</div>
<div className="modifiers subgrid" onClick={this.initState}>
<button className="modifier">AC</button>
</div>
<div className="operations subgrid">
<button className="operation" onClick={this.onClickOperation}>
/
</button>
<button className="operation" onClick={this.onClickOperation}>
X
</button>
<button className="operation" onClick={this.onClickOperation}>
-
</button>
<button className="operation" onClick={this.onClickOperation}>
+
</button>
<button className="operation" onClick={this.onClickOperation}>
=
</button>
</div>
</div>
</>
);
}
}
export default Calculator;
20 changes: 20 additions & 0 deletions src/components/Digits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import PropTypes from 'prop-types';
const DIGITS = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0];

const Digits = ({ onClick }) => {
return (
<div className="digits flex">
{DIGITS.map((digit) => (
<button className="digit" onClick={onClick(digit)} key={digit}>
{digit}
</button>
))}
</div>
);
};

Digits.propTypes = {
onClick: PropTypes.func,
};

export default Digits;
21 changes: 21 additions & 0 deletions src/components/Expression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import PropTypes from 'prop-types';

const Expression = ({ isError, firstOperand, operation, secondOperand }) => {
return (
<h1 id="total">
{isError
? '오류'
: `${firstOperand}
${operation ?? ''}
${secondOperand < 0 ? '' : secondOperand}`}
</h1>
);
};

Expression.propTypes = {
isError: PropTypes.bool,
firstOperand: PropTypes.number,
secondOperand: PropTypes.number,
operation: PropTypes.string,
};
export default Expression;
18 changes: 18 additions & 0 deletions src/components/Modifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import PropTypes from 'prop-types';
import { memo } from 'react';

const Modifier = ({ onClick }) => {
return (
<div className="modifiers subgrid">
<button className="modifier" onClick={onClick}>
AC
</button>
</div>
);
};

Modifier.propTypes = {
onClick: PropTypes.func,
};

export default memo(Modifier);
24 changes: 24 additions & 0 deletions src/components/Operations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import PropTypes from 'prop-types';
const OPERATIONS = ['/', 'X', '-', '+', '='];

const Operations = ({ onClick }) => {
return (
<div className="operations subgrid">
{OPERATIONS.map((operation) => (
<button
className="operation"
onClick={onClick(operation)}
key={operation}
>
{operation}
</button>
))}
</div>
);
};

Operations.propTypes = {
onClick: PropTypes.func,
};

export default Operations;
27 changes: 27 additions & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const calculateByOperation = {
'/': (firstOperand, secondOperand) =>
Math.floor(firstOperand / secondOperand),
X: (firstOperand, secondOperand) => firstOperand * secondOperand,
'-': (firstOperand, secondOperand) => firstOperand - secondOperand,
'+': (firstOperand, secondOperand) => firstOperand + secondOperand,
};

export const computeExpression = ({
firstOperand,
secondOperand,
operation,
}) => {
const calculator = calculateByOperation[operation];
if (!calculator) throw new Error('잘못된 연산자를 입력하였습니다.');
return calculator(firstOperand, secondOperand);
};

export const hasInput = ({ firstOperand, secondOperand, operation }) =>
firstOperand !== 0 || secondOperand !== -1 || operation !== null;

export const computeNextOperand = (currentOperand, digit) => {
currentOperand = String(currentOperand);
return currentOperand.length >= 3
? Number(`${currentOperand.slice(0, -1)}${digit}`)
: Number(currentOperand + digit);
};