React - 5. 숫자야구

원종현·2021년 4월 30일
0

react

목록 보기
5/5
post-thumbnail

2021.04.30

주석

  • js에서 주석 : //내용입력
  • React(jsx)에서 주석 : {/* 내용입력 */}

import와 require 비교

  • 기본적으로 requireimport는 모듈 키워드이다.
  • 외부 파일이나 라이브러리를 불러올 때 사용한다.
  • commonJS(require)ES2015(import) 모듈 문법의 차이이다.
  • Node에서는 require만 지원하기 때문에 import구문을 사용하려면 babel이 필요하다.

require

  • require는 NodeJs에서 사용되고 있는 CommonJS 키워드이다.

  • 불러오기

        var foo = require("foo");
        var bar = requre("foo").bar;
  • 내보내기

        module.exports = foo;
        exports.bar = bar;
  • 모듈 정의하기

        //모듈 전체를 export, module.exports = export default
        module.exports = module;
    
        // 함수나 변수를 직접 export가능
        exports.moduleFunc = function() {};
        module.exports = { bar: 'bar'}; // exports.bar = 'bar'; 같다.

import

  • import는 ES2015에서 새롭게 도입된 키워드이다.

  • Babel과 같은 ES6 코드를 변환(transpile)해주는 도구를 사용할 수 없는 경우에는 require 키워드를 사용해야 한다.

  • 불러오기

        import foo from "foo";
        import { bar } from "foo";  
  • 내보내기

        export default foo;
        export { bar };
  • 모듈 정의하기

        //모듈 전체를 export, export default는 파일내에서 한 번만 사용가능하다.
        const module = {};
        export default module; // default로 export한 경우 불러올때 import module from '~~~';
    
        // 함수나 변수를 직접 export가능, export는 여러번 가능하다.
        export function moduleFunc() {};
        export const bar = 'bar'; //그냥 export한 경우 불러올때 import {bar} from '~~~';
  • export defaultmodule.exports는 호환되지만 엄밀히 따지면 다르다.

  • exports되는 게 객체나 배열이면 구조 분해할 수 있다.

map

  • react에서 반복문을 사용하는 방법 중 하나이다.
[].map(() => {
    //식 입력
});

//ex 2배의 수 만들기
[1,2,3,4].map((num) => {
    num * 2
});
//render() {return ()} 구문에 JSX문법으로 작성하기
//중괄호 {}를 이용한 JSX문법에 포함 시킬 수 있다.
const numbers = [1, 2, 3, 4, 5];
<ul>
{
    numbers.map((number) => {
        return (
            <li>{number}</li>
        );
    })
}
</ul>
//화살표 함수를 사용하면 return 생략이 가능하다 
//화살표 함수에서 ()소괄호는 return을 의미한다.
<ul>
{
    numbers.map((number) => (
            <li>{number}</li>
        )   
    );
}
</ul>
//아예 소괄호까지 제거해도 return 생략 가능
// const a = (props) => console.log(props) 중괄호가 없는 것은 바로 return을 뜻 함
<ul>
{
    numbers.map((number) => 
        <li>{number}</li>
    )
}
</ul>
  • 반복문을 돌릴 경우 공통된 부분을 return()안에 작성해 주고, 바뀌는 부분은 중괄호 {} 로 감싼다.
  • map을 사용할 때는 key를 추가해 줘야 한다.
  • react가 key를 보고 같은 컴포넌트인지 아닌지 판단하므로 key를 작성하지 않으면 에러 발생 (화면에 표시는 안되지만 성능최적화시 사용된다.)

key

  • key는 react가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 도와준다.
  • key는 속성에 안정적인 고유성을 부여하기 위해 배열 내부의 속성에 지정한다.
  • key는 고유한 값으로 지정해 준다.
  • 배열 내 항목의 인덱스를 key로 사용할 수 있다.
    • key로 인덱스를 사용하는 것은 권장하지 않는다. 성능 저하나 컴포넌트의 state와 관련된 문제가 발생할 수 있다.

    • 나중에 성능 최적화 시 문제가 될 수 있음

      const todoItems = todos.map((todo, index) =>
          <li key={index}>
              {todo.text}
          </li>
      );

컴포넌트 분리와 Props

  • 개념적으로 컴포넌트는 Javascript 함수와 유사하다.
  • 코드의 가독성과 컴포넌트의 재사용성을 위해서 중복되거나 여러 컴포넌트에서 사용되는 부분을 따로 컴포넌트로 만들어서 분리한다.
  • 컴포넌트로 분리 할 경우 성능 최적화를 할 때 좋다.
  • Props : 속성을 나타내는 데이터이다.
  • Props객체 인자를 받은 후 React 엘리먼트를 반환한다 = 유효한 React 컴포넌트이다.
  • React가 사용자 정의 컴포넌트로 작성한 엘리먼트를 발견하면 JSX 어트리뷰트와 자식을 해당 컴포넌트에 단일 객체로 전달하는데 이 객체가 Props이다.
    • props는 부모한테서 받은 유산이다!!

메서드 바인딩

  • class 내에서 함수를 작성할 경우(사용자가 만든 함수) 화살표 함수를 사용하지 않으면 에러가 발생했었다.
  • 화살표 함수를 사용하지 않을 경우 this를 사용하지 못 한다.
  • 화살표 하무가 bind(this)를 자동으로 해준다.
class NumberBaseball extends Component {
    state = {
        value: '',
    };

    onSubmit() {
        console.log(this.state.value); 
        // this를 찾지 못한다 - 에러 발생
    }
}

// 위 문제를 해결하기 위해서 메서드 바인딩이 필요하다.
class NumberBaseball extends Component {
    //constructor를 작성해줘야 한다.
    constructor(props) {
        super(props);
        state = {
            value: '',
        };
        this.onSubmit = this.onSubmit.bind(this);
    }

    onSubmit() {
        console.log(this.state.value); 
    }
}

숫자야구 만들기

  • js에서 기본적으로 배열에 값을 추가할 때 push를 사용하지만 react에서는 push를 사용하면 안된다.
  • push를 통해서 값을 추가하면 새로운 배열이 생성된 것이 아닌 기본 배열에 값만 추가된 것이다.
  • 이 경우, react는 무엇이 바뀌었는지 감지를 할 수 없다.
  • 따라서 새로운 배열을 만들고 Spread 연산자를 사용해서 기존배열을 복사해 넣고 새로운 값을 넣어준다.
  • react 렌더링 기준이 예전 state와 현재 state가 다르면 렌더링을 해준다.
  • 따라서 react에서 배열에 값을 추가할 경우 push를 사용하지 않고 Spread 연산자를 사용해 새로운 배열을 만들어 준다.
    const array = [];
    array.push(1);
    //array = [1] 이 된다.
    // array === array => true 기존 배열에 값만 추가된 것이므로 true가 반환된다.
    const array2 = [...array, 2]
    // 이때 array === array2 => false 다른 새로운 변수가 생성된 것이므로 false가 반환된다. 서로 다르기 떄문에 react가 감지 할 수 있다.

React 렌더링 조건

  1. Props가 변경되었을 때
  2. State가 변경되었을 때
  3. forceUpdate() 를 실행하였을 때
  4. 부모 컴포넌트가 렌더링되었을 때

코드

NumberBaseball.jsx 파일

  • class 문법
import React, { Component, createRef } from "react";
import Try from "./Try";

function getNumbers() {
    // 겹치지 않는 랜덤한 숫자 4개를 뽑는 함수
    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(), // ex: [1,3,5,7]
        tries: [], // push 쓰면 안 돼요
    };

    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 = () => {
    //겹치지 않는 랜덤한 숫자 4개를 뽑는 함수
    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;

Try.jsx 파일

  • class 문법
import React, { Component } from "react";

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

export default Try;
  • hooks 문법
import React, { memo } from "react";

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

성능 개선 Tips

  • 크롬 브라우저에 React Devtools 확장 프로그램을 설치해 준다.
  • 개발자 도구에서 설정(톱니바퀴 아이콘)에 들어가 Highlight Updates 활성화
  • 렌더링이 될 때마다 해당 컴포넌트에 하이라이트 효과가 나타난다.
  • react에서 state가 바뀔때 렌더링이 되야하는데 state는 바뀌지 않고 setState()만 호출해도 렌더링이 된다. 이럴때는 shouldComponentUpdate()를 사용한다.
// render()메서드 처럼 react에서 지원하는 메서드이다.
shouldComponentUpdate(nextProps, nextState, nextContext) {
  //어떤 경우에 렌더링을 할 지를 직접 작성해 준다.
  //예를들어 
  if(this.state !== nextState) {//현재 state와 미래 state가 다르면
  	return true; //렌더링해준다.
  }
  return false;
}
  • shouldComponentUpdate()메서드의 사용이 복잡한 경우
  1. class 문법일 경우 기존에 사용하는 Component대신 PureComponent를 사용한다.
import React, {PureComponent} from 'react';

class Test extends PureComponent { }
  • PureComponent : shouldComponentUpdate()를 알아서 구현한 Component이다.
  1. hooks 문법일 경우 memo()를 사용한다.
  • 컴포넌트를 memo()로 감싸주면 된다.
import React, {memo} from 'react';

const Test = memo( () => {} );
  • class 문법에서 hooks와 비슷하게 ref 사용하는 방법
    - React.createRef사용
import React, { Component, createRef } from 

class Test extends Component {
  
  onSubmit = () => {
    this.inputRef.current.focus();
  }
  
  inputRef = createRef();

  render() {
    return (
      <>
      	<form onSubmit={this.onSubmit}>
      	  <input ref={this.inputRef} />
        </form>
      </>
    )
  }
}
  • Props는 부모 컴포넌트에서 바꿔야지 자식 컴포넌트에서 바꾸면 안된다.
profile
프론트엔드 엔지니어를 목표로 공부하는 중입니다!.

0개의 댓글