[웹 게임을 만들며 배우는 React] 숫자야구 (props, import & require, 성능 문제 해결)

안지수·2023년 3월 26일
0

👑 import와 require 비교

-> 2개 모두, export된 컴포넌트를 불러오기 위한 문법이라는 점은 같다.

🤶 require: NODE 모듈 문법, 파일 어디서나 사용 가능


-> 'module.export'로 내보낸 것을 불러올 때 사용!

🤶 import: ES2015 문법, 파일 상단에서만 가능


-> 'export default'로 내보낸 것을 불러올 때 사용!

  • export와 export default의 차이: export는 객체의 이름 유지, export default는 지정 가능.
  • export default는 한 모듈 당 한 번만 사용 가능!

-> { } 형태는 구조 분해된 것인데, export 되는 자료형이 객체({ })나 배열([ ])이면 구조 분해할 수 있다.
--> 노드에서는 import 쓰면 에러!! 그러나, 바벨이 require로 바꿔준다. 하지만 웹팩(webpack.config.js)은 node로 돌아가므로, require을 써줘야 한다!!

⭕ 정리:
모듈별로 파일을 관리. 내보내거나 불러올 때, import나 export를 사용한다. import 와 require는 모두 export된 것을 불러오기 위함이다. import를 쓰려면, export default로 내보내야 함. require쓰려면, module.export 써야 함. 노드에서는 require를 사용함! import 사용해도 바벨이 바꿔주긴 해. 근데 웹 팩은 node로 돌아가므로 require 써야 함

👑 리액트 반복문

(value랑 onChange는 항상 세트!!! -> 만약 같이 안할거면 defaultValue로)
-> map 함수 통해 구현
: 객체로 써줘도 되고, key는 고유해야 함, 반복되는 걸 배열로 만들어!

👑 component 분리와 props

  • 컴포넌트 분리: 반복문 때문에 많이 발생하는 성능 문제 해결 가능 (가독성, 성능 최적화, 재사용성)
  • props: 컴포넌트 연결 시, 연결고리 만들어줌 (부모 자식 관계), 어떠한 값을 컴포넌트에게 넘겨줄 때 사용

  • tries: [...this.state.tries, {try: this.state.value, result: '${strike} 스트라이크, ${ball}볼 입니다'}]
    -> tries에서, push 안쓰고 새로운 배열 만들어서 리액트가 뭐가 바뀌었는지 알게 해줘야 함!!
    -> 렌더링은 리액트가 알아서 해주고, 우리는 state만 바꿔주면 됨


    ---> 다른 컴포넌트에 있는 값을 사용하고 싶은 컴포넌트에서 매개변수로 props 받아옴!! (값을 실제로 불러오는 컴포넌트에 props!)

    -> 비구조화 할당 (구조분해) 문법을 이용하면, 더 간단하게 표현 가능!

⭕ 정리:
상위 컴포넌트가 다른 컴포넌트들을 import 해주는 것임. props는 어떤 값을 컴포넌트에 넘겨줄 때 사용한다. 또한, 다른 파일에 있는 값을 사용할 파일에서 매개변수로 props 받아줌!!

👑 숫자야구 class 컴포넌트와 hooks로 전환

  • class 버젼:
import React, {Component, createRef} from 'react';
import Try from './Try';

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 NumberBaseball extends Component{
    state={
        result:'',
        value: '',
        answer: getNumbers(),
        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 (
                        <Try key={`${i + 1}차 시도 :`} tryInfo={v} />
                    );
                })}
            </ul>
        </>
        );
    }
}

export default NumberBaseball;
  • hooks 버젼:
import React, {useRef, useState, useCallback} from 'react';
import Try from "./Try";

const getNumbers = () => {
  const candidates = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  const array = [];
  for (let i = 0; i < 4; i += 1) {
    const chosen = candidates.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
    array.push(chosen);
  }
  return array;
};

const NumberBaseball = () => {
  const [answer, setAnswer] = useState(getNumbers());
  const [value, setValue] = useState('');
  const [result, setResult] = useState('');
  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) {
        setResult(`10번 넘게 틀려서 실패! 답은 ${answer.join(',')}였습니다!`); // state set은 비동기
        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]) {
            console.log('strike', answerArray[i], answer[i]);
            strike += 1;
          } else if (answer.includes(answerArray[i])) {
            console.log('ball', answerArray[i], answer.indexOf(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}
        />
        <button>입력!</button>
      </form>
      <div>시도: {tries.length}</div>
      <ul>
        {tries.map((v, i) => (
          <Try key={`${i + 1}차 시도 : ${v.try}`} tryInfo={v}/>
        ))}
      </ul>
    </>
  );
};

export default NumberBaseball;

<바뀐 부분>
1.
2.
3.
-> class가 아닌 const로. state가 아닌, const로 useState 이용!
4.

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) {
        setResult(`10번 넘게 틀려서 실패! 답은 ${answer.join(',')}였습니다!`); // state set은 비동기
        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]) {
            console.log('strike', answerArray[i], answer[i]);
            strike += 1;
          } else if (answer.includes(answerArray[i])) {
            console.log('ball', answerArray[i], answer.indexOf(answerArray[i]));
            ball += 1;
          }
        }
        setTries(t => ([
          ...t,
          {
            try: value,
            result: `${strike} 스트라이크, ${ball} 볼입니다.`,
          }
        ]));
        setValue('');
        inputEl.current.focus();
      }
    }
  }, [value, answer]);

-> setState 사용하지 않음


  1. -> this 사용하지 않음

---> hooks는 setState, this 사용하지 않음, const로 화살표 함수로!!

👑 성능 문제 해결- 'Shouldcomponentupdate'

렌더링:

  • state 바뀐 경우
  • props 바뀐 경우
  • 부모 컴포넌트가 렌더링된 경우
    --> 부모 컴포넌트에서 상태를 변경하였지만, 자식 컴포넌트에는 영향이 가지 않는 경우!! 자식 컴포넌트는 불필요한 렌더링을 하여, 성능 손실이 발생하게 된다.
    -> SCU 통해서, 조건을 걸어서 필요한 경우에만 렌더링 하도록!

👑 억울한 자식 리렌더링 막기- React.PureComponent

: shouldcomponent update를 알아서 구현한 컴포넌트
-> class component 에만 쓸 수 있음
-> state가 바뀌었는지 자동으로 알아내줌 (바뀐 경우에만 렌더링), 옛날 객체나 배열 가지고 오지말고 새로 만들어라
-> 부모 컴포넌트가 리렌더링되면, 자식 컴포넌트도 리렌더링 됨

  • 함수 컴포넌트에서 pureComponent와 같은 기능: react의 memo
  • memo: 부모 컴포넌트가 리렌더링 되었을 때, 자식 컴포넌트가 리렌더링되는 것을 막아줌. 그러나, props나 state 바뀌었을 때는 여전히 리렌더링

-> displayname: 원상태로 되돌려줌
----> 부모 리렌더링 시 자식도 리렌더링 되는 것을 막는 방법: class component-purecomponent/ 함수 컴포넌트: memo/ 직접: shouldcomponent update
(컴포넌트가 복잡해지면, purecomponent가 안되는 경우도 있음)

⭕ 정리:
'Shouldcomponentupdate' 와 'React.PureComponent' 와 memo 3개를 배웠다. 모두 바뀌어야하는 부분만 렌더링하도록 해주는 성능 최적화 API이다. 첫번째는 직접 조건을 걸어 렌더링을 시켜주는 것이다. 2번째는 class component에서 쓰이는 것으로, 부모에 변화가 있어 렌더링 되더라도, 자식에 직접적인 변화가 없으면 자식 컴포넌트에서는 렌더링이 되지 않는다. memo는 함수 컴포넌트에서 purecomponent와 같은 역할을 하는 것이다.
그런데, 리액트는 얕은비교를 하기 때문에, state를 업데이트할 때, 직접 업데이트를 하면 업데이트가 되지 않는다. 따라서 새로운 오브젝트를 새로 만들어, 그것을 업데이트 해주어야 한다. (위으 try와 같이)

👑 React.createRef

  • ref: 특정 노드나 컴포넌트에 레퍼런스 값을 만들어줌. ref를 통해서 노드가 리액트 요소에 접근하여, 값을 가져올 수 있음

<컴포넌트에 따른 레퍼런스를 만드는 방법>

  • class component:React.createRef- current로 접근 가능
  • 함수 component: useRef (훅)

⭕ 정리:
ref는 그 컴포넌트나 노드를 참조하기 위한 레퍼런스를 만드는 것이다. 그런데 어떤 컴포넌트냐에 따라 다르다. class component에서는 createRef를, 함수 component에서는 useRef를 사용해준다.

👑 기타 지식들

  • 컴포넌트에서 함수를 바깥으로 분리했을 때, 화살표 함수로 안쓰면, this를 쓸 수 없음(화살표 안쓰려면, contructor 써줘야 함)
  • jsx에서의 주석: {/* */}
  • props와 state 연결하기
    -> render에서 this.setState 쓰면 안됨 (무한반복)
    -> props는 부모가 바꿔야하고, 자식이 바꾸면 안됨 (state로 만든 뒤에, 바꿔야 함)
  • 옛날 state로 현재 state 만들 때: 함수형 state!! prevState!!
  • 함수 component hooks에서는 함수를 포함. 함수의 return 값이 변수에 들어감
  1. setState 2가지- prevState, 그냥 객체로
  2. 컴포넌트 3가지 - class, hooks, 함수
  3. ref(함수-예전 state 가져올 수 있고 다른 동작 더 넣어서 세밀한 작업할 수 있음, create) ->그 차이들을 알아두면 좋음
  • class component: createRef, purecomponent
  • 함수 컴포넌트: useRef, hooks, memo
profile
지수의 취준, 개발일기

0개의 댓글