setState는 비동기이다.

dispatch({type:CLICK_CELL});
dispatch({type:CHANGE_TURN});
console.log(state.turn); //바뀐 turn값이 나오지 않는다.

원래 React의 setState와 useReducer는 state가 다 비동기적으로 바뀐다.

(Redux는 동기적으로 바뀜)

그래서 setState나 dispatch를 한 다음에 바로 state를 console.log로 출력해보면 의도한 값이 안나올 수도 있다.

 

※ 따라서 스테이트를 변경한 후 뭔가 처리할 때는 useEffect를 사용해야한다.

(dispatch나 setState를 한 다음 바로 해당 state를 사용하는 로직이 나타나면 의도한 값이 아닐 수 있어서 에러 발생 가능성이 높다.)

 

강의에서는 테이블 클릭 이벤트 ▶ CLICK_CELL 액션 생성 ▶ dispatch 호출 ▶ reducer 호출 ▶tableData, recentCell state변경 ▶ useEffect에서 변경된 tableData를 기반으로 승리자가 있는지 검사 할 때 사용했다.

useEffect(()=>{
	const [row,cell] = recentCell;
    if(row < 0) return ; //최초로 랜더링 될 때 실행되는 것 방지
    //승리자가 있는지 검사
    //무승부 검사
    //턴변경
    ...
},[state.recentCell]);

 

강의에서의 비동기 state변경 문제

강의 내용 중에 dispatch가 연달아 발생하는 곳이 있는데 여기서 문제가 발생한다.

CLICK_CELL을 dispatch 하면 reducer에서는 CLICK_CELL에 해당하는 로직이 수행되는데 그 와중에 바로 CHANGE_TURN까지 같이 되어버려서 CLICK_CELL이 제대로 작동하지 않는 현상이 있었다. (CLICK_CELL로직 중에 state.turn을 사용하는 곳이 있었음. 원래 O가 들어가야하는데 X가 들어가는 문제가 생김. 클릭한 당시의 나의 턴이 아닌 상대방 턴으로 넘어가버림.)

이게 setState는 모아서 처리하는 것 처럼 dispach도 모아서 한번에 처리하면서 그렇게 된건가? 싶기도함..

 

여기서 정말 다시 강조하심..

제일 중요 : 디스패치가 스테이트를 바꾸는게 비동기이다.

 

정리 - 왜 useReducer를 사용하는가?

[state관리의 용이성]

  • 이전에는 state를 여러 개 만들었는데 그 state들이 나중에 10개, 100개가 될 수가 있다.
  • 관리해야하는 변수들이 너무 많아지고 setTable, setTableData, setTurn 이러한 것들이 너무 많아져서 가독성을 헤칠수도 있다.
  • 그래서 한번에 state를 모아서 처리하는 것이 필요하다. (initialState를 객체로 만드는 이유)

[setState 함수 관리의 용이성]

  • state를 한번에 모아서 관리하므로 setState함수들도 한방에 처리해주는 것도 필요함. 그것이 dispatch, action, reducer의 역할이다.

React가 Redux의 개념을 도입해서 useReducer가 생기게 된 것.

 

정리 - useReducer 실행순서

  • 한번에 모아둔 state들은 액션을 통해서만 변경한다.
  1. 이벤트 발생
  2. action 생성 (action type == action name)
  3. action 실행 (dispatch)
  4. reducer가 action을 수행함. == 우리가 정의해둔 대로 state를 바꾼다.
  5. state를 바꿀 때는 불변성을 지키는 것이 매우 중요!

액션 처리 순서

이벤트 발생 ▶ 액션생성 ▶ 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;

+ Recent posts