
이렇게 자동으로 1~45 사이의 숫자 중 7개를 랜덤으로 뽑아주는 로또 추첨기 를 만들어 볼 예정이다!
여기서, 변하는 부분을 생각해보면
1. 당첨 숫자 6개
2. 보너스 숫자 1개
3. 한 번 더! 버튼 등장
이정도로 생각할 수 있다
여기서, 당첨 숫자 총 7개를 뽑는 것은 함수로 밖으로 빼서 숫자 7개를 먼저 뽑은 뒤, 화면에는 뽑아둔 숫자를 출력하는 형태로 코드를 작성할 예정이다.
먼저 state 부분을 살펴보면 state 부분은 화면에서 변화될 부분을 적는 것인데,
그럼 먼저, getWinNumbers() 함수에 대해서 알아보자 
const numbers = [...Array(45).keys()].map(x => x + 1); :
이제 화면에 렌더링 되는 부분을 찾아보자

여기서 중요한 부분만 살펴보자
winBalls.map((v) => ...) 는 배열의 각 요소를 순회하면서 <Ball> 컴포넌트를 생성한다.
<Ball key={v} number={v} />는 숫자를 number라는 props로 받는 <Ball> 컴포넌트를 생성한다.key={v}는 각 요소에 고유의 키를 부여하여 리액트가 효율적으로 리스트를 업데이트할 수 있도록 한다. {bonus && <Ball number={bonus} />}
<Ball> 컴포넌트를 렌더링한다.<Ball number={bonus} />는 bonus라는 숫자를 number라는 props로 받는 <Ball> 컴포넌트를 생성한다. {redo && <button onClick={this.onClickRedo}>한 번 더!</button>}
<button onClick={this.onClickRedo}>한 번 더!</button>은 "한 번 더!"라는 텍스트가 표시된 버튼을 생성한다.그럼 이제 무엇을 만들어야 할까?
먼저, 화면이 로딩이 된 후, setTimeout()메서드를 이용해 숫자가 하나씩 1초 간격으로 뜰 수 있도록 제공을 해주어야 한다.
--> componentDidMount()메서드를 이용하자!
(화면이 로딩이 된 후, 비동기 적으로 처리를 하기 위해서)

먼저 componentDidMount()부분을 살펴보자
winNumbers에는 이전에 getNumbers()로 미리 숫자를 받아서 저장을 해두었다.
여기서 할 부분은 그 숫자들을 setTimeout을 이용해서 1초씩 더해서 볼을 화면에 출력하는 것이다.
for문을 사용해서 winNumbers.length - 1만큼 순회한다 (-1은 마지막 하나는 bonus 번호이기 때문)
순회하면서, winBalls 배열에 숫자를 담아둔다.
저장된 이전 값들은 prevState를 사용해서 담아야 하는건 이제 알 고 있지?!
다 담은 뒤, 보너스 번호는 winNumbers[6] 에 담겨 있기 때문에, 바로 담아두고, redo: true로 바꿔둔다.
--> 보너스 번호가 나올 때, 한 번 더 버튼이 등장! 하도록 하기 위해서이다.
그리고 이 둘의 setTimeout()을 timeOut 배열에 담아두었다.
이제 componentWillUnmount()을 살펴보자.
timeOut 배열을 for문으로 순회하면서. clearTimeout()메서드를 사용한다.
()괄호 안에는 this.timeOut[i]를 작성해 둔다.
for문 이외에도 forEach문을 사용할 수 있다
componentWillUnmount() {
this.timeOut.forEach(i => {
claerTimeout(i)
})
}
이렇게 작성하는거 더 간단해 보이기도 한다!
이제 마지막, onClickRedo() 함수를 작성해보자

redo 버튼을 누르면 한 번 더 ! 를 실행하기 떄문에 모든 내용을 초기화를 시켜주면 된다 . 간단하지 ?
가장 중요한 부분을 까먹고 있었다 --> <Ball/>를 작성하러 가보아야 한다!
어? 모든 기능을 다 작성해둔 것 같은데 뭘 더 작성해야 하나? 싶지만,
<Ball/> 에는 숫자별로 색깔을 구분해주는 기능을 추가할 것이다!

PureComponent의 이점
PureComponent는 shouldComponentUpdate를 자동으로 구현하여, props와 state가 변경되지 않은 경우 리렌더링을 방지한다. 이는 특히 많은 하위 컴포넌트가 있는 경우 성능 향상에 도움이 된다.
--> Ball 컴포넌트는 number props가 변경될 때만 리렌더링되며, 불필요한 렌더링을 줄여 성능을 향상시킬 수 있다.
const { number } = this.props; 로
여기서 number = {v}로 부모에서 보낸 props를 받아온다.
이후, background라는 속성을 사용해서 공의 색상을 지정한뒤,
style = {{background}} 속성을 사용해 지정해둔다.

이후 실행을 해보니, 어라? 한 번 더 버튼이 제대로 실행이 되지 않는다!
그 이유는 onClickRedo() 함수를 실행할 때, 초기화를 시켜주었지만, 번호를 다시 보여주는 componentDidMount()에서 정의해둔 것을 재실행을 시켜주지 않아서다!
그렇다면, 이때 componentDidUpdate() 메서드를 사용해보자 !

이 메서드는 이전 props와 state와 현재 props와 state를 비교하여 필요한 동작을 수행할 수 있다. 예를 들어, 이전 상태와 현재 상태를 비교하여 특정 조건에 따라 새로운 데이터를 가져오거나 UI를 업데이트할 수 있다.
componentDidUpdate()는 조건문을 아주~~~ 잘 사용해야 한다.
componentDidUpdate() 가 실행되는 이유가, 현재 한번 더 ! 버튼이 눌렀기 때문이다. 이에 한 번 더! 버튼이 눌리게 되면, winBalls.length === 0 이 된다.
즉, winBalls.length === 0이면, this.runTimeOut()을 실행시킨다.
runTimeOut() 은 뭐냐면?
이전에 componentDidMount()에서 정의한 setTimeout() 메서드다.
중복되는 내용은 함수로 꺼내서 하나에 정의한 후, 이렇게 가져다 쓰는게 코드도 깔끔해지고, 가독성도 좋아진다!
자, 이제 모두 다 제대로 실행이 되는 듯 하다.
그럼, componentDidMount()와, componentDidUpdate() 는 언제 실행되는지 console.log()를 찍어서 확인해보자


componentDidUpdate()는 버튼이 눌리지 않았음에도 호출은 계속 이루어지고 있는 것을 확인할 수 있다.
호출은 계속 일어나지만, 조건에 맞을 때에만 실행된다. 그렇기 때문에 componentDidUpdate()에서 조건을 잘 작성해야 오류나 무한 로딩이 일어나지 않는다. !
import React, {
useEffect,
useRef,
useState,
useMemo,
useCallback,
//useMemo 는 함수의 리턴값을 기억하고,
//useCallback은 함수 자체를 기억하는 것
} from 'react';
import Ball from './ball';
function getWinNumbers() {
console.log('getWinNumbers');
const numbers = [...Array(45).keys()].map((x) => x + 1);
const shuffle = [];
while (numbers.length > 0) {
shuffle.push(
numbers.splice(Math.floor(Math.random() * numbers.length), 1)[0]
);
}
const bonusNumber = shuffle[shuffle.length - 1];
const winNumbers = shuffle.slice(0, 6).sort((a, b) => a - b);
return [...winNumbers, bonusNumber];
}
const Lotto = () => {
const lottoNumbers = useMemo(() => getWinNumbers(), []);
const [winNumbers, setWinNumbers] = useState(lottoNumbers);
const [winBalls, setWinBalls] = useState([]);
const [bonus, setBonus] = useState(null);
const [redo, setRedo] = useState(false);
const timeOuts = useRef([]);
const runTimeOut = () => {
for (let i = 0; i < winNumbers.length - 1; i++) {
timeOuts.current[i] = setTimeout(() => {
setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
}, (i + 1) * 1000);
}
// 보너스 번호를 설정하는 타임아웃은 반복문 밖으로 이동합니다.
timeOuts.current[6] = setTimeout(() => {
setBonus(winNumbers[6]);
setRedo(true);
}, 7000);
};
useEffect(() => {
runTimeOut();
return () => {
timeOuts.current.forEach((v) => clearTimeout(v));
};
}, [timeOuts.current]);
const onClickRedo = useCallback(() => {
console.log(winNumbers);
setWinNumbers(getWinNumbers());
setWinBalls([]);
setBonus(null);
setRedo(false);
timeOuts.current = [];
}, [winBalls]);
return (
<>
<div>당첨 숫자</div>
<div>
{winBalls.map((v) => (
<Ball key={v} number={v} />
))}
</div>
<div>보너스!</div>
{bonus && <Ball number={bonus} />}
{redo && <button onClick={onClickRedo}>한 번 더</button>}
</>
);
};
export default Lotto;
여기서 중요한 부분만 살펴보자

이 부분은 runTimeOut 함수를 정의하고, useEffect 훅을 사용하여 컴포넌트가 마운트되거나 timeOuts.current 값이 변경될 때마다 실행되도록 설정한다.
useEffect 훅:
이렇게 함으로써, 컴포넌트가 마운트되거나 timeOuts.current 값이 변경될 때마다 숫자를 화면에 표시하는 타임아웃을 설정하고, 컴포넌트가 언마운트되기 전에는 모든 타임아웃을 해제하여 메모리 누수를 방지한다.
useEffect()는 컴포넌트가 렌더링된 후 한 번 실행되며, 이후에는 두 번째 매개변수로 전달된 의존성 배열에 따라 실행 여부가 결정된다.
의존성 배열이 빈 배열([])인 경우: 한 번 실행된 이후에는 다시 실행되지 않는다.
의존성 배열에 값이 있는 경우: 해당 값이 변경될 때마다 다시 실행된다.
useMemo
: useMemo는 계산 비용이 많이 드는 함수의 결과를 기억하여 불필요한 재계산을 방지
const lottoNumbers = useMemo(() => getWinNumbers(), []);
const [winNumbers, setWinNumbers] = useState(lottoNumbers);
첫 번째 매개변수: 기억할 값이나 함수를 포함하는 콜백 함수이다.
두 번째 매개변수 (의존성 배열): 이 배열에 포함된 값이 변경될 때만 새로운 값을 계산하고 기억한다. 배열 내의 값이 변경되지 않으면 기억된 값을 계속 재사용한다.
--> useMemo는 한 번 계산된 값을 계속해서 재사용한다.
만약, 이 배열에 값이 포함되어 있다면, 이 값이 변경될 때마다 새로운 값을 계산하고 기억한다.
useCallback
: useCallback은 새로운 함수를 생성하는 비용이 큰 경우에 사용된다.
이 훅을 사용하면 함수를 기억하여, 의존성 배열의 값이 변경될 때만 새로운 함수를 생성하고 그렇지 않으면 이전에 생성된 함수를 재사용한다.
onClickRedo함수에서 사용해 보았다.
두번째 배열에 포함된 값이 변경될 때마다 첫 번째 매개변수로 전달된 콜백 함수가 다시 생성되고, 그 결과가 업데이트 된다. 위의 코드에서는 winBalls를 의존성으로 설정했으므로, winBalls 값이 변경될 때마다 onClickRedo 함수가 새로 생성된다.
onClickRedo 함수가 useCallback으로 메모이제이션되어 있고, 의존성 배열에 winBalls가 포함되어 있기 때문에, winBalls의 값이 변경될 때마다 새로운 onClickRedo 함수가 생성된다. 이로 인해 console.log(winNumbers) 역시 새로운 값으로 출력된다.
useCallback을 사용하여 함수를 메모이제이션하면 해당 함수는 의존성 배열의 값이 변경될 때마다 새로운 함수 인스턴스를 생성하므로, 항상 최신 상태의 값을 가지게 된다.
따라서 onClickRedo 함수 내에서 사용되는 winNumbers 값도 항상 최신 상태를 유지한다.
둘을 헷갈려 하는 경우가 많다고 한다!
공통점:
차이점:
매개변수:
사용 시점:
1 . 조건문 안에 넣으면 절대 절대 안된다.
2 . useCallback, useEffect, useMemo 안에 useState 사용 금지!
3. 순서가 정해진 for문 안에서는 useState 사용가능 하긴 하지만,
되도록이면 그냥 무조건 최상단에 빼서 실행 순서가 같게끔 !