과거 프로그래밍 자료들/React

[웹 게임을 만들며 배우는 React] - 숫자야구(useState), 렌더링 문제

평부 2022. 9. 28. 12:11

 

 

출처: https://www.inflearn.com/course/web-game-react/dashboard

 

[무료] 웹 게임을 만들며 배우는 React - 인프런 | 강의

웹게임을 통해 리액트를 배워봅니다. Class, Hooks를 모두 익히며, Context API와 React Router, 웹팩, 바벨까지 추가로 배웁니다., - 강의 소개 | 인프런...

www.inflearn.com

 

 

* Try.jsx를 따로 빼서 NumberBaseball.jsx에서 호출

* useState 

참고 : https://react.vlpt.us/basic/07-useState.html

 

7. useState 를 통해 컴포넌트에서 바뀌는 값 관리하기 · GitBook

7. useState 를 통해 컴포넌트에서 바뀌는 값 관리하기 지금까지 우리가 리액트 컴포넌트를 만들 때는, 동적인 부분이 하나도 없었습니다. 값이 바뀌는 일이 없었죠. 이번에는 컴포넌트에서 보여줘

react.vlpt.us

 

▶ 컴포넌트에서 상태 관리하기

▶ 값을 넣을 경우(1.)와 함수를 넣을 경우(2.) 존재

▶ 함수를 넣을 경우 answer는 함수의 리턴값으로 값이 설정됨(다시 실행되지 않음)

 

//1. answer에 1이 설정됨
const [answer, setAnswer] = useState(1); 

//2. 함수를 넣을 경우
const getNumbers = () => {
  // 숫자 네 개를 겹치지 않고 랜덤하게 뽑는 함수
  const candidate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  const array = [];
  for (let i = 0; i < 4; i += 1) {
    const chosen = candidate.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
    array.push(chosen);
  }
  return array;
};

const [answer, setAnswer] = useState(getNumbers);
//const [answer, setAnswer] = useState(getNumbers()); 
//=> 함수가 아닌 함수 호출도 가능은 하나 그럴 경우 한 번만 사용하면 되는 함수가 여러 번 불러오게 됨

 

* class 사용

[NumberBaseballClass.jsx]

▶ push 사용 불가 : 바로 배열에 값을 넣기보단 기존 배열 복사 후 새로운 값을 넣어야 함 

▶ 배열 복사 참고 : https://enfanthoon.tistory.com/137

 

[React] - 15) 리액트에서 배열을 처리하는 방법

안녕하세요 이번 포스팅에서는 리액트에서 배열을 처리하는 가장 기본적인 방법에 대해 알아보겠습니다. 리액트의 불변성을 유지해주어야 한다, 라는 특성 상 리액트에서 배열을 처리하는 방

enfanthoon.tistory.com

더보기
 this.setState((prevState) => {
        return {
          result: "홈런!",
          tries: [...prevState.tries, { try: value, result: "홈런!" }],
        };
      });
import React, { Component, createRef } from "react";
import TryClass from "./TryClass";

function getNumbers() {
  // 숫자 네 개를 겹치지 않고 랜덤하게 뽑는 함수
  const candidate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  const array = [];
  for (let i = 0; i < 4; i += 1) {
    const chosen = candidate.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
    array.push(chosen);
  }
  return array;
}

class NumberBaseballClass extends Component {
  state = {
    result: "",
    value: "",
    answer: getNumbers(), // ex: [1,3,5,7]
    tries: [], 
  };

  onSubmitForm = (e) => {
    const { value, tries, answer } = this.state;
    e.preventDefault();
    if (value === answer.join("")) {
      this.setState((prevState) => {
        return {
          result: "홈런!",
          tries: [...prevState.tries, { try: value, result: "홈런!" }],
        };
      });
      alert("게임을 다시 시작합니다!");
      this.setState({
        value: "",
        answer: getNumbers(),
        tries: [],
      });
      this.inputRef.current.focus();
    } else {
      // 답 틀렸으면
      const answerArray = value.split("").map((v) => parseInt(v));
      let strike = 0;
      let ball = 0;
      if (tries.length >= 9) {
        // 10번 이상 틀렸을 때
        this.setState({
          result: `10번 넘게 틀려서 실패! 답은 ${answer.join(",")}였습니다!`,
        });
        alert("게임을 다시 시작합니다!");
        this.setState({
          value: "",
          answer: getNumbers(),
          tries: [],
        });
        this.inputRef.current.focus();
      } else {
        for (let i = 0; i < 4; i += 1) {
          if (answerArray[i] === answer[i]) {
            strike += 1;
          } else if (answer.includes(answerArray[i])) {
            ball += 1;
          }
        }
        this.setState((prevState) => {
          return {
            tries: [
              ...prevState.tries,
              { try: value, result: `${strike} 스트라이크, ${ball} 볼입니다` },
            ],
            value: "",
          };
        });
        this.inputRef.current.focus();
      }
    }
  };

  onChangeInput = (e) => {
    console.log(this.state.answer);
    this.setState({
      value: e.target.value,
    });
  };

  inputRef = createRef(); // this.inputRef
  render() {
    const { result, value, tries } = this.state;
    return (
      <>
        <h1>{result}</h1>
        <form onSubmit={this.onSubmitForm}>
          <input
            ref={this.inputRef}
            maxLength={4}
            value={value}
            onChange={this.onChangeInput}
          />
        </form>
        <div>시도: {tries.length}</div>
        <ul>
          {tries.map((v, i) => {
            return <TryClass key={`${i + 1}차 시도 :`} tryInfo={v} />;
          })}
        </ul>
      </>
    );
  }
}

export default NumberBaseballClass;

 

 

[TryClass.jsx]

import React, { Component } from "react";

class TryClass extends Component {
  render() {
    const { tryInfo } = this.props;
    return (
      <li>
        <div>{tryInfo.try}</div>
        <div>{tryInfo.result}</div>
      </li>
    );
  }
}

export default TryClass;

 

 

[client.jsx]

import React from "react";
import { createRoot } from "react-dom/client";
import NumberBaseball from "./NumberBaseballClass";

createRoot(document.getElementById("root")).render(<NumberBaseball />);

 

 

* Hooks 사용

[NumberBaseball.jsx]

▶ 위에서 언급한 useState에 함수 넣기와 setAnswer(getNumbers())는 구분해야 함

▶ 참고 : https://ba-gotocode131.tistory.com/202?category=987174 

 

함수와 함수 호출 차이, 고차함수

* 참고한 강의 https://www.youtube.com/watch?v=NS1cIsWlFGI&list=PLcqDmjxt30Rt9wmSlw1u6sBYr-aZmpNB3&index=1 [강의 들은 목적] - 함수와 함수 호출의 차이를 정확히 알고 싶었음 - 고차함수가 많이 헷갈리는데..

ba-gotocode131.tistory.com

//함수 
const [answer, setAnswer] = useState(getNumbers); //lazy init

//함수 호출
setAnswer(getNumbers());  //리턴한 array를 setAnswer로 받아줌(호출해서 넣어줌)
import React, { useState, useRef, useCallback } from "react";
import Try from "./Try";

const getNumbers = () => {
  // 숫자 네 개를 겹치지 않고 랜덤하게 뽑는 함수
  const candidate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  const array = [];
  for (let i = 0; i < 4; i += 1) {
    const chosen = candidate.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
    array.push(chosen);
  }
  return array;
};

const NumberBaseballClass = () => {
  const [result, setResult] = useState("");
  const [value, setValue] = useState("");
  const [answer, setAnswer] = useState(getNumbers); //lazy init
  const [tries, setTries] = useState([]);
  const inputEl = useRef(null);

  const onSubmitForm = useCallback(
    (e) => {
      e.preventDefault();
      if (value === answer.join("")) {
        setTries((t) => [
          ...t,
          {
            try: value,
            result: "홈런!",
          },
        ]);
        setResult("홈런!");
        alert("게임을 다시 시작합니다!");
        setValue("");
        setAnswer(getNumbers());
        setTries([]);
        inputEl.current.focus();
      } else {
        // 답 틀렸으면
        const answerArray = value.split("").map((v) => parseInt(v));
        let strike = 0;
        let ball = 0;
        if (tries.length >= 9) {
          // 10번 이상 틀렸을 때
          setResult(`10번 넘게 틀려서 실패! 답은 ${answer.join(",")}였습니다!`);
          alert("게임을 다시 시작합니다!");
          setValue("");
          setAnswer(getNumbers());
          setTries([]);
          inputEl.current.focus();
        } else {
          console.log("답은", answer.join(""));
          for (let i = 0; i < 4; i += 1) {
            if (answerArray[i] === answer[i]) {
              strike += 1;
            } else if (answer.includes(answerArray[i])) {
              ball += 1;
            }
          }
          setTries((t) => [
            ...t,
            {
              try: value,
              result: `${strike} 스트라이크, ${ball} 볼입니다`,
            },
          ]);
          setValue("");
          inputEl.current.focus();
        }
      }
    },
    [value, answer]
  );

  const onChangeInput = useCallback((e) => setValue(e.target.value), []);

  return (
    <>
      <h1>{result}</h1>
      <form onSubmit={onSubmitForm}>
        <input
          ref={inputEl}
          maxLength={4}
          value={value}
          onChange={onChangeInput}
        />
      </form>
      <div>시도: {tries.length}</div>
      <ul>
        {tries.map((v, i) => {
          return <Try key={`${i + 1}차 시도 :`} tryInfo={v} />;
        })}
      </ul>
    </>
  );
};

export default NumberBaseballClass;

 

 

[Try.jsx]

import React, { Component } from "react";

const Try = ({ tryInfo }) => { //props 자리가 구조분해한 tryInfo가 들어감
  return (
    <li>
      <div>{tryInfo.try}</div>
      <div>{tryInfo.result}</div>
    </li>
  );
};

export default Try;

 

 

[client.jsx]

import React from "react";
import { createRoot } from "react-dom/client";
import NumberBaseball from "./NumberBaseball";

createRoot(document.getElementById("root")).render(<NumberBaseball />);

 

 

* 렌더링 문제 : 성능 최적화

- 자식 컴포넌트가 리렌더링 되는 경우

1. props가 바뀌는 경우

2. state가 바뀌는 경우

3. 부모 컴포넌트가 바뀌는 경우 자식 컴포넌트도 영향 받음 -> 성능에 문제 있을 수 있음

 

▶ props를 통해 NumberBaseball(부모)와 Try(자식)간 부모자식간의 관계가 생김

NumberBaseball이 리렌더링 될 때마다 Try가 리렌더링됨

(1234 0 스트라이크, 2 볼입니다 | 4568 1 스트라이크, 2 볼입니다 | 9989 1 스트라이크, 0 볼입니다 등등 리렌더링됨)

 

해결책 1 : shouldComponentUpdate 사용

더보기
import React, { Component } from "react";

//렌더링 컴포넌트를 보기 위한 예시
class Test extends Component {
  state = {
    counter: 0,
  };

  shouldComponentUpdate(nextProps, nextState, nextComponent) {
    //언제 렌더링이 될지를 설정해야 함
    if (this.state.counter !== nextState.counter) {
      return true; //렌더링 됨
    }
    return false; //렌더링 안 됨
  }
  onClick = () => {
    this.setState({});
  };
  render() {
    console.log("렌더링", this.state);
    return (
      <div>
        <button onClick={this.onClick}>클릭</button>
      </div>
    );
  }
}

export default Test;

 

 

해결책 2 : PureComponent (클래스) | memo(함수 컴포넌트) 사용

▶ 단, 클래스에서만 적용됨

▶ 함수 컴포넌트는 사용할 수 없는가? => memo 사용

  PureComponent  memo
state 달라졌을 경우 막아줌 막아주지 못함
props 달라졌을 경우 막아줌 막아주지 못함
부모 컴포넌트 리렌더링 시 자식
컴포넌트 리렌더링 될 때
막아줌 막아줌

 

 

(함수 컴포넌트) [Try.jsx]

//수정 전
import React from "react";

const Try = ({ tryInfo }) => {
  return (
    <li>
      <div>{tryInfo.try}</div>
      <div>{tryInfo.result}</div>
    </li>
  );
};
Try.displayName = "Try";

export default Try;

//수정 후 
import React, { memo } from "react";

const Try = memo(({ tryInfo }) => {
  return (
    <li>
      <div>{tryInfo.try}</div>
      <div>{tryInfo.result}</div>
    </li>
  );
});
Try.displayName = "Try"

export default Try;

 

(클래스) [TryClass.jsx]

//수정 전
import React, { Component } from "react";

class TryClass extends Component {
  render() {
    const { tryInfo } = this.props;
    return (
      <li>
        <div>{tryInfo.try}</div>
        <div>{tryInfo.result}</div>
      </li>
    );
  }
}

export default TryClass;

//수정 후
import React, { PureComponent } from "react";

class TryClass extends PureComponent {
  render() {
    const { tryInfo } = this.props;
    return (
      <li>
        <div>{tryInfo.try}</div>
        <div>{tryInfo.result}</div>
      </li>
    );
  }
}

export default TryClass;