기록
7. 틱택토 (useReducer) 본문
useReducer를 배울 시간
리덕스의 핵심 부분.
그래서 리듀서를 배우면 리덕스랑 비슷한 기능을 구현할 수 있음.
그러면 리덕스를 대체할 수도 있느냐?
간단한 소규모앱을 만들 때는 useReducer랑 contextAPI 조합으로 대체할 수 있음
근데 비동기 부분 처리할 때 좀 불편해서 결국 리덕스를 쓸거임
그래서 이번 시간엔 컴포넌트를 잘게 나눠보려고 함. 가로/세로/테이블 잘게!
TicTacToe, Table, Tr, Td
작은 것부터 만들어서 쌓아올리는게 편할 듯.
Td들이 모여서 Tr들이 모여서 Table. TicTacToe에는 Table을 불러오고.
이렇게 쪼갤 수 있는 만큼 쪼개야, 나중에 성능최적화할 때도 좋음
//Td
import React from "react";
const Td = () => {
return <td>{""}</td>;
};
export default Td;
//Tr
import React from "react";
import Td from "./Td";
const Tr = () => {
return (
<tr>
<Td>{""}</Td>
</tr>
);
};
export default Tr;
//Table
import React from 'react';
imporn Tr from './Tr';
const Table = () => {
return(
<table>
<Tr>{''}</Tr>
</table>
)
}
export default Table
//TicTacToe 상위 컴포넌트
import React from "react";
import Table from "./Table";
const TicTacToe = () => {
const [winner, setWinner] = useState("");
const [turn, setTurn] = useState("O");
const [tableDate, setTableDate] = useState([['','',''],['','',''],['','',''])
return (
<>
<Table />
{winner && <div>{winner}님의 승리</div>}
</>
);
};
export default TicTacToe;
현재 이 상황.
근데 유저가 실제로 클릭을 하는 건 Td인데, 그걸 가장 상위 TicTacToe가 관리하게 되는 구조니까
TicTacToe에서 Table - Tr - Td까지 건너 건너 데이터를 전달해줘야 하는데
심지어 winner, setWinner, ... 6개의 state까지 전달해줘야 함
그래서 context API를 이용해서 개선하는데, 이건 뒤에서 배워보고
오늘은 state의 개수를 줄이는 useReducer에 대해 배울 것.
state가 점점 늘어나면 관리가 힘들어지고 하위 컴포넌트로 넘겨주기도 너무 힘듦
이걸 useReducer를 이용하면 딱 하나의 state와 하나의 setState로 통일을 할 수 있게 됨
reducer함수는 컴포넌트 바깥에 작성함.
const initialState = {
winner: "",
turn: "O",
tableDate: [
["", "", ""],
["", "", ""],
["", "", ""],
],
};
const reducer = (state, action) => {
}
const TicTacToe = () => {
const [state, dispatch] = useReducer(reducer, initialState);
// const [winner, setWinner] = useState("");
// const [turn, setTurn] = useState("O");
// const [tableDate, setTableDate] = useState([['','',''],['','',''],['','','']])
useReducer(reducer함수, initialState)
initialState는 초기값 state.
reducer는 함수, state를 어떻게 바꿀지 적는 곳(setState역할)
state에서 initialState를 만들었기 때문에 state.winner (구조분해할당하면 안써도 됨)
return (
<>
<Table onClick={onClickTable} />
{state.winner && <div>{state.winner}님의 승리</div>}
</>
);
그리고 예시로 테이블을 클릭했을 때 turn을 화면에 보이게 해보자
const onClickTable = useCallback(() => {
dispatch({ type: SET_WINNER, winner: "O" });
}, []);
dispatch를 사용하는데 dispatch안에는 action이라고 부르는 객체를 넣음
액션 안에는 type(액션의 이름을 의미함)과 state가 있음
걔네를 action.type / action.winner 이렇게 부름
정리하면, dispatch가 action을 실행하는 것.
Tip)
action.type은 대문자로 하는게 보통의 규칙이고,
따로 전역변수로 선언해놓는게 편함!
action을 dispatch할 때마다. 액션을 실행할 때마다 reducer의 해당 액션 부분이 실행됨!
const SET_WINNER = "SET_WINNER";
const reducer = (state, action) => {
switch (action.type) {
case SET_WINNER:
return {
...state,
winner: action.winner,
};
}
};
많은 액션들이 있으니까 각 상황에 맞게 구별할거임
액션을 실행하면 그 액션이 뭔지 구별을 해서 state를 어떻게 바꿀지.
action.type이 'SET_WINNER'일 때, ~~~를 실행시키겠다.
state를 직접적으로 state.winner = action.winner 이렇게 기존 state를 바꾸면 안되고,
새롭게 객체를 만들어서 변경될 값만 바꿔줘야 함.(불변성 유지)
...state로 기존 state를 복사해오고, 새로운 무언가로 변경시키기.
그리고 table을 클릭했을 때 이 동작을 실행시킬거니까, Table.jsx도 수정해야 함.
props를 받아와야 함. 이벤트를 onClick으로 받아오겠음
const Table = ({onClick}) => {
return(
<table onClick={onClick}>
<Tr>{''}</Tr>
</table>
)
}
그러면 테이블을 클릭했을 때 O님의 승리 라는 멘트가 화면에 보이게 됨
지금 이 상황을 정리해보자면,
state는 직접 수정할 수 없음
수정하려면 action을 만들어서 실행(dispatch)해야 함
이 action을 어떻게 처리할지는 reducer에서 관리
앞으로 state를 바꿀 때마다 이러한 방식을 이용할 예정임.
이제 3x3 테이블을 만들어 보자.
//TicTacToe
return (
<>
<Table onClick={onClickTable} tableData={state.tableData} />
{state.winner && <div>{state.winner}님의 승리</div>}
</>
);
//Table
const Table = ({ onClick, tableData }) => {
return (
<table onClick={onClick}>
{Array(tableData.length).fill().map((tr, i) => (
<Tr rowData={tableData[i]} />
))}
</table>
);
};
//Tr
const Tr = ({ rowData }) => {
return (
<tr>
{Array(rowData.length).fill().map((td) => (<Td>{""}</Td>))}
</tr>
);
};
Array(tableData.length).fill().map()
요소가 3개인 배열을 tr로 만들었고
각각의 데이터들이 rowData={tableData[i]} 로 들어감.
(솔직히 이 부분 알고리즘은 좀 이해가 안됨..일단 패스)
이제 유저가 클릭한 칸이 몇번째 줄, 몇번째 칸인지를 알아내야 함
이제부터 컴포넌트 왔다갔다 많이 함..
Table에 있던 onClick은 Td 컴포넌트로.
Table에 rowIndex={i} 는 몇번째 줄인지.
Tr에 rowIndex={rowIndex} cellIndex={i} 는 몇째줄 몇번째 칸인지.
const Table = ({ tableData }) => {
return (
<table>
{Array(tableData.length)
.fill()
.map((tr, i) => (
<Tr rowIndex={i} rowData={tableData[i]} />
))}
</table>
);
};
const Tr = ({ rowData, rowIndex }) => {
return (
<tr>
{Array(rowData.length)
.fill()
.map((td, i) => (
<Td rowIndex={rowIndex} cellIndex={i}>
{""}
</Td>
))}
</tr>
);
};
그리고 Td와 TicTacToe 를 주목!
TicTacToe에서 변수를 export하고 Td에서 import 하는 점.
그 변수는 dispatch에서 쓰인다는 점.
dispatch는 뭐냐면, 칸을 클릭했을 때 클릭한 칸을 상태에 반영하는 액션을 담고 있음.
그 액션은 물론 reducer에서 관리함
//Td
import { CLICK_CELL } from "./TicTacToe";
const Td = ({ rowIndex, cellIndex }) => {
const onClickTd = useCallback(() => {
console.log(rowIndex, cellIndex);
dispatch({ type: CLICK_CELL, row: rowIndex, cell: cellIndex });
}, []);
return <td onClick={onClickTd}>{""}</td>;
};
//TicTacToe
export const SET_WINNER = "SET_WINNER";
export const CLICK_CELL = "CLICK_CELL";
const reducer = (state, action) => {
switch (action.type) {
case SET_WINNER:
return {
...state,
winner: action.winner,
};
case CLICK_CELL:
return {
...state,
};
}
};
reducer에서 하는 작업에 주목.
클릭한 칸에 [ ' ', 'O', 'X'] 이런 식으로 값을 넣어주면, 화면에도 반영이 되니까.
(리액트의 장점: 데이터를 바꾸면 알아서 화면에 반영함)
근데 불변성에 항상 주의하고.
기존의 테이블데이터를 얕은 복사해주고
case CLICK_CELL:{
const tableData = [...state.tableData] //기존 테이블데이터를 얕은복사
tableDate[action.row] = [...tableData[action.row]];
tableData[action.row][action.cell] = state.turn;
return {
...state,
tableData,
}};
액션의 row도 마찬가지로 얕은 복사를 해줘야 함. (객체가 있으면 얕은복사를 해준다고 생각하면 됨)
(좀 복잡해지는데... 이 부분은 immer라는 라이브러리로 가독성 문제를 해결할 수 있다)
그리고 칸에는 현재 turn
그걸 이제 return에 tableData
tableData에 우리가 원하는 부분만 불변성 지키면서 바꾼 것.
그 다음 클릭을 하고나면 턴이 바껴야 함
case SET_CHANGE: {
return {
...state,
turn: state.turn === "O" ? "X" : "O",
};
}
//Td
const Td = ({ rowIndex, cellIndex, dispatch, cellData }) => {
const onClickTd = useCallback(() => {
console.log(rowIndex, cellIndex);
dispatch({ type: CLICK_CELL, row: rowIndex, cell: cellIndex }); // 칸 클릭
dispatch({ type: CHANGE_TURN }); // 턴 전환
}, []);
return <td onClick={onClickTd}>{cellData}</td>;
};
여기서 중요한 건, dispatch를 TicTacToe -> Table -> Tr -> Td로 넘겨야 한다는 것.(context API의 필요성)
tableData -> rowData -> cellData 이것도 마찬가지 계속 넘겨줘야 함.
훅스에서 props 받아오는 구조분해할당 부분에서 받아오는 것! 꼭 체크
7.4 틱택토 구현하기
1. 이미 선택된 칸을 다시 클릭할 수 없게 하기
2. 승부 판독
1. 아주 간단함.
const onClickTd = useCallback(() => {
console.log(rowIndex, cellIndex);
if (cellData) {
return;
}
dispatch({ type: CLICK_CELL, row: rowIndex, cell: cellIndex }); // 칸 클릭
dispatch({ type: CHANGE_TURN }); // 턴 전환
}, [cellData]);
cellData가 이미 있으면 그냥 리턴.
그리고 두번째인자에 cellData 넣기. cellData가 계속 바뀌니까.
이렇게 하면 한 번 클릭한 칸은, 다시 클릭해도 바뀌지 않음.
(두번째 인자를 비워두면, 이미 O인 칸을 클릭했을 때 X로 바뀜. 계속 전환됨. )
이렇게 useCallback에는 바뀔 여지가 있는 값을 두번째 인자로 넣자.
2. 승리조건을 어디에 적는 게 좋을까?
리액트에선 state가 비동기임. 비동기적으로 바뀜
dispatch에서 state 바꾸는 것도 비동기임(리덕스는 동기적으로 바뀜)
따라서 비동기인 state에서 무언가를 처리하려면 무조건 useEffect를 쓰자.
내가 클릭한 칸을 기억해서 그 칸이랑 연결된 줄만 체크하도록 짜볼거임.
const initialState = {
recentCell: [-1, -1],
};
...
case CLICK_CELL: {
const tableData = [...state.tableData];
tableData[action.row] = [...tableData[action.row]];
tableData[action.row][action.cell] = state.turn;
return {
...state,
tableData,
recentCell: [action.row, action.cell],
};
}
...
const TicTacToe = () => {
...
useEffect(() => {
const [row, cell] = recentCell;
if (row < 0) {
return;
}
let win = false;
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;
}
근데 비동기 문제로 CHANGE_TURN을 Td에서 삭제해야겠음.
삭제하고, 3줄 체크하는 조건문에 넣자. 3줄이 완성이 안됐을 때 턴 전환하는 걸로.
(Td에서 클릭하고나면 턴 전환되게 만든 것 때문에 3줄이 완성됐는데도 턴을 전환해서 생긴 문제. 비동기니까 한 번에 모아서 처리하므로.)
이 뒤로는 계속 알고리즘,,, 생략
정리하면,
지금까지는 useState를 이용해서 state를 여러개 만들었는데,
나중가면 state가 열개 백개 많아질 수 있음.
그러면 변수가 너무 많아짐. 심지어 setState 세트니까 2배로 많아짐..
그래서 state로 한 방에 처리하고, setState들도 dispatch로 한 방에 모아서 처리하기 위해 useReducer를 사용함
useReducer는 원래 리덕스에 있는 개념인데 리액트가 도입한 개념.
그래서 앞으로는 state를 하나로 모아두고 얘를 action을 통해서만 바꾸자.
그리고 액션이 실행되면 reducer에 정의한 대로 state를 바꿈. 이때 불변성에 항상 주의
이렇게 useState가 너무 많아지면 useReducer를 한 번 고려해보는 것도 좋다.
7.5 성능최적화
나는 0,0 칸을 클릭했는데 전체가 하이라이팅되는 문제..
Td에 콘솔을 찍어보니 칸이 9번 리렌더링 되고 있었음..
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]);
그럴 땐 이렇게 props 값을 두번째 인자에 넣고, 같은지 다른지 결과로 어떤 값이 바뀌고 안바뀌는지 체크.
그러면 콘솔에 f f f f 였다가 t t t f 로 찍히네...
false가 바뀌는 값. 즉 얘때문에 리렌더링이 발생하는 것.
그러면 지금 cellData가 바뀌는건데... 그거는 내가 클릭한 칸만 바뀌고 있는데 부모에서 문제가 있는 듯...
근데 그냥 Td랑 Tr 컴포넌트를 memo로 감쌌더니 해결됐음!
(React.memo랑 useMemo 혼동 주의)
자식 컴포넌트에 memo를 적용하면 부모에도 적용할 수 있게 됨.
'React.js > zerocho - React 웹게임' 카테고리의 다른 글
6.4 로또추첨기 훅스로 전환(useMemo, useCallback) (0) | 2021.02.01 |
---|---|
6. 로또추첨기 (component-) (0) | 2021.01.31 |
5.5 가위바위보 훅스로 전환(useEffect) (0) | 2021.01.31 |
5. 가위바위보게임(라이프사이클) (0) | 2021.01.31 |
리액트 강좌 - 4. 반응속도게임 (0) | 2021.01.30 |