useReducer 복습

  • Redux에서 차용한 것
  • state를 하나로 묶어주는 역할 state를 바꿀 때는 action을 dispatch해서 reducer에서 바꾼다.
  • Redux와의 차이점 : Redux는 state를 동기적으로 바꾸는데 반해, reducer는 비동기적으로 바꾼다.

Context API

  • props를 넘겨줄 때 부모와 자식 관계가 깊어질 수록 전달하는 것이 힘들어짐.
  • 그것을 해결하기 위한 API

state를 바꾸고자 할 때 props로 받은 dipatch로 부모 컴포넌트에 액션을 보내는 방식 대신 Context API로 바로 보내는 방식을 사용할 것.

 

MineSearch.jsx

import React from 'react';
import MineTable from "./MineTable";
import {useReducer} from "react/index";
import Form from "./Form";

const initalState = {
    tableData: [],
    timer: 0,
    result: '',
}

const reducer = (state, action)=>{
    switch (action.type){
        default:
            return state;
    }
}
const MineSearch = () => {
    const[state,dispatch]=useReducer(reducer(), initalState);

    return (
        <>
            <Form />
            <div>{state.timer}</div>
            <MineTable />
            <div>{state.result}</div>
        </>
    );
};

export default MineSearch;

 

Form.jsx

import React, {useCallback, useState} from 'react';

const Form = () => {
    const [cell,setCell]=useState(10);
    const [row,setRow]=useState(10);
    const [mine,setMine]=useState(20);

    const onChangeRow = useCallback(e=>{
        setRow(e.target.value);
    },[]);

    const onChangeCell = useCallback(e=>{
        setCell(e.target.value);
    },[]);

    const onChangeMine = useCallback((param) => (e)=>{
        console.log(param);
        setMine(e.target.value);
    },[]);

    const onClickButton = useCallback(()=>{

    },[]);

    return (
        <div>
            <input type="number" placeholder="세로" value={row} onChange={onChangeRow}/>
            <input type="number" placeholder="가로" value={cell} onChange={onChangeCell}/>
            <input type="number" placeholder="지뢰" value={mine} onChange={onChangeMine(1)}/>
            <button onClick={onClickButton}>시작</button>
        </div>
    );
};

export default Form;

 

MineTable.jsx

import React from 'react';

const MineTable = () => {
    return (
        <table>
            
        </table>
    );
};

export default MineTable;

 

MineTr.jsx

import React from 'react';

const MineTr = () => {
    return (
        <div>
            
        </div>
    );
};

export default MineTr;

 

MineTd.jsx

import React from 'react';

const MineTd = () => {
    return (
        <div>
            
        </div>
    );
};

export default MineTd;

1. useCallback 사용하기

보통 함수를 props로 넘겨줄 때 useCallback을 사용하여 넘겨준다. (굳이 props로 안넘기는 상황에서도 많이 사용하는거 같음) 

근데 useCallback 주의점은 함수 내부에서 '바뀌는' state나 props를 사용하는 로직이 있다면 의존배열에 꼭 추가시켜주어야한다.

기억력이 너무 강력해서 처음 state값 그대로 가지고 있어버리는 문제를 발생시킬 수 있다.

 

2. 어떤 값 때문에 리렌더링 되는지 찾는 방법

const Td = ({rowIndex, cellIndex, dispatch, cellData}) => {
	const ref = useRef([]);
    useEffect(()=>{
    	console.log(rowIndex === ref.current[0], cellIndex=ref.current[1], dispatch=ref.current[2],cellData=ref.current[3]);
    	ref.current = [rowIndex, cellIndex, dispatch, cellData];
    },[rowIndex, cellIndex, dispatch, cellData]);
}

console.log에서 false가 나오는 값 때문에 리렌더링이 일어나는 것이다.

 

3. React.memo 사용하기

부모가 리렌더링 될 때 자식의 불필요한 리렌더링을 막고 싶다면 React.memo를 사용하자!

(강의에서는 이것으로 해결함!! 내가 클릭한 td만 리렌더링 되도록)

 

4. useMemo로 컴포넌트 자체를 기억할 수도 있다.

최후의 수단으로 활용해보라고 하심.

내부에 들어있는 내용물이 바뀌었을 때만 새로 랜더링하고 그 외에는 랜더링하지 않는 예제.

const Tr = ({rowData, rowIndex, dispatch})=>{
	...
    return (
    	{
        	Array(rowData.length).fill().map((td,i)=>{
            	useMemo(
                	() => <Td ~ ></Td>,
                    [rowData[i]]
                )
            });
        }
    )
}

 

※ react dev tools로 highlight를 사용해 최적화해야할 것들을 고를 때 빨간색만 조심하면 된다고하심. 실제로 빨간색은 서비스할 때 성능상의 문제가 나타난다고 하셨다.

액션 처리 순서

이벤트 발생 ▶ 액션생성 ▶ dispatch 호출 ▶ reducer에서 액션 처리

 

액션 타입들은 export 해주는 게 좋다.

export const SET_WINNER = 'SET_WINNER';
export const CLICK_CELL = 'CLICK_CELL';

자식이나 다른 js에서도 dispatch를 호출할 때 액션 타입을 가져다 쓰기위해서 export를 붙인다.

하지만 내가 직접 코딩한 경우 자식에서 dispatch를 호출하는 경우가 없었기 때문에 사실 붙이지 않아도 무방했다.

 

useReducer / redux 에서는 불변성을 꼭 지켜야한다. (사실 react 전체적으로 중요함)

setState함수를 설명할 때도 불변성에 대해서 계속 설명을 했었다.

reducer함수도 결국에는 state를 변경하는 함수이기 때문에 state를 변경할 때 객체상태(object state)는 무조건 불변성을 지켜주어야한다.

...
case CLICK_CELL:
	const tableData = [...state.tableData];
    tableData[row] = [...tableData[row]];
    tableData[row][cell] = state.turn;
	return {
    	...state,
        tableData,
    }

...

나중에는 immer.js라는 라이브러리로 가독성과 편리함을 지킬 수 있다고한다. (무료강좌에서는 immer의 존재만 알고있으라고하심)

 

불변성을 지킨다라는 것은?

메모리영역에서 값을 변경할 수 없게 한다. (값을 바꿀 때 값이 아닌 콜스택의 주소값이 바뀌게 한다.)

그렇다면 왜 react에서 불변성을 지켜야하는가?

state변화 감지 기준이 콜스택의 주소값이기 때문에 콜스택의 주소값을 변경해야 state가 변경되었구나! 하고 리렌더링을 시켜주기 때문이다.

원시값은 그냥 변경해도 불변성이 지켜지지만 (콜스택의 주소값이 변경되지만) 참조타입의 경우 콜스택의 주소값이 변경되지 않기 때문에 spread연산자나 immer 라이브러리를 사용해야 한다.

 

const a = {b:1, c:2};
const b = a;
console.log(b===a); //true : 불변성이 지켜지지 않는다! (객체가 같다는 것은 참조주소가 같다는 것)

const c = {...a};
console.log(c === a); //false : 불변성이 지켜짐.

위의 내용은 spread연션자로 불변성을 지킨 예시이다. 그냥 대입해버리면 주소값이 대입되어버려서 불변성이 지켜지지 않지만 spread 연산자로 얉은 복사를 하면 불변성이 지켜지는 것을 볼 수 있다.

 

ContextAPI를 쓰는 경우

아직 배우지 않았지만 강좌에서 dispatch 함수를 tictactoe ▶ table ▶ tr ▶td 까지 넘겨줘야하는 경우가 있었다. 만약에 부모~넘겨줘야하는 자식 컴포넌트 사이에 10개의 컴포넌트가 있으면 얼마나 힘들까?

이럴 때 쓰는 API가 ContextAPI 라고 한다. tictactoe에서 바로 td로 props를 넘길 수 있게 해준다고 한다.

강좌에서는 ContextAPI의 필요성을 느끼기 위해서 없이 넘기셨다고 함.

 

참고

https://narup.tistory.com/268

 

[React] 불변성이란? 불변성을 지켜야 하는 이유

1. 개요 React를 빠르게 배울 때 값을 변경할 때 useState를 사용해야 한다, 불변성을 유지해야 한다, immer를 사용해야 한다, spread 연산자를 사용해야 한다, 공식처럼 생각하고 있었는데 javascript의 메

narup.tistory.com

 

import React, {useCallback, useEffect, useReducer} from 'react';
import Table from "./Table";

const initialState = {
    winner: '',
    turn : 'O',
    tableData : [['','',''],['','',''],['','','']],
    recentPos: [], //해당 값이 바뀔 때마다 useEffect에서 게임이 끝났는지 체크, 턴 변경
}

const SET_WINNER = "SET_WINNER";
const CLICK_CELL = 'CLICK_CELL';
const TURN_CHANGE = 'TURN_CHANGE';
const RESET_GAME = 'RESET_GAME';

const reducer = (state,action) => {
    switch (action.type){
        case SET_WINNER:
            return {
                ...state,
                winner: action.winner,
            }
        case CLICK_CELL:
            const {row,cell} = action;
            const tableData = [...state.tableData];
            tableData[row] = [...tableData[row]];
            if(tableData[row][cell] === ''){
                tableData[row][cell] = state.turn;
                return {
                    ...state,
                    tableData,
                    recentPos: [row,cell],
                }
            }
            return {
                ...state,
            }
        case TURN_CHANGE:
            return {
                ...state,
                turn : state.turn === 'O' ? 'X' : 'O',
            }
        case RESET_GAME:
            return {
                winner: '',
                turn : 'O',
                tableData : [['','',''],['','',''],['','','']],
                recentPos: [], //해당 값이 바뀔 때마다 useEffect에서 게임이 끝났는지 체크, 턴 변경
            }
    }
}

const TicTacToe = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    const tableOnClick = useCallback((e) => {
        if(state.winner !== '') return ;
        const {target} = e;
        const tr = target.parentElement;
        const rowIndex = tr.rowIndex;
        const cellIndex = target.cellIndex;
        dispatch({type:CLICK_CELL, row:rowIndex, cell:cellIndex});
    },[state.winner]);

    const winnerCheck = ({recentPos, tableData, turn}) => {
        let end = true;
        //가로줄 검사

        tableData[recentPos[0]].forEach(value => {
            if(value !== turn) end = false;
        });
        if(end) return true;
        //세로줄 검사
        end=true;
        tableData.forEach((arr,index)=>{
            if(arr[recentPos[1]] !== turn) end = false;
        });
        if(end)return true;

        //대각선 검사
        if((tableData[0][0] === turn && tableData[1][1] === turn && tableData[2][2] === turn)
            || (tableData[0][2] === turn && tableData[1][1] === turn && tableData[2][0] === turn) ){
            end = true;
        }
        return end;
    }
    const fullCheck = (tableData) => {
        let isFull = true;
        tableData.forEach(arr => arr.forEach(v => {
            if(v === '')isFull = false;
        }));
        return isFull;
    }
    const onResetClick = () => {
        dispatch({type:RESET_GAME});
    }

    useEffect(()=>{
        const {recentPos, tableData, turn} = state;
        if(recentPos.length ===0)return ;
        const end = winnerCheck(state);
        if(end){
            //승리자 나옴.
            dispatch({type:SET_WINNER, winner:turn});
        } else {
            if(fullCheck(tableData)){
                dispatch({type:SET_WINNER, winner:'draw!!!'});
            }else{
                dispatch({type:TURN_CHANGE});
            }
        }
    },[state.recentPos]);

    return (
        <>
            <Table onClick={tableOnClick} tableData={state.tableData}/>
            {
                state.winner &&
                <>
                    <div>WINNER IS {state.winner}</div>
                    <button onClick={onResetClick}>RESET</button>
                </>
            }
        </>
    );
};

export default TicTacToe;

디스패치 / 액션 / 리듀서

useReducer를 사용하면 state를 변경해야할 때 dispatch함수를 호출하고, 액션 객체를 전달해서 state를 변경한다.

  • dispatch : 액션을 실행해주는 함수.
  • 액션 객체 : dispatch에 전달되는 객체로 리듀서가 실행해야할 로직을 알려주는 action.type과 리듀서가 변경해야할 state값을 가지고 있다.
  • reducer : 액션 객체를 해석해서 state를 직접 바꿔주는 함수. (리듀서는 dispatch가 실행될 때 마다 자동으로 호출된다.) --> reducer함수에서 반환하는 값은 새로운 state이다.

useReducer를 사용하면 state 변경 순서는 아래와 같아진다.

  1. 이벤트 발생
  2. dispatch 호출 (이벤트 객체를 만들어서 dispatch 함수 호출 시 이벤트 객체를 넘겨준다.)
  3. dispatch가 호출되면 자동으로 리듀서가 호출되면서 액션 객체를 해석하여 특정 로직 실행 (state변경)

 

reducer함수에서 주의해야할 점은 불변성을 꼭 지켜야한다는 것!

reducer함수에서 반환하는 값은 새로운 state인데 항상 새로운 객체를 만들어 바뀐 값만 바꿔줘야한다. 이를 위해 spread연산자를 활용해서 불변성을 지키자!

 

ACTION TYPE

  • 액션 타입은 리듀서함수에서 어떤 state를 수정할 로직을 실행해야할지 결정하는 중요한 정보이다.
  • 액션 타입은 상수로 따로 빼두는 것이좋다.
  • 액션 타입은 대문자로 하는 것이 커뮤니티 규칙이다.

 

 

TicTacToe.jsx

import React, {useCallback, useReducer, useState} from "react";
import Table from "./Table";

//state 초기값
const initialState = {
    winner : '',
    turn: 'O',
    tableData:[['','',''],['','',''],['','','']],
}
//액션 타입은 상수로 따로 빠두는것이 좋다.
//액션의 이름은 대문자로 하는것이 커뮤니티 규칙!
const SET_WINNER = 'SET_WINNER';

//리듀서 == 함수
const reducer = (state, action) => {
    //리듀서는 액션을 디스패치 할 때마다 실행된다.
    switch(action.type){
        case SET_WINNER:
            //state.winner = action.winner; 이렇게 하면 안된다!!!!
            //우리는 항상 새로운 객체를 만들어서 바뀐 값만 바꿔줘야한다. 불변성 지켜!.
            return {
                ...state,
                winner: action.winner,
            }

    }
}

const TicTacToe = () => {
    /*
    const [winner, setWinner] = useState('');
    const [turn, setTurn] = useState('O');
    const [tableData, setTableData] = useState([['','',''],['','',''],['','','']]);
    */
    const [state, dispatch] = useReducer(reducer, initialState);

    //컴포넌트에 있는 함수들은 다 useCallback으로 감싸주자.
    const onClickTable = useCallback(()=>{
        //dispatch는 액션을 실행해주는 함수이다. dispatch에 전달되는 값은 액션객체이다.
        //액션 객체를 해석해서 state를 직접 바꿔주는 역할을 하는 것이 리듀서이다.
        dispatch({type:SET_WINNER, winner:'O'});
    },[]);

    return(
        <>
            <Table onClick={onClickTable} tableData={state.tableData}/>
            {
                state.winner && <div>{state.winner}님의 승리</div>
            }
        </>
    );
}

export default TicTacToe;

 

Table.jsx

import React from 'react';
import Tr from "./Tr";

const Table = ({onClick, tableData}) => {
    return (
        <table onClick={onClick}>
            {Array(tableData.length).fill().map((tr,i)=><Tr rowData={tableData[i]}/>)}
        </table>
    );
};

export default Table;

 

Tr.jsx

import React from 'react';
import Td from "./Td";

const Tr = ({rowData}) => {
    return (
        <tr>
            {
                Array(rowData.length).fill().map((td)=><Td/>)
            }
        </tr>
    );
};

export default Tr;

 

useReducer 왜 필요한가?

state의 갯수가 너무 많아지면 state와 setState함수들의 관리가 매우 어려워 지고, props로 넘겨줄 때 매우 많은 것을 넘겨줘야하는 경우가 왕창 생긴다.

그럴 때 state자체의 갯수를 줄이기 위해 필요하다고 함. (아직 감이 안오긴한다.)

 

TicTacToe.jsx의 구조는 아래와 같다.

TicTacToe > Table > Tr > Td

state들은 모두 TicTacToe.jsx에 있고 우리가 실제로 누르는 것은 Td컴포넌트 인데.. 이렇게 되면 state들과 setState함수들을 Table, Tr을 거쳐 Td까지 전달해주어야 한다.

 

이런 번거로운 경우를 해결하기 위해 ContextAPI를 많이 사용하지만 ContextAPI는 다음시간에 다뤄볼것이고, 이번에는 state 자체의 개수를 줄이는 useReducer를 다뤄본다고 함.

 

현재 winner, turn, tableData라는 state가 있는데 useReducer를 사용하면 딱 하나의 state와 하나의 setState함수로 통일할 수 있다고 한다.

    //아래의 세가지 스테이트를 한개로 통일 가능.
    const [winner, setWinner] = useState('');
    const [turn, setTurn] = useState('O');
    const [tableData, setTableData] = useState([['','',''],['','',''],['','','']]);
    
    //아래와 같이 통일이 가능하다.
    const [state, dispatch] = useReducer(reducer, initialState);

복잡한 state들이 한개로 통일되니까 가독성이 훨씬 좋아지는 것 같다.

 

1. 이를 위해선 당연히 useReducer의 import가 필요하다.

import {useReducer} from 'react';

 

2. initialState(초기 상태값들)

//state 초기값
const initialState = {
    winner : '',
    turn: 'O',
    tableData:[['','',''],['','',''],['','','']],
}

 

3. reducer (배열의 리듀서 함수처럼 어떤것을 줄이기 위한 함수!)

//리듀서 == 함수
const reducer = (state, action) => {

}

 

※ ContextAPI + useReducer = 소규모 앱에서 리덕스를 대체할 순 있지만 규모가 큰 앱이면 결국엔 리덕스가 필요하다고함.

 

 

현재까지의 코드

TicTacToe.jsx

import React, {useReducer, useState} from "react";
import Table from "./Table";

//state 초기값
const initialState = {
    winner : '',
    turn: 'O',
    tableData:[['','',''],['','',''],['','','']],
}
//리듀서 == 함수
const reducer = (state, action) => {

}

const TicTacToe = () => {
    const [winner, setWinner] = useState('');
    const [turn, setTurn] = useState('O');
    const [tableData, setTableData] = useState([['','',''],['','',''],['','','']]);

    const [state, dispatch] = useReducer(reducer, initialState);

    return(
        <>
            <Table />
            {
                winner && <div>{winner}님의 승리</div>
            }
        </>
    );
}

export default TicTacToe;

 

Table.jsx

import React from 'react';
import Tr from "./Tr";

const Table = () => {
    return (
        <Tr>
            
        </Tr>
    );
};

export default Table;

 

Tr.jsx

import React from 'react';
import Td from "./Td";

const Tr = () => {
    return (
        <Td>

        </Td>
    );
};

export default Tr;

 

Td.jsx

import React from 'react';

const Td = () => {
    return (
        <td>

        </td>
    );
};

export default Td;

useEffect : 2번째 파람이 빈 배열 == componentDidMout와 동일

배열에 요소가 있다면 ? componentDidMout, componentDidUpdate 둘 다 수행

 

timerRef (강의 내용에서의 timeouts.current) 가 바뀌는 시점을 헷갈렸다.

왜 강의에서 timeouts.current를 useEffect의 2번째 파라미터로 넣었는지 헷갈렸는데... 다시 생각해보니 내가 헷갈렸던 것...

 

timerRef.current는 배열이기 때문에 push, shift 또는 배열 인덱스로 직접 접근하는 것은 배열 참조(배열의 주소)가 바뀌는 것이 아니기 때문에 바뀌는 것으로 간주하지 않는다.

React에서 배열이나 객체로 된 state나 ref, prorps가 바뀌는 것은 배열의 참조가 바뀔 때 이므로

timerRef.current = []

이렇게나

 tirmerRef.current = [...prev, 0];

이렇게 spread 연산자를 사용하여 새로운 참조를 만들어 줄 때 바뀌는 것이다.

 

 tirmerRef.current[7] = 100; //이건 변경된것으로 간주하지 못한다. 그냥 같은 배열주소에 하나 더 추가된것일뿐
 //변경된 것으로 간주되려면 배열참조가 바뀌어야함!

가위바위보에서 굳이 useEffect나 componentDidMount 에서만 setInterval을 사용하는건 아니고, 필요할 때 setInterval을 쓰고 componentWillUnMount에서 정리만 잘하면 된다.

 

Hooks는 함수 컴포넌트를 말하는 것이 아니라 useEffect, useState 같은 것들을 말하는 것이다.

 

Lotto.jsx (내가 만든 코드 + 강의코드)

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

const winBall = () => {
    const balls = Array(45).fill(0).map((v,i) => i+1);
    const result = [];
    while(balls.length > 0){
        const randIndex = Math.floor(Math.random() * balls.length); // <= 0 x < length
        result.push(balls.splice(randIndex, 1)[0]);
    }
    const bonus = result[result.length - 1]; //맨 마지막
    const wins = result.slice(0,6).sort((a,b)=>a-b); // 0~5
    console.log(wins, bonus);
    return [...wins, bonus];
}

class Lotto extends Component{
    state = {
        winNumbers: winBall(),
        winBalls:[],
        bonus:null,
        redo:false,
    }
    timeouts=[];

    componentDidMount() {
        console.log('componentDidMount')
        this.startLotto();
    }

    onClickRedo = () => {
        this.setState({
            winNumbers: winBall(),
            winBalls:[],
            bonus:null,
            redo:false
        });
        this.timeouts = [];
    }

    componentDidUpdate(prevProps, prevState, snapshot) {
        if(this.state.winBalls.length === 0){
            this.startLotto();
        }
    }

    componentWillUnmount() {
        this.timeouts.forEach(v => clearTimeout(v));
    }

    startLotto = () => {
        const length = this.state.winNumbers.length - 1;
        for(let i=0; i<length; i++){
            const timerId = setTimeout(()=>{
                this.setState((prev)=>{
                    const winBall = prev.winNumbers.slice(i,i+1)[0];

                    return {
                        winNumbers:[...prev.winNumbers],
                        winBalls:[...prev.winBalls, winBall],
                        bonus:null,
                        redo:false
                    }
                });
            },1000 * (i+1));
            this.timeouts.push(timerId);
        }

        this.timeouts[6] = setTimeout(()=>{
            this.setState({bonus: this.state.winNumbers[length],redo:true});
        }, 1000 * this.state.winNumbers.length);
    }

    render() {
        const {winNumbers, winBalls, bonus, redo} = this.state;
        return (
            <>
                {winBalls.map(v=><Ball number={v} key={v+1}/>)}

                {
                    bonus && <><div className="bonus__comment">보너스!</div><Ball number={bonus} /></>
                }
                {
                    redo && <button onClick={this.onClickRedo}>한번더!</button>
                }
            </>
        )
    }
}

export default Lotto;

 

 

Ball.jsx

import React, {memo} from 'react';
const Ball = memo(({number})=>{
    let background, color = null;
    if(number >= 40){
        background = 'red';
    }else if(number >= 30){
        background = 'green';
    }else if(number >= 20){
        background = 'yellow';
    }else if(number >= 10){
        background = 'blue';
        color = 'white';
    }else{
        background = 'aqua';
    }

    return (
        <>
            <div className="ball" style={{background, color}}>
                {number}
            </div>
        </>
    )
});

export default Ball;

 

 

LottoHooks.jsx

import React, {useEffect, useRef, useState} from "react";
import Ball from "./Ball";

const LottoHooks = () => {
    const winBall = () => {
        const balls = Array(45).fill(0).map((v,i) => i+1);
        const result = [];
        while(balls.length > 0){
            const randIndex = Math.floor(Math.random() * balls.length); // <= 0 x < length
            result.push(balls.splice(randIndex, 1)[0]);
        }
        const bonus = result[result.length - 1]; //맨 마지막
        const wins = result.slice(0,6).sort((a,b)=>a-b); // 0~5
        console.log(wins, bonus);
        return [...wins, bonus];
    }

    const [winNumbers, setWinNumbers] = useState(winBall);
    const [winBalls, setWinBalls] = useState([]);
    const [bonus, setBonus] = useState(null);
    const [redo,setRedo] = useState(false);
    const timerRef = useRef([]);


    useEffect(()=>{
        for(let i=0; i<6; i++){
            const timerId = setTimeout(()=>{
                setWinBalls((prev)=>{
                    return [...prev, winNumbers[i]];
                });
            },(i+1) * 1000);
            timerRef.current.push(timerId);
        }
        timerRef.current[6] = setTimeout(()=>{
            setBonus(winNumbers[6]);
            setRedo(true);
        },7000);
    },[winNumbers]);

    const onClickRedo = (param) => (e) => {
        console.log(param);
        console.log(e.target);
        setWinBalls([]);
        setWinNumbers(winBall);
        setBonus(null);
        setRedo(false);
        timerRef.current = [];
    }

    return (
        <>
            {
                winBalls.map((v,i) => <Ball number={v} key={v+i}/>)
            }
            {
                bonus && <><div>보너스!!</div><Ball number={bonus} /></>
            }
            {
                redo && <button onClick={onClickRedo(1)}>한번더</button>
            }
        </>
    );
}

export default LottoHooks;

커스텀 훅

  • 어떤 특정한 Hook 2개 이상이 반복된다면 커스텀 훅을 만들어낸다.
  • 커스텀 훅은 react의 기본 hooks의 조합으로 이루어진다. (기본 훅 여러 개가 세트로 움직인다면? 3개를 세트로 묶어서 커스텀 훅 1개로 퉁 쳐버리면 된다.)
import {useEffect, useRef} from "react";

// 기능
// useInterval(()=>{
//  console.log('hello');
// }, isRunning ? 1000 : null );
function useInterval(callback, delay){
    const savedCallback = useRef();

    useEffect(() => {
        savedCallback.current = callback;
    });

    useEffect(()=>{

        //왜 tick이라는 함수로 감싸서 실행했을까?
        function tick(){
            savedCallback.current();
        }

        if(delay !== null){
            let id = setInterval(tick, delay);
            return () => clearInterval(id);
        }
    },[delay]);

    return savedCallback.current;
}

export default useInterval;

강의에서 만들어 본 useInterval이라는 (useRef + setInterval) 커스텀 훅이다.

 

왜 tick이라는 내부함수가 필요한지에 대해서도 설명해주는데 이해하기 좀 난해하다.

 

따로 기억할것.

※useRef는 항상 최신의 객체를 참조할 수 있게 해준다 라는 것을 기억하자.

비교

state가 바뀌었을 때 

  • class의 경우 componentDidMount나 componentDidUpdate에서 모든 state를 조건문으로 분기처리한다.
  • hooks의 경우 useEffect의 2번째 파라미터인 의존배열만 다르게 하여 useEffect를 여러 개 사용하면 된다. (배열에는 꼭 useEffect를 다시 실행할 값만 넣으세요)

state를 동시에 담당하는 법(?)

  • componentDidMount는 [result, imgCoords, score] 이러한 state들을 한번에 담당한다.  (하나의 componentDidMount 에서 state별로 조건분기를 하든 어떻게 하든 하나의 클래스에는 하나의 라이프사이클 메소드만 존재한다고 말하고 싶은거같다.) --> (componentDidUpdate,componentWillUnmount 도 마찬가지이다.)
  • hooks에서는 result용 useEffect, imgCoords용 useEffect 이렇게 useEffect를 state별로 하나씩 만들 수 있다. 하지만 하나의 useEffect에서 동시에 result, imgCoords 2개의 state를 담당할 수 도 있긴함.
  •  
//클래스에서는 하나의 라이프사이클 메서드에서 state여러 개를 한번에 처리
componentDidMount(){
	this.setState({
    	imgCoord: 3,
        score: 1,
        result: 2,
    })
}


//hooks의 useEffect에서는 각각 처리하거나, 묶어서 처리 가능
useEffect(()=>{
	setImgCoord(2);
	setScore(0);
},[imgCoord, score]);
useEffect(()=>{
	setResult('');
},[result]);

 

useEffect vs useLayoutEffect

  • useEffect는 렌더링이 실행되고 나서 실행된다.
  • useLayoutEffect는 렌더링 되기 전에 실행돼서 화면 바뀌는것을 감지하는 Effect이다. (자주 사용안한다고 하심)

 

복습

  • 부모 컴포넌트에서 자식 컴포넌트를 가지고 있을 때 부모 컴포넌트가 리렌더링 되면 자식 컴포넌트도 무조건 리렌더링 되는데 부모 컴포넌트가 리렌더링 될 때 굳이 자식 컴포넌트를 리렌더링 할 필요가 없을 때 React.memo를 사용하면 자식 컴포넌트는 리렌더링 되지 않는다.
  • useEffect 하나에서 componentDidUpdate,componentWillUnmount, componentDidMount 세 개를 동시에 처리할 수 있다고 생각하면 편리할 듯!

 

 

 

+ Recent posts