#7 useReducer / 틱택토

sham·2021년 8월 23일
0

인프런에 올라간 제로초님의 강의를 보고 정리한 내용입니다.
https://www.inflearn.com/course/web-game-react


코드

함수 버전(Hooks)

Td.jsx

import React, {useCallback, memo}  from 'react';
import {CLICK_CELL} from "./TicTacToeFunction";

const Td = memo(({cellData, rowIndex, cellIndex, dispatch}) => {
  console.log("Td render");
  const onCilckTd = useCallback(() => {
    if (cellData) {
      return ;
    }
    dispatch({type:CLICK_CELL, row:rowIndex, cell:cellIndex});
  }, [cellData]);
  return (
    <td rowIndex={rowIndex} cellIndex={cellIndex} onClick={onCilckTd} cellData={cellData}>{cellData}</td>
  )
});

export default Td;

Tr.jsx

import React, {memo} from 'react';
import Td from './Td';

const Tr = memo(({rowData, rowIndex, dispatch}) => {
  return (
    <tr>
      {Array(rowData.length).fill().map((td, i) => (<Td key={i} rowIndex={rowIndex} cellIndex={i} dispatch={dispatch} cellData={rowData[i]}>{''}</Td>))}
    </tr>
  )
});

export default Tr;

Table.jsx

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

const Table = ({tableData, dispatch}) => {
  console.log("Table render");
  return (
    <table>
      {Array(tableData.length).fill().map((tr, i) => (<Tr key={i} rowIndex={i} rowData={tableData[i]} dispatch={dispatch}/>))}
    </table>
  );
};

export default Table;

TicTacToe.jsx

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

const initalState = {
	winner : "",
	turn : "O",
	tableData : [
		['','',''],
		['','',''],
		['','','']],
	recentCell : [-1, -1],
};

export const SET_WINNER = "SET_WINNER";
export const CLICK_CELL = "CLICK_CELL";
export const CHANGE_TURN = "CHANGE_TURN";
export const RESET_GAME = "RESET_GAME";



const reducer = (state, action) => {
	switch(action.type){
		case SET_WINNER :
			return {
				...state,
				winner: action.winner,
		}
		case CLICK_CELL : 
		const newTableData = [...state.tableData];
		newTableData[action.row] = [...state.tableData[action.row]];
		newTableData[action.row][action.cell] = state.turn; //O턴이냐, X턴이냐
		console.log(state.tableData);
			return {
				...state,
				tableData: newTableData,
				recentCell : [action.row, action.cell],
			}
		case CHANGE_TURN :
		return {
			...state,
			turn : state.turn === 'O' ? 'X' : 'O',
		};
		case RESET_GAME :
		return {
			...state,
			winner : "",
			turn : "O",
			tableData : [
				['','',''],
				['','',''],
				['','','']],
			recentCell : [-1, -1],
		}
		default :
			return state;
	}
}

const TicTacToe = () => {
	const [state, dispatch] = useReducer(reducer, initalState);
	const {tableData, turn, winner, recentCell} =  state;

	useEffect(() => {	
	let win = false;
	const [row, cell] = recentCell;
	if (row < 0) {
		return ;
	}
	if (tableData[row][0] === turn && tableData[row][1] === turn && tableData[row][2] === turn) {
      win = true;
    }
    if (tableData[0][cell] === turn && tableData[1][cell] === turn && tableData[2][cell] === turn) {
      win = true;
    }
    if (tableData[0][0] === turn && tableData[1][1] === turn && tableData[2][2] === turn) {
      win = true;
    }
    if (tableData[0][2] === turn && tableData[1][1] === turn && tableData[2][0] === turn) {
      win = true;
     }
	if (win) {
		dispatch({type : SET_WINNER, winner : turn});
		setTimeout(() => dispatch({type : RESET_GAME}), 1000);
	} else {
		let all = true;
		tableData.forEach((row) => {
			row.forEach((cell) => {
				if (!cell) {
					all = false;
				}
			})
		})
		if (all)
			dispatch({type : RESET_GAME});
	}
	dispatch({type:CHANGE_TURN});

}, [recentCell])

	return (
		<>
		<Table tableData={tableData} dispatch={dispatch}/>
		{winner && <div>{winner}님의 승리!</div>}
		</>
	)
}

export default TicTacToe;

#7-1 틱택토의 구조

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

const Td = () => {
  return (
    <td>{''}</td>
  )
}

export default Td;

//Tr.jsx
import React from 'react';
import Td from './Td';

const Tr = () => {
  return (
    <Td>{''}</Td>
  )
}

export default Tr;

//Table.jsx
import React from 'react';
import Tr from './Tr';

const Table = ({ tableData, dispatch }) => {
  return (
    <Tr>{''}</Tr>
  );
};

export default Table;

//TicTacToe 
import React, {useRef, useState, useReducer} from 'react';
import Table from './Table'

const TicTacToe = () => {
	const [state, dispatch] = useReducer(reducer, initalState)
	const [winner, setWinner] = useState("null");
	const [turn, setTurn] = useState("0");
	const [tableData, setTableData] = useState([['','',''],['','',''],['','','']]);
	return (
		<>
		<Table/>
		{winner && <div>{winner}님의 승리</div>}
		</>
	)
}

export default TicTacToe;

table, tr, td(전체 표, 가로 줄, 개별 셀)를 각각 컴포넌트로 지정.

전체적인 구조는 TicTacToe → table → tr → td 형태로 되어있다.

TicTacToe의 state가 props형태로 자식 컴포넌트에 전달된다.

#7-2 reducer, action. dispatch의 관계

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

const Td = () => {
  return (
    <td>{''}</td>
  )
}

export default Td; 
//Tr.jsx
import React from 'react';
import Td from './Td';

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

export default Tr;

//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;

// TicTacToe.jsx 
import React, {useRef, useState, useReducer, useCallback} from 'react';
import Table from './Table'

const initalState = {
	winner : "",
	turn : "0",
	tableData : [['','',''],['','',''],['','','']],
};

const SET_WINNER = "SET_WINNER";

const reducer = (state, action) => {
	switch(action.type){
		case SET_WINNER :
			return {
				...state,
				winner: action.winner 
		}		
	}
}

const TicTacToe = () => {
	const [state, dispatch] = useReducer(reducer, initalState);
	const onClickTable = useCallback(() => {
	dispatch({type : SET_WINNER, winner : 'O'});
	}, []);

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

export default TicTacToe;

자식 컴포넌트에 넣는 함수는 useCallback을 적용해줘야 한다.

배열.length로 길이를 구할 때 요소가 배열이더라도 그 배열의 요소를 구하거나 하지는 않는다.

useReducer

Hooks가 들어오며 생긴 hook 중 하나, 리덕스(Redux)의 핵심부분인 reducer를 거의 그대로 들여왔다.

소규모 앱에서 useReducer와 컨텍스트 API 조합으로 리덕스를 대체할 수 있다.

  • 규모가 커지면 비동기 처리를 위해 리덕스를 쓰게 된다.

기존의 Hooks는 state마다 setState를 해줘야 했지만 useReducer를 쓰면 한 번의 작업으로 모든 state를 설정해줄 수 있다.

const [state, dispatch] = useReducer(reducer, initalState)

reducer에는 state를 제어하는 함수가, initalState에는 state의 초기값을 설정하는 객체를 넣어준다.

state

state에는 reducer 에서 설정한 state가 들어간다.

클래스처럼 state.state으로 접근할 수 있다.

dispatch

dispatch 에는 (리덕스에서도 쓰이는) 액션 객체가 들어간다. 액션 객체 안에는 type이 들어간다.

dispatch({type : 'SET_WINNER', state : '0'}, [])

  • {type : 'SET_WINNER', state : '0'} 객체가 action 객체가 된다.
  • action 객체를 state로dispatch 할 수 있다.
  • action 객체를 어떻게 처리할지는 reducer에서 처리한다.

액션 객체만으로는 state를 바꿀 수 없다. 액션을 해석해서 state를 직접 바꾸는 역할을 하는 것이 reducer이다.

액션의 이름은 상수로 빼는 편이 좋다.

state가 있고, 액션을 dispatch해서 state를 바꾸는데, 그것을 어떻게 바꿀지는 reducer에 써준다.


reducer

const reducer = (state, action) => {
	switch(action.type)
		case 'SET_WINNER' :
			// state.winner = action.winner; 안돼!
			return {
				...state,
				winner: action.winner
}

액션 객체를 구분해서 실제로 실행하는 역할을 한다.

액션을 실행하면 해당 액션 객체가 reducer의 action 인자로 들어온다. state에는 기존의 state가 들어온다.

action 객체의 type을 switch에 집어넣어서 해당되는 case가 있다면 state를 수정한다.

state에 접근해서 직접 바꾸면 안 된다. 기존 state의 값과 수정된 값을 가진 새로운 객체를 만들어 리턴하는 방식으로 바꿔야만 한다.

initalState

state를 객체형식으로 선언한다. 클래스의 state과 동일한 모습이다.


#7-3 action 만들어 dispatch 구현

//Td.jsx
import React, {useCallback} from 'react';
import {CLICK_CELL, CHANGE_TURN} from "./TicTacToeFunction";

const Td = ({cellData, rowIndex, cellIndex, dispatch}) => {
  console.log(rowIndex, cellIndex);
  const onCilckTd = useCallback(() => {
    console.log(rowIndex, cellIndex);
    dispatch({type:CLICK_CELL, row:rowIndex, cell:cellIndex});
    dispatch({type:CHANGE_TURN});
  }, []);
  return (
    <td rowIndex={rowIndex} cellIndex={cellIndex} onClick={onCilckTd} cellData={cellData}>{cellData}</td>
  )
}

export default Td;

//Tr.jsx
import React from 'react';
import Td from './Td';

const Tr = ({rowData, rowIndex, dispatch}) => {
  return (
    <tr>
      {Array(rowData.length).fill().map((td, i) => (<Td rowIndex={rowIndex} cellIndex={i} dispatch={dispatch} cellData={rowData[i]}>{''}</Td>))}
    </tr>
  )
}

export default Tr;

//Table.jsx
import React from 'react';
import Tr from './Tr';

const Table = ({tableData, dispatch}) => {

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

export default Table;

//TicTacToe.jsx
import React, {useRef, useState, useReducer, useCallback} from 'react';
import Table from './Table'

const initalState = {
	winner : "",
	turn : "O",
	tableData : [
		['','',''],
		['','',''],
		['','','']],
};

export const SET_WINNER = "SET_WINNER";
export const CLICK_CELL = "CLICK_CELL";
export const CHANGE_TURN = "CHANGE_TURN";

const reducer = (state, action) => {
	switch(action.type){
		case SET_WINNER :
			return {
				...state,
				winner: action.winner,
		}
		case CLICK_CELL : 
		const newTableData = [...state.tableData];
		newTableData[action.row] = [...state.tableData[action.row]];
		newTableData[action.row][action.cell] = state.turn; //O턴이냐, X턴이냐
		console.log(state.tableData);
			return {
				...state,
				tableData: newTableData,
			}
		case CHANGE_TURN :
		return {
			...state,
			turn : state.turn === 'O' ? 'X' : 'O',
		};
	}
}

const TicTacToe = () => {
	const [state, dispatch] = useReducer(reducer, initalState);

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

export default TicTacToe;

칸(td)을 클릭했을 때 해당 좌표에 대한 정보를 가지고 있어야 한다.

자식 컴포넌트로 props를 넘겨주며 최종적으로는 td가 자기 자신에 대한 x, y좌표를 알아야 한다.

이를 위해 Table → Tr에서 열에 대한 정보인 ['','',''] 가 전송되고, Tr → Td에서 셀에 대한 정보인 [''] 가 전송된다.

td에서 dispatch로 액션을 보내면 최상위 컴포넌트인 TicTacToe의 reducer에서 그것을 실행한다.

물론 td가 dispatch를 쓸 수 있도록 TicTacToe에서 td까지 dispatch를 props로 넘겨줘야만 한다.

컨텍스트 API를 사용하면 깊이가 아무리 깊어도 직통으로 보낼 수 있다.

const reducer = (state, action) => {
	switch(action.type){
		case SET_WINNER :
			return {
				...state,
				winner: action.winner,
		}
		case CLICK_CELL : 
		const newTableData = [...state.tableData];
		newTableData[action.row] = [...state[action.row]];
		newTableData[action.row][action.cell] = state.turn; //O턴이냐, X턴이냐
			return {
				...state,
				tableData: newTableData,
			}
		case CHANGE_TURN :
		return {
			...state,
			turn : state.turn === 'O' ? 'X' : 'O',
		};
	}
}

td에서 보낸 액션을 실행할 때, tableData를 수정하려면 몇 차례 절차를 걸쳐야만 한다.

객체가 있다면 불변성을 지키기 위해 얕은 복사(...)를 해주어야 한다고 생각하는 편이 좋다.

const a = {aa : 1, bb : 2}; 
const b = a;
a === b // true
const c = {...a};
a === c // false

리액트의 불변성을 유지하기 위한 장치이다.

대입과 얕은 복사는 엄연히 다르다.

immer라는 라이브러리로 가독성 문제를 해결할 수 있다.


#7-4 틱택토 구현

 //Td.jsx
import React, {useCallback} from 'react';
import {CLICK_CELL} from "./TicTacToeFunction";

const Td = ({cellData, rowIndex, cellIndex, dispatch}) => {
  console.log(rowIndex, cellIndex);
  const onCilckTd = useCallback(() => {
    console.log(rowIndex, cellIndex);
    if (cellData) {
      return ;
    }
    dispatch({type:CLICK_CELL, row:rowIndex, cell:cellIndex});
  }, [cellData]);
  return (
    <td rowIndex={rowIndex} cellIndex={cellIndex} onClick={onCilckTd} cellData={cellData}>{cellData}</td>
  )
}

export default Td; 

//TicTacToe.jsx
import React, {useRef, useState, useReducer, useEffect, useCallback} from 'react';
import Table from './Table'

const initalState = {
	winner : "",
	turn : "O",
	tableData : [
		['','',''],
		['','',''],
		['','','']],
	recentCell : [-1, -1],
};

export const SET_WINNER = "SET_WINNER";
export const CLICK_CELL = "CLICK_CELL";
export const CHANGE_TURN = "CHANGE_TURN";
export const RESET_GAME = "RESET_GAME";

const reducer = (state, action) => {
	switch(action.type){
		case SET_WINNER :
			return {
				...state,
				winner: action.winner,
		}
		case CLICK_CELL : 
		const newTableData = [...state.tableData];
		newTableData[action.row] = [...state.tableData[action.row]];
		newTableData[action.row][action.cell] = state.turn; //O턴이냐, X턴이냐
		console.log(state.tableData);
			return {
				...state,
				tableData: newTableData,
				recentCell : [action.row, action.cell],
			}
		case CHANGE_TURN :
		return {
			...state,
			turn : state.turn === 'O' ? 'X' : 'O',
		};
		case RESET_GAME :
		return {
			...state,
			winner : "",
			turn : "O",
			tableData : [
				['','',''],
				['','',''],
				['','','']],
			recentCell : [-1, -1],
		}
		default :
			return state;
	}
}

const TicTacToe = () => {
	const [state, dispatch] = useReducer(reducer, initalState);
	const {tableData, turn, winner, recentCell} =  state;

	useEffect(() => {	
	let win = false;
	const [row, cell] = recentCell;
	if (row < 0) {
		return ;
	}
	if (tableData[row][0] === turn && tableData[row][1] === turn && tableData[row][2] === turn) {
      win = true;
    }
    if (tableData[0][cell] === turn && tableData[1][cell] === turn && tableData[2][cell] === turn) {
      win = true;
    }
    if (tableData[0][0] === turn && tableData[1][1] === turn && tableData[2][2] === turn) {
      win = true;
    }
    if (tableData[0][2] === turn && tableData[1][1] === turn && tableData[2][0] === turn) {
      win = true;
     }
	if (win) {
		dispatch({type : SET_WINNER, winner : turn});
		setTimeout(() => dispatch({type : RESET_GAME}), 1000);
	} else {
		let all = true;
		tableData.forEach((row) => {
			row.forEach((cell) => {
				if (!cell) {
					all = false;
				}
			})
		})
		if (all)
			dispatch({type : RESET_GAME});
	}
	dispatch({type:CHANGE_TURN});

}, [recentCell])

	return (
		<>
		<Table tableData={tableData} dispatch={dispatch}/>
		{winner && <div>{winner}님의 승리!</div>}
		</>
	)
}

export default TicTacToe;

입력된 칸은 클릭 못하게, 게임 결과 구현

useEffect는 처음 실행될 때(렌더링될 때)도 렌더링한다.

dispatch로 state를 바꾸는 것은 비동기적으로 변한다. 어느 한 시점에서 state의 값을 출력하더라도 실제 state의 값은 다를 수 있다.

dispatch({type:CLICK_CELL, row:rowIndex, cell:cellIndex});
dispatch({type:CHANGE_TURN});

부분에서 셀에 정보가 등록되기도 전에 turn이 바뀌어 버리는 문제

  • TicTacToe 파일에서 useEffect 안에서 종료 조건을 판단한 후 성립하지 않으면 턴을 바꾸게끔 해서 해결

state를 바꿀 때는 불변성이 중요하다.

state가 많아질 때는 useReducer를 고려해보는 게 어떨까?


#7-5 테이블 최적화

//Td.jsx
import React, {useCallback, memo}  from 'react';
import {CLICK_CELL} from "./TicTacToeFunction";

const Td = memo(({cellData, rowIndex, cellIndex, dispatch}) => {
  console.log("Td render");
  const onCilckTd = useCallback(() => {
    if (cellData) {
      return ;
    }
    dispatch({type:CLICK_CELL, row:rowIndex, cell:cellIndex});
  }, [cellData]);
  return (
    <td rowIndex={rowIndex} cellIndex={cellIndex} onClick={onCilckTd} cellData={cellData}>{cellData}</td>
  )
});

export default Td;

//Tr.jsx
import React, {memo} from 'react';
import Td from './Td';

const Tr = memo(({rowData, rowIndex, dispatch}) => {
  return (
    <tr>
      {Array(rowData.length).fill().map((td, i) => (<Td key={i} rowIndex={rowIndex} cellIndex={i} dispatch={dispatch} cellData={rowData[i]}>{''}</Td>))}
    </tr>
  )
});

export default Tr;

불필요하게 렌더링되는 부분을 최적화.

props로 받은 데이터(state)들은 useCallback으로 감싸주는 편이 좋다.

바뀔 가능성이 있는 데이터들은 useCallback의 두 번째 인자([]) 넣어두면 값이 바뀔 때마다 다시 실행된다.

memo

일반적인 방법은 컴포넌트에 memo를 씌우는 것.

자식 컴포넌트가 전부 memo가 적용되면 바로 상위의 컴포넌트도 memo로 적용할 수 있다.

useEffect

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

useEffect와 useRef로 무엇때문에 반복되는 것인지를 검사할 수 있다.

useMemo

import React, {useMemo} from 'react';
import Td from './Td';

const Tr = ({rowData, rowIndex, dispatch}) => {
  return (
    <tr>
      {Array(rowData.length).fill().map((td, i) => (
      useMemo(
        () => <Td key={i} rowIndex={rowIndex} cellIndex={i} dispatch={dispatch} cellData={rowData[i]}>{''}</Td>,
        [rowData[i]],
      ) 
      ))}
    </tr>
  )
}

export default Tr;

useMemo로 컴포넌트 자체를 기억하게 하는 방법도 있다.

두 번째 인자에 들어온 값이 변하기 전까지는 컴포넌트를 유지한다.

최적화가 필수는 아니다. 허나 크롬으로 검사할 때 컴포넌트가 세빨갛게 물든 상태라면 해라.

profile
씨앗 개발자

0개의 댓글

관련 채용 정보