반응속도체크 코딩

전체코드

import React, {Component} from "react";

class Response extends Component{
    state = {
        state:'waiting',
        message:'클릭해서 시작하세요.',
        result:[],
    }
    timer = null;
    startTime = 0;

    onClickScreen = e => {
        const {state, message, result} = this.state;
        const {target} = e;
        if(state === 'waiting'){ //기달 -> 준비
            this.setState({state:'ready', message:'색이 바뀌면 클릭해주세요'});
            const max = 2000; //2초
            const min = 500 // 0.5초
            const randTime = Math.floor(Math.random() * (max - min + 1)) + min;
            this.timer = setTimeout(()=>{
                this.setState({state:'now', message:'클릭해주세요!'});
                this.startTime = new Date().getTime();
            },randTime);
        }else if(state === 'ready'){
            //너무 성급하다 다시 시작하겠다 멘트
            alert('너무 성급합니다. 다시 시작하겠습니다.');
            this.setState({state:'waiting', message:'클릭해서 시작하세요.'});
            clearTimeout(this.timer);
        }else if(state === 'now'){
            const endTime = new Date().getTime();
            clearTimeout(this.timer);
            const responseTime = endTime - this.startTime;
            this.setState((prevState) => {
                return {result:[...prevState.result, responseTime], state:'waiting', message: '클릭해서 시작하세요.'}
            });
        }
    }

    renderAverage = () => {
        const {result} = this.state;
        return result.length === 0 ?
            null:
            <div>평균 시간{this.state.result.reduce((a,c)=>a+c) / this.state.result.length }ms</div>
    }

    render() {
        const {state, message, result} = this.state;
        return (
            <>
                <div
                    id="screen"
                    className={state}
                    onClick={this.onClickScreen}
                >
                    {message}
                </div>
                {this.renderAverage()}
                {
                    result.map(v => <div>{v}ms</div>)
                }
            </>
        );
    }
}

export default Response;

강의 보기 전에 미리 코딩해봄. 그래서 강의랑 좀 다름.

 

※ setTimeout은 call stack으로 넘어가서 실행이 되는데 콜스택으로 넘어가더라도 clearTimeout으로 취소 가능함.

jsx안에서 for문과 if문을 사용하는것이 매우 힘들다.

그래서 map, reduce같은 배열의 메서드를 사용하는데 이번에는 반응속도 게임을 만들어보면서 평균 시간을 구할 때 reduce를 사용해보자.

 

renderAverage = () => {
        const {result} = this.state;
        return result.length === 0 ?
            null:
            <div>평균 시간{this.state.result.reduce((a,c)=>a+c) / this.state.result.length }ms</div>
}

render() {
        const {state, message, result} = this.state;
        return (
            <>
                <div
                    id="screen"
                    className={state}
                    onClick={this.onClickScreen}
                >
                    {message}
                </div>
                {this.renderAverage()}
            </>
        );
    }

보통 JSX에서 조건문을 사용할 떄는 삼항연사자를 많이 사용하거나 값A && 값B 이러한 평가식을 많이 사용한다.

해당 예시에서는 삼항연사자를 사용했다.

 

result.length === 0 ? null : <div>{~}</div>

이렇게 하면 result.length가 0이 아닐 경우에만 <div>{~}</div>를 렌더링한다.

1. 클래스 컴포넌트의 render() 함수 안에서 this.setState() 하지않기

this.setState()를 하면 리렌더가 일어나서 redner()함수가 재실행되는데 거기서 this.setState()를 해버리면 무한루프가 되어버려 성능상에 문제가 생긴다.

 

2. props는 부모가 바꿔줘야한다.

하지만 실무에서는 props를 자식에서 바꿔야하는 경우가 생기는데 그때는 state로 만들어서 바꾼다고 한다.  그래야 부모에게 영향이 안간다. (아니 setState하는 것을 부모가 자식에게 넘겨주면 안되나...???) 

import React, {memo, useState} from "react";

const TryHooks = memo(({index, value, test}) => {
    const [result, setResult] = useState(value);

    const onClick = () => {
        setResult('1');
    }
    return (
        <li onClick={onClick}>
            {index}. <b>{result}</b> {test}
        </li>
    )
});
TryHooks.displayName = 'TryHooks'
export default TryHooks;

요런 느낌이다.

프롭스를 바꾸면 안되는 이유가 자식이 프롭스를 바꾸면 부모가 뜻하지 않게 바뀌어버리기 때문이라고 한다...

 

 

[클래스 컴포넌트에서 프롭스를 state로 변경하는 예시]

class Try extends PureComponent {
	
    //프롭스를 state로 만들어준다. (변경하기 위해)
    state = {
    	result: this.props.result,
        try: this.props.try,
    }
	
    render(){
    	...
    }

}

 

3. 컨텍스트

컨텍스트란? 프롭스를 바로 전달해줄 수 있는 프롭스의 진화형

 

예를 들어 A -> B -> C -> D -> E -> F -> G 이런 컴포넌트 구조가 있을 때 A -> ... -> G 로 프롭스를 넘기려고 한다고 하자.

프롭스는 자식에게 밖에 전달하지 못하므로 중간에 B,C,D,E,F를 거쳐서 가야한다. 프롭스를 받는다는 것은 잘못하면 리렌더링이 일어난다는 의미와 같다.

쓸데없이 가지고 있으면 리렌더링이 일어나는 위험이 높아져버린다.

 

바로 전달해주는 방법이 필요한데 그것이 바로 컨텍스트, 리덕스이다. 

React.createRef는 클래스 컴포넌트에서도 함수 컴포넌트의 useRef hooks처럼 사용 할 수 있는 방법이다.

useRef처럼 사용하기 때문에 current도 붙여줘야한다.

 

react에서 dom reference를 가져오는 방법

1. class에서 가져오기

input;
onRefInput = c =>{
    this.input = c;
}

render(){
	...
    <input ref={this.onRefInput} />
    ...
}

 

2. hooks에서 가져오기

import {useRef} = from "react";

const inputRef = useRef(null);

//ref 사용하기
const use = e => {
	inputRef.current.focus();
}

return (
	...
    <input type="text" maxLength={4} ref={inputRef}
    ...
)

 

 

근데 클래스 컴포넌트에서도 함수 컴포넌트처럼 가져오는 방법이 있다.

import React, {Component, createRef} from "react";

createRef를 import한다.

 

/*
onRefInput = c => {
    this.input = c;
}
*/

//위의 코드를 대체한다.
input = createRef();

 

render(){
	return (
    	<input ref={this.input} />
    )
}

이렇게 하면 된다.

 

근데 사용할 때도 hooks와 같이 current를 사용해줘야한다.

this.input.current.focus(); 이렇게. current를 더 써줘야하니까 더 안좋은게 아니냐고 반문할 수도 있지만 hooks와 통일성이 있기 때문에 외울 것이 하나 줄어드는 장점을 가진다.

 

 

shouldComponentUpdate 을 구현하는 것이 복잡하다면 extends Component 대신 extends PureComponent를 하면 된다. PureComponent는 shouldComponentUpdate를 알아서 구현한 컴포넌트라고 한다.

 

PureComponent의 원리는 state들이 바뀌었는지 안바뀌었는지 판단해서 shouldComponent에 return true를 할지 false를할지 결정한다. 하지만 state가 array이거나 object라면 불변성을 지켜야만 변경점을 알아차리고 리렌더링을 해준다. (이건 일반 Component도 마찬가지)

 

강의에서 말하고 싶었던 것 : {a:1}에서 setState {a:1}을 할 때 새로 렌더링하므로 state에 객체 구조를 안쓰는게 좋다고한다.

(배열안에 객체넣고 그 안에 배열넣고 이렇게 자료구조를 짜는 것은 적합하지 않다고함.)

 

클래스 컴포넌트의 PureComponent 정리

1. PureComponent는 state가 바뀌었는지 안바뀌었는지를 알아차리기 때문에 바뀌는 경우에만 리렌더링을 한다. (그러므로 PureComponent 를 자주 써라) 

2. state가 객체나 배열이라면 불변성을 지키기 위해 spread 연산자를 사용해서 매번 새로운 배열, 객체를 만들어주자.

 

 

3-9번 부터의 목표

숫자야구에서 input창에 글을 쓰면 <TryHooks /> 컴포넌트까지 리렌더링이 되어서 성능 낭비가 발생한다.

부모 컴포넌트가 리렌더링 되면서 자식 컴포넌트까지 리렌더링 되는 상황인데 이것을 해결하고자 한다.

 

React.memo

그렇다면 어떻게 해결해야할까?

<TryHooks /> 는 클래스형 컴포넌트가 아니라 함수 컴포넌트라서 PureComponent을 사용할 수 가 없다.

함수 컴포넌트에서도 PureComponent 와 같은 역할을 해주는 것이 있는데 React.memo를 사용하면 된다.

 

memo의 역할은 부모 컴포넌트가 리렌더링 됐을 때 자식 컴포넌트가 리렌더링 되는 것을 막아준다.대신에 state나 props가 바뀌었을 때는 여전히 리렌더링 된다. (당연히 되어야하는 것이기도 하다.)

import React, {memo} from "react";

const TryHooks = memo(({index, value, test}) => {
    return (
        <li>
            {index}. <b>{value}</b> {test}
        </li>
    )
});
TryHooks.displayName = 'TryHooks'
export default TryHooks;

memo로 감싸주기만 하면 된다.

TryHooks.displayName = 'TryHooks'

해당 부분은 memo를 감싸면 개발자도구에서 보는 컴포넌트 이름이 이상하게 나오는데 그것을 우리가 설정한 이름으로 보이게 해주는 부분이다.

 

 

1. Props를 자주 사용하다 보면 생기는 문제

렌더링이 자주 일어나서 성능이 안좋아지는 문제가 있다.

 

해당 문제를 해결하기 위해서 크롬 확장 프로그램 중에 React Devtools를 설치해서 props나 state부분을 살펴볼 수 있다는 내용이었음..

 

리렌더가 되는 타이밍 : state나 props가 바뀌었을 때 (props가 state였을 때 변경된다.. 일반 변수 넘긴 후 변경해봤는데 안됨..)

 

강의 내용에서 클래스 컴포넌트로 테스를 해봤는데 리렌더가 되는 타이밍은 사실 this.setState가 호출되는 시점이었다. (state가 바뀌지 않은 상황이더라도 호출은 되었으니 리렌더가 일어남.)

 

이것을 방지하려면

shouldComponentUpdate

을 구현해서 이전값과 다를 때만 렌더링되도록 true를 반환하면 되는데 클래스컴포넌트 문법이라서 따로 따라하지는 않았다.

Hooks 변경한 코드

import React, {Component, useState, useRef} from "react";
import TryHooks from "./TryHooks";

function getNumbers(){
    const numbers = new Array(9).fill(0).map((v,i)=>v+i+1);
    const shuffle = [];
    while(numbers.length > 0){
        const rand = Math.floor(Math.random() * numbers.length);
        const number = numbers.splice(rand,1)[0];
        shuffle.push(number);
    }
    console.log(shuffle);
    return shuffle.slice(0,4);
}

const NumberBaseballHooks = () => {
    const [value, setValue] = useState('');
    const [result, setResult] = useState('');
    const [answer, setAnswer] = useState(getNumbers());
    const [tries, setTries] = useState([]);
    const inputRef = useRef(null);
    const onSubmitForm = e => {
        e.preventDefault();
        const answerStr = answer.join('');
        if(answerStr === value){
            //홈런
            setResult('홈런!');
            setValue('');
            setTries((prev)=>{
                return [...prev, `${value}:홈런!`]
            });
            setTimeout(()=>{
                alert('게임을 다시 시작합니다.');
                setResult('');
                setAnswer(getNumbers());
                setTries([]);
            },100);

        } else {
            if(tries.length >= 9){
                setResult(`10번 넘게 틀려서 실패! 정답은 ${answerStr} 였습니다`);
                setValue('');
                alert('게임을 다시 시작합니다.');
                setResult('');
                setAnswer(getNumbers());
                setTries([]);
            }else{
                //볼, 스트라이크 판단
                let ball = 0;
                let strike = 0;
                for(let i=0; i<4; i++){
                    if(answerStr[i] === value[i]){
                        strike++;
                    }else if(answerStr.includes(value[i])){
                        ball++;
                    }
                }
                setTries((prev)=>{
                    return [...prev, `${value}:${strike}S ${ball}B`];
                })
                setResult('땡');
                setValue('');
            }
        }
        inputRef.current.focus();
    }

    const onChangeInput = e => {
        const value = e.target.value;
        setValue(value);
    }

    return (
        <>
            <h1>{result}</h1>
            <form onSubmit={onSubmitForm}>
                <input type="text" maxLength={4} ref={inputRef} value={value} onChange={onChangeInput}/>
                <button>입력</button>
            </form>
            <div>시도: {tries.length}</div>
            <ul>
                {
                    tries.map((v,i) => {
                        return (<TryHooks key={v+i} value={v} index={i+1} />);
                    })
                }
            </ul>
        </>
    )
}
export default NumberBaseballHooks;

NumberBaseballHooks.jsx

 

import React from "react";

const TryHooks = ({index, value}) => {
    return (
        <li>
            {index}. <b>{value}</b>
        </li>
    )
}
export default TryHooks;

TryHooks.jsx

 

 

근데 여기서 문제가 있었다.

input에 value를 변경하면 setValue가 value state를 변경하면서 리렌더가 되는데 리렌더가 되면서 NumberBaseballHooks 내부의 로직이 재실행 되면서 getNumbers()까지 재호출 되는 문제가 있었다.

그래서 위의 사진과 같이 console.log가 input에 타자 칠 때마다 나오는 것.

 

const [answer, setAnswer] = useState(getNumbers());

해당 코드가 문제였는데 저렇게 한다고해서 state가 리렌더 할 때마다 바뀌는 것은 아니고 호출만 여러 번 되는것이다.

하지만 호출이 되는 것도 문제이다. 

useState가 자동으로 getNumbers()를 첫 번째 호출한 리턴값만 state에 저장하고 그 다음부터는 리턴값을 무시해버린다. 하지만 리렌더 시에 getNumbers함수가 계속 재호출 되긴한다. (호출값을 넣어버렸기 때문에) 만약 getNumbers가 아주 오래걸리는 작업을 하는 함수라면 매우 비효율적이다.

 

그럴 때는 아래와 같이 호출해서 넘기지 말고 함수 자체를 넘기면 된다. (lazy init 이라고 한다.)

const [answer, setAnswer] = useState(getNumbers);

 

useState 용법이 2가지고 있는데

1. useState(초기값) - 일반적으로 쓰이는 경우로 초기값으로 state를 설정해준다.

2. useState(함수) - 함수를 그대로 넣는 경우인데 함수의 리턴값이 초기값으로 설정이 되지만 그 다음부터 이 함수는 실행되지 않는다. 

1. 구조분해 할당으로 state, props 간편하게 사용하기

onSubmitForm = e => {
	const {result, value, tries,answer} = this.state;
 	...
}

render(){
	const {result, value, tries} = this.state;
    ...
}

NumberBaseball.js

const {index, value} = this.props;

Try.js

2. class 밖으로 뺄 수 있는 함수

this를 사용하지 않으면 class 밖으로 뺄 수 있다. (getNumbers 처럼)

 

리액트에서 state를 직접 바꾸는건 금지!

리랜더해주는 조건이 예전 state와 현재 state가 달라야 리랜더를 해주는데 직접 바꿔버리면 달라지지 않는다.

그래서 불변성을 잘 지켜야함. 

특히 객체나 배열! 주의

tries: [...tries, `${this.state.value}: 홈런!`],

-기존 배열을 유지시켜주고 싶다면 spred 연산자를 사용해주면 좋다.

-this.state.tries[0] = 10;, this.state.tires.push(20);      <----이런식으로 하면 안됌!!

 

직접 만든 전체코드

import React, {Component} from "react";
import Try from "./try";

//숫자 내게 랜덤으로 뽑기 함수
function getNumbers(){
    const numbers = new Array(9).fill(0).map((v,i)=>v+i+1);
    const shuffle = [];
    while(numbers.length > 0){
        const rand = Math.floor(Math.random() * numbers.length);
        const number = numbers.splice(rand,1)[0];
        shuffle.push(number);
    }
    console.log(shuffle);
    return shuffle.slice(0,4);
}


class NumberBaseball extends Component{
    state = {
        value:'',
        result:'',
        answer : getNumbers(),
        tries:[],
    }
    onChangeInput = e => {
        const value = e.target.value;
        this.setState({value});
    }

    onSubmitForm = e => {
        e.preventDefault();
        const tries = this.state.tries;
        const ans = this.state.answer.join('')
        if(this.state.value === ans){
            this.setState({
                result: '홈런!',
                value: '',
                tries: [...tries, `${this.state.value}: 홈런!`],
            });
            alert('게임을 재시작합니다.');
            this.setState({
                value:'',
                answer: getNumbers(),
                tries: [],
                result:'',
            })
        } else {
            //볼, 스트라이크 판단
            let ball = 0;
            let strike = 0;

            if(this.state.tries.length >= 9){
                this.setState({
                    result : `10번 넘게 틀려서 실패! 정답은 ${ans} 였습니다`,
                    tries,
                });
                setTimeout(()=>{
                    alert('게임을 재시작합니다.');
                    this.setState({
                        value:'',
                        answer: getNumbers(),
                        tries: [],
                        result:'',
                    })
                },10);
            }else{
                for (let i = 0; i < this.state.value.length; i++) {
                    if (this.state.value[i] === ans[i]) {
                        strike++;
                    } else if (this.state.value.indexOf(ans[i]) > -1) {
                        ball++;
                    }
                }
                if(ball === 0 && strike === 0){ //out
                    tries.push(`${this.state.value}: OUT!`);
                }else{
                    tries.push(`${this.state.value}: ${strike}S ${ball}B`);
                }

                this.setState({
                    value: '',
                    result:'땡',
                    tries,
                });
            }

        }
        this.input.focus();
    }

    input;
    onRefInput = c =>{
        this.input = c;
    }
    fruits = [
        {fruit: '사과', taste:'맛없다.'},
        {fruit: '바나나', taste:'맛없다.'},
        {fruit: '호랑이', taste:'맛없다.'},
        {fruit: '구울', taste:'맛없다.'},
        {fruit: '구렁이', taste:'맛없다.'},
        {fruit: '고우와니', taste:'맛없다.'},
    ]

    render() {
        return (
            <>
                <h1>{this.state.result}</h1>
                <form onSubmit={this.onSubmitForm}>
                    <input type="text" maxLength={4} ref={this.onRefInput} value={this.state.value} onChange={this.onChangeInput}/>
                    <button>입력</button>
                </form>
                <div>시도: {this.state.tries.length}</div>
                <ul>
                    {
                        this.state.tries.map((v,i) => {
                            return (<Try key={v+i} value={v} index={i+1} />);
                        })
                    }
                </ul>
            </>
        );
    }
}
export default NumberBaseball;

NumberBaseball.jsx

 

import React, {Component} from "react";

class Try extends Component{
    render() {
        return (
        <li>
            {this.props.index}. <b>{this.props.value}</b>
        </li>
        );
    }
}
export default Try;

 

Try.jsx

리액트의 대부분의 문제는 props에서 발생한다.

자식의 자식에게 물려준다거나 더 깊이 들어가는 복잡한 경우가 생긴다.

그것을 방지하기 위해서 컨텍스트, 리덕스 같은 기술이 쓰인다.

(리액트에는 컨텍스트가 있고 컨텍스트가 좀 더 복잡한 일을 할 수 있게 만든 것이 리덕스(은행역할))

 

리액트 JSX 에서 주석처리하기

{/*<h1>{this.state.result}</h1>*/}

 

바인딩 부분은 영상을 보는게 나을 것 같다.

중요한 내용은 아니고 class 컴포넌트에서 화살표 함수를 쓰는 이유를 알려주심. (this를 신경쓰지 않기 위해서.. 화살표함수는 bind(this);를 자동으로해준다.)

 

+ Recent posts