TIL 22-04-13

thisisyjin·2022년 4월 13일
0

TIL 📝

목록 보기
22/113

React.js

Today I Learned ... react.js

🙋‍♂️ React.js Lecture

🙋‍ My Dev Blog


React Lecture CH 6

1 - 로또 추첨기 컴포넌트
2 - setTimeout 중복 사용
3 - componentDidUpdate
4 - useEffect - 업데이트 감지
5 - useMemo, useCallback
6 - Hooks Tips

로또 추첨기 컴포넌트

  • setTimeout
  • useMemo, useCallback

초기 코드 작성

Lotto.jsx

import React, { Component } from 'react';
import Ball from './Ball'

function getWinNumbers() {
  console.log('getWinNumbers');
  const candidate = Array(45)
    .fill()
    .map((v, i) => i + 1);
  const shuffle = [];
  while (candidate.length > 0) {
    shuffle.push(
      candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]
    );
  }
  const bonusNumber = shuffle[shuffle.length - 1];
  const winNumbers = shuffle.slice(0, 6).sort((p, c) => p - c); // 오름차순
  return [...winNumbers, bonusNumber];
}

class Lotto extends Component {
  state = {
    winNumbers: getWinNumbers(), // [...winNumbers, bonusNumber]
    winBalls: [],
    bonus: null,
    redo: false,
  };

 onClickRedo = () => {};

  render() {
    const { winBalls, bonus, redo } = this.state;
    return (
      <>
        <div>당첨 숫자</div>
        <div id="결과창">
          {winBalls.map((v) => (
            <Ball key={v} number={v} />
          ))}
        </div>
         <div>보너스!</div>
        {bonus && <Ball number={bonus} />}
        <button onClick={redo ? this.onClickRedo : () => {}}>한번 더!</button>
      </>
    );
  }
}

export default Lotto;

숫자 뽑기 로직

function getWinNumbers() {
  console.log('getWinNumbers');
  const candidate = Array(45)
    .fill()
    .map((v, i) => i + 1);
  const shuffle = [];
  while (candidate.length > 0) {
    shuffle.push(
      candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]
    );
  }
  const bonusNumber = shuffle[shuffle.length - 1];
  const winNumbers = shuffle.slice(0, 6).sort((p, c) => p - c); // 오름차순
  return [...winNumbers, bonusNumber];
}
  1. candidate 배열
  • Array함수를 new 없이 일반 함수로 사용함.
  • 인수로 들어간 45는 요소의 개수 (=length)
  • fill()메서드로 배열에 요소를 채움 (여기서는 공백)
  • map()메서드로 'index + 1' 을 한 값을 갖는 새로운 배열을 반환하여 candidate에 저장.
    -> i는 0~44까지이므로 i+1을 하여 1~45까지의 값이 저장됨.
  1. shuffle 배열 (순서 섞기)
  • candidate배열에서 shuffle 배열로 이동시킴.
  • 랜덤으로 뽑아서 1개씩 splice하여 push함. (cut)
    -> while(candidate.length > 0) 이므로 length가 0이 되면 false
    -> 루프 종료
  1. bonusNumber과 winNumbers
    bonusNumber은 shuffle 배열의 마지막 요소.
    winNumbers는 [0]~5 까지 여섯개 요소를 뽑고, sort 하여 오름차순 정렬.
  • 오름차순내림차순
    sort((a, b) => a - b)sort((a, b) => a + b)
  1. return [...winNumbers, bonusNumber]
  • 여기서 ...는 RestParams가 아닌 spread 연산자임.
  • winNumbers를 펼치고, 마지막 요소는 bonusNumber인 배열을 리턴함.

-> 즉, getWinNumbers() = [1,3,6,8,11,22,7]

+) 하위 컴포넌트인 Ball을 분리하여 임포트해주었다.

  • 주로 하위 컴포넌트로 분리하는 것은 반복문에서 발생한다.

하위 컴포넌트 작성

Ball.jsx

import React, { memo } from 'react';

// state를 안쓸 때는 - 함수 컴포넌트로. (Hooks 아님)
const Ball = memo(({ number }) => {
  let background;
  // 공 색깔 정하기
  if (number <= 10) {
    background = 'red';
  } else if (number <= 20) {
    background = 'orange';
  } else if (number <= 30) {
    background = 'yellow';
  } else if (number <= 40) {
    background = 'blue';
  } else {
    background = 'green';
  }
  return (
    <div className="ball" style={{ background }}>
      {number}
    </div>
  );
});

export default Ball;

// 하이어오더 컴포넌트 (memo) = 컴포넌트를 컴포넌트로 감쌈. pure component 역할을 함.

함수형 컴포넌트

state를 사용하지 않아도 되는 하위 컴포넌트는 함수형 컴포넌트로 작성해준다.

  • ❗️ 함수형 컴포넌트 ≠ Hooks (같지 않다!)
  • Hooks는 useState, useEffect, useRef 등을 사용하는 함수형 컴포넌트임.
  • 주로 하위 컴포넌트 중 가장 마지막 컴포넌트는 데이터를 다루지 않으므로, (state 등) PureComponent로 작성해준다.
  • 위에서는 state가 없으므로 함수형 컴포넌트로 작성해준 것.

함수형에서 PureComponent 역할을 하려면?
-> memo() 로 감싸준다.

memo

  • 하이어오더 컴포넌트.
    (컴포넌트를 다른 컴포넌트로 감쌈)
  • 함수형 컴포넌트를 PureComponent로 만들기 위해서는 React.memo()를 컴포넌트에 감싸준다.

style 작성

  • 원래 React에서 스타일을 적용할 때는 보통
    scss, styled-component 등을 사용하지만,
    우선은 index.html에 직접 <style>태그를 적용해보자.
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>로또 추첨기</title>
    <style>
      .ball {
        display: inline-block;
        border: 1px solid black;
        border-radius: 20px;
        width: 40px;
        height: 40px;
        line-height: 40px;
        font-size: 20px;
        text-align: center;
        margin-right: 20px;
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
    <script src="./dist/app.js"></script>
  </body>
</html>

-> div.ball을 동그랗게 만들어주고, border과 width*height를 정해줌.


setTimeout 여러번 적용

import React, { Component } from 'react';
import Ball from './Ball';

function getWinNumbers() {
  console.log('getWinNumbers');
  const candidate = Array(45)
    .fill()
    .map((v, i) => i + 1);
  const shuffle = [];
  while (candidate.length > 0) {
    shuffle.push(
      candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]
    );
  }
  const bonusNumber = shuffle[shuffle.length - 1];
  const winNumbers = shuffle.slice(0, 6).sort((p, c) => p - c); // 오름차순
  return [...winNumbers, bonusNumber];
}

class Lotto extends Component {
  state = {
    winNumbers: getWinNumbers(), // [...winNumbers, bonusNumber]
    winBalls: [],
    bonus: null,
    redo: false,
  };

  componentDidMount() {
    for (let i = 0; i < this.state.winNumbers.length - 1; i++) {
      // length - 1을 해서 마지막 보너스 공은 빼줌
      setTimeout(() => {
        this.setState((prevState) => {
          return {
            winBalls: [...prevState.winBalls, winNumbers[i]],
          };
        });
      }, (i + 1) * 1000);
    }
  }

  onClickRedo = () => {};

  render() {
    const { winBalls, bonus, redo } = this.state;
    return (
      <>
        <div>당첨 숫자</div>
        <div id="결과창">
          {winBalls.map((v) => (
            <Ball key={v} number={v} />
          ))}
        </div>
        <div>보너스!</div>
        {bonus && <Ball number={bonus} />}
        <button onClick={redo ? this.onClickRedo : () => {}}>한번 더!</button>
      </>
    );
  }
}

export default Lotto;
  • 랜덤한 숫자가 적힌 ball이 1초 간격으로 나와야 하므로
    setTimeout을 여러번 작성해야 한다.
  • 또한, 시작하자마자 뜨기 때문에 componentDidMount에 입력해준다.
  • for문으로 winNumbers 배열의 요소들을 this.state.winBalls로 대입함.
    (push 대신 spread 연산자로 배열에 추가함)
  • 이전 state를 사용해야 하므로, setState를 함수형으로 작성해준다.

import React, { Component } from 'react';
import Ball from './Ball';

function getWinNumbers() {
  console.log('getWinNumbers');
  const candidate = Array(45)
    .fill()
    .map((v, i) => i + 1);
  const shuffle = [];
  while (candidate.length > 0) {
    shuffle.push(
      candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]
    );
  }
  const bonusNumber = shuffle[shuffle.length - 1];
  const winNumbers = shuffle.slice(0, 6).sort((p, c) => p - c); // 오름차순
  return [...winNumbers, bonusNumber];
}

class Lotto extends Component {
  state = {
    winNumbers: getWinNumbers(), // [...winNumbers, bonusNumber]
    winBalls: [],
    bonus: null,
    redo: false,
  };

  timeouts = [];

  runTimeouts = () => {
    const { winNumbers } = this.state;
    for (let i = 0; i < this.state.winNumbers.length - 1; i++) {
      // length - 1을 해서 마지막 보너스 공은 빼줌
      this.timeouts[i] = setTimeout(() => {
        this.setState((prevState) => {
          const { winNumbers } = this.state;
          return {
            winBalls: [...prevState.winBalls, winNumbers[i]],
          };
        });
      }, (i + 1) * 1000);
    }
    this.timeouts[6] = setTimeout(() => {
      this.setState({
        bonus: winNumbers[6],
        redo: true,
      });
    }, 7000);
  };

  componentDidMount() {
    this.runTimeouts();
  }

  componentWillUnmount() {
    this.timeouts.forEach((v) => clearTimeout(v));
  }

  render() {
    const { winBalls, bonus, redo } = this.state;
    return (
      <>
        <div>당첨 숫자</div>
        <div id="결과창">
          {winBalls.map((v) => (
            <Ball key={v} number={v} />
          ))}
        </div>
        <div>보너스!</div>
        {bonus && <Ball number={bonus} />}
        {redo && <button onClick={this.onClickRedo}>한번 더!</button>}
      </>
    );
  }
}

export default Lotto;

timeouts는 클래스의 프로퍼티로, state처럼 값이 바뀌면 리렌더링 될 필요가 없을 떄 사용함.

  • 여기서는 clearTimeout을 해줘야하므로 (componentWillUnmount에서) 식별자가 필요해서 사용했음.
componentWillUnmount() {
    this.timeouts.forEach((v) => clearTimeout(v));
  }
  • timeouts 라는 배열에 setTimeout 함수 7개가 담겨있음.
    -> 배열 메서드인 forEach로 각 요소를 순회하면서 clearTimeout 해주면 됨!

Result (1)

  1. 1초 간격으로 winNumbers와 마지막 bonusNumber이 나온다.
    -> 하위 컴포넌트 <Ball />로 props인 number에 자신의 state winBallsbonus를 전달해줌.

  2. 마지막에 bonus까지 나오게 되면, '한번 더' 버튼이 렌더링 된다.

{redo && <button onClick={this.onClickRedo}>한번 더!</button>}
  • 마지막으로, onClickRedo 함수만 작성하면 끝!
    -> 모든 state를 초기화하고, setTimeout을 하는 함수인
    runTimeouts을 다시한번 실행함.

  • 🔻 runTimeouts 함수
runTimeouts = () => {
    const { winNumbers } = this.state;
    for (let i = 0; i < this.state.winNumbers.length - 1; i++) {
      // length - 1을 해서 마지막 보너스 공은 빼줌
      this.timeouts[i] = setTimeout(() => {
        this.setState((prevState) => {
          const { winNumbers } = this.state;
          return {
            winBalls: [...prevState.winBalls, winNumbers[i]],
          };
        });
      }, (i + 1) * 1000);
    }
  
    this.timeouts[6] = setTimeout(() => {
      this.setState({
        bonus: winNumbers[6],
        redo: true,
      });
    }, 7000);
  };
  1. i=0 ~ 5까지 this.timeouts[i]에 setTimeout 저장
    -> winBalls에 이전 winBalls + winNumbers 추가
    (push 대신 ...으로 요소 추가)
  1. for loop 종료 후, this.timeouts[6]에 setTimeout 저장
    -> bonus에 winNumbers[6] 추가 후, redo를 true로 설정
    -> button 렌더링 되게 (JSX부분 단축평가 &&에 의해)

최종 코드 완성

import React, { Component } from 'react';
import Ball from './Ball';

function getWinNumbers() {
  console.log('getWinNumbers');
  const candidate = Array(45)
    .fill()
    .map((v, i) => i + 1);
  const shuffle = [];
  while (candidate.length > 0) {
    shuffle.push(
      candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]
    );
  }
  const bonusNumber = shuffle[shuffle.length - 1];
  const winNumbers = shuffle.slice(0, 6).sort((p, c) => p - c); // 오름차순
  return [...winNumbers, bonusNumber];
}

class Lotto extends Component {
  state = {
    winNumbers: getWinNumbers(), // [...winNumbers, bonusNumber]
    winBalls: [],
    bonus: null,
    redo: false,
  };

  timeouts = [];

  runTimeouts = () => {
    const { winNumbers } = this.state;
    for (let i = 0; i < this.state.winNumbers.length - 1; i++) {
      // length - 1을 해서 마지막 보너스 공은 빼줌
      this.timeouts[i] = setTimeout(() => {
        this.setState((prevState) => {
          const { winNumbers } = this.state;
          return {
            winBalls: [...prevState.winBalls, winNumbers[i]],
          };
        });
      }, (i + 1) * 1000);
    }
    this.timeouts[6] = setTimeout(() => {
      this.setState({
        bonus: winNumbers[6],
        redo: true,
      });
    }, 7000);
  };

  componentDidMount() {
    this.runTimeouts();
  }

  componentWillUnmount() {
    this.timeouts.forEach((v) => clearTimeout(v));
  }

  onClickRedo = () => {
    this.setState({
      winNumbers: getWinNumbers(), // [...winNumbers, bonusNumber]
      winBalls: [],
      bonus: null,
      redo: false,
    });
    this.runTimeouts();
  };

  render() {
    const { winBalls, bonus, redo } = this.state;
    return (
      <>
        <div>당첨 숫자</div>
        <div id="결과창">
          {winBalls.map((v) => (
            <Ball key={v} number={v} />
          ))}
        </div>
        <div>보너스!</div>
        {bonus && <Ball number={bonus} />}
        {redo && <button onClick={this.onClickRedo}>한번 더!</button>}
      </>
    );
  }
}

export default Lotto;

✅ onClickRedo 함수

onClickRedo = () => {
    this.setState({
      winNumbers: getWinNumbers(), // [...winNumbers, bonusNumber]
      winBalls: [],
      bonus: null,
      redo: false,
    });
    this.runTimeouts();
  };

onClickRedo

  • setState로 모든 state를 초기값으로 다시 리셋함
  • this.runTimeouts() 실행
    -> setTimeout() 총 7개 실행 (위에 코드 참조)

Result (2)

  • 버튼 클릭시 처음부터 다시 뽑음.
  • 반드시 winNumbers도 초기화 -> getWinNumbers() 한번 더 실행하여 새로운 수 뽑아야 함!
profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글