해당 강의에서부터 클래스 컴포넌트와 함수컴포넌트(hooks)가 조금씩 달라진다.

Hooks에는 componentDidMount / componentDidUpdate / componentWillUnmount 이런 메서드가 존재하지 않는다.

1:1로 대응되진 않지만 useEffect로 비슷한 역할을 하게 할 순 있다.

 

useEffect

useEffect(()=>{
	//componentDidMount + componentDidUpdate 합쳐놓은 역할
    
    
    return ()=>{ //componentWillUnmount역할
    }

},[]);

 

내 코드는 강의 코드와 달라서 설명할 수 없지만 강의에서 외워두라고 한것!

  1. useEffect의 두 번째 파라미터는 어떤 값이 바뀔 때마다 useEffect를 실행하고 싶은지 설정하고 싶을 때 사용한다. 위의 예시처럼 빈 배열 [] 을 넣는다면 최초 렌더링 시 딱 한번 실행한다는 의미.
  2. 강의 코드
useEffect(()=>{
	console.log('다시 실행');
    interval.current = setInterval(()=>{setImageCoord(0);},100);
    
    return ()=>{
    	console.log('종료');
        clearInterval(interval.current);
    }
},[imgCoord]);

imgCoord가 바뀔 때마다 해당 useEffect가 실행된다.

그래서 해당 코드는 interval실행하고 return 부분에 있는 코드에 의해 clearInterval을 반복하기 때문에 setTimeout과 동일한 기능을 한다.

 

함수 컴포넌트의 특성 상 state/props가 바뀌면 함수 내부의 코드가 전체 재실행 된다. (클래스형 컴포넌트는 render함수만 재실행) 

 

useEffect에 대해서 외워야할 것 정리

■ 라이프사이클
◎ 해당 컴포넌트가 마운트될 때 실행되는 함수
◎ 해당 컴포넌트가 언마운트될 때 실행

useEffect(()=>{
    return ()=>{
    	//clean up 함수  --> 컴포넌트가 언마운트될 때 실행
    }
},[])


※ 언마운트 될 때는?(JSX라고 가정) showComponent가 false가 될 때 언마운트 되고, true가 될 때 마운트 된다.

{
	showComponent && <Todo/>
}



◎ 리렌더시 마다 실행 (보통 실수일 가능성이 높다.)

useEffect(()=>{
})



◎ todo가 바뀔 때마다 실행 (clean up과의 순서가 매우 중요함)

useEffect(()=>{
	//todo가 바뀔 때 마다 실행됨.
	return ()=>{
		//todo가 바뀌기 직전에 실행됨.
	}
},[todo])


※ 순서가 중요하다 (clean up은 바뀌기 전의 값이라고 생각하자!)
todo가 a->b로 바뀐다고 가정하면
1. a의 useEffect 실행 (최초 1회만 실행되는거 같다.)
2. a의 clean up  실행 
3. b의 useEffect 실행 

 

그 다음에 b -> c로  바뀐다면?

1. b의 clean up 실행

2. c의 useEffect실행 

만약에 a,b,c가 바뀔 때마다 실행되고 싶은데 각각 실행되는 코드가 다르다면? useEffect를 여러개 만들면 된다.
그리고 dependency 배열에 각각 넣어주면됨.

ComponentDidMount

  • render()가 최초 실행 후 실행되는 메서드. (state변경으로 인한 리렌더링이 일어나도 해당 메서드는 재실행되지 않는다.)
  • 해당 메서드에 비동기 요청을 많이 한다.

[비동기 함수 중 setInterval 주의 점]

  1. setInterval을 하고 취소를 하지 않으면 해당 컴포넌트가 사라져도 무한히 실행된다.
  2. setInterval을 하고 취소를 하지 않고, 해당 컴포넌트를 제거, 생성을 반복하면 취소되지 않은 setInterval은 중첩되어 문제가 생긴다.

 

 

복습 : 라이프사이클

클래스 컴포넌트 라이프사이클

  • 생성자(constructor) → render → ref설정 → componentDidMount
  • setState / props 바뀔 때 → shouldComponentUpdate (리렌더할지말지 결정하는 함수) → render → componentDidUpdate
  • 부모가 나를 없앴을 때 → componetWillUnmount → 소멸

 

라이프사이클 관련 메서드

componentDidMount : 렌더가 처음실행되고 componentDidMount가 실행된다. (state변경으로 인한 리렌더링이 일어나도 실행되지 않는다.)

componentDidUpdate  : 리렌더링 후에는 해당 메소드가 실행된다.

componentWillUnmount : 컴포넌트가 제거되기 직전

 

 

RSP 강의 안보고 짠 전체코드

import React, {Component} from "react";

//클래스의 경우 -> constructor -> render -> ref설정 -> componentDidMount ->
// setState, props 바뀔 때 -> shouldComponentUpdate -> render -> componentDidUpdate
// 부모가 나를 없앴을 때 componentWillUnmount -> 소멸
class RSP extends Component{
    state = {
        result:'',
        score:0,
        imgCoord:0,
    }

    intervalId = null;
    position = 0;
    clickable = true;

    // 렌더가 처음실행되고 componentDidMount가 실행된다. (state변경으로 인한 리렌더링이 일어나도 실행되지 않는다.)
    componentDidMount() {
        //여기에 비동기 요청을 많이 한다.
        //만약에 setInterval을 여기다가 쓰고 취소하는 코드를 쓰지않으면 RSP 컴포넌트가 사라지더라도 계속 setInterval은 실행된다.
        //만약에 RSP 컴포넌트가 붙었다가 떼어지면 setInterval이 1번 중첩되어있지만 RSP컴포넌트가 한번 더 붙으면 setInterval이 2번 중첩되어 실행된다.
        this.startRsp();
    }

    //리렌더링 후에는 해당 메소드가 실행된다.
    componentDidUpdate(prevProps, prevState, snapshot) {
    }

    //컴포넌트가 제거되기 직전
    componentWillUnmount() {
        //componentDidMount, componentDidUpdate에서 비동기 작업을 했는데 그게 남아있으면 문제가 되기 떄문에 그런 애들을 정리해주는 것이 componentWillUnmount 이다.

        clearInterval(this.intervalId);
    }

    onClick = (e) => {
        if(!this.clickable)return; //연속클릭 방지
        this.clickable = false;
        const {target} = e;
        const id = target.id;
        clearInterval(this.intervalId);
        let userValue = 0;
        if(id === 'scissor'){
            userValue = 1; 
        }else if(id === 'paper'){
            userValue = 2;
        }

        if(userValue === this.position){
            console.log('무승부!');
            this.setState({result:'비겼습니다.'});
        } else if((userValue+1) % 3 === this.position){
            console.log('이겼어');
            this.setState((prev) => ({...prev, score:prev.score+1, result:'이겼습니다.'}));
        } else{
            console.log('졌어')
            this.setState((prev) => ({...prev, score:prev.score-1, result:'졌습니다.'}));
        }
        setTimeout(()=>{this.startRsp(); },1000)
        setTimeout(()=>this.clickable = true, 1500)
    }

    startRsp(){
        const positionArr = [0, 142, 284];
        let currentIndex = 0;
        if(this.intervalId)clearInterval(this.intervalId);
        this.intervalId = setInterval(()=>{
            this.position = currentIndex % positionArr.length;
            const pix = positionArr[this.position];
            this.setState({imgCoord:`-${pix}px`});
            //console.log(positionArr[index], this.state);
            currentIndex++;
        },100);
    }

    render() {
        const {result, score, imgCoord} = this.state;

        return(
            <>
                <div id="computer" style={{background:`url(https://en.pimg.jp/023/182/267/1/23182267.jpg) ${imgCoord} 0`}}></div>
                <button id="rock" className="btn" onClick={this.onClick}>바위</button>
                <button id="scissor" className="btn" onClick={this.onClick}>가위</button>
                <button id="paper" className="btn" onClick={this.onClick}>보</button>
                <div>{score}</div>
                <div>{result}</div>
            </>
        )
    }
}

export default RSP

렌더함수가 실행되면 React가 JSX를 DOM에 딱 붙여주는데 그 순간을 Catch하여 무언가 할 수 있다.

 

React의 라이프사이클 소개

우선 class 기반으로 라이프사이클을 알아보자.

1. client.jsx에 의해 렌더링 되었을 때

constructor -> render -> ref설정 -> componentDidMount -> 화면에 표시된다.

 

2. setState / props 바뀌었을 때 

shouldComponentUpdate -> render -> componentDidUpdate

 

3. 부모가 나를 없앴을 때

componentWillUnmount -> 소멸

강의를 보지않고 혼자서 hooks로 전환했을 때는 버그가 있었다. setTimeout이 제대로 취소되지 않았고, startTime이 제대로 설정되지 않아서 평균 반응속도가 이상하게 나오는 현상이 발생했다. 

강의를 보고 이유를 알아보니 React Hooks에서 값이 바뀌는 부분은 그냥 사용하면 안되고 useRef를 사용해야한다는 것을 새롭게 알게 되었다.

useRef vs useState

useRef와 useState의 차이 : 렌더링의 차이가 있다.

useRef는 값이 바뀌어도 렌더링이 안되지만 useState는 값이 바뀌면 리렌더링이 발생한다.

 

useRef 사용처 2가지

1. useRef는 DOM을 넣는거 말고 다른 방식으로 사용되는게 값이 바뀌기는 하지만 화면에 영향을 끼치고 싶지는 않을 때 사용한다.

2. DOM을 넣어두고 DOM을 직접 제어하고 싶을 때 사용

 

※ useRef는 current써주는것에 주의해야한다.

 

import React, {useRef, useState} from "react";
import response from "./Response";

const ResponseHooks = () => {
    const [state,setState] = useState('waiting');
    const [message, setMessage] = useState('클릭해서 시작하세요.');
    const [result, setResult] = useState([]);
    const timer = useRef(null);
    const startTime = useRef(0);

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

    }

    const onResetClick = () => {
        setResult([]);
    }

    const renderAverage = () => {
        return result.length === 0 ?
            null:
            <>
                <div>평균 시간{result.reduce((a,c)=>a+c) / result.length }ms</div>
                <button onClick={onResetClick}>리셋</button>
            </>
    }

    return(
        <>
            <div
                id="screen"
                className={state}
                onClick={onClickScreen}
            >
                {message}
            </div>
            {renderAverage()}
        </>
    )
}

export default ResponseHooks;

hooks로 변환된 ResponseHooks.jsx 전체코드

반응속도체크 코딩

전체코드

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>를 렌더링한다.

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와 통일성이 있기 때문에 외울 것이 하나 줄어드는 장점을 가진다.

 

 

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 처럼)

 

+ Recent posts